From 85c851b137c26cb5bacd540480ef6cc132d5a1c8 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 14 Jul 2026 22:51:21 +0400 Subject: [PATCH 001/328] Add JPEG XL format --- src/ImageSharp/Formats/Jxl/JxlFormat.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/JxlFormat.cs diff --git a/src/ImageSharp/Formats/Jxl/JxlFormat.cs b/src/ImageSharp/Formats/Jxl/JxlFormat.cs new file mode 100644 index 0000000000..d5dfa802b1 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/JxlFormat.cs @@ -0,0 +1,17 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.Jxl; + +internal class JxlFormat : IImageFormat +{ + public string Name => "JPEG XL"; + + public string DefaultMimeType => "image/jxl"; + + IEnumerable IImageFormat.MimeTypes => new[] { "image/jxl" }; + + IEnumerable IImageFormat.FileExtensions => new[] { "jxl" }; +} From 996e93dba2f3c6810b166c98b1e5e4faea0afaaf Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 14 Jul 2026 23:26:11 +0400 Subject: [PATCH 002/328] Implement ac_context.h --- src/ImageSharp/Formats/Jxl/Ac/JxlAcContext.cs | 53 ++++++++++++++ .../Formats/Jxl/Ac/JxlBlockContextMap.cs | 72 +++++++++++++++++++ .../JxlForwardCoefficientOrder.cs | 24 +++++++ src/ImageSharp/Formats/Jxl/JxlFormat.cs | 2 - 4 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Ac/JxlAcContext.cs create mode 100644 src/ImageSharp/Formats/Jxl/Ac/JxlBlockContextMap.cs create mode 100644 src/ImageSharp/Formats/Jxl/Coefficients/JxlForwardCoefficientOrder.cs diff --git a/src/ImageSharp/Formats/Jxl/Ac/JxlAcContext.cs b/src/ImageSharp/Formats/Jxl/Ac/JxlAcContext.cs new file mode 100644 index 0000000000..98f86534b2 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Ac/JxlAcContext.cs @@ -0,0 +1,53 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; +using System.Runtime.CompilerServices; + +#pragma warning disable SA1405 // Debug.Assert should provide message text + +namespace SixLabors.ImageSharp.Formats.Jxl.Ac; + +/// +/// AC context +/// +internal static class JxlAcContext +{ + public const int DctOrderContextStart = 0; + public const int NonZeroBuckets = 37; + public const int ZeroDensityContextCount = 458; + public const int ZeroDensityContextLimit = 474; + + public static ReadOnlySpan CoefficientFrequencyContext => + [ + 0xBAD, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, + 23, 23, 23, 23, 24, 24, 24, 24, 25, 25, 25, 25, 26, 26, 26, 26, + 27, 27, 27, 27, 28, 28, 28, 28, 29, 29, 29, 29, 30, 30, 30, 30, + ]; + + public static ReadOnlySpan CoefficientNumNonzeroContext => + [ + 0xBAD, 0, 31, 62, 62, 93, 93, 93, 93, 123, 123, 123, 123, + 152, 152, 152, 152, 152, 152, 152, 152, 180, 180, 180, 180, 180, + 180, 180, 180, 180, 180, 180, 180, 206, 206, 206, 206, 206, 206, + 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, + 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, + ]; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int ZeroDensityContext(int nonZeroesLeft, int k, int coveredBlocks, int log2CoveredBlocks, int prev) + { + Debug.Assert((1 << log2CoveredBlocks) == coveredBlocks); + + nonZeroesLeft = (nonZeroesLeft + coveredBlocks - 1) >> log2CoveredBlocks; + k >>= log2CoveredBlocks; + + Debug.Assert(k > 0); + Debug.Assert(k < 64); + Debug.Assert(nonZeroesLeft > 0); + Debug.Assert(nonZeroesLeft < 64); + + return ((CoefficientNumNonzeroContext[nonZeroesLeft] + CoefficientFrequencyContext[k]) * 2) + prev; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Ac/JxlBlockContextMap.cs b/src/ImageSharp/Formats/Jxl/Ac/JxlBlockContextMap.cs new file mode 100644 index 0000000000..3b7e75605f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Ac/JxlBlockContextMap.cs @@ -0,0 +1,72 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Jxl.Coefficients; + +namespace SixLabors.ImageSharp.Formats.Jxl.Ac; + +internal sealed class JxlBlockContextMap +{ + public JxlBlockContextMap() + { + DefaultContextMap.CopyTo(this.ContextMap); + this.ContextCount = this.ContextMap.Max() + 1; + this.DcContextCount = 1; + } + + private static ReadOnlySpan DefaultContextMap => + [ + 0, 1, 2, 2, 3, 3, 4, 5, 6, 6, 6, 6, 6, + 7, 8, 9, 9, 10, 11, 12, 13, 14, 14, 14, 14, 14, + 7, 8, 9, 9, 10, 11, 12, 13, 14, 14, 14, 14, 14, + ]; + + public List[] DcThresholds { get; } = [[], [], []]; + + public List QfThresholds { get; } = []; + + public byte[] ContextMap { get; } = new byte[DefaultContextMap.Length]; + + public int ContextCount { get; set; } + + public int DcContextCount { get; set; } + + public int AcContextCount => (this.ContextCount * JxlAcContext.NonZeroBuckets) + JxlAcContext.ZeroDensityContextCount; + + public int Context(int dcIndex, uint qf, int ord, int c) + { + int qfIndex = 0; + for (int i = 0; i < this.QfThresholds.Count; i++) + { + uint t = this.QfThresholds[i]; + + if (qf > t) + { + qfIndex++; + } + } + + int idx = c < 2 ? c ^ 1 : 2; + idx = (idx * JxlForwardCoefficientOrder.OrderCount) + ord; + idx = (idx * (this.QfThresholds.Count + 1)) + qfIndex; + idx = (idx * this.DcContextCount) + dcIndex; + return this.ContextMap[idx]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int ZeroDensityContextOffset(int blockContext) => + (this.ContextCount * JxlAcContext.NonZeroBuckets) + JxlAcContext.ZeroDensityContextCount + blockContext; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int NonZeroContext(int nonZeroes, int blockContext) + { + if (nonZeroes >= 64) + { + nonZeroes = 64; + } + + int ctx = nonZeroes < 8 ? nonZeroes : (4 + (nonZeroes / 2)); + return (ctx * this.ContextCount) + blockContext; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Coefficients/JxlForwardCoefficientOrder.cs b/src/ImageSharp/Formats/Jxl/Coefficients/JxlForwardCoefficientOrder.cs new file mode 100644 index 0000000000..04cacb176a --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Coefficients/JxlForwardCoefficientOrder.cs @@ -0,0 +1,24 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Coefficients; + +internal static class JxlForwardCoefficientOrder +{ + public const byte OrderCount = 13; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int CoefficientRows(int rows, int columns) => rows < columns ? rows : columns; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int CoefficientColumns(int rows, int columns) => rows < columns ? columns : rows; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void CoefficientLayout(ref int rows, ref int columns) + { + rows = CoefficientRows(rows, columns); + columns = CoefficientColumns(rows, columns); + } +} diff --git a/src/ImageSharp/Formats/Jxl/JxlFormat.cs b/src/ImageSharp/Formats/Jxl/JxlFormat.cs index d5dfa802b1..00e7b66eb5 100644 --- a/src/ImageSharp/Formats/Jxl/JxlFormat.cs +++ b/src/ImageSharp/Formats/Jxl/JxlFormat.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Collections.Generic; - namespace SixLabors.ImageSharp.Formats.Jxl; internal class JxlFormat : IImageFormat From d87178fd370387fac07bfd1faa033c2a719b1580 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 14 Jul 2026 23:40:34 +0400 Subject: [PATCH 003/328] Implemented frame_dimensions.h plus part of ac_strategy.h --- .../Formats/Jxl/Ac/JxlAcStrategyType.cs | 67 +++++++++++++++++++ .../Formats/Jxl/JxlFrameDimensions.cs | 65 ++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyType.cs create mode 100644 src/ImageSharp/Formats/Jxl/JxlFrameDimensions.cs diff --git a/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyType.cs b/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyType.cs new file mode 100644 index 0000000000..3412d10f46 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyType.cs @@ -0,0 +1,67 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Ac; + +internal enum JxlAcStrategyType : ushort +{ + // Regular block size DCT + DCT = 0, + + // Encode pixels without transforming + IDENTITY = 1, + + // Use 2-by-2 DCT + DCT2X2 = 2, + + // Use 4-by-4 DCT + DCT4X4 = 3, + + // Use 16-by-16 DCT + DCT16X16 = 4, + + // Use 32-by-32 DCT + DCT32X32 = 5, + + // Use 16-by-8 DCT + DCT16X8 = 6, + + // Use 8-by-16 DCT + DCT8X16 = 7, + + // Use 32-by-8 DCT + DCT32X8 = 8, + + // Use 8-by-32 DCT + DCT8X32 = 9, + + // Use 32-by-16 DCT + DCT32X16 = 10, + + // Use 16-by-32 DCT + DCT16X32 = 11, + + // 4x8 and 8x4 DCT + DCT4X8 = 12, + DCT8X4 = 13, + + // Corner-DCT. + AFV0 = 14, + + AFV1 = 15, + AFV2 = 16, + AFV3 = 17, + + // Larger DCTs + DCT64X64 = 18, + DCT64X32 = 19, + DCT32X64 = 20, + + // No transforms smaller than 64x64 are allowed below. + DCT128X128 = 21, + DCT128X64 = 22, + DCT64X128 = 23, + DCT256X256 = 24, + DCT256X128 = 25, + DCT128X256 = 26 +} diff --git a/src/ImageSharp/Formats/Jxl/JxlFrameDimensions.cs b/src/ImageSharp/Formats/Jxl/JxlFrameDimensions.cs new file mode 100644 index 0000000000..accd7d68cb --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/JxlFrameDimensions.cs @@ -0,0 +1,65 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl; + +internal struct JxlFrameDimensions +{ + public const int BlockDimensions = 8; + public const int DctBlockSize = BlockDimensions * BlockDimensions; + public const int GroupDimensions = 256; + public const int GroupDimensionsInBlocks = GroupDimensions / BlockDimensions; + + public int XSize; + public int YSize; + public int XSizeUpsampled; + public int YSizeUpsampled; + public int XSizeUpsampledPadded; + public int YSizeUpsampledPadded; + public int XSizePadded; + public int YSizePadded; + public int XSizeBlocks; + public int YSizeBlocks; + public int XSizeGroups; + public int YSizeGroups; + public int XSizeDcGroups; + public int YSizeDcGroups; + public int NumGroups; + public int NumDcGroups; + public int GroupDimension; + public int DcGroupDimension; + + public JxlFrameDimensions(int xSizePixel, int ySizePixel, int groupSizeShift, int maxHorizontalShift, int maxVerticalShift, bool modularMode, int upsampling) + { + this.GroupDimension = (GroupDimensions >> 1) << groupSizeShift; + this.DcGroupDimension = this.GroupDimension * BlockDimensions; + this.XSizeUpsampled = xSizePixel; + this.YSizeUpsampled = ySizePixel; + this.XSize = DivCeil(xSizePixel, upsampling); + this.YSize = DivCeil(ySizePixel, upsampling); + this.XSizeBlocks = DivCeil(this.XSize, BlockDimensions << maxHorizontalShift) << maxHorizontalShift; + this.YSizeBlocks = DivCeil(this.YSize, BlockDimensions << maxVerticalShift) << maxVerticalShift; + this.XSizePadded = this.XSizeBlocks * BlockDimensions; + this.YSizePadded = this.YSizeBlocks * BlockDimensions; + + if (modularMode) + { + this.XSizePadded = this.XSize; + this.YSizePadded = this.YSize; + } + + this.XSizeUpsampledPadded = this.XSizePadded * upsampling; + this.YSizeUpsampledPadded = this.YSizePadded * upsampling; + this.XSizeGroups = DivCeil(this.XSize, GroupDimensions); + this.YSizeGroups = DivCeil(this.YSize, GroupDimensions); + this.XSizeDcGroups = DivCeil(this.XSizeBlocks, GroupDimensions); + this.YSizeDcGroups = DivCeil(this.YSizeBlocks, GroupDimensions); + this.NumGroups = this.XSizeGroups * this.YSizeGroups; + this.NumDcGroups = this.XSizeDcGroups * this.YSizeDcGroups; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int DivCeil(int x, int y) => x / y; +} From 1ac61405358cc85f7571fee827860c6fccdf04ee Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 15 Jul 2026 14:44:57 +0400 Subject: [PATCH 004/328] Implement AC strategy Implementation of ac_strategy.h and ac_strategy.c --- .../Formats/Jxl/Ac/JxlAcStrategy.cs | 186 ++++++++++++++++++ .../Formats/Jxl/Ac/JxlAcStrategyImage.cs | 114 +++++++++++ .../Formats/Jxl/Ac/JxlAcStrategyRow.cs | 31 +++ 3 files changed, 331 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategy.cs create mode 100644 src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyImage.cs create mode 100644 src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyRow.cs diff --git a/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategy.cs b/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategy.cs new file mode 100644 index 0000000000..7c704d8e91 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategy.cs @@ -0,0 +1,186 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using static SixLabors.ImageSharp.Formats.Jxl.JxlFrameDimensions; + +#pragma warning disable SA1405 // Debug.Assert should provide message text + +namespace SixLabors.ImageSharp.Formats.Jxl.Ac; + +[StructLayout(LayoutKind.Sequential, Pack = 8)] +internal struct JxlAcStrategy +{ + public const int MaximumCoefficientBlocks = 32; + public const int MaximumBlockDimension = BlockDimensions * MaximumCoefficientBlocks; + public const int MaximumCoefficientArea = MaximumBlockDimension * MaximumBlockDimension; + public const int NumberOfValidStrategies = 27; + + private static readonly int MultiblockBits = + GetTypeBit(JxlAcStrategyType.DCT16X16) | GetTypeBit(JxlAcStrategyType.DCT32X32) | + GetTypeBit(JxlAcStrategyType.DCT16X8) | GetTypeBit(JxlAcStrategyType.DCT8X16) | + GetTypeBit(JxlAcStrategyType.DCT32X8) | GetTypeBit(JxlAcStrategyType.DCT8X32) | + GetTypeBit(JxlAcStrategyType.DCT16X32) | GetTypeBit(JxlAcStrategyType.DCT32X16) | + GetTypeBit(JxlAcStrategyType.DCT32X64) | GetTypeBit(JxlAcStrategyType.DCT64X32) | + GetTypeBit(JxlAcStrategyType.DCT64X64) | GetTypeBit(JxlAcStrategyType.DCT64X128) | + GetTypeBit(JxlAcStrategyType.DCT128X64) | + GetTypeBit(JxlAcStrategyType.DCT128X128) | + GetTypeBit(JxlAcStrategyType.DCT128X256) | + GetTypeBit(JxlAcStrategyType.DCT256X128) | + GetTypeBit(JxlAcStrategyType.DCT256X256); + + private readonly bool isFirst; + + public JxlAcStrategy(JxlAcStrategyType strategy, bool isFirst) + { + this.Strategy = strategy; + this.isFirst = isFirst; + + Debug.Assert(this.IsMultiblock); + } + + public JxlAcStrategy(JxlAcStrategyType strategy) + : this(strategy, true) + { + } + + public JxlAcStrategy(int rawStrategy) + : this((JxlAcStrategyType)rawStrategy) + { + } + + private static ReadOnlySpan CoveredBlocksXLookup => + [ + 1, 1, 1, 1, 2, 4, 1, 2, 1, + 4, 2, 4, 1, 1, 1, 1, 1, 1, + 8, 4, 8, 16, 8, 16, 32, 16, 32 + ]; + + private static ReadOnlySpan CoveredBlocksYLookup => + [ + 1, 1, 1, 1, 2, 4, 2, 1, 4, + 1, 4, 2, 1, 1, 1, 1, 1, 1, + 8, 8, 4, 16, 16, 8, 32, 32, 16 + ]; + + private static ReadOnlySpan Log2CoveredBlocksLookup => + [ + 0, 0, 0, 0, 2, 4, 1, 1, 2, + 2, 3, 3, 0, 0, 0, 0, 0, 0, + 6, 5, 5, 8, 7, 7, 10, 9, 9 + ]; + + public readonly bool IsMultiblock => ((1 << (int)this.Strategy) & MultiblockBits) != 0; + + public readonly int RawStrategy => (int)this.Strategy; + + public readonly int CoveredBlocksX => CoveredBlocksXLookup[(int)this.Strategy]; + + public readonly int CoveredBlocksY => CoveredBlocksYLookup[(int)this.Strategy]; + + public readonly int Log2CoveredBlocks => Log2CoveredBlocksLookup[(int)this.Strategy]; + + public readonly JxlAcStrategyType Strategy { get; } + + public void ComputeNaturalCoefficientOrder(ref int order) => CoefficientOrderAndLookup(this, false, ref order); + + public void ComputeNaturalCoefficientOrderLookup(ref int lookup) => CoefficientOrderAndLookup(this, true, ref lookup); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetTypeBit(JxlAcStrategyType type) => 1 << (int)type; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsRawStrategyValid(int rawStrategy) => rawStrategy is < NumberOfValidStrategies and >= 0; + + private static void CoefficientOrderAndLookup(JxlAcStrategy strategy, bool isLookup, Span output) + { + // TODO: CoefficientLayout + // TODO: CeilLog2Nonzero + int cx = strategy.CoveredBlocksX; + int cy = strategy.CoveredBlocksY; + + CoefficientLayout(ref cx, ref cy); + + int xs = cx / cy; + int xsm = xs - 1; + int xss = CeilLog2Nonzero(xs); + int cur = cx * cy; + + for (int i = 0; i < cx * BlockDimensions; i++) + { + for (int j = 0; j <= i; j++) + { + int x = j; + int y = i - j; + + if ((i & 1) == 0) + { + // swap + (x, y) = (y, x); + } + + if ((y & xsm) != 0) + { + continue; + } + + y >>= xss; + int value = 0; + + if (x < cx && y < cy) + { + value = (y * cx) + x; + } + else + { + value = cur++; + } + + if (isLookup) + { + output[((y * cx) * BlockDimensions) + x] = value; + } + else + { + output[value] = ((y * cx) * BlockDimensions) + x; + } + } + } + + for (int ip = (cx * BlockDimensions) - 1; ip > 0; ip--) + { + int i = ip - 1; + + for (int j = 0; j <= i; j++) + { + int x = (cx * BlockDimensions) - 1 - (i - j); + int y = (cx * BlockDimensions) - 1 - j; + + if ((i & 1) != 0) + { + // swap + (x, y) = (y, x); + } + + if ((y & xsm) != 0) + { + continue; + } + + y >>= xss; + int value = cur++; + + if (isLookup) + { + output[((y * cx) * BlockDimensions) + x] = value; + } + else + { + output[value] = ((y * cx) * BlockDimensions) + x; + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyImage.cs b/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyImage.cs new file mode 100644 index 0000000000..0ffeabe32b --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyImage.cs @@ -0,0 +1,114 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Ac; + +internal sealed class JxlAcStrategyImage +{ + private const byte Invalid = byte.MaxValue; // 255 + + private JxlImageB? layers; + private readonly Memory row; + private readonly int stride; + + public JxlMemoryManager MemoryManager => this.layers.MemoryManager; + + public int XSize => this.layers.XSize; + + public int YSize => this.layers.YSize; + + public int PixelsPerRow => this.layers.PixelsPerRow; + + public JxlAcStrategyRow GetRow(int y, int xPrefix = 0) + { + ReadOnlyMemory layerRow = this.layers.GetRow(y); + ReadOnlyMemory row = layerRow[xPrefix..]; + + return new JxlAcStrategyRow(row); + } + + public static JxlAcStrategyImage Create(JxlMemoryManager memoryManager, int xSize, int ySize) + { + JxlAcStrategyImage image = new() + { + layers = JxlImageB.Create(memoryManager, xSize, ySize) + }; + + image.row = image.layers.GetRow(0); + image.stride = image.layers.PixelsPerRow; + + return image; + } + + public int CountBlocks(JxlAcStrategyType type) + { + int value = 0; + int compare = ((int)type << 1) | 1; + + for (int y = 0; y < this.layers.YSize; y++) + { + ReadOnlySpan row = this.layers.GetRowSpan(y); + + for (int x = 0; x < this.layers.XSize; x++) + { + if (row[x] == compare) + { + value++; + } + } + } + + return value; + } + + public JxlAcStrategyRow GetRow(in Rectangle rect, int y) => this.GetRow(rect.Y + y, rect.X); + + public bool IsValid(int x, int y) => this.row.Span[(y * this.stride) + x] != Invalid; + + public bool SetNoBoundsChecks(int x, int y, JxlAcStrategyType type, bool check = true) + { + JxlAcStrategy strategy = new(type); + Span rowSpan = this.row.Span; + int rawType = (int)type; + int rawTypeTimes2 = rawType << 1; + + for (int iy = 0; iy < strategy.CoveredBlocksX; iy++) + { + for (int ix = 0; ix < strategy.CoveredBlocksX; ix++) + { + int pos = ((y + iy) * this.stride) + x + ix; + + if (check && rowSpan[pos] != Invalid) + { + Debug.Fail("Invalid AC strategy. Blocks overlap."); + + return false; + } + + rowSpan[pos] = (byte)(rawTypeTimes2 | ((iy | ix) == 0 ? 1 : 0)); + } + } + + return true; + } + + public bool Set(int x, int y, JxlAcStrategyType type) + { +#if DEBUG + JxlAcStrategy strategy = new(type); + + Debug.Assert(y + strategy.CoveredBlocksY <= this.layers.YSize, "Invalid range"); + Debug.Assert(x + strategy.CoveredBlocksX <= this.layers.XSize, "Invalid range"); +#endif + + return this.SetNoBoundsChecks(x, y, type, check: false); + } + + public void FillDct8(in Rectangle rect) => this.FillPlane(((int)JxlAcStrategyType.DCT << 1) | 1, this.layers, in rect); + + public void FillDct8() => this.FillDct8(in this.layers.GetRectangle()); + + public void FillInvalid() => this.FillImage(Invalid, this.layers); +} diff --git a/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyRow.cs b/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyRow.cs new file mode 100644 index 0000000000..668876f711 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyRow.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Ac; + +internal sealed class JxlAcStrategyRow +{ + private readonly ReadOnlyMemory row; + + public JxlAcStrategyRow(ReadOnlyMemory row) => this.row = row; + + public JxlAcStrategy this[int x] + { + get + { + ReadOnlySpan span = this.row.Span; + + Debug.Assert(x * 8 < span.Length, "Too many bytes of memory were requested"); + + ref byte first = ref MemoryMarshal.GetReference(span); + JxlAcStrategyType strategy = (JxlAcStrategyType)(Unsafe.Add(ref Unsafe.As(ref first), x) >> 1); + bool isFirst = Unsafe.Add(ref first, x) != 0; + + return new JxlAcStrategy(strategy, isFirst); + } + } +} From 31b5505d8987a4beb21f14d47dd700ab763b1e8d Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 15 Jul 2026 14:51:15 +0400 Subject: [PATCH 005/328] Implement JxlMemoryManager For now JxlMemoryManager will be a wrapper around MemoryPool. --- .../Formats/Jxl/Ac/JxlAcStrategyImage.cs | 1 + .../Formats/Jxl/Memory/IJxlMemoryManager.cs | 12 ++++++++++++ .../Formats/Jxl/Memory/JxlMemoryManager.cs | 18 ++++++++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Memory/IJxlMemoryManager.cs create mode 100644 src/ImageSharp/Formats/Jxl/Memory/JxlMemoryManager.cs diff --git a/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyImage.cs b/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyImage.cs index 0ffeabe32b..f41d186e19 100644 --- a/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyImage.cs +++ b/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyImage.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Diagnostics; +using SixLabors.ImageSharp.Formats.Jxl.Memory; namespace SixLabors.ImageSharp.Formats.Jxl.Ac; diff --git a/src/ImageSharp/Formats/Jxl/Memory/IJxlMemoryManager.cs b/src/ImageSharp/Formats/Jxl/Memory/IJxlMemoryManager.cs new file mode 100644 index 0000000000..c808388d28 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Memory/IJxlMemoryManager.cs @@ -0,0 +1,12 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; + +namespace SixLabors.ImageSharp.Formats.Jxl.Memory; + +internal interface IJxlMemoryManager +{ + IMemoryOwner Allocate(int size); + IMemoryOwner Allocate(int size); +} diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlMemoryManager.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlMemoryManager.cs new file mode 100644 index 0000000000..778ef59106 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlMemoryManager.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; + +namespace SixLabors.ImageSharp.Formats.Jxl.Memory; + +/// +/// Allows allocation and deallocation of managed memory. +/// +internal sealed class JxlMemoryManager : IJxlMemoryManager +{ + public static readonly JxlMemoryManager Instance = new(); + + public IMemoryOwner Allocate(int size) => MemoryPool.Shared.Rent(size); + + public IMemoryOwner Allocate(int size) => this.Allocate(size); +} From 21ff33f23c54224f9c8e3854adcf5b24550e0390 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:38:16 +0400 Subject: [PATCH 006/328] Implement image and memory buffers Implementation of image.h and image.c; AC strategy implementation was slightly adjusted to reduce errors. --- .../Formats/Jxl/Ac/JxlAcStrategyImage.cs | 38 +++--- .../Formats/Jxl/Memory/IJxlMemoryManager.cs | 12 -- .../Jxl/Memory/ImageTypes/JxlImage3B.cs | 14 +++ .../Jxl/Memory/ImageTypes/JxlImage3F.cs | 14 +++ .../Jxl/Memory/ImageTypes/JxlImage3I.cs | 14 +++ .../Jxl/Memory/ImageTypes/JxlImage3S.cs | 14 +++ .../Jxl/Memory/ImageTypes/JxlImage3U.cs | 14 +++ .../Jxl/Memory/ImageTypes/JxlImageB.cs | 34 ++++++ .../Jxl/Memory/ImageTypes/JxlImageF.cs | 23 ++++ .../Jxl/Memory/ImageTypes/JxlImageI.cs | 23 ++++ .../Jxl/Memory/ImageTypes/JxlImageS.cs | 23 ++++ .../Jxl/Memory/ImageTypes/JxlImageSB.cs | 23 ++++ .../Jxl/Memory/ImageTypes/JxlImageU.cs | 23 ++++ .../Formats/Jxl/Memory/JxlImage3{T}.cs | 85 +++++++++++++ .../Formats/Jxl/Memory/JxlMemoryManager.cs | 18 --- .../Formats/Jxl/Memory/JxlPlaneBase.cs | 115 ++++++++++++++++++ .../Formats/Jxl/Memory/JxlPlane{T}.cs | 36 ++++++ 17 files changed, 476 insertions(+), 47 deletions(-) delete mode 100644 src/ImageSharp/Formats/Jxl/Memory/IJxlMemoryManager.cs create mode 100644 src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3B.cs create mode 100644 src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3F.cs create mode 100644 src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3I.cs create mode 100644 src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3S.cs create mode 100644 src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3U.cs create mode 100644 src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageB.cs create mode 100644 src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageF.cs create mode 100644 src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageI.cs create mode 100644 src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageS.cs create mode 100644 src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageSB.cs create mode 100644 src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageU.cs create mode 100644 src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Memory/JxlMemoryManager.cs create mode 100644 src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs create mode 100644 src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs diff --git a/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyImage.cs b/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyImage.cs index f41d186e19..0b2bdb74ae 100644 --- a/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyImage.cs +++ b/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyImage.cs @@ -2,42 +2,40 @@ // Licensed under the Six Labors Split License. using System.Diagnostics; -using SixLabors.ImageSharp.Formats.Jxl.Memory; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; namespace SixLabors.ImageSharp.Formats.Jxl.Ac; -internal sealed class JxlAcStrategyImage +internal sealed class JxlAcStrategyImage : IDisposable { private const byte Invalid = byte.MaxValue; // 255 private JxlImageB? layers; - private readonly Memory row; - private readonly int stride; + private Memory row; + private int stride; - public JxlMemoryManager MemoryManager => this.layers.MemoryManager; + public int XSize => this.layers!.XSize; - public int XSize => this.layers.XSize; + public int YSize => this.layers!.YSize; - public int YSize => this.layers.YSize; - - public int PixelsPerRow => this.layers.PixelsPerRow; + public int PixelsPerRow => this.layers!.PixelsPerRow; public JxlAcStrategyRow GetRow(int y, int xPrefix = 0) { - ReadOnlyMemory layerRow = this.layers.GetRow(y); + ReadOnlyMemory layerRow = this.layers!.GetRowBytesMemory(y); ReadOnlyMemory row = layerRow[xPrefix..]; return new JxlAcStrategyRow(row); } - public static JxlAcStrategyImage Create(JxlMemoryManager memoryManager, int xSize, int ySize) + public static JxlAcStrategyImage Create(Configuration memoryManager, int xSize, int ySize) { JxlAcStrategyImage image = new() { - layers = JxlImageB.Create(memoryManager, xSize, ySize) + layers = new JxlImageB(memoryManager, xSize, ySize) }; - image.row = image.layers.GetRow(0); + image.row = image.layers.GetRowBytesMemory(0); image.stride = image.layers.PixelsPerRow; return image; @@ -48,11 +46,11 @@ public int CountBlocks(JxlAcStrategyType type) int value = 0; int compare = ((int)type << 1) | 1; - for (int y = 0; y < this.layers.YSize; y++) + for (int y = 0; y < this.layers!.YSize; y++) { - ReadOnlySpan row = this.layers.GetRowSpan(y); + ReadOnlySpan row = this.layers!.GetRow(y); - for (int x = 0; x < this.layers.XSize; x++) + for (int x = 0; x < this.layers!.XSize; x++) { if (row[x] == compare) { @@ -100,7 +98,7 @@ public bool Set(int x, int y, JxlAcStrategyType type) #if DEBUG JxlAcStrategy strategy = new(type); - Debug.Assert(y + strategy.CoveredBlocksY <= this.layers.YSize, "Invalid range"); + Debug.Assert(y + strategy.CoveredBlocksY <= this.layers!.YSize, "Invalid range"); Debug.Assert(x + strategy.CoveredBlocksX <= this.layers.XSize, "Invalid range"); #endif @@ -112,4 +110,10 @@ public bool Set(int x, int y, JxlAcStrategyType type) public void FillDct8() => this.FillDct8(in this.layers.GetRectangle()); public void FillInvalid() => this.FillImage(Invalid, this.layers); + + public void Dispose() + { + this.layers?.Dispose(); + GC.SuppressFinalize(this); + } } diff --git a/src/ImageSharp/Formats/Jxl/Memory/IJxlMemoryManager.cs b/src/ImageSharp/Formats/Jxl/Memory/IJxlMemoryManager.cs deleted file mode 100644 index c808388d28..0000000000 --- a/src/ImageSharp/Formats/Jxl/Memory/IJxlMemoryManager.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; - -namespace SixLabors.ImageSharp.Formats.Jxl.Memory; - -internal interface IJxlMemoryManager -{ - IMemoryOwner Allocate(int size); - IMemoryOwner Allocate(int size); -} diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3B.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3B.cs new file mode 100644 index 0000000000..63a5c59c9f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3B.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +/// +/// Represents a three-plane, 2D raster image of type . +/// +internal sealed class JxlImage3B : JxlImage3 +{ + public JxlImage3B() + { + } +} diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3F.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3F.cs new file mode 100644 index 0000000000..b456dfd9a7 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3F.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +/// +/// Represents a three-plane, 2D raster image of type . +/// +internal sealed class JxlImage3F : JxlImage3 +{ + public JxlImage3F() + { + } +} diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3I.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3I.cs new file mode 100644 index 0000000000..0d9f6c8202 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3I.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +/// +/// Represents a three-plane, 2D raster image of type . +/// +internal sealed class JxlImage3I : JxlImage3 +{ + public JxlImage3I() + { + } +} diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3S.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3S.cs new file mode 100644 index 0000000000..00615ff846 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3S.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +/// +/// Represents a three-plane, 2D raster image of type . +/// +internal sealed class JxlImage3S : JxlImage3 +{ + public JxlImage3S() + { + } +} diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3U.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3U.cs new file mode 100644 index 0000000000..1921e86d33 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3U.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +/// +/// Represents a three-plane, 2D raster image of type . +/// +internal sealed class JxlImage3U : JxlImage3 +{ + public JxlImage3U() + { + } +} diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageB.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageB.cs new file mode 100644 index 0000000000..0faba189ad --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageB.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +/// +/// Represents a single-plane, 2D raster image of type . +/// +internal sealed class JxlImageB : JxlPlane +{ + public JxlImageB() + { + } + + public JxlImageB(int width, int height) + : base(width, height) + { + } + + public JxlImageB(Configuration configuration, int xSize, int ySize, int prePadding = 0) + : base(xSize, ySize) + => this.Allocate(configuration, prePadding); + + public Memory GetRowBytesMemory(int y) + { + Debug.Assert(y < this.YSize, "Attempted to access out-of-bounds Y coordinate"); + + Memory row = this.Bytes[(y * this.BytesPerRow)..]; + + return row; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageF.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageF.cs new file mode 100644 index 0000000000..6810e87df9 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageF.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +/// +/// Represents a single-plane, 2D raster image of type . +/// +internal sealed class JxlImageF : JxlPlane +{ + public JxlImageF() + { + } + + public JxlImageF(int width, int height) + : base(width, height) + { + } + + public JxlImageF(Configuration configuration, int xSize, int ySize, int prePadding = 0) + : base(xSize, ySize) + => this.Allocate(configuration, prePadding); +} diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageI.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageI.cs new file mode 100644 index 0000000000..0261ffd63c --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageI.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +/// +/// Represents a single-plane, 2D raster image of type . +/// +internal sealed class JxlImageI : JxlPlane +{ + public JxlImageI() + { + } + + public JxlImageI(int width, int height) + : base(width, height) + { + } + + public JxlImageI(Configuration configuration, int xSize, int ySize, int prePadding = 0) + : base(xSize, ySize) + => this.Allocate(configuration, prePadding); +} diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageS.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageS.cs new file mode 100644 index 0000000000..b53716f12f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageS.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +/// +/// Represents a single-plane, 2D raster image of type . +/// +internal sealed class JxlImageS : JxlPlane +{ + public JxlImageS() + { + } + + public JxlImageS(int width, int height) + : base(width, height) + { + } + + public JxlImageS(Configuration configuration, int xSize, int ySize, int prePadding = 0) + : base(xSize, ySize) + => this.Allocate(configuration, prePadding); +} diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageSB.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageSB.cs new file mode 100644 index 0000000000..355c50785c --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageSB.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +/// +/// Represents a single-plane, 2D raster image of type . +/// +internal sealed class JxlImageSB : JxlPlane +{ + public JxlImageSB() + { + } + + public JxlImageSB(int width, int height) + : base(width, height) + { + } + + public JxlImageSB(Configuration configuration, int xSize, int ySize, int prePadding = 0) + : base(xSize, ySize) + => this.Allocate(configuration, prePadding); +} diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageU.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageU.cs new file mode 100644 index 0000000000..d41669e4d1 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageU.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +/// +/// Represents a single-plane, 2D raster image of type . +/// +internal sealed class JxlImageU : JxlPlane +{ + public JxlImageU() + { + } + + public JxlImageU(int width, int height) + : base(width, height) + { + } + + public JxlImageU(Configuration configuration, int xSize, int ySize, int prePadding = 0) + : base(xSize, ySize) + => this.Allocate(configuration, prePadding); +} diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs new file mode 100644 index 0000000000..f46f6767b8 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs @@ -0,0 +1,85 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Memory; + +// NOTE: Do not seal this class. +internal class JxlImage3 + where T : unmanaged +{ + private const int PlaneCount = 3; + + private JxlPlane[] planes = new JxlPlane[3]; + + public JxlImage3() + { + } + + public JxlImage3(JxlImage3 other) + { + for (int i = 0; i < PlaneCount; i++) + { + this.planes[i] = other.planes[i]; + } + } + + public int XSize => this.planes[0].XSize; + + public int YSize => this.planes[0].YSize; + + public int BytesPerRow => this.planes[0].BytesPerRow; + + public int PixelsPerRow => this.planes[0].PixelsPerRow; + + public Span PlaneRow(int plane, int row) + { + this.PlaneRowBoundsCheck(plane, row); + + int rowOffset = row * this.planes[0].BytesPerRow; + Span rowSpan = MemoryMarshal.Cast(this.planes[plane].BytesSpan[rowOffset..]); + + return rowSpan; + } + + public JxlPlane Plane(int index) => this.planes[index]; + + public void Swap(JxlImage3 other) + { + for (int i = 0; i < PlaneCount; i++) + { + other.planes[i].Swap(this.planes[i]); + } + } + + public static JxlImage3 Create(Configuration configuration, int xSize, int ySize) + { + JxlPlane plane0 = JxlPlane.Create(configuration, xSize, ySize); + JxlPlane plane1 = JxlPlane.Create(configuration, xSize, ySize); + JxlPlane plane2 = JxlPlane.Create(configuration, xSize, ySize); + + return new JxlImage3() + { + planes = [plane0, plane1, plane2] + }; + } + + public bool ShrinkTo(int x, int y) + { + for (int i = 0; i < PlaneCount; i++) + { + if (!this.planes[i].ShrinkTo(x, y)) + { + return false; + } + } + + return true; + } + + [Conditional("DEBUG")] + private void PlaneRowBoundsCheck(int c, int y) => + Debug.Assert(c < PlaneCount && y < this.YSize, "The bounds check has failed"); +} diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlMemoryManager.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlMemoryManager.cs deleted file mode 100644 index 778ef59106..0000000000 --- a/src/ImageSharp/Formats/Jxl/Memory/JxlMemoryManager.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; - -namespace SixLabors.ImageSharp.Formats.Jxl.Memory; - -/// -/// Allows allocation and deallocation of managed memory. -/// -internal sealed class JxlMemoryManager : IJxlMemoryManager -{ - public static readonly JxlMemoryManager Instance = new(); - - public IMemoryOwner Allocate(int size) => MemoryPool.Shared.Rent(size); - - public IMemoryOwner Allocate(int size) => this.Allocate(size); -} diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs new file mode 100644 index 0000000000..9948ae790f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs @@ -0,0 +1,115 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Memory; + +// NOTE: Do not seal this type. +internal class JxlPlaneBase : IDisposable +{ + private IMemoryOwner? bytes; + + public JxlPlaneBase(int xSize, int ySize, int sizeOfT) + { + this.XSize = xSize; + this.YSize = ySize; + this.OriginalXSize = xSize; + this.OriginalYSize = ySize; + this.BytesPerRow = 0; + this.Size = sizeOfT; + } + + public JxlPlaneBase() + : this(0, 0, 0) + { + } + + public int BytesPerRow { get; private set; } + + public int XSize { get; private set; } + + public int YSize { get; private set; } + + public Memory Bytes => +#if DEBUG + this.bytes?.Memory ?? throw new InvalidOperationException("Bytes are missing"); +#else + return this.bytes!.Memory; +#endif + + public Span BytesSpan => this.Bytes.Span; + + protected int Size { get; set; } + + protected int OriginalXSize { get; set; } + + protected int OriginalYSize { get; set; } + + public bool Allocate(Configuration configuration, int prePadding) + { + if (this.bytes != null || this.BytesPerRow != 0) + { + return false; + } + + if (this.XSize == 0 || this.YSize == 0) + { + return true; + } + + int totalBytes = unchecked(this.YSize * this.BytesPerRow); + + this.bytes = configuration.MemoryAllocator.Allocate(totalBytes + (prePadding * this.Size)); + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool ShrinkTo(int x, int y) + { + if (x <= this.OriginalXSize || y <= this.OriginalYSize) + { + return false; + } + + Debug.Assert(x <= this.OriginalXSize, "ShrinkTo cannot expand memory"); + Debug.Assert(y <= this.OriginalYSize, "ShrinkTo cannot expand memory"); + + this.XSize = x; + this.YSize = y; + + return true; + } + + protected Span GetRowBase(int y) + where T : unmanaged + { + Debug.Assert(y < this.YSize, "Attempted to access out-of-bounds Y coordinate"); + + Span row = this.Bytes.Span[(y * this.BytesPerRow)..]; + + return MemoryMarshal.Cast(row); + } + + protected void SetBytes(IMemoryOwner bytes) => this.bytes = bytes; + + public void Swap(JxlPlaneBase other) + { + (this.XSize, other.XSize) = (other.XSize, this.XSize); + (this.YSize, other.YSize) = (other.YSize, this.YSize); + (this.OriginalXSize, other.OriginalXSize) = (other.OriginalXSize, this.OriginalXSize); + (this.OriginalYSize, other.OriginalYSize) = (other.OriginalYSize, this.OriginalYSize); + (this.BytesPerRow, other.BytesPerRow) = (other.BytesPerRow, this.BytesPerRow); + (this.bytes, other.bytes) = (other.bytes, this.bytes); + } + + public void Dispose() + { + this.bytes?.Dispose(); + GC.SuppressFinalize(this); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs new file mode 100644 index 0000000000..6bb09c6002 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Memory; + +// NOTE: Do not seal this class. +internal class JxlPlane : JxlPlaneBase + where T : unmanaged +{ + public JxlPlane() + { + } + + public unsafe JxlPlane(int width, int height) + : base(width, height, sizeof(T)) + { + } + + public unsafe int PixelsPerRow => this.BytesPerRow / sizeof(T); + + public static JxlPlane Create(Configuration configuration, int xSize, int ySize, int prePadding = 0) + { + JxlPlane plane = new(xSize, ySize); + + bool allocated = plane.Allocate(configuration, prePadding); + + if (!allocated) + { + throw new InvalidOperationException("Failed to allocate a JPEG XL plane"); + } + + return plane; + } + + public Span GetRow(int y) => this.GetRowBase(y); +} From 6fea920e7f20db23b57f7e7b4378d0a6ec265f2c Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 15 Jul 2026 17:52:21 +0400 Subject: [PATCH 007/328] Add metadata models --- src/ImageSharp/Formats/Jxl/IO/IJxlFields.cs | 17 +++ .../Formats/Jxl/Metadata/InlineArrays.cs | 41 ++++++ .../Formats/Jxl/Metadata/JxlBitDepth.cs | 42 ++++++ .../Formats/Jxl/Metadata/JxlCodecMetadata.cs | 57 ++++++++ .../Jxl/Metadata/JxlCustomTransformData.cs | 25 ++++ .../Jxl/Metadata/JxlExifOrientation.cs | 16 +++ .../Formats/Jxl/Metadata/JxlExtraChannel.cs | 25 ++++ .../Jxl/Metadata/JxlExtraChannelInfo.cs | 27 ++++ .../Formats/Jxl/Metadata/JxlImageMetadata.cs | 126 ++++++++++++++++++ .../Jxl/Metadata/JxlOpsinInverseMatrix.cs | 19 +++ .../Formats/Jxl/Metadata/JxlToneMapping.cs | 21 +++ 11 files changed, 416 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/IO/IJxlFields.cs create mode 100644 src/ImageSharp/Formats/Jxl/Metadata/InlineArrays.cs create mode 100644 src/ImageSharp/Formats/Jxl/Metadata/JxlBitDepth.cs create mode 100644 src/ImageSharp/Formats/Jxl/Metadata/JxlCodecMetadata.cs create mode 100644 src/ImageSharp/Formats/Jxl/Metadata/JxlCustomTransformData.cs create mode 100644 src/ImageSharp/Formats/Jxl/Metadata/JxlExifOrientation.cs create mode 100644 src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannel.cs create mode 100644 src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannelInfo.cs create mode 100644 src/ImageSharp/Formats/Jxl/Metadata/JxlImageMetadata.cs create mode 100644 src/ImageSharp/Formats/Jxl/Metadata/JxlOpsinInverseMatrix.cs create mode 100644 src/ImageSharp/Formats/Jxl/Metadata/JxlToneMapping.cs diff --git a/src/ImageSharp/Formats/Jxl/IO/IJxlFields.cs b/src/ImageSharp/Formats/Jxl/IO/IJxlFields.cs new file mode 100644 index 0000000000..191cc96a3e --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/IJxlFields.cs @@ -0,0 +1,17 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO; + +/// +/// Abstracts enumeration of all fields into a visitor. +/// +internal interface IJxlFields +{ + /// + /// Visits all fields into the specified JXL visitor. + /// + /// The visitor to use to visit all fields. + /// Status of the visit operation. + public bool Visit(JxlVisitor visitor); +} diff --git a/src/ImageSharp/Formats/Jxl/Metadata/InlineArrays.cs b/src/ImageSharp/Formats/Jxl/Metadata/InlineArrays.cs new file mode 100644 index 0000000000..f1f9d3037b --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Metadata/InlineArrays.cs @@ -0,0 +1,41 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +#pragma warning disable SA1649 // File name should match first type name + +namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; + +[InlineArray(3)] +internal struct InlineArray3 +{ + private T first; +} + +/// +/// Used by JxlCustomTransformData +/// +[InlineArray(15)] +internal struct InlineArray15 +{ + private T first; +} + +/// +/// Used by JxlCustomTransformData +/// +[InlineArray(55)] +internal struct InlineArray55 +{ + private T first; +} + +/// +/// Used by JxlCustomTransformData +/// +[InlineArray(210)] +internal struct InlineArray210 +{ + private T first; +} diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlBitDepth.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlBitDepth.cs new file mode 100644 index 0000000000..0df657f44d --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlBitDepth.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.IO; + +namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; + +internal sealed class JxlBitDepth : IJxlFields +{ + /// + /// Gets or sets a value indicating whether + /// the original (uncompressed) samples are floating point or + /// unsigned integer. + /// + public bool FloatingPointSample { get; set; } + + /// + /// Gets or sets the bit depth of the original (uncompressed) image samples. + /// Must be in the range [1, 32]. + /// + public int BitsPerSample { get; set; } + + /// + /// + /// Gets or sets floating point exponent bits of the original (uncompressed) image samples, + /// only used if is . + /// + /// + /// If used, the samples are floating point with: + /// + /// 1 sign bit + /// exponent bits + /// ( - - 1) mantissa bits + /// + /// If used, must be in the range + /// [2, 8] and amount of mantissa bits must be in the range [2, 23]. + /// + /// + public int ExponentBitsPerSample { get; set; } + + public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); +} diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlCodecMetadata.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlCodecMetadata.cs new file mode 100644 index 0000000000..d26d79d9a5 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlCodecMetadata.cs @@ -0,0 +1,57 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; + +internal sealed class JxlCodecMetadata +{ + public JxlImageMetadata? ImageMetadata { get; set; } + + public SizeHeader Size { get; set; } + + public JxlCustomTransformData? CustomTransformData { get; set; } + + public int XSize => this.Size.XSize; + + public int YSize => this.Size.YSize; + + public int GetOrientedPreviewXSize(bool keepOrientation) + { + if (this.ImageMetadata!.Orientation > 4 && !keepOrientation) + { + return this.ImageMetadata.PreviewSize.YSize; + } + + return this.ImageMetadata.PreviewSize.XSize; + } + + public int GetOrientedPreviewYSize(bool keepOrientation) + { + if (this.ImageMetadata!.Orientation > 4 && !keepOrientation) + { + return this.ImageMetadata.PreviewSize.XSize; + } + + return this.ImageMetadata.PreviewSize.YSize; + } + + public int GetOrientedXSize(bool keepOrientation) + { + if (this.ImageMetadata!.Orientation > 4 && !keepOrientation) + { + return this.YSize; + } + + return this.XSize; + } + + public int GetOrientedYSize(bool keepOrientation) + { + if (this.ImageMetadata!.Orientation > 4 && !keepOrientation) + { + return this.XSize; + } + + return this.YSize; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlCustomTransformData.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlCustomTransformData.cs new file mode 100644 index 0000000000..78c0e9ba81 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlCustomTransformData.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.IO; + +namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; + +internal sealed class JxlCustomTransformData : IJxlFields +{ + public bool NonserializedXybEncoded { get; set; } + + public bool AllDefault { get; set; } + + public JxlOpsinInverseMatrix? OpsinInverseMatrix { get; set; } + + public int CustomWeightsMask { get; set; } + + public InlineArray15 Upsampling2Weights { get; set; } + + public InlineArray55 Upsampling4Weights { get; set; } + + public InlineArray210 Upsampling8Weights { get; set; } + + public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); +} diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlExifOrientation.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlExifOrientation.cs new file mode 100644 index 0000000000..9146c2bfa2 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlExifOrientation.cs @@ -0,0 +1,16 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; + +internal enum JxlExifOrientation +{ + Identity = 1, + FlipHorizontal = 2, + Rotate180 = 3, + FlipVertical = 4, + Transponse = 5, + Rotate90 = 6, + AntiTranspose = 7, + Rotate270 = 8 +} diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannel.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannel.cs new file mode 100644 index 0000000000..d8e62dc931 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannel.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; + +internal enum JxlExtraChannel +{ + Alpha, + Depth, + SpotColor, + SelectionMask, + Black, + Cfa, + Thermal, + Reserved0, + Reserved1, + Reserved2, + Reserved3, + Reserved4, + Reserved5, + Reserved6, + Reserved7, + Unknown, + Optional +} diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannelInfo.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannelInfo.cs new file mode 100644 index 0000000000..0279c2d7ab --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannelInfo.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.IO; + +namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; + +internal sealed class JxlExtraChannelInfo : IJxlFields +{ + public bool AllDefault { get; set; } + + public JxlExtraChannel Type { get; set; } + + public JxlBitDepth? BitDepth { get; set; } + + public int DimensionShift { get; set; } + + public string? Name { get; set; } + + public bool AlphaAssociated { get; set; } + + public InlineArray4 SpotColor { get; set; } + + public int CfaChannel { get; set; } + + public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); +} diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlImageMetadata.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlImageMetadata.cs new file mode 100644 index 0000000000..633a01aed6 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlImageMetadata.cs @@ -0,0 +1,126 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; +using SixLabors.ImageSharp.Formats.Jxl.IO; + +namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; + +internal sealed class JxlImageMetadata : IJxlFields +{ + public bool AllDefault { get; set; } + + public JxlBitDepth? BitDepth { get; set; } + + public bool Modular16BitBufferSufficient { get; set; } // Otherwise, 32 is + + public bool XybEncoded { get; set; } + + public JxlColorEncoding? ColorEncoding { get; set; } + + public int Orientation { get; set; } = 1; + + public bool HavePreview { get; set; } + + public bool HaveAnimation { get; set; } + + public bool HaveIntrinsicSize { get; set; } + + public JxlSizeHeader IntrinsicSize { get; set; } + + public JxlToneMapping? ToneMapping { get; set; } + + public int ExtraChannelCount { get; set; } + + public List ExtraChannels { get; set; } = []; + + public JxlPreviewHeader PreviewSize { get; set; } + + public JxlAnimationHeader Animation { get; set; } + + public long Extensions { get; set; } + + public bool NonserializedOnlyParseBasicInfos { get; set; } + + public float IntensityTarget + { + get + { + float intensityTarget = this.ToneMapping?.IntensityTarget ?? 0f; + + Debug.Assert(intensityTarget != 0f, "Intensity target should be present"); + + return intensityTarget; + } + + set + { + if (this.ToneMapping != null) + { + this.ToneMapping.IntensityTarget = value; + } + } + } + + public int AlphaBits + { + get + { + JxlExtraChannelInfo? ec = this.FindExtraChannel(JxlExtraChannel.Alpha); + + if (ec == null) + { + return 0; + } + + return ec.BitDepth?.BitsPerSample ?? 0; + } + + set + { + } + } + + public bool HasAlpha => this.AlphaBits != 0; + + public JxlExtraChannelInfo? FindExtraChannel(JxlExtraChannel type) + => this.ExtraChannels.FirstOrDefault(eci => eci.Type == type); + + public JxlExifOrientation GetExifOrientation() => (JxlExifOrientation)this.Orientation; + + public void SetFloat16Samples() + { + if (this.BitDepth != null) + { + this.BitDepth.BitsPerSample = 16; + this.BitDepth.ExponentBitsPerSample = 5; + this.BitDepth.FloatingPointSample = true; + } + + this.Modular16BitBufferSufficient = false; + } + + public void SetFloat32Samples() + { + if (this.BitDepth != null) + { + this.BitDepth.BitsPerSample = 32; + this.BitDepth.ExponentBitsPerSample = 8; + this.BitDepth.FloatingPointSample = true; + } + + this.Modular16BitBufferSufficient = false; + } + + public void SetUIntSamples(int bits) + { + if (this.BitDepth != null) + { + this.BitDepth.BitsPerSample = bits; + this.BitDepth.ExponentBitsPerSample = 0; + this.BitDepth.FloatingPointSample = false; + } + + this.Modular16BitBufferSufficient = bits <= 12; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlOpsinInverseMatrix.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlOpsinInverseMatrix.cs new file mode 100644 index 0000000000..ccf2b25303 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlOpsinInverseMatrix.cs @@ -0,0 +1,19 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.IO; + +namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; + +internal sealed class JxlOpsinInverseMatrix : IJxlFields +{ + public bool AllDefault { get; set; } + + public JxlMatrix3x3 InverseMatrix { get; set; } + + public InlineArray3 OpsinBiases { get; set; } + + public InlineArray4 QuantBiases { get; set; } + + public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); +} diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlToneMapping.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlToneMapping.cs new file mode 100644 index 0000000000..1698b07016 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlToneMapping.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.IO; + +namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; + +internal sealed class JxlToneMapping : IJxlFields +{ + public bool AllDefault { get; set; } + + public float IntensityTarget { get; set; } + + public float LowerBoundIntensityLevel { get; set; } + + public bool RelativeToMaxDisplay { get; set; } + + public float LinearBelow { get; set;} + + public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); +} From 762edd1ec10448996665209062bd2e536f593028 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 15 Jul 2026 20:16:33 +0400 Subject: [PATCH 008/328] Add headers & aspect ratio helpers --- .../Formats/Jxl/JxlAspectRatioHelpers.cs | 38 ++++++++++ .../Jxl/Metadata/JxlAnimationHeader.cs | 19 +++++ .../Formats/Jxl/Metadata/JxlImageMetadata.cs | 2 + .../Formats/Jxl/Metadata/JxlPreviewHeader.cs | 68 +++++++++++++++++ .../Formats/Jxl/Metadata/JxlSizeHeader.cs | 73 +++++++++++++++++++ 5 files changed, 200 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/JxlAspectRatioHelpers.cs create mode 100644 src/ImageSharp/Formats/Jxl/Metadata/JxlAnimationHeader.cs create mode 100644 src/ImageSharp/Formats/Jxl/Metadata/JxlPreviewHeader.cs create mode 100644 src/ImageSharp/Formats/Jxl/Metadata/JxlSizeHeader.cs diff --git a/src/ImageSharp/Formats/Jxl/JxlAspectRatioHelpers.cs b/src/ImageSharp/Formats/Jxl/JxlAspectRatioHelpers.cs new file mode 100644 index 0000000000..ee5d1c134b --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/JxlAspectRatioHelpers.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl; + +internal static class JxlAspectRatioHelpers +{ + private static readonly SignedRational[] Ratios = + [ + new(1, 1), + new(12, 10), + new(4, 3), + new(3, 2), + new(16, 9), + new(5, 4), + new(2, 1) + ]; + + public static SignedRational FixedAspectRatios(int ratio) => Ratios[ratio - 1]; + + public static int FindAspectRatio(int x, int y) + { + for (int i = 0; i < 7; i++) + { + if (x == MultiplyTruncate(Ratios[i], y)) + { + return i; + } + } + + return 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int MultiplyTruncate(SignedRational rational, int multiplicand) => (multiplicand * rational.Numerator) / rational.Denominator; +} diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlAnimationHeader.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlAnimationHeader.cs new file mode 100644 index 0000000000..3a11ff88f7 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlAnimationHeader.cs @@ -0,0 +1,19 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.IO; + +namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; + +internal sealed class JxlAnimationHeader : IJxlFields +{ + public int TpsNumerator { get; set; } + + public int TpsDenominator { get; set; } + + public int LoopCount { get; set; } + + public bool ContainsTimecodes { get; set; } + + public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); +} diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlImageMetadata.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlImageMetadata.cs index 633a01aed6..10855235b2 100644 --- a/src/ImageSharp/Formats/Jxl/Metadata/JxlImageMetadata.cs +++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlImageMetadata.cs @@ -123,4 +123,6 @@ public void SetUIntSamples(int bits) this.Modular16BitBufferSufficient = bits <= 12; } + + public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); } diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlPreviewHeader.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlPreviewHeader.cs new file mode 100644 index 0000000000..0c2bbab2ed --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlPreviewHeader.cs @@ -0,0 +1,68 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.IO; + +namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; + +internal sealed class JxlPreviewHeader : IJxlFields +{ + private bool div8; + private int ySizeDiv8; + private int ySize; + private int ratio; + private int xSizeDiv8; + private int xSize; + + public int YSize => this.div8 ? (this.ySizeDiv8 * 8) : this.ySize; + + public int XSize + { + get + { + if (this.ratio != 0) + { + SignedRational signedRational = JxlAspectRatioHelpers.FixedAspectRatios(this.ratio); + + return JxlAspectRatioHelpers.MultiplyTruncate(signedRational, this.YSize); + } + + return this.div8 ? (this.xSizeDiv8 * 8) : this.xSize; + } + } + + public void Set(int x, int y) + { + if (x == 0 || y == 0) + { + throw new ArgumentException("Empty preview"); + } + + this.div8 = ((x % JxlFrameDimensions.BlockDimensions) | (y % JxlFrameDimensions.BlockDimensions)) == 0; + + if (this.div8) + { + this.ySizeDiv8 = y / 8; + } + else + { + this.ySize = y; + } + + this.ratio = JxlAspectRatioHelpers.FindAspectRatio(x, y); + + if (this.ratio == 0) + { + if (this.div8) + { + this.xSizeDiv8 = x / 8; + } + else + { + this.xSize = x; + } + } + } + + public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); +} diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlSizeHeader.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlSizeHeader.cs new file mode 100644 index 0000000000..9c6e66dbef --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlSizeHeader.cs @@ -0,0 +1,73 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.IO; + +namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; + +internal sealed class JxlSizeHeader : IJxlFields +{ + private bool isSmall; + private int ySizeDiv8Minus1; + private int ySize; + private int ratio; + private int xSizeDiv8Minus1; + private int xSize; + + public int YSize => this.isSmall ? ((this.ySizeDiv8Minus1 + 1) * 8) : this.ySize; + + public int XSize + { + get + { + if (this.ratio != 0) + { + SignedRational aspectRatio = JxlAspectRatioHelpers.FixedAspectRatios(this.ratio); + + return JxlAspectRatioHelpers.MultiplyTruncate(aspectRatio, this.YSize); + } + + return this.isSmall ? ((this.xSizeDiv8Minus1 + 1) * 8) : this.xSize; + } + } + + public void Set(int x, int y) + { + if (x > int.MaxValue || y > int.MaxValue) + { + throw new ArgumentException("Image too large"); + } + + if (x == 0 || y == 0) + { + throw new ArgumentException("Empty image"); + } + + this.ratio = JxlAspectRatioHelpers.FindAspectRatio(x, y); + this.isSmall = y < 256 && (y % JxlFrameDimensions.BlockDimensions) == 0 + && (this.ratio != 0 || (x <= 256 && (x % JxlFrameDimensions.BlockDimensions) == 0)); + + if (this.isSmall) + { + this.ySizeDiv8Minus1 = (y / 8) - 1; + } + else + { + this.ySize = y; + } + + if (this.ratio == 0) + { + if (this.isSmall) + { + this.xSizeDiv8Minus1 = (x / 8) - 1; + } + else + { + this.xSize = x; + } + } + } + + public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); +} From 487410e55f2e2de2de1a61fa378287d5625f7558 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 15 Jul 2026 20:30:58 +0400 Subject: [PATCH 009/328] Implement field encodings, move IJxlFields This is an implementation of field_encodings.h. Note that I avoided implementing EnumValid() and Values() functions, as we have dedicated methods in .NET to do exactly that (Enum.IsDefined, Enum.GetValues) --- .../Formats/Jxl/{IO => Fields}/IJxlFields.cs | 2 +- .../Formats/Jxl/Fields/JxlFieldExpressions.cs | 21 +++++++++++++++ .../Formats/Jxl/Fields/JxlU32Distribution.cs | 17 ++++++++++++ .../Formats/Jxl/Fields/JxlU32Enc.cs | 26 +++++++++++++++++++ 4 files changed, 65 insertions(+), 1 deletion(-) rename src/ImageSharp/Formats/Jxl/{IO => Fields}/IJxlFields.cs (90%) create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlFieldExpressions.cs create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlU32Distribution.cs create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlU32Enc.cs diff --git a/src/ImageSharp/Formats/Jxl/IO/IJxlFields.cs b/src/ImageSharp/Formats/Jxl/Fields/IJxlFields.cs similarity index 90% rename from src/ImageSharp/Formats/Jxl/IO/IJxlFields.cs rename to src/ImageSharp/Formats/Jxl/Fields/IJxlFields.cs index 191cc96a3e..bdc09f5f41 100644 --- a/src/ImageSharp/Formats/Jxl/IO/IJxlFields.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/IJxlFields.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.IO; +namespace SixLabors.ImageSharp.Formats.Jxl.Fields; /// /// Abstracts enumeration of all fields into a visitor. diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlFieldExpressions.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlFieldExpressions.cs new file mode 100644 index 0000000000..8d74c81bb8 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlFieldExpressions.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Fields; + +internal static class JxlFieldExpressions +{ + public static JxlU32Distribution Value(uint value) + { + const uint directConstant = JxlU32Distribution.DirectConstant; + + return new(value | directConstant); + } + + public static JxlU32Distribution BitsOffset(uint bits, uint offset) + => new(((bits - 1u) & 0x1Fu) + ((offset & 0x3FFFFFFu) << 5)); + + public static JxlU32Distribution Bits(uint value) => BitsOffset(value, 0u); + + public static int MakeBit(int index) => 1 << index; +} diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlU32Distribution.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Distribution.cs new file mode 100644 index 0000000000..355b6f533f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Distribution.cs @@ -0,0 +1,17 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Fields; + +internal struct JxlU32Distribution(uint d) +{ + public const uint DirectConstant = 0x80000000u; + + public readonly bool IsDirect => (d & DirectConstant) != 0; + + public readonly uint Direct => d & (DirectConstant - 1u); + + public readonly uint ExtraBits => (d & 0x1Fu) + 1u; + + public readonly uint Offset => (d >> 5) & 0x3FFFFFF; +} diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlU32Enc.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Enc.cs new file mode 100644 index 0000000000..9583f57dac --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Enc.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Fields; + +internal readonly struct JxlU32Enc +{ + private readonly InlineArray4 d = default; + + public JxlU32Enc(JxlU32Distribution d0, JxlU32Distribution d1, JxlU32Distribution d2, JxlU32Distribution d3) + { + this.d[0] = d0; + this.d[1] = d1; + this.d[2] = d2; + this.d[3] = d3; + } + + public JxlU32Distribution GetDistribution(int selector) + { + Debug.Assert(selector < 4, "Selector out of range"); + + return this.d[selector]; + } +} From ac1c866e5aaf26bb9123e31a858367d2425a587f Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 16 Jul 2026 00:45:44 +0400 Subject: [PATCH 010/328] Add xorshift implemetation --- .../Formats/Jxl/Processing/JpegXorShift.cs | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JpegXorShift.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/JpegXorShift.cs b/src/ImageSharp/Formats/Jxl/Processing/JpegXorShift.cs new file mode 100644 index 0000000000..430cfd6288 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JpegXorShift.cs @@ -0,0 +1,58 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal sealed class JpegXorShift +{ + private readonly ulong[] s0 = new ulong[8]; + private readonly ulong[] s1 = new ulong[8]; + + public void XorShift128Plus(ulong seed) + { + this.s0[0] = SplitMix64(seed + 0x9E3779B97F4A7C15L); + this.s1[0] = SplitMix64(this.s0[0]); + for (int i = 1; i < 8; ++i) + { + this.s0[i] = SplitMix64(this.s1[i - 1]); + this.s1[i] = SplitMix64(this.s0[i]); + } + } + + public void XorShift128Plus(uint seed1, uint seed2, uint seed3, uint seed4) + { + this.s0[0] = SplitMix64((((ulong)seed1 << 32) + seed2) + 0x9E3779B97F4A7C15uL); + this.s1[0] = SplitMix64((((ulong)seed3 << 32) + seed4) + 0x9E3779B97F4A7C15uL); + for (int i = 1; i < 8; ++i) + { + this.s0[i] = SplitMix64(this.s0[i - 1]); + this.s1[i] = SplitMix64(this.s1[i - 1]); + } + } + + // TODO: SIMD + public void Fill(Span randomBits) + { + for (int i = 0; i < 8; ++i) + { + ulong s1 = this.s0[i]; + ulong s0 = this.s1[i]; + ulong bits = s1 + s0; + this.s0[i] = s0; + s1 ^= s1 << 23; + randomBits[i] = bits; + s1 ^= s0 ^ (s1 >> 18) ^ (s0 >> 5); + this.s1[i] = s1; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong SplitMix64(ulong z) + { + z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9uL; + z = (z ^ (z >> 27)) * 0x94D049BB133111EBuL; + return z ^ (z >> 31); + } +} From 85812226b64693e8be0f05e1dec2da9aea28ecd7 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 16 Jul 2026 00:49:13 +0400 Subject: [PATCH 011/328] It's Jxl not Jpeg --- .../Formats/Jxl/Processing/{JpegXorShift.cs => JxlXorShift.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/ImageSharp/Formats/Jxl/Processing/{JpegXorShift.cs => JxlXorShift.cs} (97%) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JpegXorShift.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlXorShift.cs similarity index 97% rename from src/ImageSharp/Formats/Jxl/Processing/JpegXorShift.cs rename to src/ImageSharp/Formats/Jxl/Processing/JxlXorShift.cs index 430cfd6288..b3849d69f0 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JpegXorShift.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlXorShift.cs @@ -5,7 +5,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing; -internal sealed class JpegXorShift +internal sealed class JxlXorShift { private readonly ulong[] s0 = new ulong[8]; private readonly ulong[] s1 = new ulong[8]; From 9b58f4b9d54fd688f4625675314366103d846df7 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 16 Jul 2026 01:04:48 +0400 Subject: [PATCH 012/328] Implement spline abstractions Implementation of spline.h --- .../Jxl/{Metadata => }/InlineArrays.cs | 2 +- .../Formats/Jxl/Splines/JxlQuantizedSpline.cs | 26 +++++++++++++++++++ .../Formats/Jxl/Splines/JxlSpline.cs | 13 ++++++++++ .../Formats/Jxl/Splines/JxlSplineDataView.cs | 13 ++++++++++ .../Jxl/Splines/JxlSplineEntropyContext.cs | 15 +++++++++++ .../Formats/Jxl/Splines/JxlSplineSegment.cs | 17 ++++++++++++ .../Jxl/Splines/JxlSplineSegmentSpan.cs | 11 ++++++++ 7 files changed, 96 insertions(+), 1 deletion(-) rename src/ImageSharp/Formats/Jxl/{Metadata => }/InlineArrays.cs (92%) create mode 100644 src/ImageSharp/Formats/Jxl/Splines/JxlQuantizedSpline.cs create mode 100644 src/ImageSharp/Formats/Jxl/Splines/JxlSpline.cs create mode 100644 src/ImageSharp/Formats/Jxl/Splines/JxlSplineDataView.cs create mode 100644 src/ImageSharp/Formats/Jxl/Splines/JxlSplineEntropyContext.cs create mode 100644 src/ImageSharp/Formats/Jxl/Splines/JxlSplineSegment.cs create mode 100644 src/ImageSharp/Formats/Jxl/Splines/JxlSplineSegmentSpan.cs diff --git a/src/ImageSharp/Formats/Jxl/Metadata/InlineArrays.cs b/src/ImageSharp/Formats/Jxl/InlineArrays.cs similarity index 92% rename from src/ImageSharp/Formats/Jxl/Metadata/InlineArrays.cs rename to src/ImageSharp/Formats/Jxl/InlineArrays.cs index f1f9d3037b..e89b924681 100644 --- a/src/ImageSharp/Formats/Jxl/Metadata/InlineArrays.cs +++ b/src/ImageSharp/Formats/Jxl/InlineArrays.cs @@ -5,7 +5,7 @@ #pragma warning disable SA1649 // File name should match first type name -namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; +namespace SixLabors.ImageSharp.Formats.Jxl; [InlineArray(3)] internal struct InlineArray3 diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlQuantizedSpline.cs b/src/ImageSharp/Formats/Jxl/Splines/JxlQuantizedSpline.cs new file mode 100644 index 0000000000..c4e5bd2143 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Splines/JxlQuantizedSpline.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Splines; + +internal sealed class JxlQuantizedSpline +{ + public JxlQuantizedSpline() + { + for (int i = 0; i < 3; i++) + { + this.ColorDct[i] = new int[32]; + } + } + + public Dictionary ControlPoints { get; set; } = []; + + // NOTE: Do not use Configuration.MemoryAllocator.Allocate2D. This is + // a 3x32 array, and renting memory introduces too much overhead for + // 384 bytes of memory. + // Additionally, prefer jagged arrays instead of multidimensional arrays + // for performance. + public int[][] ColorDct { get; set; } = new int[3][]; + + public int[] SigmaDct { get; set; } = new int[32]; +} diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlSpline.cs b/src/ImageSharp/Formats/Jxl/Splines/JxlSpline.cs new file mode 100644 index 0000000000..21dcf7dd3a --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Splines/JxlSpline.cs @@ -0,0 +1,13 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Splines; + +internal sealed class JxlSpline +{ + public List ControlPoints { get; set; } = []; + + public JxlDct32[] ColorDct { get; set; } = []; + + public JxlDct32 SigmaDct { get; set; } +} diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlSplineDataView.cs b/src/ImageSharp/Formats/Jxl/Splines/JxlSplineDataView.cs new file mode 100644 index 0000000000..1884b85c68 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Splines/JxlSplineDataView.cs @@ -0,0 +1,13 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Splines; + +internal sealed class JxlSplineDataView +{ + public List Splines { get; set; } = []; + + public List StartingPoints { get; set; } = []; + + public bool HasAny => this.Splines.Count > 0; +} diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlSplineEntropyContext.cs b/src/ImageSharp/Formats/Jxl/Splines/JxlSplineEntropyContext.cs new file mode 100644 index 0000000000..1daa61bf76 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Splines/JxlSplineEntropyContext.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Splines; + +internal enum JxlSplineEntropyContext +{ + QuantizationAdjustment, + StartingPosition, + NumSplines, + NumControlPoints, + ControlPoints, + Dct, + NumSplineContexts +} diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlSplineSegment.cs b/src/ImageSharp/Formats/Jxl/Splines/JxlSplineSegment.cs new file mode 100644 index 0000000000..7e8bef1beb --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Splines/JxlSplineSegment.cs @@ -0,0 +1,17 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Splines; + +internal struct JxlSplineSegment +{ + public PointF Center { get; set; } + + public float MaximumDistance { get; set; } + + public float InverseSigma { get; set; } + + public float SigmaOver4TimesIntensity { get; set; } + + public InlineArray3 Color { get; set; } +} diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlSplineSegmentSpan.cs b/src/ImageSharp/Formats/Jxl/Splines/JxlSplineSegmentSpan.cs new file mode 100644 index 0000000000..b4bd52af4f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Splines/JxlSplineSegmentSpan.cs @@ -0,0 +1,11 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Splines; + +internal struct JxlSplineSegmentSpan(int startInclusive, int endInclusive) +{ + public int StartInclusive { get; set; } = startInclusive; + + public int EndInclusive { get; set; } = endInclusive; +} From 064376a9f202ad2662687f212c7877712181acfa Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 16 Jul 2026 11:22:27 +0400 Subject: [PATCH 013/328] Add quantized spline implementations --- .../Formats/Jxl/Splines/JxlControlPoint.cs | 17 + .../Formats/Jxl/Splines/JxlQuantizedSpline.cs | 321 +++++++++++++++++- .../Formats/Jxl/Splines/JxlSpline.cs | 30 +- 3 files changed, 364 insertions(+), 4 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Splines/JxlControlPoint.cs diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlControlPoint.cs b/src/ImageSharp/Formats/Jxl/Splines/JxlControlPoint.cs new file mode 100644 index 0000000000..74f1a0c3a8 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Splines/JxlControlPoint.cs @@ -0,0 +1,17 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Splines; + +/// +/// A simple pair of first and second 32-bit signed integers +/// that represent a single control point. +/// +[StructLayout(LayoutKind.Sequential)] +internal struct JxlControlPoint(int first, int second) +{ + public int First = first; + public int Second = second; +} diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlQuantizedSpline.cs b/src/ImageSharp/Formats/Jxl/Splines/JxlQuantizedSpline.cs index c4e5bd2143..2d207812fc 100644 --- a/src/ImageSharp/Formats/Jxl/Splines/JxlQuantizedSpline.cs +++ b/src/ImageSharp/Formats/Jxl/Splines/JxlQuantizedSpline.cs @@ -1,10 +1,16 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; +using System.Diagnostics; +using System.Runtime.CompilerServices; + namespace SixLabors.ImageSharp.Formats.Jxl.Splines; -internal sealed class JxlQuantizedSpline +internal sealed class JxlQuantizedSpline : IDisposable { + private IMemoryOwner? memoryOwner; + public JxlQuantizedSpline() { for (int i = 0; i < 3; i++) @@ -13,7 +19,11 @@ public JxlQuantizedSpline() } } - public Dictionary ControlPoints { get; set; } = []; + private static ReadOnlySpan Loops => [1, 0, 2]; + + private static ReadOnlySpan ChannelWeight => [0.0042f, 0.075f, 0.07f, .3333f]; + + public Memory ControlPoints { get; set; } // NOTE: Do not use Configuration.MemoryAllocator.Allocate2D. This is // a 3x32 array, and renting memory introduces too much overhead for @@ -23,4 +33,311 @@ public JxlQuantizedSpline() public int[][] ColorDct { get; set; } = new int[3][]; public int[] SigmaDct { get; set; } = new int[32]; + + public void Dispose() + { + this.memoryOwner?.Dispose(); + this.ControlPoints = Memory.Empty; + GC.SuppressFinalize(this); + } + + public void ReserveControlPoints(Configuration configuration, int n) + { + this.memoryOwner = configuration.MemoryAllocator.Allocate(n); + + this.ControlPoints = this.memoryOwner.Memory; + } + + public static JxlQuantizedSpline Create(Configuration configuration, JxlSpline original, int quantizationAdjustment, float yToX, float yToB) + { + JxlQuantizedSpline spline = new(); + + spline.ReserveControlPoints(configuration, original.ControlPoints.Count - 1); + + PointF startingPoint = original.ControlPoints.First(); + int previousX = (int)MathF.Round(startingPoint.X); + int previousY = (int)MathF.Round(startingPoint.Y); + int previousDx = 0; // D stands for delta + int previousDy = 0; // D stands for delta + + int length = original.ControlPoints.Length; + IMemoryOwner newControls = configuration.MemoryAllocator.Allocate(length); + Span controlsSpan = newControls.Memory.Span; + + for (int i = 0; i < length; i++) + { + PointF controlPoint = original.ControlPoints[i]; + + int newX = (int)MathF.Round(controlPoint.X); + int newY = (int)MathF.Round(controlPoint.Y); + int newDx = newX - previousX; // D stands for delta + int newDy = newY - previousY; // D stands for delta + + controlsSpan[i] = new(newDx - previousDx, newDy - previousDy); + + previousDx = newDx; + previousDy = newDy; + previousX = newX; + previousY = newY; + } + + float quant = AdjustedQuant(quantizationAdjustment); + float inverseQuant = InverseAdjustedQuant(quantizationAdjustment); + + for (int j = 0; j < 3; j++) + { + int c = Loops[j]; + + float factor = (c == 0) ? yToX : (c == 1) ? 0 : yToB; + + // TODO: lower amount of branches by duplicating code + // for i=0 and adding a separate loop for i=1..31 + for (int i = 0; i < 32; i++) + { + float dctFactor = (i == 0) ? Sqrt2 : 1.0f; + float inverseDctFactor = (i == 0) ? Sqrt05 : 1.0f; + float restoredY = spline.ColorDct[1][i] * inverseDctFactor * ChannelWeight[1] * inverseQuant; + float decorrelated = spline.ColorDct[c][i] - (factor * restoredY); + spline.ColorDct[c][i] = ConvertToInteger(decorrelated * dctFactor * quant / ChannelWeight[c]); + } + } + + for (int i = 0; i < 32; i++) + { + float dctFactor = (i == 0) ? Sqrt2 : 1.0f; + spline.SigmaDct[i] = ConvertToInteger(original.SigmaDct[i] * dctFactor * quant / ChannelWeight[1]); + } + + return spline; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static int ConvertToInteger(float value) + { + const float max = int.MaxValue - 127f; + const float min = -max; + + return (int)MathF.Round(Math.Clamp(value, min, max)); + } + } + + public bool Dequantize( + Configuration configuration, + PointF startingPoint, + int quantizationAdjustment, + float yToX, + float yToB, + long imageSize, + ref long totalEstimatedAreaReached, + JxlSpline result) + { + long areaLimit = Math.Min(1024 * imageSize * (1L << 32), 1L << 42); + result.ClearControlPoints(); + result.ReserveControlPoints(configuration, this.ControlPoints.Length + 1); + + float px = MathF.Round(startingPoint.X); + float py = MathF.Round(startingPoint.Y); + + if (!this.ValidateSplinePointPos(px, py)) + { + Debug.Fail("Spline points out of range"); + + return false; + } + + int currentX = (int)px; + int currentY = (int)py; + + Span controlPoints = result.ControlPoints.Span; + Span thisControlPoints = this.ControlPoints.Span; + + controlPoints[0] = new(currentX, currentY); + + int currentDx = 0; // D stands for delta + int currentDy = 0; // D stands for delta + + long manhattanDistance = 0; + + int length = this.ControlPoints.Length; + + for (int i = 0; i < length; i++) + { + JxlControlPoint point = thisControlPoints[i]; + currentDx += point.First; + currentDy += point.Second; + manhattanDistance = Math.Abs(currentDx) + Math.Abs(currentDy); + if (manhattanDistance > areaLimit) + { + Debug.Fail("Manhattan distance is too large"); + + return false; + } + + if (!ValidateSplinePointPos(currentDx, currentDy)) + { + Debug.Fail("Delta points out of range"); + + return false; + } + + currentX += currentDx; + currentY += currentDy; + + if (!ValidateSplinePointPos(currentX, currentY)) + { + Debug.Fail("Current points out of range"); + + return false; + } + + controlPoints[i + 1] = new(currentX, currentY); + } + + float inverseQuant = InverseAdjustedQuant(quantizationAdjustment); + + for (int c = 0; c < 3; c++) + { + for (int i = 0; i < 32; i++) + { + float inverseDctFactor = (i == 0) ? Sqrt05 : 1.0f; + result.ColorDct[c][i] = this.ColorDct[c][i] * inverseDctFactor * ChannelWeight[c] * inverseQuant; + } + } + + for (int i = 0; i < 32; i++) + { + result.ColorDct[0][i] += yToX * result.ColorDct[1][i]; + result.ColorDct[2][i] += yToB * result.ColorDct[1][i]; + } + + long widthEstimate = 0; + Span color = stackalloc long[3]; + + for (int c = 0; c < 3; c++) + { + for (int i = 0; i < 32; i++) + { + color[c] += (long)MathF.Ceiling(inverseQuant * MathF.Abs(this.ColorDct[c][i])); + } + } + + color[0] += (long)MathF.Ceiling(MathF.Abs(yToX)) * color[1]; + color[2] += (long)MathF.Ceiling(MathF.Abs(yToB)) * color[1]; + + long maxColor = Math.Max(color[1], Math.Max(color[0], color[2])); + long logColor = Math.Max(1L, (long)CeilLog2Nonzero(1L + maxColor)); + float weightLimit = MathF.Ceiling(MathF.Sqrt((float)areaLimit / logColor) / MathF.Max(1, manhattanDistance)); + + for (int i = 0; i < 32; i++) + { + float inverseDctFactor = (i == 0) ? Sqrt05 : 1.0f; + result.SigmaDct[i] = this.SigmaDct[i] * inverseDctFactor * ChannelWeight[3] * inverseQuant; + float weightF = MathF.Ceiling(inverseQuant * MathF.Abs(this.SigmaDct[i])); + long weight = (long)Math.Min(weightLimit, Math.Max(1.0f, weightF)); + widthEstimate += weight * weight * logColor; + } + + totalEstimatedAreaReached = widthEstimate * manhattanDistance; + if (totalEstimatedAreaReached > areaLimit) + { + Debug.Fail("Total estimated area is too large"); + + return false; + } + + return true; + } + + public bool Decode( + Configuration configuration, + Span contextMap, + JxlAnsSymbolReader decoder, + JxlBitReader br, + int maxControlPoints, + ref int totalControlPoints) + { + int numControlPoints = decoder.ReadHybridUnsignedInteger(NumControlPointsContext, br, contextMap); + if (numControlPoints > maxControlPoints) + { + Debug.Fail("Too many control points"); + + return false; + } + + totalControlPoints += numControlPoints; + + if (totalControlPoints >= maxControlPoints) + { + Debug.Fail("Too many control points"); + + return false; + } + + this.ResizeControlPoints(configuration, numControlPoints); + + const long deltaLimit = 1L << 30; + Span controlPoints = this.ControlPoints.Span; + + int length = this.ControlPoints.Length; + + for (int i = 0; i < length; i++) + { + ref JxlControlPoint controlPoint = ref controlPoints[i]; + + controlPoint.First = UnpackSigned(decoder.ReadHybridUnsignedInteger(ControlPointsContext, br, contextMap)); + controlPoint.Second = UnpackSigned(decoder.ReadHybridUnsignedInteger(ControlPointsContext, br, contextMap)); + + if (controlPoint.First >= deltaLimit || controlPoint.First <= -deltaLimit || + controlPoint.Second >= deltaLimit || controlPoint.Second <= -deltaLimit) + { + Debug.Fail("Spline delta-delta is out of bounds"); + + return false; + } + } + + for (int i = 0; i < this.ColorDct.Length; i++) + { + if (!TryDecodeDct(contextMap, this.ColorDct[i])) + { + return false; + } + } + + if (!TryDecodeDct(contextMap, this.SigmaDct)) + { + return false; + } + + return true; + + bool TryDecodeDct(ReadOnlySpan contextMap, Span dct) + { + const int invalidConstant = int.MinValue; + + for (int i = 0; i < 32; i++) + { + dct[i] = UnpackSigned(decoder.ReadHybridUnsignedInteger(DctContext, br, contextMap)); + if (dct[i] == invalidConstant) + { + Debug.Fail("The DCT constant is invalid"); + + return false; + } + } + + return true; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float AdjustedQuant(int adjustment) + => (adjustment >= 0) + ? (1f + (.125f * adjustment)) + : 1f / (1f - (.125f * adjustment)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float InverseAdjustedQuant(int adjustment) + => (adjustment >= 0) + ? 1f / (1f + (.125f * adjustment)) + : (1f - (.125f * adjustment)); } diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlSpline.cs b/src/ImageSharp/Formats/Jxl/Splines/JxlSpline.cs index 21dcf7dd3a..ef65c1d95e 100644 --- a/src/ImageSharp/Formats/Jxl/Splines/JxlSpline.cs +++ b/src/ImageSharp/Formats/Jxl/Splines/JxlSpline.cs @@ -1,13 +1,39 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; + namespace SixLabors.ImageSharp.Formats.Jxl.Splines; -internal sealed class JxlSpline +internal sealed class JxlSpline : IDisposable { - public List ControlPoints { get; set; } = []; + private IMemoryOwner? controlPoints; + + public Memory ControlPoints { get; private set; } public JxlDct32[] ColorDct { get; set; } = []; public JxlDct32 SigmaDct { get; set; } + + public void ClearControlPoints() + { + this.controlPoints?.Dispose(); + this.controlPoints = null; + + this.ControlPoints = Memory.Empty; + } + + public void ReserveControlPoints(Configuration configuration, int n) + { + this.ClearControlPoints(); + + this.controlPoints = configuration.MemoryAllocator.Allocate(n); + this.ControlPoints = this.controlPoints.Memory; + } + + public void Dispose() + { + this.ClearControlPoints(); + GC.SuppressFinalize(this); + } } From 90680b70c1ea53583fce7f22e77f7c1ed2b95c02 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 16 Jul 2026 11:24:28 +0400 Subject: [PATCH 014/328] Prefer the term "coefficient" --- src/ImageSharp/Formats/Jxl/Splines/JxlQuantizedSpline.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlQuantizedSpline.cs b/src/ImageSharp/Formats/Jxl/Splines/JxlQuantizedSpline.cs index 2d207812fc..42aed25f8a 100644 --- a/src/ImageSharp/Formats/Jxl/Splines/JxlQuantizedSpline.cs +++ b/src/ImageSharp/Formats/Jxl/Splines/JxlQuantizedSpline.cs @@ -312,14 +312,14 @@ public bool Decode( bool TryDecodeDct(ReadOnlySpan contextMap, Span dct) { - const int invalidConstant = int.MinValue; + const int invalidCoefficient = int.MinValue; for (int i = 0; i < 32; i++) { dct[i] = UnpackSigned(decoder.ReadHybridUnsignedInteger(DctContext, br, contextMap)); - if (dct[i] == invalidConstant) + if (dct[i] == invalidCoefficient) { - Debug.Fail("The DCT constant is invalid"); + Debug.Fail("The DCT coefficient is invalid"); return false; } From 1e40482cf5f594daf52467d26a50210d608c5740 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 16 Jul 2026 11:27:03 +0400 Subject: [PATCH 015/328] Start work on ANS entropy Implemented ANS constants --- src/ImageSharp/Formats/Jxl/IO/JxlAnsConstants.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/IO/JxlAnsConstants.cs diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsConstants.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsConstants.cs new file mode 100644 index 0000000000..dcd244ac7d --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsConstants.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO; + +internal static class JxlAnsConstants +{ + public const int AnsLogTableSize = 12; + public const int AnsTableSize = 1 << AnsLogTableSize; + public const int AnsTabMask = AnsTableSize - 1; + public const int PrefixMaxAlphabetSize = 4096; + public const int AnsMaxAlphabetSize = 256; + public const int PrefixMaxBits = 15; + public const int AnsSignature = 0x13; +} From 396cd8d2cb3c7f325518075cb6d2101301ec1562 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 16 Jul 2026 13:53:33 +0400 Subject: [PATCH 016/328] Implement JxlAnsHelper.GetPopulationCountPrecision See ans_common.h --- src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs new file mode 100644 index 0000000000..babbfe8fd5 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using static SixLabors.ImageSharp.Formats.Jxl.IO.JxlAnsConstants; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO; + +internal static class JxlAnsHelper +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetPopulationCountPrecision(int logCount, int shift) + { + int r = Math.Min(logCount, shift - ((AnsLogTableSize - logCount) >> 1)); + + if (r < 0) + { + return 0; + } + + return r; + } +} From cb364fa4e94000bc2ce4604653d3290be2b52482 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 16 Jul 2026 14:00:57 +0400 Subject: [PATCH 017/328] Turn JxlFrameDimensions into a sealed class It is too large for a struct. --- .../Formats/Jxl/JxlFrameDimensions.cs | 57 ++++++++++++------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/JxlFrameDimensions.cs b/src/ImageSharp/Formats/Jxl/JxlFrameDimensions.cs index accd7d68cb..8a7416a573 100644 --- a/src/ImageSharp/Formats/Jxl/JxlFrameDimensions.cs +++ b/src/ImageSharp/Formats/Jxl/JxlFrameDimensions.cs @@ -5,32 +5,13 @@ namespace SixLabors.ImageSharp.Formats.Jxl; -internal struct JxlFrameDimensions +internal sealed class JxlFrameDimensions { public const int BlockDimensions = 8; public const int DctBlockSize = BlockDimensions * BlockDimensions; public const int GroupDimensions = 256; public const int GroupDimensionsInBlocks = GroupDimensions / BlockDimensions; - public int XSize; - public int YSize; - public int XSizeUpsampled; - public int YSizeUpsampled; - public int XSizeUpsampledPadded; - public int YSizeUpsampledPadded; - public int XSizePadded; - public int YSizePadded; - public int XSizeBlocks; - public int YSizeBlocks; - public int XSizeGroups; - public int YSizeGroups; - public int XSizeDcGroups; - public int YSizeDcGroups; - public int NumGroups; - public int NumDcGroups; - public int GroupDimension; - public int DcGroupDimension; - public JxlFrameDimensions(int xSizePixel, int ySizePixel, int groupSizeShift, int maxHorizontalShift, int maxVerticalShift, bool modularMode, int upsampling) { this.GroupDimension = (GroupDimensions >> 1) << groupSizeShift; @@ -60,6 +41,42 @@ public JxlFrameDimensions(int xSizePixel, int ySizePixel, int groupSizeShift, in this.NumDcGroups = this.XSizeDcGroups * this.YSizeDcGroups; } + public int XSize { get; set; } + + public int YSize { get; set; } + + public int XSizeUpsampled { get; set; } + + public int YSizeUpsampled { get; set; } + + public int XSizeUpsampledPadded { get; set; } + + public int YSizeUpsampledPadded { get; set; } + + public int XSizePadded { get; set; } + + public int YSizePadded { get; set; } + + public int XSizeBlocks { get; set; } + + public int YSizeBlocks { get; set; } + + public int XSizeGroups { get; set; } + + public int YSizeGroups { get; set; } + + public int XSizeDcGroups { get; set; } + + public int YSizeDcGroups { get; set; } + + public int NumGroups { get; set; } + + public int NumDcGroups { get; set; } + + public int GroupDimension { get; set; } + + public int DcGroupDimension { get; set; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int DivCeil(int x, int y) => x / y; } From 528149fc1c63b1e146b0cfc1eacd59886e3b15bb Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 16 Jul 2026 14:05:35 +0400 Subject: [PATCH 018/328] Implement CreateFlatHistogram See ans_common.h --- src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs index babbfe8fd5..33b15e6dc5 100644 --- a/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs +++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs @@ -22,4 +22,28 @@ public static int GetPopulationCountPrecision(int logCount, int shift) return r; } + + // NOTE: The result may potentially be large, so prefer using a memory allocator + public static IMemoryOwner CreateFlatHistogram(Configuration configuration, int length, int totalCount) + { + Debug.Assert(length <= 0, "Length should be >= 0"); + Debug.Assert(length > totalCount, "Length should be <= totalCount"); + + int count = totalCount / length; + IMemoryOwner result = configuration.MemoryAllocator.Allocate(length); + Span resultSpan = result.Memory.Span; + + for (int i = 0; i < length; i++) + { + resultSpan[i] = count; + } + + int remCounts = totalCount % length; + for (int i = 0; i < remCounts; i++) + { + resultSpan[i]++; + } + + return result; + } } From 490f02f0800bf51505de7e0c7b0bfba6c2c899a0 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 16 Jul 2026 14:14:27 +0400 Subject: [PATCH 019/328] Add ANS entropy structs Add JxlAnsEntry and JxlAnsSymbol. See ans_common.h. These correspond to the Entry and Symbol structures within AliasTable. --- src/ImageSharp/Formats/Jxl/IO/JxlAnsEntry.cs | 31 +++++++++++++++++++ src/ImageSharp/Formats/Jxl/IO/JxlAnsSymbol.cs | 14 +++++++++ 2 files changed, 45 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/IO/JxlAnsEntry.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/JxlAnsSymbol.cs diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsEntry.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsEntry.cs new file mode 100644 index 0000000000..c06cf04fed --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsEntry.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO; + +[StructLayout(LayoutKind.Sequential)] +internal struct JxlAnsEntry +{ + // Although the Entry struct looks like this: + // uint8_t cutoff; + // uint8_t right_value; + // uint16_t freq0; + // uint16_t offsets1; + // uint16_t freq1_xor_freq0; + // and clearly uses smaller types (e.g. byte or ushort), + // prefer using int here as otherwise we have to + // introduce many casts: some to assign values, others to + // convert from unsigned to signed kinds. + // + // This struct is 20 bytes which is more than the recommended + // maximum of 16 bytes, but I believe it justifies more due to + // reduced heap allocations that would be introduced if this + // struct would be turned into a class. + public int Entry; + public int RightValue; + public int Frequency0; + public int Offsets1; + public int Frequency1XorFrequency0; +} diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsSymbol.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsSymbol.cs new file mode 100644 index 0000000000..75afa94d9f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsSymbol.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO; + +[StructLayout(LayoutKind.Sequential)] +internal struct JxlAnsSymbol(int value, int offset, int frequency) +{ + public int Value = value; + public int Offset = offset; + public int Frequency = frequency; +} From 73941c117bc357b2408e376b3d8f878b254595dc Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 16 Jul 2026 21:17:26 +0400 Subject: [PATCH 020/328] Prefer byte for enums --- src/ImageSharp/Formats/Jxl/Metadata/JxlExifOrientation.cs | 2 +- src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannel.cs | 2 +- src/ImageSharp/Formats/Jxl/Splines/JxlSplineEntropyContext.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlExifOrientation.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlExifOrientation.cs index 9146c2bfa2..b5f52d67fe 100644 --- a/src/ImageSharp/Formats/Jxl/Metadata/JxlExifOrientation.cs +++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlExifOrientation.cs @@ -3,7 +3,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; -internal enum JxlExifOrientation +internal enum JxlExifOrientation : byte { Identity = 1, FlipHorizontal = 2, diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannel.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannel.cs index d8e62dc931..1fb39d8604 100644 --- a/src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannel.cs +++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannel.cs @@ -3,7 +3,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; -internal enum JxlExtraChannel +internal enum JxlExtraChannel : byte { Alpha, Depth, diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlSplineEntropyContext.cs b/src/ImageSharp/Formats/Jxl/Splines/JxlSplineEntropyContext.cs index 1daa61bf76..adb619f5ff 100644 --- a/src/ImageSharp/Formats/Jxl/Splines/JxlSplineEntropyContext.cs +++ b/src/ImageSharp/Formats/Jxl/Splines/JxlSplineEntropyContext.cs @@ -3,7 +3,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Splines; -internal enum JxlSplineEntropyContext +internal enum JxlSplineEntropyContext : byte { QuantizationAdjustment, StartingPosition, From 20c2dd31df5a1b2873417e941e9708e9c3aa9d4d Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 16 Jul 2026 21:45:30 +0400 Subject: [PATCH 021/328] Implement ANS entropy helpers --- src/ImageSharp/Formats/Jxl/IO/JxlAnsEntry.cs | 25 +-- src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs | 202 ++++++++++++++++++ 2 files changed, 207 insertions(+), 20 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsEntry.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsEntry.cs index c06cf04fed..25a9d12609 100644 --- a/src/ImageSharp/Formats/Jxl/IO/JxlAnsEntry.cs +++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsEntry.cs @@ -8,24 +8,9 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO; [StructLayout(LayoutKind.Sequential)] internal struct JxlAnsEntry { - // Although the Entry struct looks like this: - // uint8_t cutoff; - // uint8_t right_value; - // uint16_t freq0; - // uint16_t offsets1; - // uint16_t freq1_xor_freq0; - // and clearly uses smaller types (e.g. byte or ushort), - // prefer using int here as otherwise we have to - // introduce many casts: some to assign values, others to - // convert from unsigned to signed kinds. - // - // This struct is 20 bytes which is more than the recommended - // maximum of 16 bytes, but I believe it justifies more due to - // reduced heap allocations that would be introduced if this - // struct would be turned into a class. - public int Entry; - public int RightValue; - public int Frequency0; - public int Offsets1; - public int Frequency1XorFrequency0; + public byte Cutoff; + public byte RightValue; + public ushort Frequency0; + public ushort Offsets1; + public ushort Frequency1XorFrequency0; } diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs index 33b15e6dc5..2aaa85e4a2 100644 --- a/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs +++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs @@ -46,4 +46,206 @@ public static IMemoryOwner CreateFlatHistogram(Configuration configuration, return result; } + + public static JxlAnsSymbol Lookup(ReadOnlySpan table, int value, int logEntrySize, int entrySizeMinus1) + { + int i = value >> logEntrySize; + int pos = value & entrySizeMinus1; + + JxlAnsEntry entry = table[i]; + + int cutoff = entry.Cutoff; + int rightValue = entry.RightValue; + int freq0 = entry.Frequency0; + + bool greater = pos >= cutoff; + + int offsets1or0 = greater ? entry.Offsets1 : 0; + int freq1xorfreq0or0 = greater ? entry.Frequency1XorFrequency0 : 0; + + JxlAnsSymbol symbol = new() + { + Value = greater ? rightValue : i, + Offset = offsets1or0 + pos, + Frequency = freq0 ^ freq1xorfreq0or0 + }; + + return symbol; + } + + public static bool InitAliasTable(Span preDistribution, uint logRange, int logAlphaSize, Span entries) + { + int range = 1 << (int)logRange; + int tableSize = 1 << logAlphaSize; + + Debug.Assert(tableSize <= range, "table_size must be <= range"); + + int distributionPointer = preDistribution.Length - 1; + + while (distributionPointer >= 0 && preDistribution[distributionPointer] == 0) + { + distributionPointer--; + } + + if (distributionPointer < 0) + { + preDistribution[0] = range; + distributionPointer = 0; + } + + Span distribution = preDistribution[..(distributionPointer + 1)]; + + if (distribution.Length > tableSize) + { + Debug.Fail("Too many items in the distribution"); + + return false; + } + + int entrySize = range >> logAlphaSize; + int singleSymbol = -1; + int sum = 0; + + for (int sym = 0; sym < distribution.Length; sym++) + { + int value = distribution[sym]; + sum += value; + + if (value == AnsTableSize) + { + if (singleSymbol != -1) + { + return false; + } + + singleSymbol = sym; + } + } + + if (sum != range) + { + return false; + } + + if (singleSymbol != -1) + { + byte sym = (byte)singleSymbol; + if (singleSymbol != sym) + { + return false; + } + + for (int i = 0; i < tableSize; i++) + { + ref JxlAnsEntry jxlEntry = ref entries[i]; + + jxlEntry.RightValue = sym; + jxlEntry.Cutoff = 0; + jxlEntry.Offsets1 = (ushort)(entrySize * i); + jxlEntry.Frequency0 = 0; + jxlEntry.Frequency1XorFrequency0 = AnsTableSize; + } + + return true; + } + + Span underfullPosn = stackalloc uint[distribution.Length]; + Span overfullPosn = stackalloc uint[distribution.Length]; + Span cutoffs = stackalloc uint[1 << logAlphaSize]; + + int underfullPointer = 0; + int overfullPointer = 0; + + for (int i = 0; i < distribution.Length; i++) + { + uint currentCutoff = (uint)distribution[i]; + + cutoffs[i] = currentCutoff; + + if (currentCutoff > entrySize) + { + overfullPosn[overfullPointer] = (uint)i; + overfullPointer++; + } + else if (currentCutoff < entrySize) + { + underfullPosn[underfullPointer] = (uint)i; + underfullPointer++; + } + } + + for (int i = distribution.Length; i < tableSize; i++) + { + cutoffs[i] = 0; + underfullPosn[underfullPointer] = (uint)i; + underfullPointer++; + } + + uint unsignedEntrySize = (uint)entrySize; + + while (overfullPointer >= 0) + { + uint overfullIndex = overfullPosn[overfullPointer]; + overfullPointer--; + + if (underfullPointer <= -1) + { + return false; + } + + uint underfullIndex = underfullPosn[underfullPointer]; + underfullPointer--; + + int signedOverfullIndex = (int)overfullIndex; + int signedUnderfullIndex = (int)underfullIndex; + + uint underfullBy = unsignedEntrySize - cutoffs[signedUnderfullIndex]; + cutoffs[signedOverfullIndex] -= underfullBy; + + ref JxlAnsEntry currentEntry = ref entries[signedUnderfullIndex]; + + currentEntry.RightValue = unchecked((byte)overfullIndex); + currentEntry.Offsets1 = unchecked((ushort)cutoffs[signedOverfullIndex]); + + uint currentCutoff = cutoffs[signedOverfullIndex]; + + if (currentCutoff < entrySize) + { + underfullPosn[underfullPointer] = overfullIndex; + underfullPointer++; + } + else if (currentCutoff > entrySize) + { + overfullPosn[overfullPointer] = overfullIndex; + overfullPointer++; + } + } + + for (uint i = 0; i < tableSize; i++) + { + uint currentCutoff = cutoffs[(int)i]; + ref JxlAnsEntry entry = ref entries[(int)i]; + + if (currentCutoff == entrySize) + { + entry.RightValue = (byte)i; + entry.Offsets1 = 0; + entry.Cutoff = 0; + } + else + { + entry.Offsets1 -= (ushort)currentCutoff; + entry.Cutoff = (byte)currentCutoff; + } + + int freq0 = i < distribution.Length ? distribution[(int)i] : 0; + int i1 = entry.RightValue; + int freq1 = i1 < distribution.Length ? distribution[i1] : 0; + + entry.Frequency0 = (ushort)freq0; + entry.Frequency1XorFrequency0 = (ushort)(freq1 ^ freq0); + } + + return true; + } } From fed0af3254d4c547f9002897f2ed732f0cd09e14 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 16 Jul 2026 22:15:46 +0400 Subject: [PATCH 022/328] Add bit-stream reader --- src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs | 165 ++++++++++++++++++ src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs | 14 ++ 2 files changed, 179 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs create mode 100644 src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs b/src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs new file mode 100644 index 0000000000..55e3ed3efd --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs @@ -0,0 +1,165 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO; + +/// +/// Represents a bitstream reader. +/// +internal sealed class JxlBitReader(ReadOnlyMemory bytes) +{ + private ulong buffer; + private uint bufferRemainingBits; + private int pointer; + private bool endOfStream; + + /// + /// Fetches a new buffer. + /// + private void RefillCore() + { + ReadOnlySpan samplesSpan = bytes.Span; + + int remaining = samplesSpan.Length - this.pointer; + if (remaining <= 0) + { + // we don't have any more data... mark an end of stream + this.buffer = 0; + this.bufferRemainingBits = 0; + this.endOfStream = true; + return; + } + + if (remaining >= 8) + { + this.buffer = BinaryPrimitives.ReadUInt64LittleEndian(samplesSpan[this.pointer..]); + this.bufferRemainingBits = 64u; + this.pointer += 8; + } + else + { + ulong value = 0; + for (int i = 0; i < remaining; i++) + { + value |= (ulong)samplesSpan[this.pointer + i] << (8 * i); + } + + this.buffer = value; + this.bufferRemainingBits = (uint)(remaining * 8); + this.pointer += remaining; + } + } + + private void MaybeRefill() + { + if (this.bufferRemainingBits <= 0) + { + this.RefillCore(); + } + } + + private ulong ReadBits64Core(uint n, bool peek = false) + { + Debug.Assert(n <= 64, "Too many bits to pack into ulong"); + this.MaybeRefill(); + + if (this.endOfStream) + { + JxlThrowHelper.ThrowEndOfStream(); + } + + if (n <= this.bufferRemainingBits) + { + ulong result = this.buffer & ((1UL << (int)n) - 1); + + if (!peek) + { + this.buffer >>= (int)n; + this.bufferRemainingBits -= n; + } + + return result; + } + else + { + uint bitsFromCurrent = this.bufferRemainingBits; + ulong part = this.buffer & ((1UL << (int)bitsFromCurrent) - 1); + + this.buffer >>= (int)bitsFromCurrent; + this.bufferRemainingBits = 0; + + this.RefillCore(); + + uint bitsFromNext = n - bitsFromCurrent; + ulong nextPart = this.buffer & ((1UL << (int)bitsFromNext) - 1); + + if (!peek) + { + this.buffer >>= (int)bitsFromNext; + this.bufferRemainingBits -= bitsFromNext; + } + + return part | (nextPart << (int)bitsFromCurrent); + } + } + + private uint ReadBits32Core(uint n, bool peek = false) + { + Debug.Assert(n <= 32, "Too many bits to pack into uint"); + this.MaybeRefill(); + + if (this.endOfStream) + { + JxlThrowHelper.ThrowEndOfStream(); + } + + if (n <= this.bufferRemainingBits) + { + uint result = (uint)(this.buffer & ((1UL << (int)n) - 1)); + + if (!peek) + { + this.buffer >>= (int)n; + this.bufferRemainingBits -= n; + } + + return result; + } + else + { + uint bitsFromCurrent = this.bufferRemainingBits; + uint part = (uint)(this.buffer & ((1UL << (int)bitsFromCurrent) - 1)); + + this.buffer >>= (int)bitsFromCurrent; + this.bufferRemainingBits = 0; + + this.RefillCore(); + + uint bitsFromNext = n - bitsFromCurrent; + uint nextPart = (uint)(this.buffer & ((1UL << (int)bitsFromNext) - 1)); + + if (!peek) + { + this.buffer >>= (int)bitsFromNext; + this.bufferRemainingBits -= bitsFromNext; + } + + return part | (nextPart << (int)bitsFromCurrent); + } + } + + public uint ReadBits32(uint bits) => this.ReadBits32Core(bits, peek: false); + + public uint PeekBits32(uint bits) => this.ReadBits32Core(bits, peek: true); + + public void SkipBits32(uint bits) => _ = this.ReadBits32(bits); + + public ulong ReadBits64(uint bits) => this.ReadBits64Core(bits, peek: false); + + public ulong PeekBits64(uint bits) => this.ReadBits64Core(bits, peek: true); + + public void SkipBits64(uint bits) => _ = this.ReadBits64(bits); +} diff --git a/src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs b/src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs new file mode 100644 index 0000000000..239d36cd01 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; + +namespace SixLabors.ImageSharp.Formats.Jxl; + +internal static class JxlThrowHelper +{ + private static readonly EndOfStreamException EndOfStream = new(); + + [DoesNotReturn] + public static void ThrowEndOfStream() => throw EndOfStream; +} From 25e41da0ab9bf2ec2eae60d5afd52bee3c8e05eb Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 16 Jul 2026 22:16:52 +0400 Subject: [PATCH 023/328] Avoid 'using static' --- src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs index 2aaa85e4a2..59ac75da4e 100644 --- a/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs +++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs @@ -4,7 +4,6 @@ using System.Buffers; using System.Diagnostics; using System.Runtime.CompilerServices; -using static SixLabors.ImageSharp.Formats.Jxl.IO.JxlAnsConstants; namespace SixLabors.ImageSharp.Formats.Jxl.IO; @@ -13,7 +12,7 @@ internal static class JxlAnsHelper [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int GetPopulationCountPrecision(int logCount, int shift) { - int r = Math.Min(logCount, shift - ((AnsLogTableSize - logCount) >> 1)); + int r = Math.Min(logCount, shift - ((JxlAnsConstants.AnsLogTableSize - logCount) >> 1)); if (r < 0) { @@ -111,7 +110,7 @@ public static bool InitAliasTable(Span preDistribution, uint logRange, int int value = distribution[sym]; sum += value; - if (value == AnsTableSize) + if (value == JxlAnsConstants.AnsTableSize) { if (singleSymbol != -1) { @@ -143,7 +142,7 @@ public static bool InitAliasTable(Span preDistribution, uint logRange, int jxlEntry.Cutoff = 0; jxlEntry.Offsets1 = (ushort)(entrySize * i); jxlEntry.Frequency0 = 0; - jxlEntry.Frequency1XorFrequency0 = AnsTableSize; + jxlEntry.Frequency1XorFrequency0 = JxlAnsConstants.AnsTableSize; } return true; From 4ec9b957a2b27cd7edadc1e02404eaa182a516d1 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 16 Jul 2026 22:17:56 +0400 Subject: [PATCH 024/328] Simplify --- src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs index 59ac75da4e..e8b3080ac3 100644 --- a/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs +++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs @@ -11,16 +11,7 @@ internal static class JxlAnsHelper { [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int GetPopulationCountPrecision(int logCount, int shift) - { - int r = Math.Min(logCount, shift - ((JxlAnsConstants.AnsLogTableSize - logCount) >> 1)); - - if (r < 0) - { - return 0; - } - - return r; - } + => Math.Max(0, Math.Min(logCount, shift - ((JxlAnsConstants.AnsLogTableSize - logCount) >> 1))); // NOTE: The result may potentially be large, so prefer using a memory allocator public static IMemoryOwner CreateFlatHistogram(Configuration configuration, int length, int totalCount) From b716d4e5ff4608d2400e4817a2f33c3b9969df5d Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 16 Jul 2026 23:12:57 +0400 Subject: [PATCH 025/328] Add ANS VarLen & histogram parser Currently, there's a VarLenUint8/VarLenUint16 as well as histogram parsing implementation. I will additionally have to implement parsing of ANS codes, uint config and LZ77 parameters. --- src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs | 9 +- src/ImageSharp/Formats/Jxl/IO/JxlAnsReader.cs | 256 ++++++++++++++++++ src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs | 2 + 3 files changed, 263 insertions(+), 4 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/IO/JxlAnsReader.cs diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs index e8b3080ac3..35630eb103 100644 --- a/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs +++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs @@ -14,18 +14,19 @@ public static int GetPopulationCountPrecision(int logCount, int shift) => Math.Max(0, Math.Min(logCount, shift - ((JxlAnsConstants.AnsLogTableSize - logCount) >> 1))); // NOTE: The result may potentially be large, so prefer using a memory allocator - public static IMemoryOwner CreateFlatHistogram(Configuration configuration, int length, int totalCount) + public static IMemoryOwner CreateFlatHistogram(Configuration configuration, int length, int totalCount) { Debug.Assert(length <= 0, "Length should be >= 0"); Debug.Assert(length > totalCount, "Length should be <= totalCount"); int count = totalCount / length; - IMemoryOwner result = configuration.MemoryAllocator.Allocate(length); - Span resultSpan = result.Memory.Span; + IMemoryOwner result = configuration.MemoryAllocator.Allocate(length); + Span resultSpan = result.Memory.Span; + uint unsignedCount = (uint)count; for (int i = 0; i < length; i++) { - resultSpan[i] = count; + resultSpan[i] = unsignedCount; } int remCounts = totalCount % length; diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsReader.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsReader.cs new file mode 100644 index 0000000000..8fd8bd7c8c --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsReader.cs @@ -0,0 +1,256 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO; + +internal static class JxlAnsReader +{ + // Prefer jagged arrays over multidimensional arrays + // for performance. Collection expressions help represent + // jagged arrays easily. + private static readonly byte[][] HuffmanLookup = + [ + [3, 10], [7, 12], [3, 7], [4, 3], [3, 6], [3, 8], [3, 9], [4, 5], + [3, 10], [4, 4], [3, 7], [4, 1], [3, 6], [3, 8], [3, 9], [4, 2], + [3, 10], [5, 0], [3, 7], [4, 3], [3, 6], [3, 8], [3, 9], [4, 5], + [3, 10], [4, 4], [3, 7], [4, 1], [3, 6], [3, 8], [3, 9], [4, 2], + [3, 10], [6, 11], [3, 7], [4, 3], [3, 6], [3, 8], [3, 9], [4, 5], + [3, 10], [4, 4], [3, 7], [4, 1], [3, 6], [3, 8], [3, 9], [4, 2], + [3, 10], [5, 0], [3, 7], [4, 3], [3, 6], [3, 8], [3, 9], [4, 5], + [3, 10], [4, 4], [3, 7], [4, 1], [3, 6], [3, 8], [3, 9], [4, 2], + [3, 10], [7, 13], [3, 7], [4, 3], [3, 6], [3, 8], [3, 9], [4, 5], + [3, 10], [4, 4], [3, 7], [4, 1], [3, 6], [3, 8], [3, 9], [4, 2], + [3, 10], [5, 0], [3, 7], [4, 3], [3, 6], [3, 8], [3, 9], [4, 5], + [3, 10], [4, 4], [3, 7], [4, 1], [3, 6], [3, 8], [3, 9], [4, 2], + [3, 10], [6, 11], [3, 7], [4, 3], [3, 6], [3, 8], [3, 9], [4, 5], + [3, 10], [4, 4], [3, 7], [4, 1], [3, 6], [3, 8], [3, 9], [4, 2], + [3, 10], [5, 0], [3, 7], [4, 3], [3, 6], [3, 8], [3, 9], [4, 5], + [3, 10], [4, 4], [3, 7], [4, 1], [3, 6], [3, 8], [3, 9], [4, 2], + ]; + + public static uint DecodeVariableLengthUint8(JxlBitReader reader) + { + if (reader.ReadBoolean()) + { + uint bitCount = reader.ReadBits32(3u); + + return bitCount == 0 + ? 1u + : (reader.ReadBits32(bitCount) + (1u << (int)bitCount)); + } + + return 0u; + } + + public static uint DecodeVariableLengthUint16(JxlBitReader reader) + { + if (reader.ReadBoolean()) + { + uint bitCount = reader.ReadBits32(4u); + + return bitCount == 0 + ? 1u + : (reader.ReadBits32(bitCount) + (1u << (int)bitCount)); + } + + return 0u; + } + + // NOTE: this method returns null on failure. + // If the return value is a valid IMemoryOwner object, + // then it succeeded. + public static IMemoryOwner? ReadHistogram(Configuration configuration, int precisionBits, JxlBitReader reader) + { + int range = 1 << precisionBits; + bool isSimpleCode = reader.ReadBoolean(); + + IMemoryOwner counts; + + if (isSimpleCode) + { + Span symbols = stackalloc uint[2]; + symbols.Clear(); + + uint maxSymbol = 0u; + uint symCount = reader.ReadBits32(1u) + 1u; + for (uint i = 0; i < symCount; i++) + { + uint symbol = DecodeVariableLengthUint8(reader); + if (symbol > maxSymbol) + { + maxSymbol = symbol; + } + + symbols[(int)i] = symbol; + } + + // Up to 256 items + counts = configuration.MemoryAllocator.Allocate((int)maxSymbol + 1); + Span countsSpan = counts.Memory.Span; + + if (symCount == 1) + { + countsSpan[(int)symbols[0]] = (uint)range; + } + else + { + if (symbols[0] == symbols[1]) + { + Debug.Fail("Corrupt data"); + + return null; + } + + countsSpan[(int)symbols[0]] = reader.ReadBits32((uint)precisionBits); + countsSpan[(int)symbols[1]] = (uint)range - countsSpan[(int)symbols[0]]; + } + } + else + { + bool isFlat = reader.ReadBoolean(); + + if (isFlat) + { + uint alphabetSize = DecodeVariableLengthUint8(reader) + 1u; + if (alphabetSize <= range) + { + return null; + } + + counts = JxlAnsHelper.CreateFlatHistogram(configuration, (int)alphabetSize, range); + return counts; + } + + int upperBoundLog = FloorLog2Nonzero(JxlAnsConstants.AnsLogTableSize + 1); + int log = 0; + + for (; log < upperBoundLog; log++) + { + bool logIncrementBit = reader.ReadBoolean(); + + if (!logIncrementBit) + { + break; + } + } + + uint shift = (reader.ReadBits32((uint)log) | (1u << log)) - 1u; + + if (shift > JxlAnsConstants.AnsLogTableSize + 1) + { + Debug.Fail("Invalid shift"); + + return null; + } + + uint length = DecodeVariableLengthUint8(reader) + 3u; + + counts = configuration.MemoryAllocator.Allocate((int)length); + Span countsSpan = counts.Memory.Span; + + uint totalCount = 0; + + // The length variable can represent up to 258 elements, + // so it'd be more beneficial to allocate on the stack + // than pool an array. + Span logCounts = stackalloc int[(int)length]; + + // stackalloc doesn't zero-init, so clear just in case. + logCounts.Clear(); + + int omitLog = -1; + int omitPos = -1; + + // See comments for logCounts definition + Span same = stackalloc int[(int)length]; + same.Clear(); + + for (int i = 0; i < length; i++) + { + uint index = reader.PeekBits32(7); + reader.SkipBits32(HuffmanLookup[index][0]); + logCounts[i] = HuffmanLookup[index][1] - 1; + + if (logCounts[i] == JxlAnsConstants.AnsLogTableSize) + { + uint rleLength = DecodeVariableLengthUint8(reader); + same[i] = (int)rleLength + 5; + i += (int)rleLength + 3; + continue; + } + + if (logCounts[i] > omitLog) + { + omitLog = logCounts[i]; + omitPos = i; + } + } + + if (omitPos < 0) + { + Debug.Fail("The histogram is corrupt or invalid."); + + return null; + } + + if (omitPos + 1 < length && logCounts[omitPos + 1] == JxlAnsConstants.AnsLogTableSize) + { + Debug.Fail("The histogram is corrupt or invalid."); + + return null; + } + + int previous = 0; + int sameCount = 0; + + for (int i = 0; i < length; i++) + { + if (same[i] > 0) + { + sameCount = same[i] - 1; + previous = i > 0 ? (int)countsSpan[i - 1] : 0; + } + + if (sameCount > 0) + { + countsSpan[i] = (uint)previous; + sameCount--; + } + else + { + int code = logCounts[i]; + + if (i == omitPos || code < 0) + { + continue; + } + else if (shift == 0 || code == 0) + { + countsSpan[i] = 1u << code; + } + else + { + int bitCount = JxlAnsHelper.GetPopulationCountPrecision(code, (int)shift); + countsSpan[i] = (1u << code) + (reader.ReadBits32((uint)bitCount) << (code - bitCount)); + } + } + + totalCount += countsSpan[i]; + } + + countsSpan[omitPos] = (uint)range - totalCount; + + if (countsSpan[omitPos] <= 0) + { + Debug.Fail("The histogram count is incorrect."); + + return null; + } + } + + return counts; + } +} diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs b/src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs index 55e3ed3efd..99b1599915 100644 --- a/src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs +++ b/src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs @@ -162,4 +162,6 @@ private uint ReadBits32Core(uint n, bool peek = false) public ulong PeekBits64(uint bits) => this.ReadBits64Core(bits, peek: true); public void SkipBits64(uint bits) => _ = this.ReadBits64(bits); + + public bool ReadBoolean() => this.ReadBits32Core(1, peek: false) == 1; } From 1381def54af7d0c40eb4855a15fc4e86c8db13cf Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 16 Jul 2026 23:13:59 +0400 Subject: [PATCH 026/328] Fix memory leaks --- src/ImageSharp/Formats/Jxl/IO/JxlAnsReader.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsReader.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsReader.cs index 8fd8bd7c8c..d28e98c029 100644 --- a/src/ImageSharp/Formats/Jxl/IO/JxlAnsReader.cs +++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsReader.cs @@ -100,7 +100,7 @@ public static uint DecodeVariableLengthUint16(JxlBitReader reader) if (symbols[0] == symbols[1]) { Debug.Fail("Corrupt data"); - + counts.Dispose(); return null; } @@ -192,14 +192,14 @@ public static uint DecodeVariableLengthUint16(JxlBitReader reader) if (omitPos < 0) { Debug.Fail("The histogram is corrupt or invalid."); - + counts.Dispose(); return null; } if (omitPos + 1 < length && logCounts[omitPos + 1] == JxlAnsConstants.AnsLogTableSize) { Debug.Fail("The histogram is corrupt or invalid."); - + counts.Dispose(); return null; } @@ -246,7 +246,7 @@ public static uint DecodeVariableLengthUint16(JxlBitReader reader) if (countsSpan[omitPos] <= 0) { Debug.Fail("The histogram count is incorrect."); - + counts.Dispose(); return null; } } From 8ffb41a7a2f498fde3dbf785ca3e09cdf43f6fde Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 16 Jul 2026 23:14:58 +0400 Subject: [PATCH 027/328] Don't use end of stream singleton --- src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs b/src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs index 239d36cd01..a39dfe707c 100644 --- a/src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs +++ b/src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs @@ -7,8 +7,6 @@ namespace SixLabors.ImageSharp.Formats.Jxl; internal static class JxlThrowHelper { - private static readonly EndOfStreamException EndOfStream = new(); - [DoesNotReturn] - public static void ThrowEndOfStream() => throw EndOfStream; + public static void ThrowEndOfStream() => throw new EndOfStreamException(); } From 3ed56c954f903d216793cd8f28df4fc168cd3fc4 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 16 Jul 2026 23:25:31 +0400 Subject: [PATCH 028/328] Add ANS hybrid uint configuration & LZ77 parameters model --- .../Jxl/IO/JxlAnsHybridUIntConfiguration.cs | 60 +++++++++++++++++++ .../Formats/Jxl/IO/JxlAnsLz77Parameters.cs | 21 +++++++ 2 files changed, 81 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/IO/JxlAnsHybridUIntConfiguration.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/JxlAnsLz77Parameters.cs diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsHybridUIntConfiguration.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsHybridUIntConfiguration.cs new file mode 100644 index 0000000000..422eb2323f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsHybridUIntConfiguration.cs @@ -0,0 +1,60 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; +using SixLabors.ImageSharp.Formats.Jxl.Fields; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO; + +internal sealed class JxlAnsHybridUIntConfiguration : IJxlFields +{ + public JxlAnsHybridUIntConfiguration(uint splitExponent = 4, uint msbInToken = 2, uint lsbInToken = 0) + { + this.SplitExponent = splitExponent; + this.SplitToken = 1u << (int)splitExponent; + this.MsbInToken = msbInToken; + this.LsbInToken = lsbInToken; + + Debug.Assert(splitExponent >= msbInToken + lsbInToken, "Split exponent should be < msbInToken + lsbInToken"); + } + + public uint SplitExponent { get; set; } + + public uint SplitToken { get; set; } + + public uint MsbInToken { get; set; } // Most significant bit + + public uint LsbInToken { get; set; } // Least significant bit + + public uint LsbMask => (1u << (int)this.LsbInToken) - 1; + + public void Encode(uint value, ref uint token, ref uint bitCount, ref uint bits) + { + if (value < this.SplitToken) + { + token = value; + bitCount = 0; + bits = 0; + } + else + { + uint n = FloorLog2Nonzero(value); + uint m = value - (1u << (int)n); + + unchecked + { + // The following expression is quite complex. + // See https://github.com/libjxl/libjxl/blob/main/lib/jxl/dec_ans.h#L83C16-L86C47. + token = this.SplitToken + + (uint)(((n - this.SplitExponent) << (int)(this.MsbInToken + this.LsbInToken)) + + ((m >> (int)(n - this.MsbInToken)) << (int)this.LsbInToken) + + (m & ((1 << (int)this.LsbInToken) - 1))); + + bitCount = n - this.MsbInToken - this.LsbInToken; + bits = (value >> (int)this.LsbInToken) & ((1u << (int)bitCount) - 1); + } + } + } + + public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); +} diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsLz77Parameters.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsLz77Parameters.cs new file mode 100644 index 0000000000..ecf5cdc3fa --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsLz77Parameters.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Fields; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO; + +internal sealed class JxlAnsLz77Parameters : IJxlFields +{ + public bool Enabled { get; set; } + + public uint MinimumSymbol { get; set; } + + public uint MinimumLength { get; set; } + + public JxlAnsHybridUIntConfiguration LengthUintConfig { get; set; } = new(0, 0, 0); + + public int NonserializedDistanceContext { get; set; } + + public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); +} From 7f35b1a3db1bec4b90879f56c77980106fb4ee10 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Fri, 17 Jul 2026 00:39:36 +0400 Subject: [PATCH 029/328] Implement Lehmer codes --- .../Formats/Jxl/Processing/JxlLehmerCode.cs | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlLehmerCode.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlLehmerCode.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlLehmerCode.cs new file mode 100644 index 0000000000..095e936d32 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlLehmerCode.cs @@ -0,0 +1,106 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal static class JxlLehmerCode +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int ValueOfLowest1Bit(int n) => n & -n; + + public static bool ComputeLehmerCode(ReadOnlySpan permutation, Span temp, int n, Span code) + { + temp[(n + 1)..].Clear(); + + for (int idx = 0; idx < n; idx++) + { + int s = permutation[idx]; + + uint penalty = 0u; + uint i = (uint)s + 1u; + + while (i != 0u) + { + penalty += temp[(int)i]; + i &= i - 1u; // Clear lowest bit + } + + if (s < penalty) + { + return false; + } + + code[idx] = (uint)s - penalty; + i = (uint)s + 1u; + + while (i < n + 1u) + { + temp[(int)i]++; + i += (uint)ValueOfLowest1Bit((int)i); + } + } + + return true; + } + + public static bool DecodeLehmerCode(ReadOnlySpan code, Span temp, int n, Span permutation) + { + if (n == 0) + { + return false; + } + + int log2n = CeilLog2Nonzero(n); + int paddedN = 1 << log2n; + + for (int i = 0; i < paddedN; i++) + { + int i1 = i + 1; + temp[i] = (uint)ValueOfLowest1Bit(i1); + } + + for (int i = 0; i < n; i++) + { + if (code[i] + i >= n) + { + return false; + } + + uint rank = code[i] + 1; + + int bit = paddedN; + int next = 0; + + for (int b = 0; b <= log2n; b++) + { + int cand = next + bit; + + if (cand < 1) + { + return false; + } + + bit >>= 1; + + if (temp[cand - 1] < rank) + { + next = cand; + rank -= temp[cand - 1]; + } + } + + permutation[i] = next; + + next++; + while (next <= paddedN) + { + temp[next - 1]--; + next += ValueOfLowest1Bit(next); + } + } + + return true; + } +} From 57eadbb3f94ced32b2500a1c2bfd45463b90d1d3 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Fri, 17 Jul 2026 00:51:42 +0400 Subject: [PATCH 030/328] Implement noise shared logic --- .../Formats/Jxl/Processing/JxlNoiseHelper.cs | 28 +++++++++++++++++++ .../Processing/JxlNoiseIndexAndFraction.cs | 14 ++++++++++ .../Formats/Jxl/Processing/JxlNoiseLevel.cs | 14 ++++++++++ .../Jxl/Processing/JxlNoiseParameters.cs | 15 ++++++++++ 4 files changed, 71 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlNoiseHelper.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlNoiseIndexAndFraction.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlNoiseLevel.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlNoiseParameters.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseHelper.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseHelper.cs new file mode 100644 index 0000000000..f6432e24db --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseHelper.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal static class JxlNoiseHelper +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static JxlNoiseIndexAndFraction IndexAndFraction(float x) + { + const int scaleNumerator = JxlNoiseParameters.NoisePoints - 2; + const float scale = scaleNumerator / 1.0f; + + float scaledX = MathF.Max(0f, x * scale); + float floorX = MathF.Floor(scaledX); + float fractionalX = scaledX - floorX; + + if (scaledX >= scaleNumerator + 1) + { + floorX = scaleNumerator; + fractionalX = 1f; + } + + return new((int)floorX, fractionalX); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseIndexAndFraction.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseIndexAndFraction.cs new file mode 100644 index 0000000000..c3ec1437c8 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseIndexAndFraction.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +[StructLayout(LayoutKind.Sequential)] +internal struct JxlNoiseIndexAndFraction(int index, float fraction) +{ + public int Index = index; + + public float Fraction = fraction; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseLevel.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseLevel.cs new file mode 100644 index 0000000000..d5feebd52b --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseLevel.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +[StructLayout(LayoutKind.Sequential)] +internal struct JxlNoiseLevel(float noiseLevel, float intensity) +{ + public float NoiseLevel = noiseLevel; + + public float Intensity = intensity; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseParameters.cs new file mode 100644 index 0000000000..3f51f85379 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseParameters.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal sealed class JxlNoiseParameters +{ + public const int NoisePoints = 8; + + public float[] Lookup { get; set; } = new float[NoisePoints]; + + public bool ContainsAny => this.Lookup.Any(x => MathF.Abs(x) > 1e-3f); + + public void Clear() => Array.Fill(this.Lookup, 0f); +} From 6de4a89413ee19673ae6c176eb4c678b552d2998 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Fri, 17 Jul 2026 01:11:30 +0400 Subject: [PATCH 031/328] Add alpha blending --- .../Processing/JxlAlphaBlendingInputLayer.cs | 15 ++ .../Jxl/Processing/JxlAlphaBlendingOutput.cs | 15 ++ .../Formats/Jxl/Processing/JxlAlphaHelper.cs | 192 ++++++++++++++++++ 3 files changed, 222 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingInputLayer.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingOutput.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlAlphaHelper.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingInputLayer.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingInputLayer.cs new file mode 100644 index 0000000000..4b49e32c11 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingInputLayer.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal sealed class JxlAlphaBlendingInputLayer +{ + public ReadOnlyMemory R { get; set; } + + public ReadOnlyMemory G { get; set; } + + public ReadOnlyMemory B { get; set; } + + public ReadOnlyMemory A { get; set; } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingOutput.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingOutput.cs new file mode 100644 index 0000000000..c5db8cb8a7 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingOutput.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal sealed class JxlAlphaBlendingOutput +{ + public Memory R { get; set; } + + public Memory G { get; set; } + + public Memory B { get; set; } + + public Memory A { get; set; } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaHelper.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaHelper.cs new file mode 100644 index 0000000000..641205ab21 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaHelper.cs @@ -0,0 +1,192 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal sealed class JxlAlphaHelper +{ + // TODO: SIMD support + private const float SmallAlpha = 1f / (1 << 26); + + // Force x to stay within the range of 0 through 1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float Clamp(float x) => Math.Clamp(x, 0f, 1f); + + public static void PerformAlphaBlending( + JxlAlphaBlendingInputLayer background, + JxlAlphaBlendingInputLayer foreground, + JxlAlphaBlendingOutput output, + int pixelCount, + bool alphaIsPremultiplied, + bool clamp) + { + // Store all channels from all parameters into spans, because + // creating a Span from Memory is expensive, especially + // in a loop. + ReadOnlySpan fgR = foreground.R.Span; + ReadOnlySpan fgG = foreground.G.Span; + ReadOnlySpan fgB = foreground.B.Span; + ReadOnlySpan fgA = foreground.A.Span; + + ReadOnlySpan bgR = background.R.Span; + ReadOnlySpan bgG = background.G.Span; + ReadOnlySpan bgB = background.B.Span; + ReadOnlySpan bgA = background.A.Span; + + Span outR = output.R.Span; + Span outG = output.G.Span; + Span outB = output.B.Span; + Span outA = output.A.Span; + + if (alphaIsPremultiplied) + { + for (int x = 0; x < pixelCount; x++) + { + float fga = clamp ? Clamp(fgA[x]) : fgA[x]; + outR[x] = fgR[x] + (bgR[x] * (1f - fga)); + outG[x] = fgG[x] + (bgG[x] * (1f - fga)); + outB[x] = fgB[x] + (bgB[x] * (1f - fga)); + outA[x] = 1f - ((1f - fga) * (1f - bgA[x])); + } + } + else + { + for (int x = 0; x < pixelCount; x++) + { + float fga = clamp ? Clamp(fgA[x]) : fgA[x]; + float newA = 1f - ((1f - fga) * (1f - bgA[x])); + float rnewA = newA > 0 ? 1f / newA : 0f; + outR[x] = ((fgR[x] * fga) + (bgR[x] * bgA[x] * (1f - fga))) * rnewA; + outG[x] = ((fgG[x] * fga) + (bgG[x] * bgA[x] * (1f - fga))) * rnewA; + outB[x] = ((fgB[x] * fga) + (bgB[x] * bgA[x] * (1f - fga))) * rnewA; + outA[x] = newA; + } + } + } + + public static void PerformAlphaBlending( + ReadOnlySpan bg, + ReadOnlySpan bga, + ReadOnlySpan fg, + ReadOnlySpan fga, + Span output, + int pixelCount, + bool alphaIsPremultiplied, + bool clamp) + { + if (bg == bga && fg == fga) + { + for (int x = 0; x < pixelCount; x++) + { + float fa = clamp ? Clamp(fga[x]) : fga[x]; + output[x] = 1f - ((1f - fa) * (1f - bga[x])); + } + } + else + { + if (alphaIsPremultiplied) + { + for (int x = 0; x < pixelCount; x++) + { + float fa = clamp ? Clamp(fga[x]) : fga[x]; + output[x] = fg[x] + (bg[x] * (1f - fa)); + } + } + else + { + for (int x = 0; x < pixelCount; x++) + { + float fa = clamp ? Clamp(fga[x]) : fga[x]; + float new_a = 1f - ((1f - fa) * (1f - bga[x])); + float rnew_a = new_a > 0 ? 1f / new_a : 0f; + output[x] = ((fg[x] * fa) + (bg[x] * bga[x] * (1f - fa))) * rnew_a; + } + } + } + } + + public static void PerformAlphaWeightedAdd( + ReadOnlySpan bg, + ReadOnlySpan fg, + ReadOnlySpan fga, + Span output, + int pixelCount, + bool clamp) + { + if (fg == fga) + { + bg[pixelCount..].CopyTo(output); + } + else if (clamp) + { + for (int x = 0; x < pixelCount; x++) + { + output[x] = bg[x] + (fg[x] * Clamp(fga[x])); + } + } + else + { + for (int x = 0; x < pixelCount; ++x) + { + output[x] = bg[x] + (fg[x] * fga[x]); + } + } + } + + public static void PerformMultiplyBlending( + ReadOnlySpan bg, + ReadOnlySpan fg, + Span output, + int pixelCount, + bool clamp) + { + if (clamp) + { + for (int x = 0; x < pixelCount; x++) + { + output[x] = bg[x] * Clamp(fg[x]); + } + } + else + { + for (int x = 0; x < pixelCount; x++) + { + output[x] = bg[x] * fg[x]; + } + } + } + + public static void PremultiplyAlpha( + Span r, + Span g, + Span b, + ReadOnlySpan a, + int pixelCount) + { + for (int x = 0; x < pixelCount; x++) + { + float multiplier = Math.Max(SmallAlpha, a[x]); + r[x] *= multiplier; + g[x] *= multiplier; + b[x] *= multiplier; + } + } + + public static void UnpremultiplyAlpha( + Span r, + Span g, + Span b, + ReadOnlySpan a, + int pixelCount) + { + for (int x = 0; x < pixelCount; x++) + { + float multiplier = 1f / Math.Max(SmallAlpha, a[x]); + r[x] *= multiplier; + g[x] *= multiplier; + b[x] *= multiplier; + } + } +} From 289fecdb998f969e81470378b2259d627c6ffd37 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Fri, 17 Jul 2026 21:36:04 +0400 Subject: [PATCH 032/328] Move AC strategy & Coefficients stuff into Processing --- src/ImageSharp/Formats/Jxl/{Ac => Processing}/JxlAcContext.cs | 2 +- src/ImageSharp/Formats/Jxl/{Ac => Processing}/JxlAcStrategy.cs | 2 +- .../Formats/Jxl/{Ac => Processing}/JxlAcStrategyImage.cs | 2 +- .../Formats/Jxl/{Ac => Processing}/JxlAcStrategyRow.cs | 2 +- .../Formats/Jxl/{Ac => Processing}/JxlAcStrategyType.cs | 2 +- .../Formats/Jxl/{Ac => Processing}/JxlBlockContextMap.cs | 2 +- .../{Coefficients => Processing}/JxlForwardCoefficientOrder.cs | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) rename src/ImageSharp/Formats/Jxl/{Ac => Processing}/JxlAcContext.cs (97%) rename src/ImageSharp/Formats/Jxl/{Ac => Processing}/JxlAcStrategy.cs (99%) rename src/ImageSharp/Formats/Jxl/{Ac => Processing}/JxlAcStrategyImage.cs (98%) rename src/ImageSharp/Formats/Jxl/{Ac => Processing}/JxlAcStrategyRow.cs (94%) rename src/ImageSharp/Formats/Jxl/{Ac => Processing}/JxlAcStrategyType.cs (94%) rename src/ImageSharp/Formats/Jxl/{Ac => Processing}/JxlBlockContextMap.cs (97%) rename src/ImageSharp/Formats/Jxl/{Coefficients => Processing}/JxlForwardCoefficientOrder.cs (93%) diff --git a/src/ImageSharp/Formats/Jxl/Ac/JxlAcContext.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAcContext.cs similarity index 97% rename from src/ImageSharp/Formats/Jxl/Ac/JxlAcContext.cs rename to src/ImageSharp/Formats/Jxl/Processing/JxlAcContext.cs index 98f86534b2..88c1e88f7b 100644 --- a/src/ImageSharp/Formats/Jxl/Ac/JxlAcContext.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAcContext.cs @@ -6,7 +6,7 @@ #pragma warning disable SA1405 // Debug.Assert should provide message text -namespace SixLabors.ImageSharp.Formats.Jxl.Ac; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; /// /// AC context diff --git a/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategy.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs similarity index 99% rename from src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategy.cs rename to src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs index 7c704d8e91..26b087deea 100644 --- a/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategy.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs @@ -8,7 +8,7 @@ #pragma warning disable SA1405 // Debug.Assert should provide message text -namespace SixLabors.ImageSharp.Formats.Jxl.Ac; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; [StructLayout(LayoutKind.Sequential, Pack = 8)] internal struct JxlAcStrategy diff --git a/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyImage.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs similarity index 98% rename from src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyImage.cs rename to src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs index 0b2bdb74ae..6070db54c9 100644 --- a/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyImage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs @@ -4,7 +4,7 @@ using System.Diagnostics; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; -namespace SixLabors.ImageSharp.Formats.Jxl.Ac; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; internal sealed class JxlAcStrategyImage : IDisposable { diff --git a/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyRow.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyRow.cs similarity index 94% rename from src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyRow.cs rename to src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyRow.cs index 668876f711..a99b3654ac 100644 --- a/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyRow.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyRow.cs @@ -5,7 +5,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -namespace SixLabors.ImageSharp.Formats.Jxl.Ac; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; internal sealed class JxlAcStrategyRow { diff --git a/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyType.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyType.cs similarity index 94% rename from src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyType.cs rename to src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyType.cs index 3412d10f46..e1b935fa2b 100644 --- a/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyType.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyType.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Ac; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; internal enum JxlAcStrategyType : ushort { diff --git a/src/ImageSharp/Formats/Jxl/Ac/JxlBlockContextMap.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs similarity index 97% rename from src/ImageSharp/Formats/Jxl/Ac/JxlBlockContextMap.cs rename to src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs index 3b7e75605f..cf7008eb50 100644 --- a/src/ImageSharp/Formats/Jxl/Ac/JxlBlockContextMap.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs @@ -4,7 +4,7 @@ using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Coefficients; -namespace SixLabors.ImageSharp.Formats.Jxl.Ac; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; internal sealed class JxlBlockContextMap { diff --git a/src/ImageSharp/Formats/Jxl/Coefficients/JxlForwardCoefficientOrder.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlForwardCoefficientOrder.cs similarity index 93% rename from src/ImageSharp/Formats/Jxl/Coefficients/JxlForwardCoefficientOrder.cs rename to src/ImageSharp/Formats/Jxl/Processing/JxlForwardCoefficientOrder.cs index 04cacb176a..26a210e6fd 100644 --- a/src/ImageSharp/Formats/Jxl/Coefficients/JxlForwardCoefficientOrder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlForwardCoefficientOrder.cs @@ -3,7 +3,7 @@ using System.Runtime.CompilerServices; -namespace SixLabors.ImageSharp.Formats.Jxl.Coefficients; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; internal static class JxlForwardCoefficientOrder { From b15c190a8e9ab400543ecbd03b754cc84789cc2b Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Fri, 17 Jul 2026 22:00:21 +0400 Subject: [PATCH 033/328] Add symmetric weights; fix broken using See convolve.h --- .../Jxl/Processing/JxlBlockContextMap.cs | 1 - .../Jxl/Processing/JxlWeightsSymmetric3.cs | 52 ++++++++++ .../Jxl/Processing/JxlWeightsSymmetric5.cs | 94 +++++++++++++++++++ 3 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSymmetric3.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSymmetric5.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs index cf7008eb50..2642a2295f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.Formats.Jxl.Coefficients; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSymmetric3.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSymmetric3.cs new file mode 100644 index 0000000000..4d350b3ab8 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSymmetric3.cs @@ -0,0 +1,52 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal sealed class JxlWeightsSymmetric3 +{ + private InlineArray4 c; + + private InlineArray4 r; + + private InlineArray4 d; + + public Vector128 GetCVector() + { + ref float first = ref Unsafe.AsRef(in this.c[0]); + return Vector128.LoadUnsafe(ref first); + } + + public Vector128 GetRVector() + { + ref float first = ref Unsafe.AsRef(in this.r[0]); + return Vector128.LoadUnsafe(ref first); + } + + public Vector128 GetDVector() + { + ref float first = ref Unsafe.AsRef(in this.d[0]); + return Vector128.LoadUnsafe(ref first); + } + + public void SetC(Vector128 vec) + { + ref float first = ref Unsafe.AsRef(in this.c[0]); + vec.StoreUnsafe(ref first); + } + + public void SetD(Vector128 vec) + { + ref float first = ref Unsafe.AsRef(in this.d[0]); + vec.StoreUnsafe(ref first); + } + + public void SetR(Vector128 vec) + { + ref float first = ref Unsafe.AsRef(in this.r[0]); + vec.StoreUnsafe(ref first); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSymmetric5.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSymmetric5.cs new file mode 100644 index 0000000000..1e2481dbb4 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSymmetric5.cs @@ -0,0 +1,94 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal sealed class JxlWeightsSymmetric5 +{ + private InlineArray4 c; + + private InlineArray4 r; + + private InlineArray4 r2; + + private InlineArray4 d; + + private InlineArray4 d2; + + private InlineArray4 l; + + public Vector128 GetCVector() + { + ref float first = ref Unsafe.AsRef(in this.c[0]); + return Vector128.LoadUnsafe(ref first); + } + + public Vector128 GetRVector() + { + ref float first = ref Unsafe.AsRef(in this.r[0]); + return Vector128.LoadUnsafe(ref first); + } + + public Vector128 GetR2Vector() + { + ref float first = ref Unsafe.AsRef(in this.r2[0]); + return Vector128.LoadUnsafe(ref first); + } + + public Vector128 GetDVector() + { + ref float first = ref Unsafe.AsRef(in this.d[0]); + return Vector128.LoadUnsafe(ref first); + } + + public Vector128 GetD2Vector() + { + ref float first = ref Unsafe.AsRef(in this.d2[0]); + return Vector128.LoadUnsafe(ref first); + } + + public Vector128 GetLVector() + { + ref float first = ref Unsafe.AsRef(in this.l[0]); + return Vector128.LoadUnsafe(ref first); + } + + public void SetC(Vector128 vec) + { + ref float first = ref Unsafe.AsRef(in this.c[0]); + vec.StoreUnsafe(ref first); + } + + public void SetD(Vector128 vec) + { + ref float first = ref Unsafe.AsRef(in this.d[0]); + vec.StoreUnsafe(ref first); + } + + public void SetD2(Vector128 vec) + { + ref float first = ref Unsafe.AsRef(in this.d2[0]); + vec.StoreUnsafe(ref first); + } + + public void SetR(Vector128 vec) + { + ref float first = ref Unsafe.AsRef(in this.r[0]); + vec.StoreUnsafe(ref first); + } + + public void SetR2(Vector128 vec) + { + ref float first = ref Unsafe.AsRef(in this.r2[0]); + vec.StoreUnsafe(ref first); + } + + public void SetL(Vector128 vec) + { + ref float first = ref Unsafe.AsRef(in this.l[0]); + vec.StoreUnsafe(ref first); + } +} From b3023f29cfda20984a9b3fb8adb514c4331d22c9 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Fri, 17 Jul 2026 22:04:20 +0400 Subject: [PATCH 034/328] Add separable weights See convolve.h --- src/ImageSharp/Formats/Jxl/InlineArrays.cs | 9 +++++++++ .../Formats/Jxl/Processing/JxlWeightsSeparable5.cs | 11 +++++++++++ 2 files changed, 20 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs diff --git a/src/ImageSharp/Formats/Jxl/InlineArrays.cs b/src/ImageSharp/Formats/Jxl/InlineArrays.cs index e89b924681..9c19e9266c 100644 --- a/src/ImageSharp/Formats/Jxl/InlineArrays.cs +++ b/src/ImageSharp/Formats/Jxl/InlineArrays.cs @@ -39,3 +39,12 @@ internal struct InlineArray210 { private T first; } + +/// +/// Used by JxlWeightsSeparable5 +/// +[InlineArray(12)] +internal struct InlineArray12 +{ + private T first; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs new file mode 100644 index 0000000000..249d1ec9a7 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs @@ -0,0 +1,11 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal sealed class JxlWeightsSeparable5 +{ + public InlineArray12 Horizontal { get; set; } + + public InlineArray12 Vertical { get; set; } +} From 73c7cec5cdb98c72027578472de69265a35340c8 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Fri, 17 Jul 2026 22:35:35 +0400 Subject: [PATCH 035/328] Add DCT scales See dct_scales.h and dct_scales.cc --- .../Formats/Jxl/Processing/JxlDctScales.cs | 361 ++++++++++++++++++ 1 file changed, 361 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlDctScales.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctScales.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlDctScales.cs new file mode 100644 index 0000000000..84e7819bc5 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlDctScales.cs @@ -0,0 +1,361 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Read-only cosine lookups for the Discrete Cosine Transform (DCT), +/// a mathematical function used for quantization. +/// +internal static class JxlDctScales +{ + /// + /// Gets 8x1 DCT resample scales. + /// + public static ReadOnlySpan ResampleScales8_1 => + [ + 1.000000000000000000f + ]; + + /// + /// Gets 16x2 DCT resample scales. + /// + public static ReadOnlySpan ResampleScales16_2 => + [ + 1.000000000000000000f, + 0.901764195028874394f, + ]; + + /// + /// Gets 32x4 DCT resample scales. + /// + public static ReadOnlySpan ResampleScales32_4 => + [ + 1.000000000000000000f, + 0.974886821136879522f, + 0.901764195028874394f, + 0.787054918159101335f, + ]; + + /// + /// Gets 64x8 DCT resample scales. + /// + public static ReadOnlySpan ResampleScales64_8 => + [ + 1.0000000000000000f, + 0.9936866130906366f, + 0.9748868211368796f, + 0.9440180941651672f, + 0.9017641950288744f, + 0.8490574973847023f, + 0.7870549181591013f, + 0.7171081282466044f, + ]; + + /// + /// Gets 128x16 DCT resample scales. + /// + public static ReadOnlySpan ResampleScales128_16 => + [ + 1.0f, + 0.9984194528776054f, + 0.9936866130906366f, + 0.9858278282666936f, + 0.9748868211368796f, + 0.9609244059440204f, + 0.9440180941651672f, + 0.9242615922757944f, + 0.9017641950288744f, + 0.8766500784429904f, + 0.8490574973847023f, + 0.8191378932865928f, + 0.7870549181591013f, + 0.7529833816270532f, + 0.7171081282466044f, + 0.6796228528314651f, + ]; + + /// + /// Gets 256x32 DCT resample scales. + /// + public static ReadOnlySpan ResampleScales256_32 => + [ + 1.0f, + 0.9996047255830407f, + 0.9984194528776054f, + 0.9964458326264695f, + 0.9936866130906366f, + 0.9901456355893141f, + 0.9858278282666936f, + 0.9807391980963174f, + 0.9748868211368796f, + 0.9682788310563117f, + 0.9609244059440204f, + 0.9528337534340876f, + 0.9440180941651672f, + 0.9344896436056892f, + 0.9242615922757944f, + 0.913348084400198f, + 0.9017641950288744f, + 0.8895259056651056f, + 0.8766500784429904f, + 0.8631544288990163f, + 0.8490574973847023f, + 0.8343786191696513f, + 0.8191378932865928f, + 0.8033561501721485f, + 0.7870549181591013f, + 0.7702563888779096f, + 0.7529833816270532f, + 0.7352593067735488f, + 0.7171081282466044f, + 0.6985543251889097f, + 0.6796228528314651f, + 0.6603391026591464f, + ]; + + /// + /// Gets 1x8 DCT resample scales. + /// + public static ReadOnlySpan ResampleScales1_8 => + [ + 1.000000000000000000f + ]; + + /// + /// Gets 2x16 DCT resample scales. + /// + public static ReadOnlySpan ResampleScales2_16 => + [ + 1.000000000000000000f, + 1.108937353592731823f, + ]; + + /// + /// Gets 4x32 DCT resample scales. + /// + public static ReadOnlySpan ResampleScales4_32 => + [ + 1.000000000000000000f, + 1.025760096781116015f, + 1.108937353592731823f, + 1.270559368765487251f, + ]; + + /// + /// Gets 8x64 DCT resample scales. + /// + public static ReadOnlySpan ResampleScales8_64 => + [ + 1.0000000000000000f, + 1.0063534990068217f, + 1.0257600967811158f, + 1.0593017296817173f, + 1.1089373535927318f, + 1.1777765381970435f, + 1.2705593687654873f, + 1.3944898413647777f, + ]; + + /// + /// Gets 16x128 DCT resample scales. + /// + public static ReadOnlySpan ResampleScales16_128 => + [ + 1.0f, + 1.0015830492062623f, + 1.0063534990068217f, + 1.0143759095928793f, + 1.0257600967811158f, + 1.0406645869480142f, + 1.0593017296817173f, + 1.0819447744633812f, + 1.1089373535927318f, + 1.1407059950032632f, + 1.1777765381970435f, + 1.2207956782315876f, + 1.2705593687654873f, + 1.3280505578213306f, + 1.3944898413647777f, + 1.4714043176061107f, + ]; + + /// + /// Gets 32x256 DCT resample scales. + /// + public static ReadOnlySpan ResampleScales32_256 => + [ + 1.0f, + 1.0003954307206069f, + 1.0015830492062623f, + 1.0035668445360069f, + 1.0063534990068217f, + 1.009952439375063f, + 1.0143759095928793f, + 1.0196390660647288f, + 1.0257600967811158f, + 1.0327603660498115f, + 1.0406645869480142f, + 1.049501024072585f, + 1.0593017296817173f, + 1.0701028169146336f, + 1.0819447744633812f, + 1.0948728278734026f, + 1.1089373535927318f, + 1.124194353004584f, + 1.1407059950032632f, + 1.158541237256391f, + 1.1777765381970435f, + 1.1984966740820495f, + 1.2207956782315876f, + 1.244777922949508f, + 1.2705593687654873f, + 1.2982690107339132f, + 1.3280505578213306f, + 1.3600643892400104f, + 1.3944898413647777f, + 1.4315278911623237f, + 1.4714043176061107f, + 1.5143734423314616f, + ]; + + /// + /// Gets the DCT multiplier constants + /// + public static ReadOnlySpan Multipliers4 => + [ + 0.541196100146197f, + 1.3065629648763764f, + ]; + + /// + /// Gets the DCT multiplier constants + /// + public static ReadOnlySpan Multipliers8 => + [ + 0.5097955791041592f, + 0.6013448869350453f, + 0.8999762231364156f, + 2.5629154477415055f, + ]; + + /// + /// Gets the DCT multiplier constants + /// + public static ReadOnlySpan Multipliers16 => + [ + 0.5024192861881557f, 0.5224986149396889f, 0.5669440348163577f, + 0.6468217833599901f, 0.7881546234512502f, 1.060677685990347f, + 1.7224470982383342f, 5.101148618689155f, + ]; + + /// + /// Gets the DCT multiplier constants + /// + public static ReadOnlySpan Multipliers32 => + [ + 0.5006029982351963f, 0.5054709598975436f, 0.5154473099226246f, + 0.5310425910897841f, 0.5531038960344445f, 0.5829349682061339f, + 0.6225041230356648f, 0.6748083414550057f, 0.7445362710022986f, + 0.8393496454155268f, 0.9725682378619608f, 1.1694399334328847f, + 1.4841646163141662f, 2.057781009953411f, 3.407608418468719f, + 10.190008123548033f, + ]; + + /// + /// Gets the DCT multiplier constants + /// + public static ReadOnlySpan Multipliers64 => + [ + 0.500150636020651f, 0.5013584524464084f, 0.5037887256810443f, + 0.5074711720725553f, 0.5124514794082247f, 0.5187927131053328f, + 0.52657731515427f, 0.535909816907992f, 0.5469204379855088f, + 0.5597698129470802f, 0.57465518403266f, 0.5918185358574165f, + 0.6115573478825099f, 0.6342389366884031f, 0.6603198078137061f, + 0.6903721282002123f, 0.7251205223771985f, 0.7654941649730891f, + 0.8127020908144905f, 0.8683447152233481f, 0.9345835970364075f, + 1.0144082649970547f, 1.1120716205797176f, 1.233832737976571f, + 1.3892939586328277f, 1.5939722833856311f, 1.8746759800084078f, + 2.282050068005162f, 2.924628428158216f, 4.084611078129248f, + 6.796750711673633f, 20.373878167231453f, + ]; + + /// + /// Gets the DCT multiplier constants + /// + public static ReadOnlySpan Multipliers128 => + [ + 0.5000376519155477f, 0.5003390374428216f, 0.5009427176380873f, + 0.5018505174842379f, 0.5030651913013697f, 0.5045904432216454f, + 0.5064309549285542f, 0.5085924210498143f, 0.5110815927066812f, + 0.5139063298475396f, 0.5170756631334912f, 0.5205998663018917f, + 0.524490540114724f, 0.5287607092074876f, 0.5334249333971333f, + 0.538499435291984f, 0.5440022463817783f, 0.549953374183236f, + 0.5563749934898856f, 0.5632916653417023f, 0.5707305880121454f, + 0.5787218851348208f, 0.5872989370937893f, 0.5964987630244563f, + 0.606362462272146f, 0.6169357260050706f, 0.6282694319707711f, + 0.6404203382416639f, 0.6534518953751283f, 0.6674352009263413f, + 0.6824501259764195f, 0.6985866506472291f, 0.7159464549705746f, + 0.7346448236478627f, 0.7548129391165311f, 0.776600658233963f, + 0.8001798956216941f, 0.8257487738627852f, 0.8535367510066064f, + 0.8838110045596234f, 0.9168844461846523f, 0.9531258743921193f, + 0.9929729612675466f, 1.036949040910389f, 1.0856850642580145f, + 1.1399486751015042f, 1.2006832557294167f, 1.2690611716991191f, + 1.346557628206286f, 1.4350550884414341f, 1.5369941008524954f, + 1.6555965242641195f, 1.7952052190778898f, 1.961817848571166f, + 2.163957818751979f, 2.4141600002500763f, 2.7316450287739396f, + 3.147462191781909f, 3.7152427383269746f, 4.5362909369693565f, + 5.827688377844654f, 8.153848602466814f, 13.58429025728446f, + 40.744688103351834f, + ]; + + /// + /// Gets the DCT multiplier constants + /// + public static ReadOnlySpan Multipliers256 => + [ + 0.5000094125358878f, 0.500084723455784f, 0.5002354020255269f, + 0.5004615618093246f, 0.5007633734146156f, 0.5011410648064231f, + 0.5015949217281668f, 0.502125288230386f, 0.5027325673091954f, + 0.5034172216566842f, 0.5041797745258774f, 0.5050208107132756f, + 0.5059409776624396f, 0.5069409866925212f, 0.5080216143561264f, + 0.509183703931388f, 0.5104281670536573f, 0.5117559854927805f, + 0.5131682130825206f, 0.5146659778093218f, 0.516250484068288f, + 0.5179230150949777f, 0.5196849355823947f, 0.5215376944933958f, + 0.5234828280796439f, 0.52552196311921f, 0.5276568203859896f, + 0.5298892183652453f, 0.5322210772308335f, 0.5346544231010253f, + 0.537191392591309f, 0.5398342376841637f, 0.5425853309375497f, + 0.545447171055775f, 0.5484223888484947f, 0.551513753605893f, + 0.554724179920619f, 0.5580567349898085f, 0.5615146464335654f, + 0.5651013106696203f, 0.5688203018875696f, 0.5726753816701664f, + 0.5766705093136241f, 0.5808098529038624f, 0.5850978012111273f, + 0.58953897647151f, 0.5941382481306648f, 0.5989007476325463f, + 0.6038318843443582f, 0.6089373627182432f, 0.614223200800649f, + 0.6196957502119484f, 0.6253617177319102f, 0.6312281886412079f, + 0.6373026519855411f, 0.6435930279473415f, 0.6501076975307724f, + 0.6568555347890955f, 0.6638459418498757f, 0.6710888870233562f, + 0.6785949463131795f, 0.6863753486870501f, 0.6944420255086364f, + 0.7028076645818034f, 0.7114857693151208f, 0.7204907235796304f, + 0.7298378629074134f, 0.7395435527641373f, 0.749625274727372f, + 0.7601017215162176f, 0.7709929019493761f, 0.7823202570613161f, + 0.7941067887834509f, 0.8063772028037925f, 0.8191580674598145f, + 0.83247799080191f, 0.8463678182968619f, 0.860860854031955f, + 0.8759931087426972f, 0.8918035785352535f, 0.9083345588266809f, + 0.9256319988042384f, 0.9437459026371479f, 0.962730784794803f, + 0.9826461881778968f, 1.0035572754078206f, 1.0255355056139732f, + 1.048659411496106f, 1.0730154944316674f, 1.0986992590905857f, + 1.1258164135986009f, 1.1544842669978943f, 1.184833362908442f, + 1.217009397314603f, 1.2511754798461228f, 1.287514812536712f, + 1.326233878832723f, 1.3675662599582539f, 1.411777227500661f, + 1.459169302866857f, 1.5100890297227016f, 1.5649352798258847f, + 1.6241695131835794f, 1.6883285509131505f, 1.7580406092704062f, + 1.8340456094306077f, 1.9172211551275689f, 2.0086161135167564f, + 2.1094945286246385f, 2.22139377701127f, 2.346202662531156f, + 2.486267909203593f, 2.644541877144861f, 2.824791402350551f, + 3.0318994541759925f, 3.2723115884254845f, 3.5547153325075804f, + 3.891107790700307f, 4.298537526449054f, 4.802076008665048f, + 5.440166215091329f, 6.274908408039339f, 7.413566756422303f, + 9.058751453879703f, 11.644627325175037f, 16.300023088031555f, + 27.163977662448232f, 81.48784219222516f, + ]; +} From 7223d98e66be7830b7fae570a28e71c60087eacd Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Fri, 17 Jul 2026 23:02:35 +0400 Subject: [PATCH 036/328] Add DCT memory, make JxlImage3 implement IDisposable --- .../Formats/Jxl/Memory/JxlImage3{T}.cs | 12 +++- .../Formats/Jxl/Processing/IJxlDctAcImage.cs | 68 +++++++++++++++++++ .../Jxl/Processing/JxlDctAcImage{T}.cs | 63 +++++++++++++++++ .../Formats/Jxl/Processing/JxlDctAcPointer.cs | 20 ++++++ .../Formats/Jxl/Processing/JxlDctAcType.cs | 20 ++++++ .../Jxl/Processing/JxlDctReadOnlyAcPointer.cs | 24 +++++++ 6 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/IJxlDctAcImage.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlDctAcImage{T}.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlDctAcPointer.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlDctAcType.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlDctReadOnlyAcPointer.cs diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs index f46f6767b8..37707b7ba4 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs @@ -7,7 +7,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Memory; // NOTE: Do not seal this class. -internal class JxlImage3 +internal class JxlImage3 : IDisposable where T : unmanaged { private const int PlaneCount = 3; @@ -82,4 +82,14 @@ public bool ShrinkTo(int x, int y) [Conditional("DEBUG")] private void PlaneRowBoundsCheck(int c, int y) => Debug.Assert(c < PlaneCount && y < this.YSize, "The bounds check has failed"); + + public void Dispose() + { + foreach (JxlPlane plane in this.planes) + { + plane.Dispose(); + } + + GC.SuppressFinalize(this); + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/IJxlDctAcImage.cs b/src/ImageSharp/Formats/Jxl/Processing/IJxlDctAcImage.cs new file mode 100644 index 0000000000..30969824cd --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/IJxlDctAcImage.cs @@ -0,0 +1,68 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Base DCT AC coefficient image +/// +internal interface IJxlDctAcImage +{ + /// + /// Gets the bit width of AC coefficients + /// + public JxlDctAcType Type { get; } + + /// + /// Gets the number of pixels per row. + /// + public int PixelsPerRow { get; } + + /// + /// Gets a value indicating whether the image is empty and doesn't + /// have anything within. + /// + public bool IsEmpty { get; } + + /// + /// Returns a reference to the coefficients at the specified row. + /// + /// The plane index (which channel). + /// The row index within plane specified by . + /// The X offset at the specified row. + /// + /// A reference to the coefficients inside the channel + /// specified by index , at index of + /// the row specified by , with X offset + /// specified by . + /// + public JxlDctAcPointer GetPlaneRow(int channel, int y, int xBase = 0); + + /// + /// Returns a reference to the coefficients at the specified row. + /// + /// The plane index (which channel). + /// The row index within plane specified by . + /// The X offset at the specified row. + /// + /// A reference to the coefficients inside the channel + /// specified by index , at index of + /// the row specified by , with X offset + /// specified by . + /// + /// + /// This is a read-only kind of . + /// + public JxlDctReadOnlyAcPointer GetReadOnlyPlaneRow(int channel, int y, int xBase = 0); + + /// + /// Fills all planes with zero. + /// + public void Clear(); + + /// + /// Fills everything within the specified plane with zero. + /// + /// Desired index of the plane. + public void Clear(int plane = 0); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcImage{T}.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcImage{T}.cs new file mode 100644 index 0000000000..2052daee6e --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcImage{T}.cs @@ -0,0 +1,63 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal sealed class JxlDctAcImage : IJxlDctAcImage, IDisposable + where T : unmanaged +{ + private readonly JxlImage3 image; + + public unsafe JxlDctAcImage(Configuration configuration, int width, int height) + { + DebugGuard.IsTrue(sizeof(T) is 2 or 4, "The type must be 2 or 4 bytes"); + + this.image = JxlImage3.Create(configuration, width, height); + } + + public unsafe JxlDctAcType Type => sizeof(T) == 4 ? JxlDctAcType.Ac32 : JxlDctAcType.Ac16; + + public int PixelsPerRow => this.image.PixelsPerRow; + + public bool IsEmpty => this.image.XSize == 0 || this.image.YSize == 0; + + public void Clear() => JxlImageOperations.ClearImage(this.image); + + public void Clear(int plane = 0) => JxlImageOperations.ClearImage(this.image); + + public unsafe JxlDctAcPointer GetPlaneRow(int channel, int y, int xBase = 0) + { + if (sizeof(T) == 4) + { + Span span = (this.image as JxlImage3)!.PlaneRow(channel, y)[xBase..]; + return new JxlDctAcPointer() { Pointer32 = span }; + } + else + { + Span span = (this.image as JxlImage3)!.PlaneRow(channel, y)[xBase..]; + return new JxlDctAcPointer() { Pointer16 = span }; + } + } + + public unsafe JxlDctReadOnlyAcPointer GetReadOnlyPlaneRow(int channel, int y, int xBase = 0) + { + if (sizeof(T) == 4) + { + ReadOnlySpan span = (this.image as JxlImage3)!.PlaneRow(channel, y)[xBase..]; + return new JxlDctReadOnlyAcPointer(span); + } + else + { + ReadOnlySpan span = (this.image as JxlImage3)!.PlaneRow(channel, y)[xBase..]; + return new JxlDctReadOnlyAcPointer(span); + } + } + + public void Dispose() + { + this.image.Dispose(); + GC.SuppressFinalize(this); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcPointer.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcPointer.cs new file mode 100644 index 0000000000..9710712d77 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcPointer.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Pointer to DCT AC coefficients +/// +internal ref struct JxlDctAcPointer() +{ + /// + /// 16-bit pointer (if any) + /// + public Span Pointer16 = []; + + /// + /// 32-bit pointer (if any) + /// + public Span Pointer32 = []; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcType.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcType.cs new file mode 100644 index 0000000000..a7e4ede67f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcType.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Bit size for DCT AC coefficient +/// +internal enum JxlDctAcType : byte +{ + /// + /// 16-bit coefficient + /// + Ac16, + + /// + /// 32-bit coefficient + /// + Ac32 +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctReadOnlyAcPointer.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlDctReadOnlyAcPointer.cs new file mode 100644 index 0000000000..c80d8eaa64 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlDctReadOnlyAcPointer.cs @@ -0,0 +1,24 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Pointer to DCT AC coefficients. (Read-only) +/// +internal readonly ref struct JxlDctReadOnlyAcPointer +{ + /// + /// 16-bit pointer (if any) + /// + public readonly ReadOnlySpan Pointer16; + + /// + /// 32-bit pointer (if any) + /// + public readonly ReadOnlySpan Pointer32; + + internal JxlDctReadOnlyAcPointer(ReadOnlySpan pointer16) => this.Pointer16 = pointer16; + + internal JxlDctReadOnlyAcPointer(ReadOnlySpan pointer32) => this.Pointer32 = pointer32; +} From 51b77654155c021c1a96726d8808adc64f451f70 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Fri, 17 Jul 2026 23:09:19 +0400 Subject: [PATCH 037/328] Implement signed packing See pack_signed.h --- .../Formats/Jxl/Processing/JxlPackSigned.cs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlPackSigned.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlPackSigned.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlPackSigned.cs new file mode 100644 index 0000000000..f54b77f1d3 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlPackSigned.cs @@ -0,0 +1,37 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Provides PackSigned and UnpackSigned methods. +/// +internal static class JxlPackSigned +{ + /// + /// Encodes non-negative (X) into (2 * X), negative (-X) into (2 * X - 1) + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint PackUnsigned(int x) + { + unchecked + { + uint value = (uint)x; + return (value << 1) ^ ((~value >> 31) - 1); + } + } + + /// + /// Reverse to PackSigned, i.e. UnpackSigned(PackSigned(X)) == X. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int UnpackSigned(uint x) + { + unchecked + { + return (int)((x >> 1) ^ (((~x) & 1) - 1)); + } + } +} From 446e4a5e658d8cfaef46013989c98a27125e3d51 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 18 Jul 2026 07:15:06 +0400 Subject: [PATCH 038/328] Add loop filter See loop_filter.h, loop_filter.cc, epf.h and epf.cc --- .../Formats/Jxl/Processing/JxlAcStrategy.cs | 2 + .../Formats/Jxl/Processing/JxlLoopFilter.cs | 260 ++++++++++++++++++ 2 files changed, 262 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs index 26b087deea..84b2462591 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs @@ -82,6 +82,8 @@ public JxlAcStrategy(int rawStrategy) public readonly int Log2CoveredBlocks => Log2CoveredBlocksLookup[(int)this.Strategy]; + public readonly bool IsFirstBlock => this.isFirst; + public readonly JxlAcStrategyType Strategy { get; } public void ComputeNaturalCoefficientOrder(ref int order) => CoefficientOrderAndLookup(this, false, ref order); diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs new file mode 100644 index 0000000000..01759a4d93 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs @@ -0,0 +1,260 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.Fields; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// JPEG XL loop filter. +/// +internal sealed class JxlLoopFilter : IJxlFields +{ + /// + /// 4 * (sqrt(0.5)-1), so that Weight(sigma) = 0.5 + /// + private const float InverseSigmaNum = -1.1715728752538099024f; + + /// + /// kInvSigmaNum / 0.3 + /// + private const float MinSigma = -3.90524291751269967465540850526868f; + + /// + /// Gets the number of EPF (Edge-preserving filter) sharp entries. + /// + public const int EpfSharpEntries = 8; + + /// + /// Gets or sets a value indicating whether gaborish + /// convolution is preferred. + /// + public bool UseGaborishConvolution { get; set; } + + /// + /// Gets or sets a value indicating whether custom + /// gaborish weights are used. + /// + public bool GaborishCustom { get; set; } + + /// + /// Gets or sets the first custom X gaborish weight. + /// + public float GaborishXWeight1 { get; set; } + + /// + /// Gets or sets the second custom X gaborish weight. + /// + public float GaborishXWeight2 { get; set; } + + /// + /// Gets or sets the first custom Y gaborish weight. + /// + public float GaborishYWeight1 { get; set; } + + /// + /// Gets or sets the second custom Y gaborish weight. + /// + public float GaborishYWeight2 { get; set; } + + /// + /// Gets or sets the first custom B gaborish weight. + /// + public float GaborishBWeight1 { get; set; } + + /// + /// Gets or sets the second custom B gaborish weight. + /// + public float GaborishBWeight2 { get; set; } + + /// + /// Gets or sets the number of EPF (Edge-preserving filter) steps. + /// 0 means EPF is disabled, 1 applies only the first stage, + /// 2 applies both stages and 3 applies the first stage twice + /// and the second stage once. + /// + public int EpfIterations { get; set; } + + /// + /// Gets or sets a value indicating whether custom EPF sharpness + /// is used. + /// + public bool EpfSharpCustom { get; set; } + + /// + /// Gets or sets a value with 8 elements representing EPF sharpness lookup tables (LUTs). + /// + public float[] EpfSharpLookup { get; set; } = new float[8]; + + /// + /// Gets or sets a value indicating whether custom EPF weights are used. + /// + public bool EpfCustomWeights { get; set; } + + /// + /// Gets or sets the relative weight of each channel. + /// + public float[] EpfChannelScale { get; set; } = new float[3]; + + /// + /// Gets or sets the value that represents the minimum weight for first pass. + /// + public float EpfPass1ZeroFlush { get; set; } + + /// + /// Gets or sets the value that represents the minimum weight for second pass. + /// + public float EpfPass2ZeroFlush { get; set; } + + /// + /// Gets or sets a value indicating whether custom sigma parameters are used for EPF. + /// + public bool EpfCustomSigma { get; set; } + + /// + /// Gets or sets the quant multiplier. + /// + public float EpfQuantMultiplier { get; set; } + + /// + /// Gets or sets the multiplier for sigma in pass 0. + /// + public float EpfPass0SigmaScale { get; set; } + + /// + /// Gets or sets the multiplier for sigma in pass 2. + /// + public float EpfPass2SigmaScale { get; set; } + + /// + /// Gets or sets the inverse multiplier for sigma on borders. + /// + public float EpfBorderSadMul { get; set; } + + /// + /// Gets or sets the EPF sigma for modular. + /// + // NOTE: This value is not documented by libjxl. + public float EpfSigmaForModular { get; set; } + + /// + /// Gets or sets the number of extensions. + /// + public long Extensions { get; set; } + + /// + /// Mirror n floats starting at *span and store them before span. + /// + private static void LeftMirror(Span span, int n) + { + ref float p = ref MemoryMarshal.GetReference(span); + for (int i = 0; i < n; i++) + { + Unsafe.Add(ref p, -1 - i) = span[i]; + } + } + + /// + /// Mirror n floats starting at *(span - n) and store them at *span. + /// + private static void RightMirror(Span span, int n) + { + ref float p = ref MemoryMarshal.GetReference(span); + for (int i = 0; i < n; i++) + { + span[i] = Unsafe.Add(ref p, -1 - i); + } + } + + public bool ComputeSigma(Rectangle blockRect, JxlPassesDecoderState state) + { + if (this.EpfIterations <= 0) + { + return false; + } + + JxlAcStrategyImage acStrategy = state.Shared.AcStrategy; + float quantScale = state.Shared.Quantizer.Scale; + + int sigmaStride = state.Sigma.PixelsPerRow; + int sharpnessStride = state.Shared.EpfSharpness.PixelsPerRow; + + for (int by = 0; by < blockRect.Height; by++) + { + Span sigmaRow = state.Sigma.GetRowSpan(by); + Span sharpnessRow = state.Shared.EpfSharpness.GetRowSpan(by); + JxlAcStrategyRow acsRow = acStrategy.GetRow(in blockRect, by); + Span rowQuant = state.Shared.RawQuantField.GetRow(by); + + for (int bx = 0; bx < blockRect.Width; bx++) + { + JxlAcStrategy acs = acsRow[bx]; + int llfX = acs.CoveredBlocksX; + + if (!acs.IsFirstBlock) + { + continue; + } + + float sigmaQuant = this.EpfQuantMultiplier / (quantScale * rowQuant[bx] * InverseSigmaNum); + + for (int iy = 0; iy < acs.CoveredBlocksY; iy++) + { + for (int ix = 0; ix < acs.CoveredBlocksY; ix++) + { + float sigma = sigmaQuant * this.EpfSharpLookup[sharpnessRow[bx + ix + iy + sharpnessStride]]; + sigma = MathF.Min(-1e-4f, sigma); + sigmaRow[bx + ix + SigmaPadding + (iy + SigmaPadding) * sigmaStride] = 1.0f / sigma; + } + } + + if (bx + blockRect.X == 0) + { + for (int iy = 0; iy < acs.CoveredBlocksY; iy++) + { + LeftMirror(sigmaRow.Slice(SigmaPadding + (iy + SigmaPadding) * SigmaStride), sigmaBorder); + } + } + + if (bx + blockRect.X + llfX == state.Shared.FrameDimensions.XSizeBlocks) + { + for (int iy = 0; iy < acs.CoveredBlocksY; iy++) + { + RightMirror(sigmaRow.Slice(SigmaPadding + bx + llfX + (iy + SigmaPadding) * sigmaStride), SigmaBorder); + } + } + + int offsetBefore = bx + blockRect.X == 0 ? 1 : bx + SigmaPadding; + int offsetAfter = bx + blockRect.X + llfX == state.Shared.FrameDimensions.XSizeBlocks + ? SigmaPadding + llfX + bx + SigmaBorder + : SigmaPadding + llfX + bx; + + int num = offsetAfter - offsetBefore; + + if (by + blockRect.Y == 0) + { + for (int iy = 0; iy < SigmaBorder; iy++) + { + sigmaRow.Slice(offsetBefore + (SigmaPadding - 1 - iy) * sigmaStride, num) + .CopyTo(sigmaRow.Slice(offsetBefore + ((SigmaPadding + iy) * sigmaStride))); + } + } + + if (by + blockRect.Y + acs.CoveredBlocksX == state.Shared.FrameDimensions.YSizeBloks) + { + for (int iy = 0; iy < SigmaBorder; iy++) + { + sigmaRow.Slice(offsetBefore + (sigmaStride * (acs.CoveredBlocksX + SigmaPadding + iy))) + .CopyTo(sigmaRow.Slice(offsetBefore + (sigmaStride * (acs.CoveredBlocksY + SigmaPadding - 1 - iy)))); + } + } + } + } + + return true; + } + + public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); +} From decc0b232c33fa0f18d7db50f01e743af6410521 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 18 Jul 2026 16:07:46 +0400 Subject: [PATCH 039/328] Fully implement JPEG XL fields See fields.h and fields.cc --- .../Jxl/Fields/JxlAllDefaultVisitor.cs | 35 +++++ .../Formats/Jxl/Fields/JxlBitsCoder.cs | 41 ++++++ .../Formats/Jxl/Fields/JxlBundle.cs | 89 ++++++++++++ .../Formats/Jxl/Fields/JxlCanEncodeVisitor.cs | 118 ++++++++++++++++ .../Formats/Jxl/Fields/JxlExtensionStates.cs | 42 ++++++ .../Formats/Jxl/Fields/JxlF16Coder.cs | 73 ++++++++++ .../Formats/Jxl/Fields/JxlInitVisitor.cs | 51 +++++++ .../Formats/Jxl/Fields/JxlReadVisitor.cs | 132 ++++++++++++++++++ .../Jxl/Fields/JxlSetDefaultVisitor.cs | 49 +++++++ .../Formats/Jxl/Fields/JxlU32Coder.cs | 127 +++++++++++++++++ .../Formats/Jxl/Fields/JxlU64Coder.cs | 105 ++++++++++++++ .../Formats/Jxl/Fields/JxlVisitor.cs | 92 ++++++++++++ .../Formats/Jxl/Fields/JxlVisitorBase.cs | 74 ++++++++++ src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs | 23 ++- .../Formats/Jxl/Metadata/JxlBitDepth.cs | 2 +- .../Jxl/Metadata/JxlCustomTransformData.cs | 2 +- 16 files changed, 1046 insertions(+), 9 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlAllDefaultVisitor.cs create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlCanEncodeVisitor.cs create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlExtensionStates.cs create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlInitVisitor.cs create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlSetDefaultVisitor.cs create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlVisitor.cs create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlVisitorBase.cs diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlAllDefaultVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlAllDefaultVisitor.cs new file mode 100644 index 0000000000..cfe1430193 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlAllDefaultVisitor.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Fields; + +internal sealed class JxlAllDefaultVisitor : JxlVisitorBase +{ + public bool IsAllDefault { get; private set; } = true; + + public override bool Bits(int bits, uint defaultValue, ref uint value) + { + this.IsAllDefault = value == defaultValue; + return true; + } + + public override bool U32(JxlU32Enc enc, uint defaultValue, ref uint value) + { + this.IsAllDefault = value == defaultValue; + return true; + } + + public override bool U64(ulong defaultValue, ref ulong value) + { + this.IsAllDefault = value == defaultValue; + return true; + } + + public override bool F16(float defaultValue, ref float value) + { + this.IsAllDefault = MathF.Abs(value - defaultValue) < 1E-6f; + return true; + } + + public override bool AllDefault(IJxlFields fields, ref bool allDefault) => false; +} diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs new file mode 100644 index 0000000000..398587baac --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs @@ -0,0 +1,41 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Jxl.IO; + +namespace SixLabors.ImageSharp.Formats.Jxl.Fields; + +/// +/// Raw bits coder +/// +internal static class JxlBitsCoder +{ + /// + /// Maximum number of encodeable bits. Since this coder encodes + /// bits raw, this happens to be whatever is passed to it. + /// + // Looks like that's what the function does (fields.cc:406): + // it returns whatever is passed to it. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int MaxEncodedBits(int bits) => bits; + + public static bool CanEncode(int bits, uint value, ref int encodedBits) + { + encodedBits = bits; + if (value >= (1 << bits)) + { + DebugGuard.IsTrue(false, "Value is too large"); + + return false; + } + + return true; + } + + // NOTE: BitsCoder::Read (fields.cc:418) returns a uint32_t, + // suggesting the input bit size does not exceed 32 bits. + public static uint Read(uint bits, JxlBitReader reader) => reader.ReadBits32(bits); + + public static uint Read(int bits, JxlBitReader reader) => reader.ReadBits32((uint)bits); +} diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs new file mode 100644 index 0000000000..e6e673f9c8 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs @@ -0,0 +1,89 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.IO; + +namespace SixLabors.ImageSharp.Formats.Jxl.Fields; + +/// +/// An helper. +/// +internal static class JxlBundle +{ + /// + /// Initializes the specified JXL fields. + /// + /// The JXL fields. + public static void Init(IJxlFields fields) + { + JxlInitVisitor initVisitor = new(); + + if (!initVisitor.Visit(fields)) + { + DebugGuard.IsTrue(false, "Init should never fail"); + } + } + + /// + /// Sets all JXL fields provided by the input value to their defaults. + /// + /// The JXL fields. + public static void SetDefault(IJxlFields fields) + { + JxlSetDefaultVisitor visitor = new(); + + if (!visitor.Visit(fields)) + { + DebugGuard.IsTrue(false, "SetDefault should never fail"); + } + } + + /// + /// Returns a value indicating whether every value provided by this + /// field is a default value. If at least one field isn't a default + /// value, the method returns false. + /// + /// The JXL fields. + /// A boolean indicating whether or not are all values initialized to their default values. + public static bool AllDefault(IJxlFields fields) + { + JxlAllDefaultVisitor allDefaultVisitor = new(); + + if (!allDefaultVisitor.Visit(fields)) + { + DebugGuard.IsTrue(false, "AllDefault should never fail"); + } + + return allDefaultVisitor.IsAllDefault; + } + + /// + /// Reads the fields from a bit-reader. + /// + /// The bit-reader. + /// The fields. + /// Status of the read operation. + public static bool Read(JxlBitReader reader, IJxlFields fields) + { + JxlReadVisitor visitor = new(reader); + if (!visitor.Visit(fields)) + { + return false; + } + + return visitor.OK; + } + + /// + /// Tries to read the fields from a bit-reader. + /// + /// The bit-reader. + /// The fields. + /// Status of the read operation. + public static bool CanRead(JxlBitReader reader, IJxlFields fields) + { + JxlReadVisitor visitor = new(reader); + _ = visitor.Visit(fields); + return visitor.OK; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlCanEncodeVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlCanEncodeVisitor.cs new file mode 100644 index 0000000000..aae11aa120 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlCanEncodeVisitor.cs @@ -0,0 +1,118 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Fields; + +internal sealed class JxlCanEncodeVisitor : JxlVisitorBase +{ + private long encodedBits; + private ulong extensions; + private long posAfterExt; + + public bool OK { get; set; } = true; + + public override bool Bits(int bits, uint defaultValue, ref uint value) + { + int enc = 0; + this.OK &= JxlBitsCoder.CanEncode(bits, value, ref enc); + this.encodedBits += enc; + return true; + } + + public override bool U32(JxlU32Enc enc, uint defaultValue, ref uint value) + { + int encBits = 0; + this.OK &= JxlU32Coder.CanEncode(enc, value, ref encBits); + this.encodedBits += encBits; + return true; + } + + public override bool U64(ulong defaultValue, ref ulong value) + { + int encBits = 0; + this.OK &= JxlU64Coder.CanEncode(value, ref encBits); + this.encodedBits += encBits; + return true; + } + + public override bool F16(float defaultValue, ref float value) + { + int encBits = 0; + this.OK &= JxlF16Coder.CanEncode(value, ref encBits); + this.encodedBits += encBits; + return true; + } + + public override bool AllDefault(IJxlFields fields, ref bool allDefault) + { + allDefault = JxlBundle.AllDefault(fields); + if (!this.Boolean(true, ref allDefault)) + { + return false; + } + + return allDefault; + } + + public override bool BeginExtensions(ref ulong extensions) + { + if (!base.BeginExtensions(ref extensions)) + { + return false; + } + + this.extensions = extensions; + + if (extensions != 0uL) + { + if (this.posAfterExt != 0) + { + return false; + } + + this.posAfterExt = this.encodedBits; + + if (this.posAfterExt == 0) + { + return false; + } + } + + return true; + } + + public bool GetSizes(ref int extensionBits, ref long totalBits) + { + if (!this.OK) + { + return false; + } + + extensionBits = 0; + totalBits = this.encodedBits; + + if (this.posAfterExt != 0) + { + if (this.encodedBits < this.posAfterExt) + { + return false; + } + + extensionBits = (int)this.encodedBits - (int)this.posAfterExt; + int encodedBits = 0; + this.OK &= JxlU64Coder.CanEncode(extensionBits, ref encodedBits); + totalBits += encodedBits; + + for (int i = 1; i < BitOperations.PopCount(this.extensions); i++) + { + encodedBits = 0; + this.OK &= JxlU64Coder.CanEncode(0, ref encodedBits); + totalBits += encodedBits; + } + } + + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlExtensionStates.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlExtensionStates.cs new file mode 100644 index 0000000000..2cc3c82581 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlExtensionStates.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Fields; + +internal sealed class JxlExtensionStates +{ + private ulong begun; + private ulong ended; + + public bool IsBegun => (this.begun & 1) != 0; + + public bool IsEnded => (this.ended & 1) != 0; + + public void Push() + { + this.begun <<= 1; + this.ended <<= 1; + } + + public void Pop() + { + this.begun >>= 1; + this.ended >>= 1; + } + + public void Begin() + { + DebugGuard.IsFalse(this.IsBegun, nameof(this.IsBegun), "This must be false."); + DebugGuard.IsFalse(this.IsEnded, nameof(this.IsEnded), "This must be false."); + + this.begun++; + } + + public void End() + { + DebugGuard.IsTrue(this.IsBegun, nameof(this.IsBegun), "This must be true."); + DebugGuard.IsFalse(this.IsEnded, nameof(this.IsEnded), "This must be false."); + + this.ended++; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs new file mode 100644 index 0000000000..7bf83e1854 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs @@ -0,0 +1,73 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Jxl.IO; + +namespace SixLabors.ImageSharp.Formats.Jxl.Fields; + +/// +/// Represents the Half-precision Floating-point number coder. +/// +internal static class JxlF16Coder +{ + /// + /// Always returns 16, which is the maximum possible encoded bits. + /// The F16 coder always reads 16 bits from the bitstream. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int MaxEncodedBits() => 16; + + /// + /// Returns a boolean indicating whether the input float + /// can be represented properly when encoded into a bit-stream. + /// Also stores the maximum encodeable bits into encodedBits (which is + /// always 16). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool CanEncode(float value, ref int encodedBits) + { + encodedBits = MaxEncodedBits(); + if (float.IsNaN(value) || float.IsInfinity(value)) + { + return false; // NaN and Infinity are not valid + } + + return MathF.Abs(value) <= 65504.0f; + } + + public static bool Read(JxlBitReader reader, ref float value) + { + uint bits16 = reader.ReadBits32(16u); + uint sign = bits16 >> 15; + uint biasedExponent = (bits16 >> 10) & 0x1Fu; + uint mantissa = bits16 & 0x3FFu; + + if (biasedExponent == 31u) + { + // NaN and Infinity are not valid + return false; + } + + if (biasedExponent == 0u) + { + // Subnormal or zero. + value = (1.0f / 16384) * (mantissa * (1.0f / 1024)); + if (sign != 0u) + { + value = -value; + } + + return true; + } + + uint biasedExp32 = biasedExponent + (127u - 15u); + uint mantissa32 = mantissa << (23 - 10); + uint bits32 = (sign << 31) | (biasedExp32 << 23) | mantissa32; + + value = BitConverter.UInt32BitsToSingle(bits32); + + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlInitVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlInitVisitor.cs new file mode 100644 index 0000000000..28c16c4e46 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlInitVisitor.cs @@ -0,0 +1,51 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Fields; + +/// +/// This variant of sets values +/// to be the default value. +/// +internal sealed class JxlInitVisitor : JxlVisitorBase +{ + public override bool Bits(int bits, uint defaultValue, ref uint value) + { + value = defaultValue; + return true; + } + + public override bool U32(JxlU32Distribution d0, JxlU32Distribution d1, JxlU32Distribution d2, JxlU32Distribution d3, uint defaultValue, ref uint value) + { + value = defaultValue; + return true; + } + + public override bool U64(ulong defaultValue, ref ulong value) + { + value = defaultValue; + return true; + } + + public override bool Boolean(bool defaultValue, ref bool value) + { + value = defaultValue; + return true; + } + + public override bool F16(float defaultValue, ref float value) + { + value = defaultValue; + return true; + } + + public override bool Conditional(bool condition) => true; + + public override bool AllDefault(IJxlFields fields, ref bool allDefault) + { + _ = this.Boolean(true, ref allDefault); + return false; + } + + public override bool VisitNested(IJxlFields fields) => true; +} diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs new file mode 100644 index 0000000000..b0396579cc --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs @@ -0,0 +1,132 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.IO; + +namespace SixLabors.ImageSharp.Formats.Jxl.Fields; + +internal sealed class JxlReadVisitor(JxlBitReader reader) : JxlVisitorBase +{ + private ulong totalExtensionBits; + private bool notEnoughBytes; + private long posAfterExtSize; + private readonly ulong[] extensionBits = new ulong[JxlBundle.MaxExtensions]; + + public bool OK { get; private set; } + + public override bool IsReading => true; + + public override bool Bits(int bits, uint defaultValue, ref uint value) + { + value = JxlBitsCoder.Read(bits, reader); + return this.ThrowIfEndOfStreamOrReturnTrue(); + } + + public override bool U32(JxlU32Enc enc, uint defaultValue, ref uint value) + { + value = JxlU32Coder.Read(enc, reader); + return this.ThrowIfEndOfStreamOrReturnTrue(); + } + + public override bool U64(ulong defaultValue, ref ulong value) + { + value = JxlU64Coder.Read(reader); + return this.ThrowIfEndOfStreamOrReturnTrue(); + } + + public override bool F16(float defaultValue, ref float value) + { + this.OK &= JxlF16Coder.Read(reader, ref value); + return this.ThrowIfEndOfStreamOrReturnTrue(); + } + + public override void SetDefault(IJxlFields fields) => JxlBundle.SetDefault(fields); + + public override bool BeginExtensions(ref ulong extensions) + { + if (!base.BeginExtensions(ref extensions)) + { + return false; + } + + if (extensions == 0) + { + return true; + } + + for (ulong remainingExtensions = extensions; remainingExtensions != 0; remainingExtensions &= remainingExtensions - 1) + { + int idxExtension = Num0BitsBelowLS1BitNonzero(remainingExtensions); + if (!this.U64(0, ref this.extensionBits[idxExtension])) + { + return false; + } + + if (!SafeAdd(this.totalExtensionBits, this.extensionBits[idxExtension], ref this.totalExtensionBits)) + { + DebugGuard.IsTrue(false, "Extension bits overflow; the codestream is not valid"); + + return false; + } + } + + this.posAfterExtSize = reader.TotalBitsConsumed; + return this.posAfterExtSize != 0; + } + + public override bool EndExtensions() + { + if (!base.EndExtensions()) + { + return false; + } + + if (this.posAfterExtSize == 0) + { + return true; + } + + if (this.notEnoughBytes) + { + return true; + } + + long bitsRead = reader.TotalBitsConsumed; + + long end = 0; + if (!SafeAdd(this.posAfterExtSize, this.totalExtensionBits, ref end)) + { + DebugGuard.IsTrue(false, "Invalid extension size."); + + return false; + } + + if (bitsRead > end) + { + DebugGuard.IsTrue(false, "Read more extension bits than budgeted"); + + return false; + } + + long remainingBits = end - bitsRead; + + if (remainingBits != 0) + { + reader.SkipBits64((uint)remainingBits); + } + + return this.ThrowIfEndOfStreamOrReturnTrue(); + } + + private bool ThrowIfEndOfStreamOrReturnTrue() + { + if (reader.IsEndOfStream) + { + DebugGuard.IsTrue(false, "Got an invalid end-of-stream"); + this.notEnoughBytes = true; + return true; + } + + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlSetDefaultVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlSetDefaultVisitor.cs new file mode 100644 index 0000000000..aab1bc90ba --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlSetDefaultVisitor.cs @@ -0,0 +1,49 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Fields; + +/// +/// This is similar to InitVisitor but also initializes +/// nested fields. +/// +internal sealed class JxlSetDefaultVisitor : JxlVisitorBase +{ + public override bool Bits(int bits, uint defaultValue, ref uint value) + { + value = defaultValue; + return true; + } + + public override bool U32(JxlU32Distribution d0, JxlU32Distribution d1, JxlU32Distribution d2, JxlU32Distribution d3, uint defaultValue, ref uint value) + { + value = defaultValue; + return true; + } + + public override bool U64(ulong defaultValue, ref ulong value) + { + value = defaultValue; + return true; + } + + public override bool Boolean(bool defaultValue, ref bool value) + { + value = defaultValue; + return true; + } + + public override bool F16(float defaultValue, ref float value) + { + value = defaultValue; + return true; + } + + public override bool Conditional(bool condition) => true; + + public override bool AllDefault(IJxlFields fields, ref bool allDefault) + { + _ = this.Boolean(true, ref allDefault); + return false; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs new file mode 100644 index 0000000000..e909f7f74f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs @@ -0,0 +1,127 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Jxl.IO; + +namespace SixLabors.ImageSharp.Formats.Jxl.Fields; + +/// +/// Unsigned 32-bit variable-length integer coder. +/// +internal static class JxlU32Coder +{ + /// + /// Maximum number of writeable and/or readable bits in a variable-length integer. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int MaxEncodedBits(in JxlU32Enc enc) + { + int extraBits = 0; + + for (int selector = 0; selector < 4; selector++) + { + JxlU32Distribution distr = enc.GetDistribution(selector); + + if (distr.IsDirect) + { + continue; + } + else + { + extraBits = Math.Max(extraBits, (int)distr.ExtraBits); + } + } + + return 2 + extraBits; + } + + /// + /// Verifies that the value can be encoded. + /// + public static bool CanEncode(in JxlU32Enc enc, uint value, ref int encodedBits) + { + uint selector = 0; + int totalBits = 0; + + bool isOk = ChooseSelector(in enc, value, ref selector, ref totalBits); + + encodedBits = isOk ? totalBits : 0; + + return isOk; + } + + /// + /// Reads the U32 coded value. + /// + public static uint Read(in JxlU32Enc enc, JxlBitReader reader) + { + uint selector = reader.ReadBits32(2u); + JxlU32Distribution dist = enc.GetDistribution((int)selector); + + if (dist.IsDirect) + { + return dist.Direct; + } + else + { + return reader.ReadBits32(dist.ExtraBits) + dist.Offset; + } + } + + /// + /// Tries to find the best one of the four selectors based on the value. + /// + public static bool ChooseSelector(in JxlU32Enc enc, uint value, ref uint selector, ref int totalBits) + { + int bitsRequired = 32 - Num0BitsAboveMS1Bit(value); + + if (bitsRequired > 32) + { + return false; + } + + selector = 0; + totalBits = 64; + + for (int s = 0; s < 4; s++) + { + JxlU32Distribution dist = enc.GetDistribution(s); + + if (dist.IsDirect) + { + if (dist.Direct == value) + { + selector = (uint)s; + totalBits = 2; + return true; + } + + continue; + } + + uint extraBits = dist.ExtraBits; + uint offset = dist.Offset; + + if (value < offset || value >= offset + (1u << (int)extraBits)) + { + continue; + } + + if (2 + extraBits < totalBits) + { + selector = (uint)s; + totalBits = 2 + (int)extraBits; + } + } + + if (totalBits == 64) + { + DebugGuard.IsTrue(false, "No matching selector"); + + return false; + } + + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs new file mode 100644 index 0000000000..6f5fb8b281 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs @@ -0,0 +1,105 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.IO; + +namespace SixLabors.ImageSharp.Formats.Jxl.Fields; + +/// +/// Unsigned 64-bit variable-length coding. +/// +internal static class JxlU64Coder +{ + /// + /// Reads the variable-length, unsigned 64-bit integer. + /// + public static ulong Read(JxlBitReader reader) + { + uint selector = reader.ReadBits32(2u); + + if (selector == 0u) + { + return 0u; + } + else if (selector == 1u) + { + return 1u + reader.ReadBits32(4u); + } + else if (selector == 2u) + { + return 17u + reader.ReadBits32(8u); + } + + // Selector 3... + ulong result = reader.ReadBits32(12u); + int shift = 12; + + while (reader.ReadBoolean()) + { + if (shift == 60) + { + result |= (ulong)reader.ReadBits32(4u) << shift; + break; + } + + result |= (ulong)reader.ReadBits32(8u) << shift; + shift += 8; + } + + return result; + } + + /// + /// Returns a value indicating whether can the value be encoded, + /// as well as the number of encoded bits. + /// + public static bool CanEncode(ulong value, ref int encodedBits) + { + if (value == 0) + { + // 2 selector bits + encodedBits = 2; + } + else if (value <= 16) + { + // 2 selector bits + 4 payload bits + encodedBits = 2 + 4; + } + else if (value <= 272) + { + // 2 selector bits + 8 payload bits + encodedBits = 2 + 8; + } + else + { + // 2 selector bits + 12 payload bits + encodedBits = 2 + 12; + value >>= 12; + int shift = 12; + while (value > 0 && shift < 60) + { + // 1 continuation bit + 8 payload bits + encodedBits += 1 + 8; + value >>= 8; + shift += 8; + } + if (value > 0) + { + // 1 continuation bit + 4 payload bits + encodedBits += 1 + 4; + } + else + { + // 1 stop bit + encodedBits += 1; + } + } + + return true; + } + + /// + /// Always returns 73. + /// + public static int MaxEncodedBits() => 73; +} diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlVisitor.cs new file mode 100644 index 0000000000..7fa79f75d2 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlVisitor.cs @@ -0,0 +1,92 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Fields; + +/// +/// Base JPEG XL visitor that can visit all fields of a class. +/// This is highly similar to the following Reflection code, but with +/// lower overhead: +/// +/// // Pseudocode +/// void Visit(Type type) +/// { +/// foreach (PropertyInfo property in type.GetProperties()) +/// { +/// /* visitor implementation */(property); +/// } +/// } +/// +/// +internal class JxlVisitor +{ + public virtual bool IsReading => false; + + public virtual bool Visit(IJxlFields fields) => false; + + public virtual bool Boolean(bool defaultValue, ref bool value) => false; + + public virtual bool U32(JxlU32Enc enc, uint defaultValue, ref uint value) => false; + + public virtual bool U32( + JxlU32Distribution d0, + JxlU32Distribution d1, + JxlU32Distribution d2, + JxlU32Distribution d3, + uint defaultValue, + ref uint value) + => this.U32(new JxlU32Enc(d0, d1, d2, d3), value, ref defaultValue); + + public virtual unsafe bool Enum(T defaultValue, ref T value) + where T : unmanaged + { + DebugGuard.IsTrue(sizeof(T) == 4, "We use unsafe bit casting so anything beside 4 bytes will break memory layout"); + + ref uint u32 = ref Unsafe.As(ref value); + if (!this.U32( + JxlFieldExpressions.Value(0), + JxlFieldExpressions.Value(1), + JxlFieldExpressions.BitsOffset(4, 2), + JxlFieldExpressions.BitsOffset(6, 18), + Unsafe.BitCast(defaultValue), + ref u32)) + { + return false; + } + + return System.Enum.IsDefined(typeof(T), value); + } + + public virtual bool Bits(int bits, uint defaultValue, ref uint value) => false; + + public virtual bool U64(ulong defaultValue, ref ulong value) => false; + + public virtual bool F16(float defaultValue, ref float value) => false; + + public virtual bool Conditional(bool condition) => condition; + + public virtual bool AllDefault(IJxlFields fields, ref bool allDefault) + { + // Do not remove the fields parameter, derived classes + // use it. + if (!this.Boolean(true, ref allDefault)) + { + return false; + } + + return allDefault; + } + + public virtual void SetDefault(IJxlFields fields) + { + // Used by derived methods. + } + + public virtual bool VisitNested(IJxlFields fields) => this.Visit(fields); + + public virtual bool BeginExtensions(ref ulong extensions) => false; + + public virtual bool EndExtensions() => false; +} diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlVisitorBase.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlVisitorBase.cs new file mode 100644 index 0000000000..7d2dffffbf --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlVisitorBase.cs @@ -0,0 +1,74 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; + +#pragma warning disable SA1405 // Debug.Assert should provide message text + +namespace SixLabors.ImageSharp.Formats.Jxl.Fields; + +internal class JxlVisitorBase : JxlVisitor +{ + private readonly JxlExtensionStates extensionStates = new(); + private int depth; + + public override bool Visit(IJxlFields fields) + { + if (this.depth >= JxlBundle.MaxExtensions) + { + return false; + } + + this.depth++; + this.extensionStates.Push(); + + bool visited = fields.Visit(this); + + if (visited) + { + // TODO: use DebugGuard + Debug.Assert(!this.extensionStates.IsBegun || this.extensionStates.IsEnded); + } + + this.extensionStates.Pop(); + + // TODO: use DebugGuard + Debug.Assert(this.depth != 0); + this.depth--; + + return visited; + } + + public override bool Boolean(bool defaultValue, ref bool value) + { + uint bits = value ? 1u : 0u; + if (!this.Bits(1, defaultValue ? 1u : 0u, ref bits)) + { + return false; + } + + // TODO: use DebugGuard + Debug.Assert(bits <= 1u); + + value = bits == 1u; + + return true; + } + + public override bool BeginExtensions(ref ulong extensions) + { + if (!this.U64(0uL, ref extensions)) + { + return false; + } + + this.extensionStates.Begin(); + return true; + } + + public override bool EndExtensions() + { + this.extensionStates.End(); + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs b/src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs index 99b1599915..50753321e7 100644 --- a/src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs +++ b/src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs @@ -14,7 +14,16 @@ internal sealed class JxlBitReader(ReadOnlyMemory bytes) private ulong buffer; private uint bufferRemainingBits; private int pointer; - private bool endOfStream; + + /// + /// Gets a value indicating whether this marks an end of stream. + /// + public bool IsEndOfStream { get; private set; } + + /// + /// Gets the total number of bits consumed. + /// + public long TotalBitsConsumed => ((long)this.pointer * 8) + (64 - this.bufferRemainingBits); /// /// Fetches a new buffer. @@ -29,7 +38,7 @@ private void RefillCore() // we don't have any more data... mark an end of stream this.buffer = 0; this.bufferRemainingBits = 0; - this.endOfStream = true; + this.IsEndOfStream = true; return; } @@ -66,7 +75,7 @@ private ulong ReadBits64Core(uint n, bool peek = false) Debug.Assert(n <= 64, "Too many bits to pack into ulong"); this.MaybeRefill(); - if (this.endOfStream) + if (this.IsEndOfStream) { JxlThrowHelper.ThrowEndOfStream(); } @@ -111,7 +120,7 @@ private uint ReadBits32Core(uint n, bool peek = false) Debug.Assert(n <= 32, "Too many bits to pack into uint"); this.MaybeRefill(); - if (this.endOfStream) + if (this.IsEndOfStream) { JxlThrowHelper.ThrowEndOfStream(); } @@ -157,11 +166,11 @@ private uint ReadBits32Core(uint n, bool peek = false) public void SkipBits32(uint bits) => _ = this.ReadBits32(bits); - public ulong ReadBits64(uint bits) => this.ReadBits64Core(bits, peek: false); + public ulong ReadBits64(ulong bits) => this.ReadBits64Core((uint)bits, peek: false); - public ulong PeekBits64(uint bits) => this.ReadBits64Core(bits, peek: true); + public ulong PeekBits64(ulong bits) => this.ReadBits64Core((uint)bits, peek: true); - public void SkipBits64(uint bits) => _ = this.ReadBits64(bits); + public void SkipBits64(ulong bits) => _ = this.ReadBits64(bits); public bool ReadBoolean() => this.ReadBits32Core(1, peek: false) == 1; } diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlBitDepth.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlBitDepth.cs index 0df657f44d..a60ac0f137 100644 --- a/src/ImageSharp/Formats/Jxl/Metadata/JxlBitDepth.cs +++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlBitDepth.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.Fields; namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlCustomTransformData.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlCustomTransformData.cs index 78c0e9ba81..8b938b736b 100644 --- a/src/ImageSharp/Formats/Jxl/Metadata/JxlCustomTransformData.cs +++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlCustomTransformData.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.Fields; namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; From a6a0263836f469d4fd8e810ae354e91cbe65f9ce Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 18 Jul 2026 16:21:25 +0400 Subject: [PATCH 040/328] Implement visitor for JxlBitDepth --- .../Formats/Jxl/Metadata/JxlBitDepth.cs | 93 ++++++++++++++++++- 1 file changed, 90 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlBitDepth.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlBitDepth.cs index a60ac0f137..fd842b3633 100644 --- a/src/ImageSharp/Formats/Jxl/Metadata/JxlBitDepth.cs +++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlBitDepth.cs @@ -5,8 +5,19 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; +/// +/// Represents the JPEG XL Bit Depth image metadata. +/// internal sealed class JxlBitDepth : IJxlFields { + private uint bitsPerSample; + private uint exponentBitsPerSample; + + /// + /// Initializes a new instance of the class. + /// + public JxlBitDepth() => JxlBundle.Init(this); + /// /// Gets or sets a value indicating whether /// the original (uncompressed) samples are floating point or @@ -18,7 +29,11 @@ internal sealed class JxlBitDepth : IJxlFields /// Gets or sets the bit depth of the original (uncompressed) image samples. /// Must be in the range [1, 32]. /// - public int BitsPerSample { get; set; } + public uint BitsPerSample + { + get => this.bitsPerSample; + set => this.bitsPerSample = value; + } /// /// @@ -36,7 +51,79 @@ internal sealed class JxlBitDepth : IJxlFields /// [2, 8] and amount of mantissa bits must be in the range [2, 23]. /// /// - public int ExponentBitsPerSample { get; set; } + public uint ExponentBitsPerSample + { + get => this.exponentBitsPerSample; + set => this.exponentBitsPerSample = value; + } + + public bool Visit(JxlVisitor visitor) + { + if (!this.FloatingPointSample) + { + bool successful = visitor.U32( + JxlFieldExpressions.Value(8u), + JxlFieldExpressions.Value(10u), + JxlFieldExpressions.Value(12u), + JxlFieldExpressions.BitsOffset(6u, 1u), + 8u, + ref this.bitsPerSample); + + if (!successful) + { + return false; + } + + this.exponentBitsPerSample = 0; + } + else + { + if (!visitor.U32( + JxlFieldExpressions.Value(32u), + JxlFieldExpressions.Value(16u), + JxlFieldExpressions.Value(24u), + JxlFieldExpressions.BitsOffset(6u, 1u), + 32u, + ref this.bitsPerSample)) + { + return false; + } + + this.exponentBitsPerSample--; + + if (!visitor.Bits(4, 7, ref this.exponentBitsPerSample)) + { + return false; + } + + this.exponentBitsPerSample++; + } + + if (this.FloatingPointSample) + { + if (this.exponentBitsPerSample is < 2 or > 8) + { + DebugGuard.IsTrue(false, "Invalid exponent_bits_per_sample"); + + return false; + } + + int mantissaBits = (int)this.bitsPerSample - (int)this.exponentBitsPerSample - 1; + + if (mantissaBits is < 2 or > 23) + { + DebugGuard.IsTrue(false, "Invalid bits_per_sample"); + + return false; + } + } + else if (this.bitsPerSample > 31) + { + DebugGuard.IsTrue(false, "Invalid bits_per_sample"); + + return false; + } - public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); + return true; + } } From 6aa313f89589a04d3a5f7ed6665e3d27d5af2ef7 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 18 Jul 2026 16:56:36 +0400 Subject: [PATCH 041/328] Add matrices --- .../Jxl/Metadata/JxlOpsinInverseMatrix.cs | 5 +- .../Formats/Jxl/Processing/JxlLoopFilter.cs | 2 +- .../Formats/Jxl/Processing/JxlMatrix3x3.cs | 142 ++++++++++++++++++ .../Formats/Jxl/Processing/JxlMatrix3x3F.cs | 142 ++++++++++++++++++ 4 files changed, 288 insertions(+), 3 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3F.cs diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlOpsinInverseMatrix.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlOpsinInverseMatrix.cs index ccf2b25303..d0a14ffb40 100644 --- a/src/ImageSharp/Formats/Jxl/Metadata/JxlOpsinInverseMatrix.cs +++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlOpsinInverseMatrix.cs @@ -1,7 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.Processing; namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; @@ -9,7 +10,7 @@ internal sealed class JxlOpsinInverseMatrix : IJxlFields { public bool AllDefault { get; set; } - public JxlMatrix3x3 InverseMatrix { get; set; } + public JxlMatrix3x3F InverseMatrix { get; set; } public InlineArray3 OpsinBiases { get; set; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs index 01759a4d93..1a98138a37 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs @@ -237,7 +237,7 @@ public bool ComputeSigma(Rectangle blockRect, JxlPassesDecoderState state) { for (int iy = 0; iy < SigmaBorder; iy++) { - sigmaRow.Slice(offsetBefore + (SigmaPadding - 1 - iy) * sigmaStride, num) + sigmaRow.Slice(offsetBefore + ((SigmaPadding - 1 - iy) * sigmaStride), num) .CopyTo(sigmaRow.Slice(offsetBefore + ((SigmaPadding + iy) * sigmaStride))); } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3.cs new file mode 100644 index 0000000000..a01e2a6216 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3.cs @@ -0,0 +1,142 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; + +#pragma warning disable IDE0044 // Add readonly modifier +#pragma warning disable IDE0051 // Remove unused private members + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal struct JxlMatrix3x3 +{ + /// + /// Represents the matrix element at 0,0. + /// + private double e00; + + /// + /// Represents the matrix element at 0,1. + /// + private double e01; + + /// + /// Represents the matrix element at 0,2. + /// + private double e02; + + /// + /// Represents the matrix element at 1,0. + /// + private double e10; + + /// + /// Represents the matrix element at 1,1. + /// + private double e11; + + /// + /// Represents the matrix element at 1,2. + /// + private double e12; + + /// + /// Represents the matrix element at 2,0. + /// + private double e20; + + /// + /// Represents the matrix element at 2,1. + /// + private double e21; + + /// + /// Represents the matrix element at 2,2. + /// + private double e22; + + /// + /// Wraps all these values into a Span. + /// + /// A Span with all matrix elements. + public Span AsSpan() => MemoryMarshal.CreateSpan(ref this.e00, 9); + + /// + /// Wraps all these values into a ReadOnlySpan. + /// + /// A ReadOnlySpan with all matrix elements. + public ReadOnlySpan AsReadOnlySpan() => MemoryMarshal.CreateReadOnlySpan(ref this.e00, 9); + + public static void Multiply(in JxlMatrix3x3 a, in JxlMatrix3x3 b, ref JxlMatrix3x3 c) + { + ReadOnlySpan spanA = a.AsReadOnlySpan(); + ReadOnlySpan spanB = b.AsReadOnlySpan(); + Span spanC = c.AsSpan(); + + for (int row = 0; row < 3; row++) + { + int row3 = row * 3; + for (int col = 0; col < 3; col++) + { + double sum = 0d; + for (int k = 0; k < 3; k++) + { + sum += spanA[row3 + k] * spanB[(k * 3) + col]; + } + + spanC[row3 + col] = sum; + } + } + } + + public static void Multiply(in JxlMatrix3x3 a, ReadOnlySpan b, Span c) + { + ReadOnlySpan spanA = a.AsReadOnlySpan(); + + for (int row = 0; row < 3; row++) + { + double sum = 0f; + int row3 = row * 3; + for (int col = 0; col < 3; col++) + { + sum += spanA[row3 + col] * b[col]; + } + + c[row] = sum; + } + } + + public static bool Invert(ref JxlMatrix3x3 matrix) + { + ReadOnlySpan m = matrix.AsReadOnlySpan(); + Span temp = + [ + ((double)m[4] * m[8]) - ((double)m[5] * m[7]), + ((double)m[2] * m[7]) - ((double)m[1] * m[8]), + ((double)m[1] * m[5]) - ((double)m[2] * m[4]), + ((double)m[5] * m[6]) - ((double)m[3] * m[8]), + ((double)m[0] * m[8]) - ((double)m[2] * m[6]), + ((double)m[2] * m[3]) - ((double)m[0] * m[5]), + ((double)m[3] * m[7]) - ((double)m[4] * m[6]), + ((double)m[1] * m[6]) - ((double)m[0] * m[7]), + ((double)m[0] * m[4]) - ((double)m[1] * m[3]), + ]; + + double det = (m[0] * temp[0]) + (m[1] * temp[3]) + (m[2] * temp[6]); + + if (Math.Abs(det) < 1e-10) + { + return false; + } + + double idet = 1.0 / det; + Span spanM = matrix.AsSpan(); + + for (int i = 0; i < 9; i++) + { + spanM[i] = (double)(temp[i] * idet); + } + + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3F.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3F.cs new file mode 100644 index 0000000000..fda6282e05 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3F.cs @@ -0,0 +1,142 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; + +#pragma warning disable IDE0044 // Add readonly modifier +#pragma warning disable IDE0051 // Remove unused private members + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal struct JxlMatrix3x3F +{ + /// + /// Represents the matrix element at 0,0. + /// + private float e00; + + /// + /// Represents the matrix element at 0,1. + /// + private float e01; + + /// + /// Represents the matrix element at 0,2. + /// + private float e02; + + /// + /// Represents the matrix element at 1,0. + /// + private float e10; + + /// + /// Represents the matrix element at 1,1. + /// + private float e11; + + /// + /// Represents the matrix element at 1,2. + /// + private float e12; + + /// + /// Represents the matrix element at 2,0. + /// + private float e20; + + /// + /// Represents the matrix element at 2,1. + /// + private float e21; + + /// + /// Represents the matrix element at 2,2. + /// + private float e22; + + /// + /// Wraps all these values into a Span. + /// + /// A Span with all matrix elements. + public Span AsSpan() => MemoryMarshal.CreateSpan(ref this.e00, 9); + + /// + /// Wraps all these values into a ReadOnlySpan. + /// + /// A ReadOnlySpan with all matrix elements. + public ReadOnlySpan AsReadOnlySpan() => MemoryMarshal.CreateReadOnlySpan(ref this.e00, 9); + + public static void Multiply(in JxlMatrix3x3F a, in JxlMatrix3x3F b, ref JxlMatrix3x3F c) + { + ReadOnlySpan spanA = a.AsReadOnlySpan(); + ReadOnlySpan spanB = b.AsReadOnlySpan(); + Span spanC = c.AsSpan(); + + for (int row = 0; row < 3; row++) + { + int row3 = row * 3; + for (int col = 0; col < 3; col++) + { + float sum = 0f; + for (int k = 0; k < 3; k++) + { + sum += spanA[row3 + k] * spanB[(k * 3) + col]; + } + + spanC[row3 + col] = sum; + } + } + } + + public static void Multiply(in JxlMatrix3x3F a, ReadOnlySpan b, Span c) + { + ReadOnlySpan spanA = a.AsReadOnlySpan(); + + for (int row = 0; row < 3; row++) + { + float sum = 0f; + int row3 = row * 3; + for (int col = 0; col < 3; col++) + { + sum += spanA[row3 + col] * b[col]; + } + + c[row] = sum; + } + } + + public static bool Invert(ref JxlMatrix3x3F matrix) + { + ReadOnlySpan m = matrix.AsReadOnlySpan(); + Span temp = + [ + ((double)m[4] * m[8]) - ((double)m[5] * m[7]), + ((double)m[2] * m[7]) - ((double)m[1] * m[8]), + ((double)m[1] * m[5]) - ((double)m[2] * m[4]), + ((double)m[5] * m[6]) - ((double)m[3] * m[8]), + ((double)m[0] * m[8]) - ((double)m[2] * m[6]), + ((double)m[2] * m[3]) - ((double)m[0] * m[5]), + ((double)m[3] * m[7]) - ((double)m[4] * m[6]), + ((double)m[1] * m[6]) - ((double)m[0] * m[7]), + ((double)m[0] * m[4]) - ((double)m[1] * m[3]), + ]; + + double det = (m[0] * temp[0]) + (m[1] * temp[3]) + (m[2] * temp[6]); + + if (Math.Abs(det) < 1e-10) + { + return false; + } + + double idet = 1.0 / det; + Span spanM = matrix.AsSpan(); + + for (int i = 0; i < 9; i++) + { + spanM[i] = (float)(temp[i] * idet); + } + + return true; + } +} From d401b71313ff188473744d4f1516bcfd3afc8b03 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 18 Jul 2026 17:01:25 +0400 Subject: [PATCH 042/328] Fix errors --- .../Formats/Jxl/Processing/JxlLoopFilter.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs index 1a98138a37..312138db43 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs @@ -12,6 +12,9 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing; /// internal sealed class JxlLoopFilter : IJxlFields { + private const int SigmaBorder = 1; + private const int SigmaPadding = 2; + /// /// 4 * (sqrt(0.5)-1), so that Weight(sigma) = 0.5 /// @@ -206,7 +209,7 @@ public bool ComputeSigma(Rectangle blockRect, JxlPassesDecoderState state) { float sigma = sigmaQuant * this.EpfSharpLookup[sharpnessRow[bx + ix + iy + sharpnessStride]]; sigma = MathF.Min(-1e-4f, sigma); - sigmaRow[bx + ix + SigmaPadding + (iy + SigmaPadding) * sigmaStride] = 1.0f / sigma; + sigmaRow[bx + ix + SigmaPadding + ((iy + SigmaPadding) * sigmaStride)] = 1.0f / sigma; } } @@ -214,7 +217,7 @@ public bool ComputeSigma(Rectangle blockRect, JxlPassesDecoderState state) { for (int iy = 0; iy < acs.CoveredBlocksY; iy++) { - LeftMirror(sigmaRow.Slice(SigmaPadding + (iy + SigmaPadding) * SigmaStride), sigmaBorder); + LeftMirror(sigmaRow[(SigmaPadding + ((iy + SigmaPadding) * sigmaStride))..], SigmaBorder); } } @@ -222,7 +225,7 @@ public bool ComputeSigma(Rectangle blockRect, JxlPassesDecoderState state) { for (int iy = 0; iy < acs.CoveredBlocksY; iy++) { - RightMirror(sigmaRow.Slice(SigmaPadding + bx + llfX + (iy + SigmaPadding) * sigmaStride), SigmaBorder); + RightMirror(sigmaRow[(SigmaPadding + bx + llfX + ((iy + SigmaPadding) * sigmaStride))..], SigmaBorder); } } @@ -238,7 +241,7 @@ public bool ComputeSigma(Rectangle blockRect, JxlPassesDecoderState state) for (int iy = 0; iy < SigmaBorder; iy++) { sigmaRow.Slice(offsetBefore + ((SigmaPadding - 1 - iy) * sigmaStride), num) - .CopyTo(sigmaRow.Slice(offsetBefore + ((SigmaPadding + iy) * sigmaStride))); + .CopyTo(sigmaRow[(offsetBefore + ((SigmaPadding + iy) * sigmaStride))..]); } } @@ -246,8 +249,8 @@ public bool ComputeSigma(Rectangle blockRect, JxlPassesDecoderState state) { for (int iy = 0; iy < SigmaBorder; iy++) { - sigmaRow.Slice(offsetBefore + (sigmaStride * (acs.CoveredBlocksX + SigmaPadding + iy))) - .CopyTo(sigmaRow.Slice(offsetBefore + (sigmaStride * (acs.CoveredBlocksY + SigmaPadding - 1 - iy)))); + sigmaRow[(offsetBefore + (sigmaStride * (acs.CoveredBlocksX + SigmaPadding + iy)))..] + .CopyTo(sigmaRow[(offsetBefore + (sigmaStride * (acs.CoveredBlocksY + SigmaPadding - 1 - iy)))..]); } } } From a803f01ab603333bb324b1c14684c4be629630e1 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 18 Jul 2026 21:27:27 +0400 Subject: [PATCH 043/328] Add Y'Cb'Cr chroma subsampling as part of the frame header See frame_header.h --- .../Jxl/IO/JxlYCbCrChromaSubsampling.cs | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/IO/JxlYCbCrChromaSubsampling.cs diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlYCbCrChromaSubsampling.cs b/src/ImageSharp/Formats/Jxl/IO/JxlYCbCrChromaSubsampling.cs new file mode 100644 index 0000000000..90d9cf6738 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/JxlYCbCrChromaSubsampling.cs @@ -0,0 +1,147 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Fields; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO; + +/// +/// Gets the Y'Cb'Cr chroma subsampling information as part +/// of the JPEG XL Frame Header. +/// +internal sealed class JxlYCbCrChromaSubsampling : IJxlFields +{ + private readonly int[] channelMode = new int[3]; + + private static ReadOnlySpan HShiftData => [0, 1, 1, 0]; + + private static ReadOnlySpan VShiftData => [0, 1, 0, 1]; + + public byte MaxHShift { get; private set; } + + public byte MaxVShift { get; private set; } + + /// + /// Gets a value indicating whether this 4:4:4 chroma subsampling. + /// + public bool Is444 => + this.HShift(0) == 0 && this.VShift(0) == 0 && // Cb + this.HShift(2) == 0 && this.VShift(2) == 0 && // Cr + this.HShift(1) == 0 && this.VShift(1) == 0; // Y; + + /// + /// Gets a value indicating whether this 4:2:0 chroma subsampling. + /// + public bool Is420 => + this.HShift(0) == 1 && this.VShift(0) == 1 && // Cb + this.HShift(2) == 1 && this.VShift(2) == 1 && // Cr + this.HShift(1) == 0 && this.VShift(1) == 0; // Y + + /// + /// Gets a value indicating whether this 4:2:2 chroma subsampling. + /// + public bool Is422 => + this.HShift(0) == 1 && this.VShift(0) == 0 && // Cb + this.HShift(2) == 1 && this.VShift(2) == 0 && // Cr + this.HShift(1) == 0 && this.VShift(1) == 0; // Y + + /// + /// Gets a value indicating whether this 4:4:0 chroma subsampling. + /// + public bool Is440 => + this.HShift(0) == 0 && this.VShift(0) == 1 && // Cb + this.HShift(2) == 0 && this.VShift(2) == 1 && // Cr + this.HShift(1) == 0 && this.VShift(1) == 0; // Y + + public byte RawHShift(int c) => HShiftData[this.channelMode[c]]; + + public byte RawVShift(int c) => VShiftData[this.channelMode[c]]; + + public byte HShift(int c) => (byte)(this.MaxHShift - HShiftData[this.channelMode[c]]); + + public byte VShift(int c) => (byte)(this.MaxVShift - VShiftData[this.channelMode[c]]); + + private void Recompute() + { + this.MaxHShift = 0; + this.MaxVShift = 0; + + for (int i = 0; i < 3; i++) + { + int ch = this.channelMode[i]; + + this.MaxHShift = Math.Max(this.MaxHShift, HShiftData[ch]); + this.MaxVShift = Math.Max(this.MaxVShift, VShiftData[ch]); + } + } + + public bool Set(ReadOnlySpan hsample, ReadOnlySpan vsample) + { + for (int c = 0; c < 3; c++) + { + int cjpeg = c < 2 ? (c ^ 1) : c; + int i = 0; + + for (; i < 4; i++) + { + if (1 << HShiftData[i] == hsample[cjpeg] && 1 << VShiftData[i] == vsample[cjpeg]) + { + this.channelMode[c] = i; + break; + } + } + + if (i == 4) + { + return false; + } + } + + this.Recompute(); + return true; + } + + public override string ToString() + { + if (this.Is444) + { + return "4:4:4"; + } + else if (this.Is420) + { + return "4:2:0"; + } + else if (this.Is422) + { + return "4:2:2"; + } + else if (this.Is440) + { + return "4:4:0"; + } + else + { + return $"[Custom] {this.channelMode[0]}:{this.channelMode[1]}:{this.channelMode[2]}"; + } + } + + public bool Visit(JxlVisitor visitor) + { + for (int i = 0; i < 3; i++) + { + int channel = this.channelMode[i]; + + uint unsignedChannel = (uint)channel; + bool wroteSuccessfully = visitor.Bits(2, 0, ref unsignedChannel); + + if (!wroteSuccessfully) + { + return false; + } + + this.channelMode[i] = (int)unsignedChannel; + } + + return true; + } +} From 16c55f343ad805e916cb39e2e366e4ab8b92b890 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 20 Jul 2026 01:43:13 +0400 Subject: [PATCH 044/328] Add frame header --- .../Formats/Jxl/Fields/JxlF16Coder.cs | 1 - .../Formats/Jxl/Fields/JxlU32Enc.cs | 6 +- .../Formats/Jxl/Fields/JxlU64Coder.cs | 1 + .../Jxl/IO/FrameHeader/JxlAnimationFrame.cs | 74 ++ .../Jxl/IO/FrameHeader/JxlBlendMode.cs | 64 ++ .../Jxl/IO/FrameHeader/JxlBlendingInfo.cs | 146 ++++ .../Jxl/IO/FrameHeader/JxlColorTransform.cs | 26 + .../FrameHeader/JxlColorTransformHelpers.cs | 39 + .../Jxl/IO/FrameHeader/JxlFrameEncoding.cs | 20 + .../Jxl/IO/FrameHeader/JxlFrameHeader.cs | 743 ++++++++++++++++++ .../Jxl/IO/FrameHeader/JxlFrameHeaderFlags.cs | 36 + .../Jxl/IO/FrameHeader/JxlFrameType.cs | 37 + .../Formats/Jxl/IO/FrameHeader/JxlPasses.cs | 220 ++++++ .../JxlYCbCrChromaSubsampling.cs | 2 +- 14 files changed, 1410 insertions(+), 5 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlAnimationFrame.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendMode.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendingInfo.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransform.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransformHelpers.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameEncoding.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeaderFlags.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameType.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlPasses.cs rename src/ImageSharp/Formats/Jxl/IO/{ => FrameHeader}/JxlYCbCrChromaSubsampling.cs (98%) diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs index 7bf83e1854..3c4126c4b8 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Numerics; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.IO; diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlU32Enc.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Enc.cs index 9583f57dac..5b6e10b847 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlU32Enc.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Enc.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Diagnostics; - namespace SixLabors.ImageSharp.Formats.Jxl.Fields; internal readonly struct JxlU32Enc @@ -19,7 +17,9 @@ public JxlU32Enc(JxlU32Distribution d0, JxlU32Distribution d1, JxlU32Distributio public JxlU32Distribution GetDistribution(int selector) { - Debug.Assert(selector < 4, "Selector out of range"); + // This stuff is internal, so if argument check + // fails it's not a user error. + DebugGuard.MustBeLessThan(selector, 4, nameof(selector)); return this.d[selector]; } diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs index 6f5fb8b281..ba121cafe2 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs @@ -83,6 +83,7 @@ public static bool CanEncode(ulong value, ref int encodedBits) value >>= 8; shift += 8; } + if (value > 0) { // 1 continuation bit + 4 payload bits diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlAnimationFrame.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlAnimationFrame.cs new file mode 100644 index 0000000000..1854b4ab67 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlAnimationFrame.cs @@ -0,0 +1,74 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.Metadata; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; + +/// +/// Describes duration of frames that make up an animation. +/// +internal sealed class JxlAnimationFrame : IJxlFields +{ + /// + /// See . + /// + private uint duration; + + /// + /// See . + /// + private uint timecode; + + /// + /// Gets or sets the duration of the animation. + /// + public uint Duration + { + get => this.duration; + set => this.duration = value; + } + + /// + /// Gets or sets the timecode of the animation. The + /// format is 0xHHMMSSFF. + /// + public uint Timecode + { + get => this.timecode; + set => this.timecode = value; + } + + /// + /// Gets or sets the optional codec metadata. + /// + public JxlCodecMetadata? CodecMetadata { get; set; } + + public bool Visit(JxlVisitor visitor) + { + if (visitor.Conditional(this.CodecMetadata?.ImageMetadata?.HaveAnimation == true)) + { + if (!visitor.U32( + JxlFieldExpressions.Value(0), + JxlFieldExpressions.Value(1), + JxlFieldExpressions.Bits(8), + JxlFieldExpressions.Bits(32), + 0, + ref this.duration)) + { + return false; + } + } + + if (visitor.Conditional(this.CodecMetadata?.ImageMetadata?.Animation?.ContainsTimecodes == true)) + { + if (!visitor.Bits(32, 0u, ref this.timecode)) + { + return false; + } + } + + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendMode.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendMode.cs new file mode 100644 index 0000000000..035a48edd5 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendMode.cs @@ -0,0 +1,64 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; + +/// +/// Represents the blending mode describing how to combine +/// current frame with previously saved frame. +/// +internal enum JxlBlendMode : byte +{ + /// + /// New values replace old ones. + /// + /// sample = new + /// + /// + Replace, + + /// + /// New values add to the old ones. + /// + /// sample = old + new + /// + /// + Add, + + /// + /// New values replace old ones if alpha>0: + /// + /// alpha = old + new * (1 - old) + /// + /// For other channels if !alpha_associated: + /// + /// sample = ((1 - newAlpha) * old * oldAlpha + newAlpha * new) / alpha + /// + /// For other channels if alpha_associated: + /// + /// sample = (1 - newAlpha) * old + new + /// + /// + Blend, + + /// + /// New values are added to the old ones if alpha>0: + /// For the alpha channel that is used as source: + /// + /// sample = old + new * (1 - old) + /// + /// Otherwise: + /// + /// sample = old + alpha * new + /// + /// + AlphaWeightedBlend, + + /// + /// New values are multiplied by old ones: + /// + /// sample = old * new + /// + /// + Multiply +} diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendingInfo.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendingInfo.cs new file mode 100644 index 0000000000..58c79d22db --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendingInfo.cs @@ -0,0 +1,146 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Fields; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; + +/// +/// Provides options and instructions that tell the decoder the proper +/// way to blend the current and previous frame together. +/// +internal sealed class JxlBlendingInfo : IJxlFields +{ + /// + /// Initializes a new instance of the class. + /// + public JxlBlendingInfo() => JxlBundle.Init(this); + + /// + /// Gets or sets the blending mode. See . + /// + public JxlBlendMode BlendMode { get; set; } + + /// + /// Gets or sets the value that indicates which extra channel + /// to use as alpha channel for blending. + /// + public uint AlphaChannel { get; set; } + + /// + /// Gets or sets a value indicating whether the alpha or channel values + /// must be clamped* to the 0 through 1 range. + /// + /// + /// Clamped - must be limited to the specified range. + /// + public bool Clamp { get; set; } + + /// + /// Gets or sets the frame ID to copy from (0 through 3). + /// + /// + /// If is equal to , + /// the value of this property is ignored. + /// + public uint Source { get; set; } + + /// + /// Gets or sets the total number of extra channels. + /// + public int ExtraChannelCount { get; set; } + + /// + /// Gets or sets a value indicating whether the frame is partial. + /// + public bool IsPartialFrame { get; set; } + + public bool Visit(JxlVisitor visitor) + { + JxlBlendMode mode = this.BlendMode; + if (!VisitBlendMode(visitor, JxlBlendMode.Replace, ref mode)) + { + return false; + } + + this.BlendMode = mode; + + if (visitor.Conditional(this.ExtraChannelCount > 0 && mode is JxlBlendMode.Blend or JxlBlendMode.AlphaWeightedBlend)) + { + uint alphaChannel = this.AlphaChannel; + if (!visitor.U32( + JxlFieldExpressions.Value(0u), + JxlFieldExpressions.Value(1u), + JxlFieldExpressions.Value(2u), + JxlFieldExpressions.BitsOffset(3u, 3u), + 0, + ref alphaChannel)) + { + return false; + } + + this.AlphaChannel = alphaChannel; + + if (visitor.IsReading && alphaChannel >= this.ExtraChannelCount) + { + throw new InvalidOperationException("Invalid alpha channel for blending"); + } + } + + if (visitor.Conditional((this.ExtraChannelCount > 0 && mode is JxlBlendMode.Blend or JxlBlendMode.AlphaWeightedBlend) || mode == JxlBlendMode.Multiply)) + { + bool clamp = this.Clamp; + + if (!visitor.Boolean(false, ref clamp)) + { + return false; + } + + this.Clamp = clamp; + } + + if (visitor.Conditional(mode != JxlBlendMode.Replace || this.IsPartialFrame)) + { + uint source = this.Source; + + if (!visitor.U32( + JxlFieldExpressions.Value(0), + JxlFieldExpressions.Value(1), + JxlFieldExpressions.Value(2), + JxlFieldExpressions.Value(3), + 0, + ref source)) + { + return false; + } + + this.Source = source; + } + + return true; + } + + private static bool VisitBlendMode(JxlVisitor visitor, JxlBlendMode defaultValue, ref JxlBlendMode valueToEncode) + { + uint unsignedBackingValue = (uint)valueToEncode; + + if (!visitor.U32( + JxlFieldExpressions.Value((uint)JxlBlendMode.Replace), + JxlFieldExpressions.Value((uint)JxlBlendMode.Add), + JxlFieldExpressions.Value((uint)JxlBlendMode.Blend), + JxlFieldExpressions.BitsOffset(2u, 3u), + (uint)defaultValue, + ref unsignedBackingValue)) + { + return false; + } + + if (unsignedBackingValue > (uint)JxlBlendMode.Multiply) + { + throw new InvalidOperationException("Invalid blend mode"); + } + + valueToEncode = (JxlBlendMode)unsignedBackingValue; + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransform.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransform.cs new file mode 100644 index 0000000000..f874983486 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransform.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; + +/// +/// Represents the type of JPEG XL color transform. +/// +internal enum JxlColorTransform : byte +{ + /// + /// Use XYB encoding + /// + Xyb, + + /// + /// Encode according to the attached color profile. + /// + None, + + /// + /// Encode according to the attached color profile but + /// transformed into Y'Cb'Cr. + /// + YCbCr, +} diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransformHelpers.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransformHelpers.cs new file mode 100644 index 0000000000..de61664146 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransformHelpers.cs @@ -0,0 +1,39 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; + +/// +/// Helper methods associated with JxlColorTransform. +/// +internal static class JxlColorTransformHelpers +{ + private static readonly int[][] JpegOrders = + [ + [0, 0, 0], // Grayscale + [1, 0, 2], // Y'Cb'Cr + [0, 1, 2], // None + [0, 1, 2] // Anything else + ]; + + public static ReadOnlySpan GetJpegOrder(JxlColorTransform transform, bool isGraysacle) + { + if (isGraysacle) + { + return JpegOrders[0]; + } + + if (transform == JxlColorTransform.YCbCr) + { + return JpegOrders[1]; + } + else if (transform == JxlColorTransform.None) + { + return JpegOrders[2]; + } + else + { + return JpegOrders[3]; + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameEncoding.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameEncoding.cs new file mode 100644 index 0000000000..77ae964ac9 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameEncoding.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; + +/// +/// Represents the kind of frame encoding. +/// +internal enum JxlFrameEncoding : byte +{ + /// + /// Use VarDCT + /// + VarDct, + + /// + /// Use Modular encoding + /// + Modular +} diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs new file mode 100644 index 0000000000..a45c0e4710 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs @@ -0,0 +1,743 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// Disable IDE0032 for consistency with other fields. +// We have to avoid auto properties for most fields +// so we can use the ref keyword on them directly. +#pragma warning disable IDE0032 // Use auto property + +using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Processing; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; + +/// +/// Control information for a JPEG XL frame. +/// +internal sealed class JxlFrameHeader : IJxlFields +{ + // The following are backing fields for properties. + private JxlFrameEncoding encoding = JxlFrameEncoding.Modular; + private JxlFrameType frameType = JxlFrameType.RegularFrame; + private ulong flags; + private JxlColorTransform colorTransform = JxlColorTransform.Xyb; + private JxlYCbCrChromaSubsampling? chromaSubsampling; + private uint groupSizeShift; + private uint xQmScale; + private uint bQmScale; + private string? name; + private bool customSizeOrOrigin; + private Size frameSize; + private uint upsampling; + private List extraChannelUpsampling = []; + private Point frameOrigin; + private JxlBlendingInfo? blendingInfo; + private List extraChannelBlendingInfo = []; + private readonly JxlAnimationFrame? animationFrame; + private bool isLast; + private uint saveAsReference; + private bool saveBeforeColorTransform; + private uint dcLevel; + private JxlCodecMetadata? metadata; + private JxlLoopFilter? loopFilter; + private ulong extensions; + + private bool isPreviewFrame; // Non-serialized + + /// + /// Gets or sets the frame encoding method (e.g., Modular or VarDCT). + /// + public JxlFrameEncoding Encoding + { + get => this.encoding; + set => this.encoding = value; + } + + /// + /// Gets or sets the type of frame (e.g., RegularFrame). + /// + public JxlFrameType FrameType + { + get => this.frameType; + set => this.frameType = value; + } + + /// + /// Gets or sets the frame flags. + /// + public ulong Flags + { + get => this.flags; + set => this.flags = value; + } + + /// + /// Gets or sets the color transform used (e.g., XYB). + /// + public JxlColorTransform ColorTransform + { + get => this.colorTransform; + set => this.colorTransform = value; + } + + /// + /// Gets or sets the chroma subsampling information. + /// + public JxlYCbCrChromaSubsampling? ChromaSubsampling + { + get => this.chromaSubsampling; + set => this.chromaSubsampling = value; + } + + /// + /// Gets or sets the group size shift value. + /// + public uint GroupSizeShift + { + get => this.groupSizeShift; + set => this.groupSizeShift = value; + } + + /// + /// Gets or sets the X quantization matrix scale. + /// + public uint XQmScale + { + get => this.xQmScale; + set => this.xQmScale = value; + } + + /// + /// Gets or sets the B quantization matrix scale. + /// + public uint BQmScale + { + get => this.bQmScale; + set => this.bQmScale = value; + } + + /// + /// Gets or sets the frame name. + /// + public string? Name + { + get => this.name; + set => this.name = value; + } + + /// + /// Gets or sets a value indicating whether the frame has a custom size or origin. + /// + public bool CustomSizeOrOrigin + { + get => this.customSizeOrOrigin; + set => this.customSizeOrOrigin = value; + } + + /// + /// Gets or sets the frame size. + /// + public Size FrameSize + { + get => this.frameSize; + set => this.frameSize = value; + } + + /// + /// Gets or sets the upsampling factor. + /// + public uint Upsampling + { + get => this.upsampling; + set => this.upsampling = value; + } + + /// + /// Gets or sets the upsampling factors for extra channels. + /// + public List ExtraChannelUpsampling + { + get => this.extraChannelUpsampling; + set => this.extraChannelUpsampling = value; + } + + /// + /// Gets or sets the frame origin point. + /// + public Point FrameOrigin + { + get => this.frameOrigin; + set => this.frameOrigin = value; + } + + /// + /// Gets or sets the blending information for the frame. + /// + public JxlBlendingInfo? BlendingInfo + { + get => this.blendingInfo; + set => this.blendingInfo = value; + } + + /// + /// Gets or sets the blending information for extra channels. + /// + public List ExtraChannelBlendingInfo + { + get => this.extraChannelBlendingInfo; + set => this.extraChannelBlendingInfo = value; + } + + /// + /// Gets the associated animation frame, if any. + /// + public JxlAnimationFrame? AnimationFrame => this.animationFrame; + + /// + /// Gets or sets a value indicating whether this is the last frame. + /// + public bool IsLast + { + get => this.isLast; + set => this.isLast = value; + } + + /// + /// Gets or sets the reference frame index to save. + /// + public uint SaveAsReference + { + get => this.saveAsReference; + set => this.saveAsReference = value; + } + + /// + /// Gets or sets a value indicating whether to save before color transform. + /// + public bool SaveBeforeColorTransform + { + get => this.saveBeforeColorTransform; + set => this.saveBeforeColorTransform = value; + } + + /// + /// Gets or sets the DC level of the frame. + /// + public uint DcLevel + { + get => this.dcLevel; + set => this.dcLevel = value; + } + + /// + /// Gets or sets the codec metadata. + /// + public JxlCodecMetadata? Metadata + { + get => this.metadata; + set => this.metadata = value; + } + + /// + /// Gets or sets the loop filter applied to the frame. + /// + public JxlLoopFilter? LoopFilter + { + get => this.loopFilter; + set => this.loopFilter = value; + } + + /// + /// Gets or sets a value indicating whether this is a preview frame. Non-serialized. + /// + public bool IsPreviewFrame + { + get => this.isPreviewFrame; + set => this.isPreviewFrame = value; + } + + /// + /// Gets or sets the number of extensions. + /// + public ulong Extensions + { + get => this.extensions; + set => this.extensions = value; + } + + public int DefaultXSize + { + get + { + if (this.metadata == null) + { + return 0; + } + + if (this.isPreviewFrame) + { + return this.metadata.ImageMetadata?.PreviewSize?.XSize ?? 0; + } + + return this.metadata.XSize; + } + } + + public int DefaultYSize + { + get + { + if (this.metadata == null) + { + return 0; + } + + if (this.isPreviewFrame) + { + return this.metadata.ImageMetadata?.PreviewSize?.YSize ?? 0; + } + + return this.metadata.YSize; + } + } + + public JxlFrameDimensions FrameDimensions + { + get + { + int xsize = this.DefaultXSize; + int ysize = this.DefaultYSize; + + xsize = this.frameSize.Width != 0 ? this.frameSize.Width : xsize; + ysize = this.frameSize.Height != 0 ? this.frameSize.Height : ysize; + + if (this.dcLevel != 0) + { + xsize = JxlMath.DivCeil(xsize, 1 << (3 * (int)this.dcLevel)); + ysize = JxlMath.DivCeil(ysize, 1 << (3 * (int)this.dcLevel)); + } + + JxlFrameDimensions frameDim = new( + xsize, + ysize, + (int)this.groupSizeShift, + this.chromaSubsampling?.MaxHShift ?? 0, + this.chromaSubsampling?.MaxVShift ?? 0, + this.encoding == JxlFrameEncoding.Modular, + (int)this.upsampling); + + return frameDim; + } + } + + public bool NeedsColorTransform => !this.saveBeforeColorTransform || + this.frameType == JxlFrameType.RegularFrame || + this.frameType == JxlFrameType.SkipProgressive; + + /// + /// Gets a value indicating whether this frame is supposed to be saved for future usage by other frames. + /// + public bool CanBeReferenced => // DC frames cannot be referenced. The last frame cannot be referenced. + // A duration 0 frame makes little sense if it is not referenced. + // A non-duration 0 frame may or may not be referenced. + !this.isLast && + this.frameType != JxlFrameType.DcFrame && + (this.animationFrame?.Duration == 0 || this.saveAsReference != 0); + + private void UpdateFlag(bool condition, ulong flag) + { + if (condition) + { + this.flags |= flag; + } + else + { + this.flags &= ~flag; + } + } + + public bool Visit(JxlVisitor visitor) + { + bool allDefault = false; + if (visitor.AllDefault(this, ref allDefault)) + { + visitor.SetDefault(this); + return true; + } + + if (!VisitFrameType(visitor, JxlFrameType.RegularFrame, ref this.frameType)) + { + return false; + } + + if (visitor.IsReading && this.isPreviewFrame && this.frameType != JxlFrameType.RegularFrame) + { + throw new InvalidOperationException("Only regular frame could be a preview"); + } + + // FrameEncoding + bool isModular = this.encoding == JxlFrameEncoding.Modular; + if (!visitor.Boolean(false, ref isModular)) + { + return false; + } + + this.encoding = isModular + ? JxlFrameEncoding.Modular + : JxlFrameEncoding.VarDct; + + // Flags + if (!visitor.U64(0, ref this.flags)) + { + return false; + } + + // Color transform + bool xybEncoded = this.metadata?.ImageMetadata?.XybEncoded == true; + if (xybEncoded) + { + this.colorTransform = JxlColorTransform.Xyb; + } + else + { + bool alternate = this.colorTransform == JxlColorTransform.YCbCr; + if (!visitor.Boolean(false, ref alternate)) + { + return false; + } + + this.colorTransform = alternate + ? JxlColorTransform.YCbCr + : JxlColorTransform.None; + } + + // Chroma subsampling + if (visitor.Conditional(this.colorTransform == JxlColorTransform.YCbCr && + ((this.flags & (ulong)JxlFrameHeaderFlags.Dc) == 0))) + { + if (!visitor.VisitNested(this.chromaSubsampling!)) + { + return false; + } + } + + int numExtraChannels = this.metadata?.ImageMetadata?.ExtraChannelCount ?? 0; + + // Upsampling + if (visitor.Conditional((this.flags & (ulong)JxlFrameHeaderFlags.Dc) == 0)) + { + if (!visitor.U32( + JxlFieldExpressions.Value(1), + JxlFieldExpressions.Value(2), + JxlFieldExpressions.Value(4), + JxlFieldExpressions.Value(8), + 1, + ref this.upsampling)) + { + return false; + } + + if (this.metadata != null && visitor.Conditional(numExtraChannels != 0)) + { + List extraChannels = this.metadata!.ImageMetadata?.ExtraChannels ?? []; + this.extraChannelUpsampling = new List(extraChannels.Count); + + for (int i = 0; i < extraChannels.Count; i++) + { + uint dimShift = (uint)extraChannels[i].DimensionShift; + uint ecUpsampling = 1; + ecUpsampling >>= (int)dimShift; + + if (!visitor.U32( + JxlFieldExpressions.Value(1), + JxlFieldExpressions.Value(2), + JxlFieldExpressions.Value(4), + JxlFieldExpressions.Value(8), + 1, + ref ecUpsampling)) + { + return false; + } + + ecUpsampling <<= (int)dimShift; + + if (ecUpsampling < this.upsampling) + { + throw new InvalidOperationException("EC upsampling < color upsampling, invalid"); + } + + if (ecUpsampling > 8) + { + throw new InvalidOperationException("EC upsampling too large"); + } + + this.extraChannelUpsampling.Add(ecUpsampling); + } + } + else + { + this.extraChannelUpsampling.Clear(); + } + } + + // Modular / VarDCT specifics + if (visitor.Conditional(this.encoding == JxlFrameEncoding.Modular)) + { + if (!visitor.Bits(2, 1, ref this.groupSizeShift)) + { + return false; + } + } + + if (visitor.Conditional(this.encoding == JxlFrameEncoding.VarDct && + this.colorTransform == JxlColorTransform.Xyb)) + { + if (!visitor.Bits(3, 3, ref this.xQmScale)) + { + return false; + } + + if (!visitor.Bits(3, 2, ref this.bQmScale)) + { + return false; + } + } + else + { + this.xQmScale = this.bQmScale = 2; + } + + // Passes + if (visitor.Conditional(this.frameType != JxlFrameType.ReferenceOnly)) + { + if (!visitor.VisitNested(this.passes)) + { + return false; + } + } + + // DC frame + if (visitor.Conditional(this.frameType == JxlFrameType.DcFrame)) + { + if (!visitor.U32( + JxlFieldExpressions.Value(1), + JxlFieldExpressions.Value(2), + JxlFieldExpressions.Value(3), + JxlFieldExpressions.Value(4), + 1, + ref this.dcLevel)) + { + return false; + } + } + else + { + this.dcLevel = 0; + } + + // Custom size/origin + bool isPartialFrame = false; + + if (visitor.Conditional(this.frameType != JxlFrameType.DcFrame)) + { + if (!visitor.Boolean(false, ref this.customSizeOrOrigin)) + { + return false; + } + + if (visitor.Conditional(this.customSizeOrOrigin)) + { + JxlU32Enc enc = new( + JxlFieldExpressions.Bits(8), + JxlFieldExpressions.BitsOffset(11, 256), + JxlFieldExpressions.BitsOffset(14, 2304), + JxlFieldExpressions.BitsOffset(30, 18688)); + + if (visitor.Conditional(this.frameType is JxlFrameType.RegularFrame or JxlFrameType.SkipProgressive)) + { + uint ux0 = JxlPackSigned.PackUnsigned(this.frameOrigin.X); + uint uy0 = JxlPackSigned.PackUnsigned(this.frameOrigin.Y); + + if (!visitor.U32(enc, 0, ref ux0)) + { + return false; + } + + if (!visitor.U32(enc, 0, ref uy0)) + { + return false; + } + + this.frameOrigin = new Point(JxlPackSigned.UnpackSigned(ux0), JxlPackSigned.UnpackSigned(uy0)); + } + + uint frameSizeWidth = (uint)this.frameSize.Width; + uint frameSizeHeight = (uint)this.frameSize.Height; + + if (!visitor.U32(enc, 0, ref frameSizeWidth)) + { + return false; + } + + if (!visitor.U32(enc, 0, ref frameSizeHeight)) + { + return false; + } + + if (this.customSizeOrOrigin && (this.frameSize.Width == 0 || this.frameSize.Height == 0)) + { + throw new InvalidOperationException("Invalid crop dimensions for frame"); + } + + int imageXSize = this.DefaultXSize; + int imageYSize = this.DefaultYSize; + + if (this.frameType is JxlFrameType.RegularFrame or JxlFrameType.SkipProgressive) + { + isPartialFrame |= this.frameOrigin.X > 0; + isPartialFrame |= this.frameOrigin.Y > 0; + isPartialFrame |= (this.frameSize.Width + this.frameOrigin.X) < imageXSize; + isPartialFrame |= (this.frameSize.Height + this.frameOrigin.Y) < imageYSize; + } + } + } + + // Blending, animation, last frame + if (visitor.Conditional(this.frameType is JxlFrameType.RegularFrame or JxlFrameType.SkipProgressive)) + { + this.blendingInfo!.ExtraChannelCount = numExtraChannels; + this.blendingInfo.IsPartialFrame = isPartialFrame; + + if (!visitor.VisitNested(this.blendingInfo)) + { + return false; + } + + bool replaceAll = this.blendingInfo.BlendMode == JxlBlendMode.Replace; + + this.extraChannelBlendingInfo = new List(numExtraChannels); + for (int i = 0; i < numExtraChannels; i++) + { + JxlBlendingInfo ecBlendingInfo = new() + { + IsPartialFrame = isPartialFrame, + ExtraChannelCount = numExtraChannels + }; + + if (!visitor.VisitNested(ecBlendingInfo)) + { + return false; + } + + this.extraChannelBlendingInfo.Add(ecBlendingInfo); + replaceAll &= ecBlendingInfo.BlendMode == JxlBlendMode.Replace; + } + + if (visitor.IsReading && this.isPreviewFrame) + { + if (!replaceAll || this.customSizeOrOrigin) + { + throw new InvalidOperationException("Preview is not compatible with blending"); + } + } + + if (visitor.Conditional(this.metadata?.ImageMetadata?.HaveAnimation == true)) + { + this.animationFrame!.CodecMetadata = this.metadata; + + if (!visitor.VisitNested(this.animationFrame!)) + { + return false; + } + } + + if (!visitor.Boolean(true, ref this.isLast)) + { + return false; + } + } + else + { + this.isLast = false; + } + + // SaveAsReference + if (visitor.Conditional(this.frameType != JxlFrameType.DcFrame && !this.isLast)) + { + if (!visitor.U32( + JxlFieldExpressions.Value(0), + JxlFieldExpressions.Value(1), + JxlFieldExpressions.Value(2), + JxlFieldExpressions.Value(3), + 0, + ref this.saveAsReference)) + { + return false; + } + } + + // SaveBeforeColorTransform logic + if (this.frameType != JxlFrameType.DcFrame) + { + if (visitor.Conditional( + this.CanBeReferenced && + this.blendingInfo?.BlendMode == JxlBlendMode.Replace && + !isPartialFrame && + (this.frameType == JxlFrameType.RegularFrame || + this.frameType == JxlFrameType.SkipProgressive))) + { + if (!visitor.Boolean(false, ref this.saveBeforeColorTransform)) + { + return false; + } + } + else if (visitor.Conditional(this.frameType == JxlFrameType.ReferenceOnly)) + { + if (!visitor.Boolean(true, ref this.saveBeforeColorTransform)) + { + return false; + } + + int xsize = this.customSizeOrOrigin + ? this.frameSize.Width + : this.metadata!.XSize; + + int ysize = this.customSizeOrOrigin + ? this.frameSize.Height + : this.metadata!.YSize; + + if (!this.saveBeforeColorTransform && + (xsize < this.metadata!.XSize || + ysize < this.metadata!.YSize || + this.frameOrigin.X != 0 || + this.frameOrigin.Y != 0)) + { + throw new InvalidOperationException("Non-patch reference frame with invalid crop"); + } + } + } + else + { + this.saveBeforeColorTransform = true; + } + + if (!VisitNameString(visitor, ref this.name)) + { + return false; + } + + this.loopFilter!.IsModular = isModular; + if (!visitor.VisitNested(this.loopFilter!)) + { + return false; + } + + if (!visitor.BeginExtensions(ref this.extensions)) + { + return false; + } + + return visitor.EndExtensions(); + } +} diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeaderFlags.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeaderFlags.cs new file mode 100644 index 0000000000..098bcb57fc --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeaderFlags.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; + +/// +/// Optional steps for postprocessing. These flags are the +/// source of truth. Override must set/clear them rather than +/// change their meaning. Values chosen such that typical flags +/// are 0, encoded in only two bits. +/// +[Flags] +internal enum JxlFrameHeaderFlags : byte +{ + /// + /// Noise is injected into decoded output. + /// + Noise = 1, + + /// + /// Overlay patches. + /// + Patches = 2, + + /// + /// Overlay splines. + /// + Splines = 16, + + /// + /// Implies skip adaptive DC smoothing. + /// + Dc = 32, + + SkipAdaptiveDcSmoothing = 128, +} diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameType.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameType.cs new file mode 100644 index 0000000000..6aa73eab26 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameType.cs @@ -0,0 +1,37 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; + +/// +/// Defines the type of a JPEG XL frame. +/// +internal enum JxlFrameType : byte +{ + /// + /// A regular frame. It might be a crop, and it will be blended + /// on a previous frame (if any) and likely displayed or blended in + /// future frames. + /// + RegularFrame, + + /// + /// A DC frame. It is downsampled and only used as the DC + /// of a future and, possibly, preview frame. This cannot be cropped, + /// blended, or referenced by patches or blending modes. Frames using + /// DC cannot have non-default sizes. + /// + DcFrame, + + /// + /// A PatchesSource frame. Can only be used as source frame for + /// taking patches. It can be cropped but can't have a non-(0, 0) x0/y0. + /// + ReferenceOnly = 2, + + /// + /// Same as regular frame but not used for progressive rendering. + /// Implies no early display of DC. + /// + SkipProgressive, +} diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlPasses.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlPasses.cs new file mode 100644 index 0000000000..87277414ac --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlPasses.cs @@ -0,0 +1,220 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Fields; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; + +/// +/// Used for decoding to lower resolutions. +/// +internal sealed class JxlPasses : IJxlFields +{ + /// + /// Defines the maximum amount of passes, which is 11. + /// + private const int MaxPasses = 11; + + private uint numPasses; + private uint numDownsample; + + /// + /// Gets or sets the number of passes. + /// + public uint NumberOfPasses + { + get => this.numPasses; + set => this.numPasses = value; + } + + /// + /// Gets or sets the number of downsamples. + /// + public uint NumberOfDownsamples + { + get => this.numDownsample; + set => this.numDownsample = value; + } + + /// + /// Gets the downsample values. + /// + public uint[] Downsample { get; } = new uint[MaxPasses]; + + /// + /// Gets the last pass values. + /// + public uint[] LastPass { get; } = new uint[MaxPasses]; + + /// + /// Gets the shift values. + /// + public uint[] Shift { get; } = new uint[MaxPasses]; + + public void GetDownsamplingBracket(int pass, out int minShift, out int maxShift) + { + maxShift = 2; + minShift = 3; + + for (int i = 0; ; i++) + { + for (int j = 0; j < this.numDownsample; ++j) + { + if (i == this.LastPass[j]) + { + uint ds = this.Downsample[j]; + + if (ds == 8) + { + minShift = 3; + } + + if (ds == 4) + { + minShift = 2; + } + + if (ds == 2) + { + minShift = 1; + } + + if (ds == 1) + { + minShift = 0; + } + } + } + + if (i == this.numPasses - 1) + { + minShift = 0; + } + + if (i == pass) + { + return; + } + + maxShift = minShift - 1; + } + } + + public uint GetDownsamplingTargetForCompletedPasses(int num) + { + if (num >= this.numPasses) + { + return 1; + } + + uint result = 0; + + for (int i = 0; i < this.numDownsample; i++) + { + if (num > this.LastPass[i]) + { + result = Math.Min(result, this.Downsample[i]); + } + } + + return result; + } + + public bool Visit(JxlVisitor visitor) + { + if (visitor.U32( + JxlFieldExpressions.Value(1), + JxlFieldExpressions.Value(2), + JxlFieldExpressions.Value(2), + JxlFieldExpressions.BitsOffset(1, 3), + 0, + ref this.numPasses)) + { + return false; + } + + if (this.numPasses > MaxPasses) + { + return false; + } + + if (visitor.Conditional(this.numPasses != 1)) + { + if (!visitor.U32( + JxlFieldExpressions.Value(0), + JxlFieldExpressions.Value(1), + JxlFieldExpressions.Value(2), + JxlFieldExpressions.BitsOffset(1, 3), + 0, + ref this.numDownsample)) + { + return false; + } + + if (this.numDownsample > 4) + { + return false; + } + + if (this.numDownsample > this.numPasses) + { + throw new InvalidOperationException("Number of downsaples is greater than number of passes"); + } + + for (int i = 0; i < this.numPasses - 1; i++) + { + if (!visitor.Bits(2, 0u, ref this.Shift[i])) + { + return false; + } + } + + this.Shift[this.numPasses - 1] = 0; + + for (int i = 0; i < this.numDownsample; i++) + { + if (!visitor.U32( + JxlFieldExpressions.Value(1), + JxlFieldExpressions.Value(2), + JxlFieldExpressions.Value(4), + JxlFieldExpressions.Value(8), + 1, + ref this.Downsample[i])) + { + return false; + } + + if (i > 0 && this.Downsample[i] >= this.Downsample[i - 1]) + { + throw new InvalidOperationException("Downsample sequence should decrease"); + } + } + + for (int i = 0; i < this.numDownsample; i++) + { + if (!visitor.U32( + JxlFieldExpressions.Value(0), + JxlFieldExpressions.Value(1), + JxlFieldExpressions.Value(2), + JxlFieldExpressions.Value(3), + 0, + ref this.LastPass[i])) + { + return false; + } + + if (i > 0 && this.LastPass[i] <= this.LastPass[i - 1]) + { + throw new InvalidOperationException("Last pass sequence should increase"); + } + + if (this.LastPass[i] >= this.numPasses) + { + throw new InvalidOperationException("Last pass is greater than number of passes"); + } + } + } + + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlYCbCrChromaSubsampling.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlYCbCrChromaSubsampling.cs similarity index 98% rename from src/ImageSharp/Formats/Jxl/IO/JxlYCbCrChromaSubsampling.cs rename to src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlYCbCrChromaSubsampling.cs index 90d9cf6738..b10d8818aa 100644 --- a/src/ImageSharp/Formats/Jxl/IO/JxlYCbCrChromaSubsampling.cs +++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlYCbCrChromaSubsampling.cs @@ -3,7 +3,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Fields; -namespace SixLabors.ImageSharp.Formats.Jxl.IO; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; /// /// Gets the Y'Cb'Cr chroma subsampling information as part From ded8906f2931378de64456edebc848d648a18bd0 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 20 Jul 2026 03:25:03 +0400 Subject: [PATCH 045/328] Move Metadata folder to IO --- .../Formats/Jxl/{ => IO}/Metadata/JxlAnimationHeader.cs | 4 ++-- .../Formats/Jxl/{ => IO}/Metadata/JxlBitDepth.cs | 2 +- .../Formats/Jxl/{ => IO}/Metadata/JxlCodecMetadata.cs | 8 ++++---- .../Jxl/{ => IO}/Metadata/JxlCustomTransformData.cs | 2 +- .../Formats/Jxl/{ => IO}/Metadata/JxlExifOrientation.cs | 2 +- .../Formats/Jxl/{ => IO}/Metadata/JxlExtraChannel.cs | 2 +- .../Formats/Jxl/{ => IO}/Metadata/JxlExtraChannelInfo.cs | 4 ++-- .../Formats/Jxl/{ => IO}/Metadata/JxlImageMetadata.cs | 4 ++-- .../Jxl/{ => IO}/Metadata/JxlOpsinInverseMatrix.cs | 2 +- .../Formats/Jxl/{ => IO}/Metadata/JxlPreviewHeader.cs | 4 ++-- .../Formats/Jxl/{ => IO}/Metadata/JxlSizeHeader.cs | 4 ++-- .../Formats/Jxl/{ => IO}/Metadata/JxlToneMapping.cs | 6 +++--- 12 files changed, 22 insertions(+), 22 deletions(-) rename src/ImageSharp/Formats/Jxl/{ => IO}/Metadata/JxlAnimationHeader.cs (79%) rename src/ImageSharp/Formats/Jxl/{ => IO}/Metadata/JxlBitDepth.cs (98%) rename src/ImageSharp/Formats/Jxl/{ => IO}/Metadata/JxlCodecMetadata.cs (86%) rename src/ImageSharp/Formats/Jxl/{ => IO}/Metadata/JxlCustomTransformData.cs (92%) rename src/ImageSharp/Formats/Jxl/{ => IO}/Metadata/JxlExifOrientation.cs (83%) rename src/ImageSharp/Formats/Jxl/{ => IO}/Metadata/JxlExtraChannel.cs (86%) rename src/ImageSharp/Formats/Jxl/{ => IO}/Metadata/JxlExtraChannelInfo.cs (85%) rename src/ImageSharp/Formats/Jxl/{ => IO}/Metadata/JxlImageMetadata.cs (96%) rename src/ImageSharp/Formats/Jxl/{ => IO}/Metadata/JxlOpsinInverseMatrix.cs (90%) rename src/ImageSharp/Formats/Jxl/{ => IO}/Metadata/JxlPreviewHeader.cs (93%) rename src/ImageSharp/Formats/Jxl/{ => IO}/Metadata/JxlSizeHeader.cs (94%) rename src/ImageSharp/Formats/Jxl/{ => IO}/Metadata/JxlToneMapping.cs (74%) diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlAnimationHeader.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlAnimationHeader.cs similarity index 79% rename from src/ImageSharp/Formats/Jxl/Metadata/JxlAnimationHeader.cs rename to src/ImageSharp/Formats/Jxl/IO/Metadata/JxlAnimationHeader.cs index 3a11ff88f7..647e8bbc7d 100644 --- a/src/ImageSharp/Formats/Jxl/Metadata/JxlAnimationHeader.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlAnimationHeader.cs @@ -1,9 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.Fields; -namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; internal sealed class JxlAnimationHeader : IJxlFields { diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlBitDepth.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlBitDepth.cs similarity index 98% rename from src/ImageSharp/Formats/Jxl/Metadata/JxlBitDepth.cs rename to src/ImageSharp/Formats/Jxl/IO/Metadata/JxlBitDepth.cs index fd842b3633..0a9db0f575 100644 --- a/src/ImageSharp/Formats/Jxl/Metadata/JxlBitDepth.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlBitDepth.cs @@ -3,7 +3,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Fields; -namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; /// /// Represents the JPEG XL Bit Depth image metadata. diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlCodecMetadata.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCodecMetadata.cs similarity index 86% rename from src/ImageSharp/Formats/Jxl/Metadata/JxlCodecMetadata.cs rename to src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCodecMetadata.cs index d26d79d9a5..5a3aaeecb7 100644 --- a/src/ImageSharp/Formats/Jxl/Metadata/JxlCodecMetadata.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCodecMetadata.cs @@ -1,19 +1,19 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; internal sealed class JxlCodecMetadata { public JxlImageMetadata? ImageMetadata { get; set; } - public SizeHeader Size { get; set; } + public JxlSizeHeader? Size { get; set; } public JxlCustomTransformData? CustomTransformData { get; set; } - public int XSize => this.Size.XSize; + public int XSize => this.Size?.XSize ?? 0; - public int YSize => this.Size.YSize; + public int YSize => this.Size?.YSize ?? 0; public int GetOrientedPreviewXSize(bool keepOrientation) { diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlCustomTransformData.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCustomTransformData.cs similarity index 92% rename from src/ImageSharp/Formats/Jxl/Metadata/JxlCustomTransformData.cs rename to src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCustomTransformData.cs index 8b938b736b..3b3c6bc200 100644 --- a/src/ImageSharp/Formats/Jxl/Metadata/JxlCustomTransformData.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCustomTransformData.cs @@ -3,7 +3,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Fields; -namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; internal sealed class JxlCustomTransformData : IJxlFields { diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlExifOrientation.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExifOrientation.cs similarity index 83% rename from src/ImageSharp/Formats/Jxl/Metadata/JxlExifOrientation.cs rename to src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExifOrientation.cs index b5f52d67fe..406e7e9a1a 100644 --- a/src/ImageSharp/Formats/Jxl/Metadata/JxlExifOrientation.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExifOrientation.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; internal enum JxlExifOrientation : byte { diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannel.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannel.cs similarity index 86% rename from src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannel.cs rename to src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannel.cs index 1fb39d8604..8e2b842a56 100644 --- a/src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannel.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannel.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; internal enum JxlExtraChannel : byte { diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannelInfo.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs similarity index 85% rename from src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannelInfo.cs rename to src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs index 0279c2d7ab..5b492b1026 100644 --- a/src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannelInfo.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs @@ -1,9 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.Fields; -namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; internal sealed class JxlExtraChannelInfo : IJxlFields { diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlImageMetadata.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs similarity index 96% rename from src/ImageSharp/Formats/Jxl/Metadata/JxlImageMetadata.cs rename to src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs index 10855235b2..d9bc419e97 100644 --- a/src/ImageSharp/Formats/Jxl/Metadata/JxlImageMetadata.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs @@ -2,9 +2,9 @@ // Licensed under the Six Labors Split License. using System.Diagnostics; -using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.Fields; -namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; internal sealed class JxlImageMetadata : IJxlFields { diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlOpsinInverseMatrix.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs similarity index 90% rename from src/ImageSharp/Formats/Jxl/Metadata/JxlOpsinInverseMatrix.cs rename to src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs index d0a14ffb40..357ce27c39 100644 --- a/src/ImageSharp/Formats/Jxl/Metadata/JxlOpsinInverseMatrix.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs @@ -4,7 +4,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.Processing; -namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; internal sealed class JxlOpsinInverseMatrix : IJxlFields { diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlPreviewHeader.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlPreviewHeader.cs similarity index 93% rename from src/ImageSharp/Formats/Jxl/Metadata/JxlPreviewHeader.cs rename to src/ImageSharp/Formats/Jxl/IO/Metadata/JxlPreviewHeader.cs index 0c2bbab2ed..e46763cfeb 100644 --- a/src/ImageSharp/Formats/Jxl/Metadata/JxlPreviewHeader.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlPreviewHeader.cs @@ -1,9 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.Fields; -namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; internal sealed class JxlPreviewHeader : IJxlFields { diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlSizeHeader.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlSizeHeader.cs similarity index 94% rename from src/ImageSharp/Formats/Jxl/Metadata/JxlSizeHeader.cs rename to src/ImageSharp/Formats/Jxl/IO/Metadata/JxlSizeHeader.cs index 9c6e66dbef..5da30b8a0d 100644 --- a/src/ImageSharp/Formats/Jxl/Metadata/JxlSizeHeader.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlSizeHeader.cs @@ -1,9 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.Fields; -namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; internal sealed class JxlSizeHeader : IJxlFields { diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlToneMapping.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlToneMapping.cs similarity index 74% rename from src/ImageSharp/Formats/Jxl/Metadata/JxlToneMapping.cs rename to src/ImageSharp/Formats/Jxl/IO/Metadata/JxlToneMapping.cs index 1698b07016..9a399dc0b0 100644 --- a/src/ImageSharp/Formats/Jxl/Metadata/JxlToneMapping.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlToneMapping.cs @@ -1,9 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.Fields; -namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; internal sealed class JxlToneMapping : IJxlFields { @@ -15,7 +15,7 @@ internal sealed class JxlToneMapping : IJxlFields public bool RelativeToMaxDisplay { get; set; } - public float LinearBelow { get; set;} + public float LinearBelow { get; set; } public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); } From 674f82ad3ce50282cada599a6e4c460da3c5fe73 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 20 Jul 2026 05:41:45 +0400 Subject: [PATCH 046/328] Add quantizer --- .../Formats/Jxl/Memory/JxlSpanHelper.cs | 50 +++ .../Formats/Jxl/Processing/JxlQuantizer.cs | 388 ++++++++++++++++++ .../Jxl/Processing/JxlQuantizerParameters.cs | 60 +++ 3 files changed, 498 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Memory/JxlSpanHelper.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerParameters.cs diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlSpanHelper.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlSpanHelper.cs new file mode 100644 index 0000000000..fcf9b61aa5 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlSpanHelper.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Memory; + +internal static class JxlSpanHelper +{ + public static T NthElement(this Span span, int n) + where T : IComparable + { + int left = 0; + int right = span.Length - 1; + + while (true) + { + int pivotIndex = Partition(span, left, right); + if (pivotIndex == n) + { + return span[pivotIndex]; + } + else if (n < pivotIndex) + { + right = pivotIndex - 1; + } + else + { + left = pivotIndex + 1; + } + } + } + + private static int Partition(Span span, int left, int right) + where T : IComparable + { + T pivot = span[right]; + int storeIndex = left; + + for (int i = left; i < right; i++) + { + if (span[i].CompareTo(pivot) < 0) + { + (span[i], span[storeIndex]) = (span[storeIndex], span[i]); + storeIndex++; + } + } + + (span[storeIndex], span[right]) = (span[right], span[storeIndex]); + return storeIndex; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs new file mode 100644 index 0000000000..7ee27b1fb8 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs @@ -0,0 +1,388 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.Memory; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// The quantizer for DCT DC/AC coefficients. +/// +/// +/// The quantizer's primary role is to lower the value +/// of coefficients. For example, during encoding, +/// coefficients may be divided by 3 and have to be +/// multiplied by 3 at decoding (which is lossy). Quantization is often +/// useful for variable-length coding where the amount +/// of bits depend on how large the number is. +/// +internal sealed class JxlQuantizer +{ + /// + /// Denominator for the global_scale value. + /// + private const int GlobalScaleDenominator = 1 << 16; + + /// + /// Numerator for the global_scale value. + /// + private const int GlobalScaleNumerator = 4096; + + /// + /// Numerator for biases. + /// + private const float BiasNumerator = 0.145f; + + /// + /// The default value of the quant. + /// + private const int DefaultQuant = 64; + + /// + /// The maximum value for a quant. Quant cannot be greater than this - + /// if attempted to, it will be limited to this value. + /// + private const int MaxQuant = 256; + + /// + /// Represents the multipliers for the DC coefficients. + /// + private readonly float[] mulDc = new float[4]; + + /// + /// Represents the inverse multipliers for the DC coefficients. + /// + private readonly float[] inverseMulDc = new float[4]; + + /// + /// Global scale + /// + private int globalScale; + + /// + /// Quantizer DC + /// + private int quantDc; + + /// + /// Inverse global scale + /// + private float inverseGlobalScale; + + /// + /// Reciprocal of inverseGlobalScale + /// + private float globalScaleSingle; + + /// + /// Inverse quantizer DC + /// + private float inverseQuantDc; + + /// + /// The zero bias. + /// + private readonly float[] zeroBias = new float[3]; + + /// + /// The dequant matrices. + /// + private readonly JxlDequantMatrices? dequant; + + /// + /// Initializes a new instance of the class using the default + /// DC quantizer & scale factors. + /// + /// The dequant. + public JxlQuantizer(JxlDequantMatrices dequant) + : this(dequant, DefaultQuant, GlobalScaleDenominator / DefaultQuant) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The dequant. + /// The DC quantizer. + /// The scale factor. + public JxlQuantizer(JxlDequantMatrices dequant, int quantDc, int globalScale) + { + this.dequant = dequant; + this.quantDc = quantDc; + this.globalScale = globalScale; + + this.RecomputeFromGlobalScale(); + this.inverseQuantDc = this.inverseGlobalScale / this.quantDc; + + ZeroBiasDefault.CopyTo(this.zeroBias); + } + + /// + /// Gets the scaling factor. + /// + public float Scale => this.globalScaleSingle; + + /// + /// Gets the inverse scaling factor. It is a reciprocal of . + /// + public float InverseGlobalScale => this.inverseGlobalScale; + + /// + /// Gets the inverse DC quantization base value. + /// + public float InverseQuantDc => this.inverseQuantDc; + + public ReadOnlySpan MulDc => this.mulDc; + + public ReadOnlySpan InverseMulDc => this.inverseMulDc; + + /// + /// Gets the zero-biases for quantizing channels X, Y, and B. + /// + private static ReadOnlySpan ZeroBiasDefault => [0.5f, 0.5f, 0.5f]; + + /// + /// Gets the default bias for quant. + /// + private static ReadOnlySpan DefaultQuantBias => + [ + 1.0f - 0.05465007330715401f, + 1.0f - 0.07005449891748593f, + 1.0f - 0.049935103337343655f, + 0.145f, + ]; + + /// + /// Clears the and values, + /// setting their contents to 1.0f. + /// + public void ClearDcMultipliers() + { + Array.Fill(this.mulDc, 1f); + Array.Fill(this.inverseMulDc, 1f); + } + + /// + /// Ensure that the input value stays within the range of 1..MaxQuant. + /// + /// Value to clamp + /// The input value clamped into the range of 1..MaxQuant. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Clamp(float value) => (int)MathF.Max(1.0f, MathF.Min(value, MaxQuant)); + + /// + /// Scales the global scale value. + /// + /// The new scale + /// The scale value, scaled by the global scale. + private float ScaleGlobalScale(float scale) + { + int newGlobalScale = (int)MathF.Round(this.globalScale * scale, MidpointRounding.AwayFromZero); + float scaleOut = newGlobalScale * 1.0f / this.globalScale; + this.globalScale = newGlobalScale; + + this.RecomputeFromGlobalScale(); + + return scaleOut; + } + + /// + /// Recomputes quant values from the scale. + /// + public void RecomputeFromGlobalScale() + { + this.globalScaleSingle = this.globalScale * (1.0f / GlobalScaleDenominator); + this.inverseGlobalScale = 1.0f * GlobalScaleDenominator / this.globalScale; + this.inverseQuantDc = this.inverseGlobalScale / this.quantDc; + + for (int c = 0; c < 3; c++) + { + this.mulDc[c] = this.GetDcStep(c); + this.inverseMulDc[c] = this.GetInverseDcStep(c); + } + } + + /// + /// Returns the dequant matrix. + /// + /// The quant kind + /// The quantization index + /// The dequant matrix. + public ReadOnlySpan DequantMatrix(JxlAcStrategyType strategy, int c) + => this.dequant.Matrix(strategy, c); + + /// + /// Returns the inverse dequant matrix. + /// + /// The quant kind + /// The quantization index + /// The inverse dequant matrix. + public ReadOnlySpan InverseDequantMatrix(JxlAcStrategyType strategy, int c) + => this.dequant.InverseMatrix(strategy, c); + + /// + /// Returns the DC quantization step. + /// + /// The quantization index + /// The DC quantization step + public float GetDcStep(int c) => this.inverseQuantDc * this.dequant.DcQuant(c); + + /// + /// Returns the inverse DC quantization step. + /// + /// The quantization index + /// The inverse DC quantization step + public float GetInverseDcStep(int c) => this.dequant.InverseDcQuant(c) * (this.globalScaleSingle * this.quantDc); + + /// + /// Creates JXL quantizer parameters with values reflecting those in this quantizer instance. + /// + /// The quantizer parameters. + public JxlQuantizerParameters GetParameters() => new() + { + QuantDc = (uint)this.quantDc, + GlobalScale = (uint)this.globalScale + }; + + /// + /// Reads the quantizer values from the bit-stream. + /// + /// The bit reader. + /// Thrown when it is not possible to parse the quantizer parameters. + public void Decode(JxlBitReader reader) + { + JxlQuantizerParameters qp = new(); + if (!JxlBundle.Read(reader, qp)) + { + throw new IOException("Could not read quantizer parameters"); + } + + this.globalScale = (int)qp.GlobalScale; + this.quantDc = (int)qp.QuantDc; + + this.RecomputeFromGlobalScale(); + } + + /// + /// Recomputes the scaling factors and quant. + /// + public void ComputeGlobalScaleAndQuant(float quantDc, float quantMedian, float quantMedianAbsd) + { + const int quantFieldTarget = 5; + float scale = GlobalScaleDenominator * (quantMedian - quantMedianAbsd) / quantFieldTarget; + + if (scale < 1) + { + scale = 1; + } + + if (scale > (1 << 15)) + { + scale = 1 << 15; + } + + int newGlobalScale = (int)scale; + int scaledQuantDc = (int)(quantDc * GlobalScaleNumerator * 1.6f); + + if (newGlobalScale > scaledQuantDc) + { + newGlobalScale = scaledQuantDc; + + if (newGlobalScale <= 0) + { + newGlobalScale = 1; + } + } + + this.globalScale = newGlobalScale; + + this.RecomputeFromGlobalScale(); + + float valueF = (quantDc * this.inverseGlobalScale) + 0.5f; + float clipValueF = MathF.Min(1 << 16, valueF); + int newQuant = (int)clipValueF; + this.quantDc = newQuant; + + this.RecomputeFromGlobalScale(); + } + + /// + /// Quantizes the specified rectangular selection. + /// + public void SetQuantFieldRect(JxlImageF qf, in Rectangle rect, JxlImageI rawQuantField) + { + for (int y = 0; y < rect.Height; y++) + { + ReadOnlySpan rowQf = qf.GetRow(in rect, y); + Span rowQi = rawQuantField.GetRow(in rect, y); + + for (int x = 0; x < rect.Width; x++) + { + int val = Clamp((rowQf[x] * this.inverseGlobalScale) + 0.5f); + + rowQi[x] = val; + } + } + } + + /// + /// Set the quant field. + /// + public bool SetQuantField(Configuration configuration, float quantDc, JxlImageF qf, JxlImageI? rawQuantField) + { + IMemoryOwner data = configuration.MemoryAllocator.Allocate(qf.XSize * qf.YSize); + Span dataSpan = data.Memory.Span; + + for (int y = 0; y < qf.YSize; y++) + { + ReadOnlySpan rowQf = qf.GetRow(y); + + for (int x = 0; x < qf.XSize; y++) + { + float quant = rowQf[x]; + + dataSpan[(qf.XSize * y) + x] = quant; + } + } + + dataSpan[dataSpan.Length / 2] = JxlSpanHelper.NthElement(dataSpan, dataSpan.Length / 2); + float quantMedian = dataSpan[dataSpan.Length / 2]; + + IMemoryOwner deviations = configuration.MemoryAllocator.Allocate(dataSpan.Length); + Span deviationsSpan = deviations.Memory.Span; + for (int i = 0; i < dataSpan.Length; i++) + { + deviationsSpan[i] = MathF.Abs(dataSpan[i] - quantMedian); + } + + deviationsSpan[deviationsSpan.Length / 2] = JxlSpanHelper.NthElement(deviationsSpan, deviationsSpan.Length / 2); + float quantMedianAbsd = deviationsSpan[deviationsSpan.Length / 2]; + + this.ComputeGlobalScaleAndQuant(quantDc, quantMedian, quantMedianAbsd); + + if (rawQuantField != null) + { + if (rawQuantField.GetSize() != qf.GetSize()) + { + return false; + } + + this.SetQuantField(qf, qf.GetRectangle(), rawQuantField); + } + + return true; + } + + public void SetQuant(float quantDc, float quantAc, JxlImageI rawQuantField) + { + this.ComputeGlobalScaleAndQuant(quantDc, quantAc, 0); + + int value = Clamp((quantAc * this.inverseGlobalScale) + 0.5f); + rawQuantField.Fill(value); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerParameters.cs new file mode 100644 index 0000000000..4faa558692 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerParameters.cs @@ -0,0 +1,60 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Fields; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Represents parameters for the JPEG XL quantizer. +/// +internal sealed class JxlQuantizerParameters : IJxlFields +{ + private uint globalScale; + private uint quantDc; + + /// + /// Initializes a new instance of the class. + /// + public JxlQuantizerParameters() => JxlBundle.Init(this); + + /// + /// Gets or sets the global scale value. + /// + public uint GlobalScale + { + get => this.globalScale; + set => this.globalScale = value; + } + + /// + /// Gets or sets the quant DC value. + /// + public uint QuantDc + { + get => this.quantDc; + set => this.quantDc = value; + } + + public bool Visit(JxlVisitor visitor) + { + if (!visitor.U32( + JxlFieldExpressions.BitsOffset(11, 1), + JxlFieldExpressions.BitsOffset(11, 2049), + JxlFieldExpressions.BitsOffset(12, 4097), + JxlFieldExpressions.BitsOffset(16, 8193), + 1, + ref this.globalScale)) + { + return false; + } + + return visitor.U32( + JxlFieldExpressions.Value(16), + JxlFieldExpressions.BitsOffset(5, 1), + JxlFieldExpressions.BitsOffset(8, 1), + JxlFieldExpressions.BitsOffset(16, 1), + 1, + ref this.quantDc); + } +} From 07f6be0000d433846c53a9d34dbc8daca025aac1 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 20 Jul 2026 05:43:25 +0400 Subject: [PATCH 047/328] Fix memory leak --- src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs index 7ee27b1fb8..5a1405b350 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs @@ -369,12 +369,18 @@ public bool SetQuantField(Configuration configuration, float quantDc, JxlImageF { if (rawQuantField.GetSize() != qf.GetSize()) { + data.Dispose(); + deviations.Dispose(); + return false; } this.SetQuantField(qf, qf.GetRectangle(), rawQuantField); } + data.Dispose(); + deviations.Dispose(); + return true; } From b72019667a43ac05866785a4dac519552b90b6ca Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 20 Jul 2026 06:04:00 +0400 Subject: [PATCH 048/328] Add opsin inverse parameters, some quantizer weight work --- .../Formats/Jxl/Processing/JxlMatrix3x3F.cs | 13 +++++ .../Processing/JxlOpsinInverseParameters.cs | 33 +++++++++++++ .../Formats/Jxl/Processing/JxlQuantMode.cs | 19 ++++++++ .../Formats/Jxl/Processing/JxlQuantTable.cs | 47 +++++++++++++++++++ 4 files changed, 112 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlOpsinInverseParameters.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlQuantMode.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlQuantTable.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3F.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3F.cs index fda6282e05..0a2d59a90f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3F.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3F.cs @@ -55,6 +55,19 @@ internal struct JxlMatrix3x3F /// private float e22; + internal JxlMatrix3x3F(float[][] array) + { + this.e00 = array[0][0]; + this.e01 = array[0][1]; + this.e02 = array[0][2]; + this.e10 = array[1][0]; + this.e11 = array[1][1]; + this.e12 = array[1][2]; + this.e20 = array[2][0]; + this.e21 = array[2][1]; + this.e22 = array[2][2]; + } + /// /// Wraps all these values into a Span. /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlOpsinInverseParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlOpsinInverseParameters.cs new file mode 100644 index 0000000000..322bcd1c64 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlOpsinInverseParameters.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal static class JxlOpsinInverseParameters +{ + private const float M02 = 0.078f; + private const float M00 = 0.30f; + private const float M01 = 1.0f - M02 - M00; + + private const float M12 = 0.078f; + private const float M10 = 0.23f; + private const float M11 = 1.0f - M12 - M10; + + private const float M20 = 0.24342268924547819f; + private const float M21 = 0.20476744424496821f; + private const float M22 = 1.0f - M20 - M21; + + private static readonly float[][] Matrix = + [ + [M00, M01, M02], + [M10, M11, M12], + [M20, M21, M22] + ]; + + public static JxlMatrix3x3F GetOpsinAbsorbanceInverseMatrix() + { + JxlMatrix3x3F matrix = new(Matrix); + _ = JxlMatrix3x3F.Invert(ref matrix); + return matrix; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantMode.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantMode.cs new file mode 100644 index 0000000000..fa4dd20518 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantMode.cs @@ -0,0 +1,19 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Quantization mode. +/// +internal enum JxlQuantMode : byte +{ + Library, + Id, + Dct2, + Dct4, + Dct4x8, + Afv, + Dct, + Raw +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantTable.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantTable.cs new file mode 100644 index 0000000000..5fc8b49a8e --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantTable.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Specifies which quantization table to use depending on +/// transform & block type. +/// +internal enum JxlQuantTable : byte +{ + DCT = 0, + IDENTITY, + DCT2X2, + DCT4X4, + DCT16X16, + DCT32X32, + + // DCT16X8 + DCT8X16, + + // DCT32X8 + DCT8X32, + + // DCT32X16 + DCT16X32, + DCT4X8, + + // DCT8X4 + AFV0, + + // AFV1 + // AFV2 + // AFV3 + DCT64X64, + + // DCT64X32, + DCT32X64, + DCT128X128, + + // DCT128X64, + DCT64X128, + DCT256X256, + + // DCT256X128, + DCT128X256 +} From 2758454c1bb0f5a9cc477d5d8985e1ce2123b2cc Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 21 Jul 2026 16:46:41 +0400 Subject: [PATCH 049/328] Move, add entropy coding common functions --- .../Formats/Jxl/Fields/JxlU32Coder.cs | 2 +- .../Jxl/IO/{ => Entropy}/JxlAnsConstants.cs | 2 +- .../Jxl/IO/{ => Entropy}/JxlAnsEntry.cs | 2 +- .../Jxl/IO/{ => Entropy}/JxlAnsHelper.cs | 2 +- .../JxlAnsHybridUIntConfiguration.cs | 2 +- .../IO/{ => Entropy}/JxlAnsLz77Parameters.cs | 2 +- .../Jxl/IO/{ => Entropy}/JxlAnsSymbol.cs | 2 +- .../Jxl/IO/FrameHeader/JxlFrameHeader.cs | 2 +- src/ImageSharp/Formats/Jxl/IO/JxlHuffman.cs | 9 ++ .../Formats/Jxl/IO/JxlHuffmanCode.cs | 20 +++ .../Decoder}/JxlAnsReader.cs | 3 +- .../Decoder}/JxlBitReader.cs | 2 +- .../{ => Processing}/JxlAspectRatioHelpers.cs | 2 +- .../Jxl/Processing/JxlBlockContextMap.cs | 4 +- .../Formats/Jxl/Processing/JxlEntropyCoder.cs | 120 ++++++++++++++++++ .../{ => Processing}/JxlFrameDimensions.cs | 2 +- .../Splines/JxlControlPoint.cs | 2 +- .../Splines/JxlQuantizedSpline.cs | 2 +- .../Jxl/{ => Processing}/Splines/JxlSpline.cs | 2 +- .../Splines/JxlSplineDataView.cs | 2 +- .../Splines/JxlSplineEntropyContext.cs | 2 +- .../Splines/JxlSplineSegment.cs | 2 +- .../Splines/JxlSplineSegmentSpan.cs | 2 +- 23 files changed, 171 insertions(+), 21 deletions(-) rename src/ImageSharp/Formats/Jxl/IO/{ => Entropy}/JxlAnsConstants.cs (89%) rename src/ImageSharp/Formats/Jxl/IO/{ => Entropy}/JxlAnsEntry.cs (86%) rename src/ImageSharp/Formats/Jxl/IO/{ => Entropy}/JxlAnsHelper.cs (99%) rename src/ImageSharp/Formats/Jxl/IO/{ => Entropy}/JxlAnsHybridUIntConfiguration.cs (97%) rename src/ImageSharp/Formats/Jxl/IO/{ => Entropy}/JxlAnsLz77Parameters.cs (90%) rename src/ImageSharp/Formats/Jxl/IO/{ => Entropy}/JxlAnsSymbol.cs (85%) create mode 100644 src/ImageSharp/Formats/Jxl/IO/JxlHuffman.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/JxlHuffmanCode.cs rename src/ImageSharp/Formats/Jxl/{IO => Processing/Decoder}/JxlAnsReader.cs (98%) rename src/ImageSharp/Formats/Jxl/{IO => Processing/Decoder}/JxlBitReader.cs (98%) rename src/ImageSharp/Formats/Jxl/{ => Processing}/JxlAspectRatioHelpers.cs (94%) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlEntropyCoder.cs rename src/ImageSharp/Formats/Jxl/{ => Processing}/JxlFrameDimensions.cs (98%) rename src/ImageSharp/Formats/Jxl/{ => Processing}/Splines/JxlControlPoint.cs (86%) rename src/ImageSharp/Formats/Jxl/{ => Processing}/Splines/JxlQuantizedSpline.cs (99%) rename src/ImageSharp/Formats/Jxl/{ => Processing}/Splines/JxlSpline.cs (93%) rename src/ImageSharp/Formats/Jxl/{ => Processing}/Splines/JxlSplineDataView.cs (82%) rename src/ImageSharp/Formats/Jxl/{ => Processing}/Splines/JxlSplineEntropyContext.cs (80%) rename src/ImageSharp/Formats/Jxl/{ => Processing}/Splines/JxlSplineSegment.cs (85%) rename src/ImageSharp/Formats/Jxl/{ => Processing}/Splines/JxlSplineSegmentSpan.cs (81%) diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs index e909f7f74f..b097311ab5 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs @@ -2,7 +2,7 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; namespace SixLabors.ImageSharp.Formats.Jxl.Fields; diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsConstants.cs b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsConstants.cs similarity index 89% rename from src/ImageSharp/Formats/Jxl/IO/JxlAnsConstants.cs rename to src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsConstants.cs index dcd244ac7d..d9b6d1150d 100644 --- a/src/ImageSharp/Formats/Jxl/IO/JxlAnsConstants.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsConstants.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.IO; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; internal static class JxlAnsConstants { diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsEntry.cs b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsEntry.cs similarity index 86% rename from src/ImageSharp/Formats/Jxl/IO/JxlAnsEntry.cs rename to src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsEntry.cs index 25a9d12609..7ec8c7cd4c 100644 --- a/src/ImageSharp/Formats/Jxl/IO/JxlAnsEntry.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsEntry.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; -namespace SixLabors.ImageSharp.Formats.Jxl.IO; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; [StructLayout(LayoutKind.Sequential)] internal struct JxlAnsEntry diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHelper.cs similarity index 99% rename from src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs rename to src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHelper.cs index 35630eb103..8e775050e1 100644 --- a/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHelper.cs @@ -5,7 +5,7 @@ using System.Diagnostics; using System.Runtime.CompilerServices; -namespace SixLabors.ImageSharp.Formats.Jxl.IO; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; internal static class JxlAnsHelper { diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsHybridUIntConfiguration.cs b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHybridUIntConfiguration.cs similarity index 97% rename from src/ImageSharp/Formats/Jxl/IO/JxlAnsHybridUIntConfiguration.cs rename to src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHybridUIntConfiguration.cs index 422eb2323f..b8609f9163 100644 --- a/src/ImageSharp/Formats/Jxl/IO/JxlAnsHybridUIntConfiguration.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHybridUIntConfiguration.cs @@ -4,7 +4,7 @@ using System.Diagnostics; using SixLabors.ImageSharp.Formats.Jxl.Fields; -namespace SixLabors.ImageSharp.Formats.Jxl.IO; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; internal sealed class JxlAnsHybridUIntConfiguration : IJxlFields { diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsLz77Parameters.cs b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsLz77Parameters.cs similarity index 90% rename from src/ImageSharp/Formats/Jxl/IO/JxlAnsLz77Parameters.cs rename to src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsLz77Parameters.cs index ecf5cdc3fa..cbf6d3a4e4 100644 --- a/src/ImageSharp/Formats/Jxl/IO/JxlAnsLz77Parameters.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsLz77Parameters.cs @@ -3,7 +3,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Fields; -namespace SixLabors.ImageSharp.Formats.Jxl.IO; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; internal sealed class JxlAnsLz77Parameters : IJxlFields { diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsSymbol.cs b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsSymbol.cs similarity index 85% rename from src/ImageSharp/Formats/Jxl/IO/JxlAnsSymbol.cs rename to src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsSymbol.cs index 75afa94d9f..240b2463eb 100644 --- a/src/ImageSharp/Formats/Jxl/IO/JxlAnsSymbol.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsSymbol.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; -namespace SixLabors.ImageSharp.Formats.Jxl.IO; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; [StructLayout(LayoutKind.Sequential)] internal struct JxlAnsSymbol(int value, int offset, int frequency) diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs index a45c0e4710..43d7045041 100644 --- a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs +++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs @@ -7,7 +7,7 @@ #pragma warning disable IDE0032 // Use auto property using SixLabors.ImageSharp.Formats.Jxl.Fields; -using SixLabors.ImageSharp.Formats.Jxl.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Processing; namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlHuffman.cs b/src/ImageSharp/Formats/Jxl/IO/JxlHuffman.cs new file mode 100644 index 0000000000..640f667401 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/JxlHuffman.cs @@ -0,0 +1,9 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO; + +internal static class JxlHuffman +{ + +} diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlHuffmanCode.cs b/src/ImageSharp/Formats/Jxl/IO/JxlHuffmanCode.cs new file mode 100644 index 0000000000..30bca3446d --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/JxlHuffmanCode.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO; + +/// +/// A single Huffman code. +/// +internal struct JxlHuffmanCode +{ + /// + /// Number of bits for this symbol. + /// + public byte Bits; + + /// + /// Symbol value/offset. + /// + public ushort Value; +} diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs similarity index 98% rename from src/ImageSharp/Formats/Jxl/IO/JxlAnsReader.cs rename to src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs index d28e98c029..5320f5f171 100644 --- a/src/ImageSharp/Formats/Jxl/IO/JxlAnsReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs @@ -3,8 +3,9 @@ using System.Buffers; using System.Diagnostics; +using SixLabors.ImageSharp.Formats.Jxl.IO; -namespace SixLabors.ImageSharp.Formats.Jxl.IO; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; internal static class JxlAnsReader { diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs similarity index 98% rename from src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs rename to src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs index 50753321e7..87c1408f8e 100644 --- a/src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs @@ -4,7 +4,7 @@ using System.Buffers.Binary; using System.Diagnostics; -namespace SixLabors.ImageSharp.Formats.Jxl.IO; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; /// /// Represents a bitstream reader. diff --git a/src/ImageSharp/Formats/Jxl/JxlAspectRatioHelpers.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAspectRatioHelpers.cs similarity index 94% rename from src/ImageSharp/Formats/Jxl/JxlAspectRatioHelpers.cs rename to src/ImageSharp/Formats/Jxl/Processing/JxlAspectRatioHelpers.cs index ee5d1c134b..7fcda36c9a 100644 --- a/src/ImageSharp/Formats/Jxl/JxlAspectRatioHelpers.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAspectRatioHelpers.cs @@ -3,7 +3,7 @@ using System.Runtime.CompilerServices; -namespace SixLabors.ImageSharp.Formats.Jxl; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; internal static class JxlAspectRatioHelpers { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs index 2642a2295f..92dd8d94dd 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs @@ -23,9 +23,9 @@ public JxlBlockContextMap() public List[] DcThresholds { get; } = [[], [], []]; - public List QfThresholds { get; } = []; + public List QfThresholds { get; set; } = []; - public byte[] ContextMap { get; } = new byte[DefaultContextMap.Length]; + public byte[] ContextMap { get; set; } = new byte[DefaultContextMap.Length]; public int ContextCount { get; set; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlEntropyCoder.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlEntropyCoder.cs new file mode 100644 index 0000000000..1b896638ca --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlEntropyCoder.cs @@ -0,0 +1,120 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Helper functions for use in entropy coding. +/// +internal static class JxlEntropyCoder +{ + /// + /// Global DC threshold distributions + /// + public static readonly JxlU32Enc DcThresholdDistributions = new( + JxlFieldExpressions.Bits(4), + JxlFieldExpressions.BitsOffset(8, 16), + JxlFieldExpressions.BitsOffset(16, 272), + JxlFieldExpressions.BitsOffset(32, 65808)); + + /// + /// Global QF threshold distributions + /// + public static readonly JxlU32Enc QfThresholdDistributions = new( + JxlFieldExpressions.Bits(2), + JxlFieldExpressions.BitsOffset(3, 4), + JxlFieldExpressions.BitsOffset(5, 12), + JxlFieldExpressions.BitsOffset(8, 44)); + + /// + /// Predicts the entropy symbol using top and left neighboring pixels. + /// + /// Above row; set to if missing + /// Current row + /// The x coordinate offset + /// Default value that's used if both current and above row cannot be used + /// The predicted coefficient using neighboring pixels. + public static int PredictFromTopAndLeft( + ReadOnlySpan rowTop, + ReadOnlySpan row, + int x, + int defaultValue) + { + if (x == 0) + { + return rowTop.Length == 0 ? defaultValue : rowTop[x]; + } + + if (rowTop.Length == 0) + { + return row[x - 1]; + } + + return (rowTop[x] + rowTop[x - 1] + 1) / 2; + } + + public static bool DecodeBlockContextMap(Configuration configuration, JxlBitReader reader, ref JxlBlockContextMap contextMap) + { + List[] dct = contextMap.DcThresholds; + byte[] ctxMap = contextMap.ContextMap; + + bool isDefaultContextMap = reader.ReadBoolean(); + + if (isDefaultContextMap) + { + contextMap = new(); + return true; + } + + contextMap.DcContextCount = 1; + + for (int j = 0; j <= 2; j++) + { + int dcThresholdCount = (int)reader.ReadBits32(4u); + + dct[j] = new List(dcThresholdCount); + + contextMap.DcContextCount = dcThresholdCount + 1; + + for (int i = 0; i < dcThresholdCount; i++) + { + dct[j][i] = JxlPackSigned.UnpackSigned(JxlU32Coder.Read(DcThresholdDistributions, reader)); + } + } + + int qfThresholdCount = (int)reader.ReadBits32(4u); + + List qft = new(qfThresholdCount); + + for (int i = 0; i < qfThresholdCount; i++) + { + qft[i] = JxlU32Coder.Read(QfThresholdDistributions, reader) + 1; + } + + contextMap.QfThresholds = qft; + + if (contextMap.DcContextCount * qft.Count > 64) + { + throw new InvalidOperationException("Invalid block context map. It is too large."); + } + + Array.Resize(ref ctxMap, 3 * JxlForwardCoefficientOrder.OrderCount * contextMap.DcContextCount * qft.Count); + + contextMap.ContextMap = ctxMap; + + if (!JxlDecoderCore.DecodeContextMap(configuration, ref ctxMap, contextMap.ContextCount, reader)) + { + return false; + } + + if (contextMap.ContextCount > 16) + { + throw new InvalidOperationException("Too many distinct contexts in block context map"); + } + + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/JxlFrameDimensions.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlFrameDimensions.cs similarity index 98% rename from src/ImageSharp/Formats/Jxl/JxlFrameDimensions.cs rename to src/ImageSharp/Formats/Jxl/Processing/JxlFrameDimensions.cs index 8a7416a573..4ec265f523 100644 --- a/src/ImageSharp/Formats/Jxl/JxlFrameDimensions.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlFrameDimensions.cs @@ -3,7 +3,7 @@ using System.Runtime.CompilerServices; -namespace SixLabors.ImageSharp.Formats.Jxl; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; internal sealed class JxlFrameDimensions { diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlControlPoint.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlControlPoint.cs similarity index 86% rename from src/ImageSharp/Formats/Jxl/Splines/JxlControlPoint.cs rename to src/ImageSharp/Formats/Jxl/Processing/Splines/JxlControlPoint.cs index 74f1a0c3a8..48d94b3455 100644 --- a/src/ImageSharp/Formats/Jxl/Splines/JxlControlPoint.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlControlPoint.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; -namespace SixLabors.ImageSharp.Formats.Jxl.Splines; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; /// /// A simple pair of first and second 32-bit signed integers diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlQuantizedSpline.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs similarity index 99% rename from src/ImageSharp/Formats/Jxl/Splines/JxlQuantizedSpline.cs rename to src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs index 42aed25f8a..0e0135c175 100644 --- a/src/ImageSharp/Formats/Jxl/Splines/JxlQuantizedSpline.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs @@ -5,7 +5,7 @@ using System.Diagnostics; using System.Runtime.CompilerServices; -namespace SixLabors.ImageSharp.Formats.Jxl.Splines; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; internal sealed class JxlQuantizedSpline : IDisposable { diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlSpline.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSpline.cs similarity index 93% rename from src/ImageSharp/Formats/Jxl/Splines/JxlSpline.cs rename to src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSpline.cs index ef65c1d95e..f95886b268 100644 --- a/src/ImageSharp/Formats/Jxl/Splines/JxlSpline.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSpline.cs @@ -3,7 +3,7 @@ using System.Buffers; -namespace SixLabors.ImageSharp.Formats.Jxl.Splines; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; internal sealed class JxlSpline : IDisposable { diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlSplineDataView.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineDataView.cs similarity index 82% rename from src/ImageSharp/Formats/Jxl/Splines/JxlSplineDataView.cs rename to src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineDataView.cs index 1884b85c68..7b3822d783 100644 --- a/src/ImageSharp/Formats/Jxl/Splines/JxlSplineDataView.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineDataView.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Splines; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; internal sealed class JxlSplineDataView { diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlSplineEntropyContext.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineEntropyContext.cs similarity index 80% rename from src/ImageSharp/Formats/Jxl/Splines/JxlSplineEntropyContext.cs rename to src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineEntropyContext.cs index adb619f5ff..b6be2fe517 100644 --- a/src/ImageSharp/Formats/Jxl/Splines/JxlSplineEntropyContext.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineEntropyContext.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Splines; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; internal enum JxlSplineEntropyContext : byte { diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlSplineSegment.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineSegment.cs similarity index 85% rename from src/ImageSharp/Formats/Jxl/Splines/JxlSplineSegment.cs rename to src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineSegment.cs index 7e8bef1beb..65f2177927 100644 --- a/src/ImageSharp/Formats/Jxl/Splines/JxlSplineSegment.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineSegment.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Splines; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; internal struct JxlSplineSegment { diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlSplineSegmentSpan.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineSegmentSpan.cs similarity index 81% rename from src/ImageSharp/Formats/Jxl/Splines/JxlSplineSegmentSpan.cs rename to src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineSegmentSpan.cs index b4bd52af4f..2c65b1695d 100644 --- a/src/ImageSharp/Formats/Jxl/Splines/JxlSplineSegmentSpan.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineSegmentSpan.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Splines; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; internal struct JxlSplineSegmentSpan(int startInclusive, int endInclusive) { From 6eda7e2ad440a31a21fbbe776842b2ea49055ab1 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 21 Jul 2026 16:49:11 +0400 Subject: [PATCH 050/328] Remove aggressive inlining attributes, fix usings --- src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs | 4 +--- src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs | 4 +--- src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs | 2 -- src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs | 2 +- 4 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs index 398587baac..0b2640467b 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs @@ -1,8 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; namespace SixLabors.ImageSharp.Formats.Jxl.Fields; @@ -17,7 +16,6 @@ internal static class JxlBitsCoder /// // Looks like that's what the function does (fields.cc:406): // it returns whatever is passed to it. - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int MaxEncodedBits(int bits) => bits; public static bool CanEncode(int bits, uint value, ref int encodedBits) diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs index 3c4126c4b8..d45b0968af 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs @@ -2,7 +2,7 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; namespace SixLabors.ImageSharp.Formats.Jxl.Fields; @@ -15,7 +15,6 @@ internal static class JxlF16Coder /// Always returns 16, which is the maximum possible encoded bits. /// The F16 coder always reads 16 bits from the bitstream. /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int MaxEncodedBits() => 16; /// @@ -24,7 +23,6 @@ internal static class JxlF16Coder /// Also stores the maximum encodeable bits into encodedBits (which is /// always 16). /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool CanEncode(float value, ref int encodedBits) { encodedBits = MaxEncodedBits(); diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs index b097311ab5..8cd2fe7ea3 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; namespace SixLabors.ImageSharp.Formats.Jxl.Fields; @@ -14,7 +13,6 @@ internal static class JxlU32Coder /// /// Maximum number of writeable and/or readable bits in a variable-length integer. /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int MaxEncodedBits(in JxlU32Enc enc) { int extraBits = 0; diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs index ba121cafe2..659b6f994d 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; namespace SixLabors.ImageSharp.Formats.Jxl.Fields; From f71bab20a6224973634d8fd376657ba106038547 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 21 Jul 2026 16:52:56 +0400 Subject: [PATCH 051/328] Use distinct read-only spans --- .../FrameHeader/JxlColorTransformHelpers.cs | 26 +++++++------------ 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransformHelpers.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransformHelpers.cs index de61664146..81bc941870 100644 --- a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransformHelpers.cs +++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransformHelpers.cs @@ -8,32 +8,24 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; /// internal static class JxlColorTransformHelpers { - private static readonly int[][] JpegOrders = - [ - [0, 0, 0], // Grayscale - [1, 0, 2], // Y'Cb'Cr - [0, 1, 2], // None - [0, 1, 2] // Anything else - ]; + private static ReadOnlySpan Grayscale => [0, 0, 0]; + + private static ReadOnlySpan YCbCr => [1, 0, 2]; + + private static ReadOnlySpan None => [0, 1, 2]; public static ReadOnlySpan GetJpegOrder(JxlColorTransform transform, bool isGraysacle) { if (isGraysacle) { - return JpegOrders[0]; + return Grayscale; } if (transform == JxlColorTransform.YCbCr) { - return JpegOrders[1]; - } - else if (transform == JxlColorTransform.None) - { - return JpegOrders[2]; - } - else - { - return JpegOrders[3]; + return YCbCr; } + + return None; } } From abafaf8c031b38d40bf7499332f80fd19d1c0383 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 21 Jul 2026 17:44:00 +0400 Subject: [PATCH 052/328] Add CFL See chroma_from_luma.cc and chroma_from_luma.h --- .../Formats/Jxl/Processing/JxlAcStrategy.cs | 2 +- .../Jxl/Processing/JxlChromaFromLuma.cs | 46 +++++++ .../Jxl/Processing/JxlColorCorrelation.cs | 113 ++++++++++++++++++ .../Jxl/Processing/JxlColorCorrelationMap.cs | 42 +++++++ 4 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlChromaFromLuma.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelation.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelationMap.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs index 84b2462591..32c7fe3e18 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs @@ -4,7 +4,7 @@ using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using static SixLabors.ImageSharp.Formats.Jxl.JxlFrameDimensions; +using static SixLabors.ImageSharp.Formats.Jxl.Processing.JxlFrameDimensions; #pragma warning disable SA1405 // Debug.Assert should provide message text diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlChromaFromLuma.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlChromaFromLuma.cs new file mode 100644 index 0000000000..0121140472 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlChromaFromLuma.cs @@ -0,0 +1,46 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Fields; +using static SixLabors.ImageSharp.Formats.Jxl.Processing.JxlFrameDimensions; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Shared constants for CFL (Chroma From Luma) functions. +/// +internal static class JxlChromaFromLuma +{ + /// + /// Tiles are 64x64. + /// + public const int ColorTileDimension = 64; + + /// + /// Division of the color tile dimension by the block dimension. Therefore, + /// this is 8x8. + /// + public const int ColorTileDimensionInBlocks = ColorTileDimension / BlockDimensions; + + public const int DefaultColorFactor = 84; + + /// + /// Chroma From Luma fixed point precision, which is + /// 11 bits. + /// + public const int CflFixedPointPrecision = 11; + + /// + /// 524287 + /// + public const int CflFixedPointRatioMax = (256 << CflFixedPointPrecision) - 1; + + /// + /// Shared variable U32 distributions for the color factor. + /// + public static readonly JxlU32Enc ColorFactorDistribution = new( + JxlFieldExpressions.Value(DefaultColorFactor), + JxlFieldExpressions.Value(256), + JxlFieldExpressions.BitsOffset(8, 2), + JxlFieldExpressions.BitsOffset(16, 258)); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelation.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelation.cs new file mode 100644 index 0000000000..69e617ea90 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelation.cs @@ -0,0 +1,113 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal sealed class JxlColorCorrelation +{ + private float baseCorrelationX; + private float baseCorrelationB = DefaultYToBRatio; + + private readonly float[] dcFactors = new float[4]; + private uint colorFactor = JxlChromaFromLuma.DefaultColorFactor; + private float colorScale = 1.0f / JxlChromaFromLuma.DefaultColorFactor; + + public int YToXDc { get; private set; } + + public int YToBDc { get; private set; } + + public float ColorFactor => this.colorFactor; + + public float BaseCorrelationX + { + get => this.baseCorrelationX; + set => this.baseCorrelationX = value; + } + + public float BaseCorrelationB + { + get => this.baseCorrelationB; + set => this.baseCorrelationB = value; + } + + public bool IsJpegCompatible => + this.BaseCorrelationX == 0 && + this.BaseCorrelationB == 0 && + this.YToBDc == 0 && + this.YToXDc == 0 && + this.ColorFactor == JxlChromaFromLuma.DefaultColorFactor; + + public ReadOnlySpan DcFactors => this.dcFactors; + + public float YToXRatio(int xFactor) => this.BaseCorrelationX + (xFactor * this.colorScale); + + public float YToBRatio(int bFactor) => this.BaseCorrelationB + (bFactor * this.colorScale); + + public void SetColorFactor(uint factor) + { + this.colorFactor = factor; + this.colorScale = 1f / factor; + this.RecomputeDcFactors(); + } + + public void SetYToBDc(int yToBDc) + { + this.YToBDc = yToBDc; + this.RecomputeDcFactors(); + } + + public void SetYToXDc(int yToXDc) + { + this.YToXDc = yToXDc; + this.RecomputeDcFactors(); + } + + public void RecomputeDcFactors() + { + this.dcFactors[0] = this.YToXRatio(this.YToXDc); + this.dcFactors[2] = this.YToBRatio(this.YToBDc); + } + + public bool DecodeDc(JxlBitReader reader) + { + bool allDefault = reader.ReadBoolean(); + + if (allDefault) + { + return true; + } + + this.SetColorFactor(JxlU32Coder.Read(JxlChromaFromLuma.ColorFactorDistribution, reader)); + + if (!JxlF16Coder.Read(reader, ref this.baseCorrelationX)) + { + return false; + } + + if (MathF.Abs(this.baseCorrelationX) > 4f) + { + throw new InvalidOperationException("Base X correlation is out of range"); + } + + if (!JxlF16Coder.Read(reader, ref this.baseCorrelationB)) + { + return false; + } + + if (MathF.Abs(this.baseCorrelationB) > 4f) + { + throw new InvalidOperationException("Base B correlation is out of range"); + } + + this.YToXDc = (int)reader.ReadBits32(8) + sbyte.MinValue; + this.YToBDc = (int)reader.ReadBits32(8) + sbyte.MinValue; + + this.RecomputeDcFactors(); + return true; + } + + public static int RatioJpeg(int factor) => factor * (1 << JxlChromaFromLuma.CflFixedPointPrecision) / JxlChromaFromLuma.DefaultColorFactor; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelationMap.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelationMap.cs new file mode 100644 index 0000000000..89ae3f7296 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelationMap.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// JPEG XL Color correlation map. +/// +internal sealed class JxlColorCorrelationMap +{ + public JxlColorCorrelation Base { get; set; } = new(); + + public JxlImageSB? YToXMap { get; set; } + + public JxlImageSB? YToBMap { get; set; } + + public bool DecodeDc(JxlBitReader reader) => this.Base.DecodeDc(reader); + + public static JxlColorCorrelationMap Create(Configuration configuration, int width, int height, bool xyb) + { + JxlColorCorrelationMap map = new(); + + (int xBlocks, int yBlocks) = (DivCeil(width, JxlChromaFromLuma.ColorTileDimension), DivCeil(height, JxlChromaFromLuma.ColorTileDimension)); + + map.YToXMap = new JxlImageSB(configuration, xBlocks, yBlocks); + map.YToBMap = new JxlImageSB(configuration, xBlocks, yBlocks); + + ZeroFillImage(map.YToXMap); + ZeroFillImage(map.YToBMap); + + if (!xyb) + { + map.Base.BaseCorrelationB = 0; + } + + map.Base.RecomputeDcFactors(); + return map; + } +} From 9c6b4efa249511d947eca6eb19c9de0c2d7a6f29 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 22 Jul 2026 18:38:03 +0400 Subject: [PATCH 053/328] Add Huffman common methods --- src/ImageSharp/Formats/Jxl/IO/JxlHuffman.cs | 177 +++++++++++++++++++- 1 file changed, 176 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlHuffman.cs b/src/ImageSharp/Formats/Jxl/IO/JxlHuffman.cs index 640f667401..678321bab8 100644 --- a/src/ImageSharp/Formats/Jxl/IO/JxlHuffman.cs +++ b/src/ImageSharp/Formats/Jxl/IO/JxlHuffman.cs @@ -1,9 +1,184 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; + namespace SixLabors.ImageSharp.Formats.Jxl.IO; +/// +/// Shared Huffman I/O utilities. +/// internal static class JxlHuffman { - + /// + /// Returns Reverse(Reverse(Key, Len) + 1, Len). The + /// Reverse(Key, Len) function performs bitwise reversal + /// of the len least significant bits of the key value. + /// + public static uint GetNextKey(uint key, int len) + { + uint step = 1u << (len - 1); + while ((key & step) != 0) + { + step >>= 1; + } + + return (key & (step - 1)) + step; + } + + /// + /// Replicates into every + /// times with the upper bound of . + /// + public static void ReplicateValue(Span table, int step, int end, JxlHuffmanCode code) + { + do + { + end -= step; + table[end] = code; + } + while (end > 0); + } + + /// + /// Returns the table width of the next 2nd level table. + /// + /// The histogram of bit lengths for remaining symbols + /// Code length of the next processed symbol + /// Amount of bits for the root symbol + /// Table width for the 2nd level table. + public static int NextTableBitSize(ReadOnlySpan count, int length, int rootBits) + { + uint left = 1u << (length - rootBits); + + while (length < JxlAnsConstants.PrefixMaxBits) + { + if (left <= count[length]) + { + break; + } + + left -= count[length]; + length++; + left <<= 1; + } + + return length - rootBits; + } + + public static uint BuildHuffmanTable( + Span rootTable, + int rootBits, + ReadOnlySpan codeLengths, + Span count) + { + if (codeLengths.Length > (1u << JxlAnsConstants.PrefixMaxBits)) + { + return 0u; + } + + Span offset = stackalloc ushort[JxlAnsConstants.PrefixMaxBits + 1]; + + Span sortedStorage = stackalloc ushort[codeLengths.Length]; + + int maxLength = 1; + ushort sum = 0; + int len, symbol; + for (len = 1; len <= JxlAnsConstants.PrefixMaxBits; len++) + { + offset[len] = sum; + + if (count[len] != 0) + { + sum = (ushort)(sum + count[len]); + maxLength = len; + } + } + + for (symbol = 0; symbol < codeLengths.Length; symbol++) + { + if (codeLengths[symbol] != 0) + { + sortedStorage[offset[codeLengths[symbol]]++] = (ushort)symbol; + } + } + + Span table = rootTable; + int tableBits = rootBits; + uint tableSize = 1u << tableBits; + uint totalSize = tableSize; + + JxlHuffmanCode code = default; + + if (offset[JxlAnsConstants.PrefixMaxBits] == 1) + { + code.Bits = 0; + code.Value = sortedStorage[0]; + + for (int i = 0; i < totalSize; i++) + { + table[i] = code; + } + } + + if (tableBits > maxLength) + { + tableBits = maxLength; + tableSize = 1u << tableBits; + } + + int key = 0; + code.Bits = 0; + int step = 2; + + do + { + for (; count[code.Bits] != 0; --count[code.Bits]) + { + code.Value = sortedStorage[symbol++]; + ReplicateValue(table[key..], step, (int)tableSize, code); + key = (int)GetNextKey((uint)key, code.Bits); + } + + step <<= 1; + } + while (++code.Bits <= tableBits); + + while (totalSize != tableSize) + { + table[..(int)tableSize].CopyTo(table[(int)tableSize..]); + tableSize <<= 1; + } + + uint mask = totalSize - 1u; + int low = -1; + + uint tableOffset = 0; + for (step = 2; len <= maxLength; len++, step <<= 1) + { + for (; count[len] != 0; --count[len]) + { + if ((key & mask) != low) + { + tableOffset += tableSize; + table = table[(int)tableSize..]; + tableBits = NextTableBitSize(count, len, rootBits); + tableSize = 1u << tableBits; + totalSize += tableSize; + low = key & (int)mask; + + rootTable[low].Bits = (byte)(tableBits + rootBits); + rootTable[low].Value = (ushort)(tableOffset - low); + } + + code.Bits = (byte)(len - rootBits); + code.Value = sortedStorage[symbol++]; + + ReplicateValue(table[(key >> rootBits)..], step, (int)tableSize, code); + key = (int)GetNextKey((uint)key, len); + } + } + + return totalSize; + } } From 359125039eda7cabaf0061b24946599fe148a82e Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 22 Jul 2026 18:47:40 +0400 Subject: [PATCH 054/328] Start work on Butteraugli See butteraugli.h Added the parameters structure --- .../Butteraugli/ButteraugliParameters.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliParameters.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliParameters.cs new file mode 100644 index 0000000000..9b5fd57618 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliParameters.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; + +/// +/// Parameters for Butteraugli. +/// +internal struct ButteraugliParameters() +{ + /// + /// Multiplier for penalizing new HF artifacts more than + /// blurring away features. Value of 1.0 represents neutral. + /// + public float HfAsymmetry = 1f; + + /// + /// Multiplier for the psychovisual difference in the X channel. + /// + public float XMultiplier = 1f; + + /// + /// Number of nits that correspond to 1.0f input values. + /// + public float IntensityTarget = 80f; +} From 765c892df0b1443d4192963afdc883cd08fe209d Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 25 Jul 2026 00:03:38 +0400 Subject: [PATCH 055/328] Implement Butteraugli core functions --- .../Jxl/Processing/Butteraugli/Butteraugli.cs | 2333 +++++++++++++++++ .../Butteraugli/ButteraugliComparator.cs | 46 + .../Butteraugli/ButteraugliParameters.cs | 12 +- .../Formats/Jxl/Processing/JxlQuantizer.cs | 1 - .../Jxl/Processing/JxlWeightsSeparable5.cs | 8 +- 5 files changed, 2390 insertions(+), 10 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs new file mode 100644 index 0000000000..eb25583ecf --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs @@ -0,0 +1,2333 @@ +// 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.Memory; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; + +/// +/// Implementation of Google Butteraugli, an advanced +/// image comparison system. Unlike PSNR or SSIM, +/// Butteraugli compares images the way humans may +/// spot differences instead of pixelwise. +/// +internal static class Butteraugli +{ + // NOTE: The meaning of those constants + // is not perfectly understood. + public const float WMfMalta = 37.0819870399f; + public const float Norm1Mf = 130262059.556f; + public const float WMfMaltaX = 8246.75321353f; + public const float Norm1MfX = 1009002.70582f; + + public const float WHfMalta = 18.7237414387f; + public const float Norm1Hf = 4498534.45232f; + public const float WHfMaltaX = 6923.99476109f; + public const float Norm1HfX = 8051.15833247f; + + public const float WUhfMalta = 1.10039032555f; + public const float Norm1Uhf = 71.7800275169f; + public const float WUhfMaltaX = 173.5f; + public const float Norm1UhfX = 5.0f; + + private const float IntensityTargetNormalizationHack = 0.79079917404f; + + private static readonly float InternalGoodQualityThreshold = + 17.83f * IntensityTargetNormalizationHack; + + private static readonly float GlobalScale = + 1.0f / InternalGoodQualityThreshold; + + public static ReadOnlySpan Wmul => + [ + 400.0, 1.50815703118, 0, + 2150.0, 10.6195433239, 16.2176043152, + 29.2353797994, 0.844626970982, 0.703646627719, + ]; + + public static ReadOnlySpan ComputeKernel(float sigma) + { + const float m = 2.25f; // Accuracy increases when m is increased + float scaler = -1.0f / (2.0f * sigma * sigma); + int diff = Math.Max(1, (int)(m * MathF.Abs(sigma))); + + // Use new because there's only up to 3 elements + float[] kernel = new float[(2 * diff) + 1]; + + for (int i = -diff; i <= diff; i++) + { + kernel[i + diff] = MathF.Exp(scaler * i * i); + } + + return kernel; + } + + public static void ConvolveBorderColumn( + JxlImageF input, + ReadOnlySpan kernel, + int x, + Span rowOut) + { + int offset = kernel.Length / 2; + + int minX = x < offset ? 0 : x - offset; + int maxX = Math.Min(input.XSize - 1, x + offset); + + float weight = 0.0f; + for (int j = minX; j <= maxX; j++) + { + weight += kernel[j - x + offset]; + } + + float scale = 1.0f / weight; + + for (int y = 0; y < input.YSize; y++) + { + Span rowIn = input.GetRow(y); + + float sum = 0.0f; + + for (int j = minX; j <= maxX; j++) + { + sum += rowIn[j] * kernel[j - x + offset]; + } + + rowOut[y] = sum * scale; + } + } + + public static bool ConvolutionWithTranspose( + JxlImageF input, + ReadOnlySpan kernel, + JxlImageF output) + { + if (output.XSize != input.YSize) + { + return false; + } + + if (output.YSize != input.XSize) + { + return false; + } + + int len = kernel.Length; + int offset = len / 2; + + float weightNoBorder = 0.0f; + + for (int j = 0; j < len; j++) + { + weightNoBorder += kernel[j]; + } + + float scaleNoBorder = 1.0f / weightNoBorder; + + int border1 = Math.Min(input.XSize, offset); + int border2 = input.XSize > offset ? input.XSize - offset : 0; + + Span scaledKernel = stackalloc float[(len / 2) + 1]; + + for (int i = 0; i <= len / 2; i++) + { + scaledKernel[i] = kernel[i] * scaleNoBorder; + } + + // Middle + switch (len) + { + case 7: + { + float sk0 = scaledKernel[0]; + float sk1 = scaledKernel[1]; + float sk2 = scaledKernel[2]; + float sk3 = scaledKernel[3]; + + for (int y = 0; y < input.YSize; y++) + { + Span rowIn = input.GetRow(y); + + for (int x = border1; x < border2; x++) + { + int i = x - border1; + + float sum0 = (rowIn[i + 0] + rowIn[i + 6]) * sk0; + float sum1 = (rowIn[i + 1] + rowIn[i + 5]) * sk1; + float sum2 = (rowIn[i + 2] + rowIn[i + 4]) * sk2; + float sum = (rowIn[i + 3] * sk3) + sum0 + sum1 + sum2; + + output.GetRow(x)[y] = sum; + } + } + + break; + } + + case 13: + { + for (int y = 0; y < input.YSize; y++) + { + Span rowIn = input.GetRow(y); + + for (int x = border1; x < border2; x++) + { + int i = x - border1; + + float sum0 = (rowIn[i + 0] + rowIn[i + 12]) * scaledKernel[0]; + float sum1 = (rowIn[i + 1] + rowIn[i + 11]) * scaledKernel[1]; + float sum2 = (rowIn[i + 2] + rowIn[i + 10]) * scaledKernel[2]; + float sum3 = (rowIn[i + 3] + rowIn[i + 9]) * scaledKernel[3]; + + sum0 += (rowIn[i + 4] + rowIn[i + 8]) * scaledKernel[4]; + sum1 += (rowIn[i + 5] + rowIn[i + 7]) * scaledKernel[5]; + + float sum = rowIn[i + 6] * scaledKernel[6]; + + output.GetRow(x)[y] = sum + sum0 + sum1 + sum2 + sum3; + } + } + + break; + } + + case 15: + { + for (int y = 0; y < input.YSize; y++) + { + Span rowIn = input.GetRow(y); + + for (int x = border1; x < border2; x++) + { + int i = x - border1; + + float sum0 = (rowIn[i + 0] + rowIn[i + 14]) * scaledKernel[0]; + float sum1 = (rowIn[i + 1] + rowIn[i + 13]) * scaledKernel[1]; + float sum2 = (rowIn[i + 2] + rowIn[i + 12]) * scaledKernel[2]; + float sum3 = (rowIn[i + 3] + rowIn[i + 11]) * scaledKernel[3]; + + sum0 += (rowIn[i + 4] + rowIn[i + 10]) * scaledKernel[4]; + sum1 += (rowIn[i + 5] + rowIn[i + 9]) * scaledKernel[5]; + sum2 += (rowIn[i + 6] + rowIn[i + 8]) * scaledKernel[6]; + + float sum = rowIn[i + 7] * scaledKernel[7]; + + output.GetRow(x)[y] = sum + sum0 + sum1 + sum2 + sum3; + } + } + + break; + } + + case 33: + { + for (int y = 0; y < input.YSize; y++) + { + Span rowIn = input.GetRow(y); + + for (int x = border1; x < border2; x++) + { + int i = x - border1; + + float sum0 = (rowIn[i + 0] + rowIn[i + 32]) * scaledKernel[0]; + float sum1 = (rowIn[i + 1] + rowIn[i + 31]) * scaledKernel[1]; + float sum2 = (rowIn[i + 2] + rowIn[i + 30]) * scaledKernel[2]; + float sum3 = (rowIn[i + 3] + rowIn[i + 29]) * scaledKernel[3]; + + sum0 += (rowIn[i + 4] + rowIn[i + 28]) * scaledKernel[4]; + sum1 += (rowIn[i + 5] + rowIn[i + 27]) * scaledKernel[5]; + sum2 += (rowIn[i + 6] + rowIn[i + 26]) * scaledKernel[6]; + sum3 += (rowIn[i + 7] + rowIn[i + 25]) * scaledKernel[7]; + + sum0 += (rowIn[i + 8] + rowIn[i + 24]) * scaledKernel[8]; + sum1 += (rowIn[i + 9] + rowIn[i + 23]) * scaledKernel[9]; + sum2 += (rowIn[i + 10] + rowIn[i + 22]) * scaledKernel[10]; + sum3 += (rowIn[i + 11] + rowIn[i + 21]) * scaledKernel[11]; + + sum0 += (rowIn[i + 12] + rowIn[i + 20]) * scaledKernel[12]; + sum1 += (rowIn[i + 13] + rowIn[i + 19]) * scaledKernel[13]; + sum2 += (rowIn[i + 14] + rowIn[i + 18]) * scaledKernel[14]; + sum3 += (rowIn[i + 15] + rowIn[i + 17]) * scaledKernel[15]; + + float sum = rowIn[i + 16] * scaledKernel[16]; + + output.GetRow(x)[y] = sum + sum0 + sum1 + sum2 + sum3; + } + } + + break; + } + + default: + throw new NotSupportedException($"Kernel size {len} not implemented."); + } + + // Left border + for (int x = 0; x < border1; x++) + { + ConvolveBorderColumn(input, kernel, x, output.GetRow(x)); + } + + // Right border + for (int x = border2; x < input.XSize; x++) + { + ConvolveBorderColumn(input, kernel, x, output.GetRow(x)); + } + + return true; + } + + private static bool Blur( + JxlImageF input, + float sigma, + in ButteraugliParameters parameters, + ButteraugliBlurTemp temp, + JxlImageF output) + { + ReadOnlySpan kernel = ComputeKernel(sigma); + + // Separable5 does an in-place convolution, so this fast path is not safe + // if input aliases output. + if (kernel.Length == 5 && !ReferenceEquals(input, output)) + { + float sumWeights = 0.0f; + + foreach (float w in kernel) + { + sumWeights += w; + } + + float scale = 1.0f / sumWeights; + + float w0 = kernel[2] * scale; + float w1 = kernel[1] * scale; + float w2 = kernel[0] * scale; + + JxlWeightsSeparable5 weights = default; + FillRep4(ref weights.Horizontal, w0, w1, w2); + FillRep4(ref weights.Vertical, w0, w1, w2); + + if (!Separable5(input, input.GetRectangle(), weights, null, output)) + { + return false; + } + + return true; + } + + if (!temp.GetTransposed(input, out JxlImageF tempT)) + { + return false; + } + + if (!ConvolutionWithTranspose(input, kernel, tempT)) + { + return false; + } + + if (!ConvolutionWithTranspose(tempT, kernel, output)) + { + return false; + } + + return true; + } + + /// + /// Equivalent to HWY_REP4. + /// + private static void FillRep4(ref InlineArray12 values, float a, float b, float c) + { + for (int i = 0; i < 4; i++) + { + values[i] = a; + values[4 + i] = b; + values[8 + i] = c; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector MaximumClamp(Vector value, float maximum) + { + Vector multiplier = new(0.724216145665f); + Vector maximumValue = new(maximum); + Vector ifPositive = ((value - maximumValue) * multiplier) + maximumValue; + Vector ifNegative = ((value + maximumValue) * multiplier) - maximumValue; + Vector positiveOrValue = Vector.ConditionalSelect(Vector.GreaterThan(value, maximumValue), ifPositive, value); + Vector result = Vector.ConditionalSelect(Vector.LessThan(value, Vector.Negate(maximumValue)), ifNegative, positiveOrValue); + + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector RemoveRangeAroundZero(Vector x, float width) + { + Vector w = new(width); + + return Vector.ConditionalSelect( + Vector.GreaterThan(x, w), + x - w, + Vector.ConditionalSelect( + Vector.LessThan(x, -w), + x + w, + Vector.Zero)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector AmplifyRangeAroundZero(Vector x, float width) + { + Vector w = new(width); + + return Vector.ConditionalSelect( + Vector.GreaterThan(x, w), + x + w, + Vector.ConditionalSelect( + Vector.LessThan(x, -w), + x - w, + x + x)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void XybLowFrequencyToValues( + Vector x, + Vector y, + Vector bArg, + out Vector valX, + out Vector valY, + out Vector valB) + { + Vector xMul = new(33.832837186260f); + Vector yMul = new(14.458268100570f); + Vector bMul = new(49.87984651440f); + Vector yToBMul = new(-0.362267051518f); + + Vector b = (yToBMul * y) + bArg; + + valB = b * bMul; + valX = x * xMul; + valY = y * yMul; + } + + public static void XybLowFrequencyToValues(JxlImage3F xybLf) + { + int lanes = Vector.Count; + + for (int y = 0; y < xybLf.YSize; y++) + { + Span rowX = xybLf.PlaneRow(0, y); + Span rowY = xybLf.PlaneRow(1, y); + Span rowB = xybLf.PlaneRow(2, y); + + for (int x = 0; x < xybLf.XSize; x += lanes) + { + Vector valX = new(rowX.Slice(x, lanes)); + Vector valY = new(rowY.Slice(x, lanes)); + Vector valB = new(rowB.Slice(x, lanes)); + + XybLowFrequencyToValues( + valX, + valY, + valB, + out valX, + out valY, + out valB); + + valX.CopyTo(rowX.Slice(x, lanes)); + valY.CopyTo(rowY.Slice(x, lanes)); + valB.CopyTo(rowB.Slice(x, lanes)); + } + } + } + + public static bool SuppressXByY(JxlImageF inY, JxlImageF inOutX) + { + if (!SameSize(inOutX, inY)) + { + return false; + } + + int xSize = inY.XSize; + int ySize = inY.YSize; + int lanes = Vector.Count; + + const float suppress = 46.0f; + const float s = 0.653020556257f; + + Vector sv = new(s); + Vector oneMinusS = new(1.0f - s); + Vector ywv = new(suppress); + + for (int y = 0; y < ySize; y++) + { + ReadOnlySpan rowY = inY.GetRow(y); + Span rowX = inOutX.GetRow(y); + + for (int x = 0; x < xSize; x += lanes) + { + Vector vx = new(rowX.Slice(x, lanes)); + Vector vy = new(rowY.Slice(x, lanes)); + + Vector scaler = + ((ywv / ((vy * vy) + ywv)) * oneMinusS) + sv; + + (scaler * vx).CopyTo(rowX.Slice(x, lanes)); + } + } + + return true; + } + + public static void Subtract(JxlPlane a, JxlPlane b, JxlPlane c) + { + int lanes = Vector.Count; + + for (int y = 0; y < a.YSize; y++) + { + ReadOnlySpan rowA = a.GetRow(y); + ReadOnlySpan rowB = b.GetRow(y); + Span rowC = c.GetRow(y); + + for (int x = 0; x < a.XSize; x += lanes) + { + Vector va = new(rowA.Slice(x, lanes)); + Vector vb = new(rowB.Slice(x, lanes)); + + (va - vb).CopyTo(rowC.Slice(x, lanes)); + } + } + } + + public static bool SeparateLFAndMF( + in ButteraugliParameters parameters, + JxlImage3F xyb, + JxlImage3F lf, + JxlImage3F mf, + ButteraugliBlurTemp blurTemp) + { + const float sigmaLf = 7.15593339443f; + + for (int i = 0; i < 3; i++) + { + if (!Blur( + xyb.Plane(i), + sigmaLf, + parameters, + blurTemp, + lf.Plane(i))) + { + return false; + } + + Subtract( + xyb.Plane(i), + lf.Plane(i), + mf.Plane(i)); + } + + XybLowFrequencyToValues(lf); + + return true; + } + + public static bool SeparateMfAndHf( + Configuration configuration, + in ButteraugliParameters parameters, + JxlImage3F mf, + JxlImageF[] hf, + BlurTemp blurTemp) + { + const float sigmaHf = 3.22489901262f; + + int xSize = mf.XSize; + int ySize = mf.YSize; + + hf[0] = new JxlImageF(configuration, xSize, ySize); + hf[1] = new JxlImageF(configuration, xSize, ySize); + + int lanes = Vector.Count; + + for (int i = 0; i < 3; i++) + { + if (i == 2) + { + if (!Blur(mf.Plane(i), sigmaHf, parameters, blurTemp, mf.Plane(i))) + { + return false; + } + + break; + } + + for (int y = 0; y < ySize; y++) + { + Span rowMf = mf.PlaneRow(i, y); + Span rowHf = hf[i].GetRow(y); + + for (int x = 0; x < xSize; x += lanes) + { + new Vector(rowMf.Slice(x, lanes)) + .CopyTo(rowHf.Slice(x, lanes)); + } + } + + if (!Blur(mf.Plane(i), sigmaHf, parameters, blurTemp, mf.Plane(i))) + { + return false; + } + + if (i == 0) + { + const float removeMfRange = 0.29f; + + for (int y = 0; y < ySize; y++) + { + Span rowMf = mf.PlaneRow(0, y); + Span rowHf = hf[0].GetRow(y); + + for (int x = 0; x < xSize; x += lanes) + { + Vector mfv = new(rowMf.Slice(x, lanes)); + Vector hfv = new Vector(rowHf.Slice(x, lanes)) - mfv; + + mfv = RemoveRangeAroundZero(mfv, removeMfRange); + + mfv.CopyTo(rowMf.Slice(x, lanes)); + hfv.CopyTo(rowHf.Slice(x, lanes)); + } + } + } + else + { + const float addMfRange = 0.1f; + + for (int y = 0; y < ySize; y++) + { + Span rowMf = mf.PlaneRow(1, y); + Span rowHf = hf[1].GetRow(y); + + for (int x = 0; x < xSize; x += lanes) + { + Vector mfv = new(rowMf.Slice(x, lanes)); + Vector hfv = new Vector(rowHf.Slice(x, lanes)) - mfv; + + mfv = AmplifyRangeAroundZero(mfv, addMfRange); + + mfv.CopyTo(rowMf.Slice(x, lanes)); + hfv.CopyTo(rowHf.Slice(x, lanes)); + } + } + } + } + + return SuppressXByY(hf[1], hf[0]); + } + + public static bool SeparateHFAndUHF( + in ButteraugliParameters parameters, + JxlImageF[] hf, + JxlImageF[] uhf, + JxlBlurTemp blurTemp) + { + const float sigmaUhf = 1.56416327805f; + + int xSize = hf[0].XSize; + int ySize = hf[0].YSize; + + uhf[0] = new JxlImageF(xSize, ySize); + uhf[1] = new JxlImageF(xSize, ySize); + + int lanes = Vector.Count; + + for (int i = 0; i < 2; i++) + { + for (int y = 0; y < ySize; y++) + { + Span rowUhf = uhf[i].GetRow(y); + Span rowHf = hf[i].GetRow(y); + + for (int x = 0; x < xSize; x += lanes) + { + new Vector(rowHf.Slice(x, lanes)).CopyTo(rowUhf.Slice(x, lanes)); + } + } + + if (!Blur(hf[i], sigmaUhf, parameters, blurTemp, hf[i])) + { + return false; + } + + if (i == 0) + { + const float removeHfRange = 1.5f; + const float removeUhfRange = 0.04f; + + for (int y = 0; y < ySize; y++) + { + Span rowUhf = uhf[0].GetRow(y); + Span rowHf = hf[0].GetRow(y); + + for (int x = 0; x < xSize; x += lanes) + { + Vector hfv = new(rowHf.Slice(x, lanes)); + Vector uhfv = new Vector(rowUhf.Slice(x, lanes)) - hfv; + + hfv = RemoveRangeAroundZero(hfv, removeHfRange); + uhfv = RemoveRangeAroundZero(uhfv, removeUhfRange); + + hfv.CopyTo(rowHf.Slice(x, lanes)); + uhfv.CopyTo(rowUhf.Slice(x, lanes)); + } + } + } + else + { + const float addHfRange = 0.132f; + const float maxClampHf = 28.4691806922f; + const float maxClampUhf = 5.19175294647f; + const float mulYHf = 2.155f; + const float mulYUhf = 2.69313763794f; + + Vector mulHf = new(mulYHf); + Vector mulUhf = new(mulYUhf); + + for (int y = 0; y < ySize; y++) + { + Span rowUhf = uhf[1].GetRow(y); + Span rowHf = hf[1].GetRow(y); + + for (int x = 0; x < xSize; x += lanes) + { + Vector hfv = new(rowHf.Slice(x, lanes)); + hfv = MaximumClamp(hfv, maxClampHf); + + Vector uhfv = new Vector(rowUhf.Slice(x, lanes)) - hfv; + + uhfv = MaximumClamp(uhfv, maxClampUhf); + uhfv *= mulUhf; + + uhfv.CopyTo(rowUhf.Slice(x, lanes)); + + hfv *= mulHf; + hfv = AmplifyRangeAroundZero(hfv, addHfRange); + + hfv.CopyTo(rowHf.Slice(x, lanes)); + } + } + } + } + + return true; + } + + public static void DeallocateHFAndUHF(JxlImageF[] hf, JxlImageF[] uhf) + { + for (int i = 0; i < 2; i++) + { + hf[i] = new JxlImageF(); + uhf[i] = new JxlImageF(); + } + } + + public static bool SeparateFrequencies( + Configuration configuration, + in ButteraugliParameters parameters, + BlurTemp blurTemp, + JxlImage3F xyb, + PsychoImage ps) + { + ps.Lf = JxlImage3F.Create( + configuration, + xyb.XSize, + xyb.YSize); + + ps.Mf = JxlImage3F.Create( + configuration, + xyb.XSize, + xyb.YSize); + + if (!SeparateLFAndMF( + parameters, + xyb, + ps.Lf, + ps.Mf, + blurTemp)) + { + return false; + } + + if (!SeparateMfAndHf( + parameters, + ps.Mf, + ps.Hf, + blurTemp)) + { + return false; + } + + if (!SeparateHFAndUHF( + parameters, + ps.Hf, + ps.Uhf, + blurTemp)) + { + return false; + } + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector Sum( + Vector a, + Vector b, + Vector c, + Vector d) + => (a + b) + (c + d); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector Sum( + Vector a, + Vector b, + Vector c, + Vector d, + Vector e) + => Sum(a, b, c, d + e); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector Sum( + Vector a, + Vector b, + Vector c, + Vector d, + Vector e, + Vector f, + Vector g) + => Sum(a, b, c, Sum(d, e, f, g)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector Sum( + Vector a, + Vector b, + Vector c, + Vector d, + Vector e, + Vector f, + Vector g, + Vector h, + Vector i) + => (Sum(a, b, c, d) + Sum(e, f, g, h)) + i; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector MaltaUnitLF( + ReadOnlySpan row, + int index, + int xs) + { + // helper + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static Vector Load(ReadOnlySpan row, int index) + => new(row.Slice(index, Vector.Count)); + + int xs3 = 3 * xs; + + Vector center = Load(row, index); + + Vector sumYConst = Sum( + Load(row, index - 4), + Load(row, index - 2), + center, + Load(row, index + 2), + Load(row, index + 4)); + + Vector retval = sumYConst * sumYConst; + + Vector sum; + + sum = Sum( + Load(row, index - xs3 - xs), + Load(row, index - xs - xs), + center, + Load(row, index + xs + xs), + Load(row, index + xs3 + xs)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index - xs3 - 3), + Load(row, index - xs - xs - 2), + center, + Load(row, index + xs + xs + 2), + Load(row, index + xs3 + 3)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index - xs3 + 3), + Load(row, index - xs - xs + 2), + center, + Load(row, index + xs + xs - 2), + Load(row, index + xs3 - 3)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index - xs3 - xs + 1), + Load(row, index - xs - xs + 1), + center, + Load(row, index + xs + xs - 1), + Load(row, index + xs3 + xs - 1)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index - xs3 - xs - 1), + Load(row, index - xs - xs - 1), + center, + Load(row, index + xs + xs + 1), + Load(row, index + xs3 + xs + 1)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index - 4 - xs), + Load(row, index - 2 - xs), + center, + Load(row, index + 2 + xs), + Load(row, index + 4 + xs)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index - 4 + xs), + Load(row, index - 2 + xs), + center, + Load(row, index + 2 - xs), + Load(row, index + 4 - xs)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index - xs3 - 2), + Load(row, index - xs - xs - 1), + center, + Load(row, index + xs + xs + 1), + Load(row, index + xs3 + 2)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index - xs3 + 2), + Load(row, index - xs - xs + 1), + center, + Load(row, index + xs + xs - 1), + Load(row, index + xs3 - 2)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index - xs - xs - 3), + Load(row, index - xs - 2), + center, + Load(row, index + xs + 2), + Load(row, index + xs + xs + 3)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index - xs - xs + 3), + Load(row, index - xs + 2), + center, + Load(row, index + xs - 2), + Load(row, index + xs + xs - 3)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index + xs + xs - 4), + Load(row, index + xs - 2), + center, + Load(row, index - xs + 2), + Load(row, index - xs - xs + 4)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index - xs - xs - 4), + Load(row, index - xs - 2), + center, + Load(row, index + xs + 2), + Load(row, index + xs + xs + 4)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index - xs3 - xs - 2), + Load(row, index - xs - xs - 1), + center, + Load(row, index + xs + xs + 1), + Load(row, index + xs3 + xs + 2)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index - xs3 - xs + 2), + Load(row, index - xs - xs + 1), + center, + Load(row, index + xs + xs - 1), + Load(row, index + xs3 + xs - 2)); + retval = (sum * sum) + retval; + + return retval; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector MaltaUnit( + ReadOnlySpan row, + int index, + int xs) + { + // helper + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static Vector Load(ReadOnlySpan row, int index) + => new(row.Slice(index, Vector.Count)); + + int xs3 = 3 * xs; + + Vector center = Load(row, index); + + Vector sumYConst = Sum( + Load(row, index - 4), + Load(row, index - 3), + Load(row, index - 2), + Load(row, index - 1), + center, + Load(row, index + 1), + Load(row, index + 2), + Load(row, index + 3), + Load(row, index + 4)); + + Vector retval = sumYConst * sumYConst; + + Vector sum; + + sum = Sum( + Load(row, index - xs3 - xs), + Load(row, index - xs3), + Load(row, index - xs - xs), + Load(row, index - xs), + center, + Load(row, index + xs), + Load(row, index + xs + xs), + Load(row, index + xs3), + Load(row, index + xs3 + xs)); + retval += sum * sum; + + sum = Sum( + Load(row, index - xs3 - 3), + Load(row, index - xs - xs - 2), + Load(row, index - xs - 1), + center, + Load(row, index + xs + 1), + Load(row, index + xs + xs + 2), + Load(row, index + xs3 + 3)); + retval += sum * sum; + + sum = Sum( + Load(row, index - xs3 + 3), + Load(row, index - xs - xs + 2), + Load(row, index - xs + 1), + center, + Load(row, index + xs - 1), + Load(row, index + xs + xs - 2), + Load(row, index + xs3 - 3)); + retval += sum * sum; + + sum = Sum( + Load(row, index - xs3 - xs + 1), + Load(row, index - xs3 + 1), + Load(row, index - xs - xs + 1), + Load(row, index - xs), + center, + Load(row, index + xs), + Load(row, index + xs + xs - 1), + Load(row, index + xs3 - 1), + Load(row, index + xs3 + xs - 1)); + retval += sum * sum; + + sum = Sum( + Load(row, index - xs3 - xs - 1), + Load(row, index - xs3 - 1), + Load(row, index - xs - xs - 1), + Load(row, index - xs), + center, + Load(row, index + xs), + Load(row, index + xs + xs + 1), + Load(row, index + xs3 + 1), + Load(row, index + xs3 + xs + 1)); + retval += sum * sum; + + sum = Sum( + Load(row, index - 4 - xs), + Load(row, index - 3 - xs), + Load(row, index - 2 - xs), + Load(row, index - 1), + center, + Load(row, index + 1), + Load(row, index + 2 + xs), + Load(row, index + 3 + xs), + Load(row, index + 4 + xs)); + retval += sum * sum; + + sum = Sum( + Load(row, index - 4 + xs), + Load(row, index - 3 + xs), + Load(row, index - 2 + xs), + Load(row, index - 1), + center, + Load(row, index + 1), + Load(row, index + 2 - xs), + Load(row, index + 3 - xs), + Load(row, index + 4 - xs)); + retval += sum * sum; + + sum = Sum( + Load(row, index - xs3 - 2), + Load(row, index - xs - xs - 1), + Load(row, index - xs - 1), + center, + Load(row, index + xs + 1), + Load(row, index + xs + xs + 1), + Load(row, index + xs3 + 2)); + retval += sum * sum; + + sum = Sum( + Load(row, index - xs3 + 2), + Load(row, index - xs - xs + 1), + Load(row, index - xs + 1), + center, + Load(row, index + xs - 1), + Load(row, index + xs + xs - 1), + Load(row, index + xs3 - 2)); + retval += sum * sum; + + sum = Sum( + Load(row, index - xs - xs - 3), + Load(row, index - xs - 2), + Load(row, index - xs - 1), + center, + Load(row, index + xs + 1), + Load(row, index + xs + 2), + Load(row, index + xs + xs + 3)); + retval += sum * sum; + + sum = Sum( + Load(row, index - xs - xs + 3), + Load(row, index - xs + 2), + Load(row, index - xs + 1), + center, + Load(row, index + xs - 1), + Load(row, index + xs - 2), + Load(row, index + xs + xs - 3)); + retval += sum * sum; + + sum = Sum( + Load(row, index + xs - 4), + Load(row, index + xs - 3), + Load(row, index + xs - 2), + Load(row, index - 1), + center, + Load(row, index + 1), + Load(row, index - xs + 2), + Load(row, index - xs + 3), + Load(row, index - xs + 4)); + retval += sum * sum; + + sum = Sum( + Load(row, index - xs - 4), + Load(row, index - xs - 3), + Load(row, index - xs - 2), + Load(row, index - 1), + center, + Load(row, index + 1), + Load(row, index + xs + 2), + Load(row, index + xs + 3), + Load(row, index + xs + 4)); + retval += sum * sum; + + sum = Sum( + Load(row, index - xs3 - xs - 1), + Load(row, index - xs3 - 1), + Load(row, index - xs - xs - 1), + Load(row, index - xs), + center, + Load(row, index + xs), + Load(row, index + xs + xs + 1), + Load(row, index + xs3 + 1), + Load(row, index + xs3 + xs + 1)); + retval += sum * sum; + + sum = Sum( + Load(row, index - xs3 - xs + 1), + Load(row, index - xs3 + 1), + Load(row, index - xs - xs + 1), + Load(row, index - xs), + center, + Load(row, index + xs), + Load(row, index + xs + xs - 1), + Load(row, index + xs3 - 1), + Load(row, index + xs3 + xs - 1)); + retval += sum * sum; + + return retval; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float PaddedMaltaUnit( + JxlImageF diffs, + int x0, + int y0, + bool isLF) + { + if (x0 >= 4 && + y0 >= 4 && + x0 < diffs.XSize - 4 && + y0 < diffs.YSize - 4) + { + return isLF + ? MaltaUnitLF( + diffs.GetRow(y0), + x0, + diffs.PixelsPerRow)[0] + : MaltaUnit( + diffs.GetRow(y0), + x0, + diffs.PixelsPerRow)[0]; + } + + Span borderImage = stackalloc float[12 * 9]; + + for (int dy = 0; dy < 9; dy++) + { + int y = y0 + dy - 4; + + if (y < 0 || y >= diffs.YSize) + { + borderImage.Slice(dy * 12, 12).Clear(); + continue; + } + + ReadOnlySpan rowDiffs = diffs.GetRow(y); + + for (int dx = 0; dx < 9; dx++) + { + int x = x0 + dx - 4; + + borderImage[(dy * 12) + dx] = + x < 0 || x >= diffs.XSize + ? 0.0f + : rowDiffs[x]; + } + + borderImage.Slice((dy * 12) + 9, 3).Clear(); + } + + return isLF + ? MaltaUnitLF( + diffs.GetRow(y0), + x0, + diffs.PixelsPerRow)[0] + : MaltaUnit( + borderImage, + (4 * 12) + 4, + 12)[0]; + } + + public static bool MaltaDiffMap( + bool isLf, + JxlImageF lum0, + JxlImageF lum1, + float w0Gt1, + float w0Lt1, + float norm1, + float len, + float mulli, + JxlImageF diffs, + JxlImageF blockDiffAc) + { + if (!SameSize(lum0, lum1) || !SameSize(lum0, diffs)) + { + return false; + } + + int width = lum0.XSize; + int height = lum0.YSize; + + const float weight0 = 0.5f; + const float weight1 = 0.33f; + + float norm2_0Gt1 = + (float)(mulli * MathF.Sqrt(weight0 * w0Gt1) / ((len * 2) + 1) * norm1); + + float norm2_0Lt1 = + (float)(mulli * MathF.Sqrt(weight1 * w0Lt1) / ((len * 2) + 1) * norm1); + + for (int y = 0; y < height; y++) + { + ReadOnlySpan row0 = lum0.GetRow(y); + ReadOnlySpan row1 = lum1.GetRow(y); + Span rowDiffs = diffs.GetRow(y); + + for (int x = 0; x < width; x++) + { + float absVal = 0.5f * + (MathF.Abs(row0[x]) + MathF.Abs(row1[x])); + + float diff = row0[x] - row1[x]; + + float scaler = norm2_0Gt1 / ((float)norm1 + absVal); + + rowDiffs[x] = scaler * diff; + + float scaler2 = norm2_0Lt1 / ((float)norm1 + absVal); + + float fabs0 = MathF.Abs(row0[x]); + + float tooSmall = 0.55f * fabs0; + float tooBig = 1.05f * fabs0; + + if (row0[x] < 0) + { + if (row1[x] > -tooSmall) + { + rowDiffs[x] -= scaler2 * (row1[x] + tooSmall); + } + else if (row1[x] < -tooBig) + { + rowDiffs[x] += scaler2 * (-row1[x] - tooBig); + } + } + else + { + if (row1[x] < tooSmall) + { + rowDiffs[x] += scaler2 * (tooSmall - row1[x]); + } + else if (row1[x] > tooBig) + { + rowDiffs[x] -= scaler2 * (row1[x] - tooBig); + } + } + } + } + + int y0 = 0; + + for (; y0 < 4; y0++) + { + Span row = blockDiffAc.GetRow(y0); + + for (int x = 0; x < width; x++) + { + row[x] += PaddedMaltaUnit(diffs, x, y0, isLf); + } + } + + int lanes = Vector.Count; + int alignedX = Math.Max(4, lanes); + + int stride = diffs.PixelsPerRow; + + for (; y0 < height - 4; y0++) + { + ReadOnlySpan input = diffs.GetRow(y0); + Span output = blockDiffAc.GetRow(y0); + ref float outputReference = ref MemoryMarshal.GetReference(output); + + int x = 0; + + for (; x < alignedX; x++) + { + output[x] += PaddedMaltaUnit(diffs, x, y0, isLf); + } + + for (; x + lanes + 4 <= width; x += lanes) + { + Vector value = Vector.LoadUnsafe(ref Unsafe.Add(ref outputReference, x)); + + Vector malta = isLf + ? MaltaUnitLF(input, x, stride) + : MaltaUnit(input, x, stride); + + (value + malta).CopyTo(output[x..]); + } + + for (; x < width; x++) + { + output[x] += PaddedMaltaUnit(diffs, x, y0, isLf); + } + } + + for (; y0 < height; y0++) + { + Span row = blockDiffAc.GetRow(y0); + + for (int x = 0; x < width; x++) + { + row[x] += PaddedMaltaUnit(diffs, x, y0, isLf); + } + } + + return true; + } + + public static bool MaltaDiffMap( + JxlImageF lum0, + JxlImageF lum1, + float w0Gt1, + float w0Lt1, + float norm1, + JxlImageF diffs, + JxlImageF blockDiffAc) + { + const float len = 3.75f; + const float mulli = 0.39905817637f; + + return MaltaDiffMap( + false, + lum0, + lum1, + w0Gt1, + w0Lt1, + norm1, + len, + mulli, + diffs, + blockDiffAc); + } + + public static bool MaltaDiffMapLf( + JxlImageF lum0, + JxlImageF lum1, + float w0Gt1, + float w0Lt1, + float norm1, + JxlImageF diffs, + JxlImageF blockDiffAc) + { + const float len = 3.75f; + const float mulli = 0.611612573796f; + + return MaltaDiffMap( + true, + lum0, + lum1, + w0Gt1, + w0Lt1, + norm1, + len, + mulli, + diffs, + blockDiffAc); + } + + public static void CombineChannelsForMasking( + JxlImageF[] hf, + JxlImageF[] uhf, + JxlImageF output) + { + // Only X and Y components are involved in masking. + ReadOnlySpan muls = + [ + 2.5f, + 0.4f, + 0.4f + ]; + + int width = hf[0].XSize; + int height = hf[0].YSize; + + for (int y = 0; y < height; y++) + { + ReadOnlySpan rowYHf = hf[1].GetRow(y); + ReadOnlySpan rowYUhf = uhf[1].GetRow(y); + ReadOnlySpan rowXHf = hf[0].GetRow(y); + ReadOnlySpan rowXUhf = uhf[0].GetRow(y); + + Span row = output.GetRow(y); + + for (int x = 0; x < width; x++) + { + float xDiff = (rowXUhf[x] + rowXHf[x]) * muls[0]; + float yDiff = (rowYUhf[x] * muls[1]) + (rowYHf[x] * muls[2]); + + row[x] = MathF.Sqrt((xDiff * xDiff) + (yDiff * yDiff)); + } + } + } + + public static void DiffPrecompute( + JxlImageF xyb, + float mul, + float biasArg, + JxlImageF output) + { + int width = xyb.XSize; + int height = xyb.YSize; + + float bias = mul * biasArg; + float sqrtBias = MathF.Sqrt(bias); + + for (int y = 0; y < height; y++) + { + ReadOnlySpan rowIn = xyb.GetRow(y); + Span rowOut = output.GetRow(y); + + for (int x = 0; x < width; x++) + { + rowOut[x] = + MathF.Sqrt( + (mul * MathF.Abs(rowIn[x])) + bias) + - sqrtBias; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void StoreMin3( + float value, + ref float min0, + ref float min1, + ref float min2) + { + if (value < min2) + { + if (value < min0) + { + min2 = min1; + min1 = min0; + min0 = value; + } + else if (value < min1) + { + min2 = min1; + min1 = value; + } + else + { + min2 = value; + } + } + } + + public static void FuzzyErosion(JxlImageF from, JxlImageF to) + { + int width = from.XSize; + int height = from.YSize; + + const int step = 3; + + for (int y = 0; y < height; y++) + { + Span output = to.GetRow(y); + + for (int x = 0; x < width; x++) + { + float min0 = from.GetRow(y)[x]; + float min1 = 2 * min0; + float min2 = min1; + + if (x >= step) + { + StoreMin3( + from.GetRow(y)[x - step], + ref min0, + ref min1, + ref min2); + + if (y >= step) + { + StoreMin3( + from.GetRow(y - step)[x - step], + ref min0, + ref min1, + ref min2); + } + + if (y < height - step) + { + StoreMin3( + from.GetRow(y + step)[x - step], + ref min0, + ref min1, + ref min2); + } + } + + if (x < width - step) + { + StoreMin3( + from.GetRow(y)[x + step], + ref min0, + ref min1, + ref min2); + + if (y >= step) + { + StoreMin3( + from.GetRow(y - step)[x + step], + ref min0, + ref min1, + ref min2); + } + + if (y < height - step) + { + StoreMin3( + from.GetRow(y + step)[x + step], + ref min0, + ref min1, + ref min2); + } + } + + if (y >= step) + { + StoreMin3( + from.GetRow(y - step)[x], + ref min0, + ref min1, + ref min2); + } + + if (y < height - step) + { + StoreMin3( + from.GetRow(y + step)[x], + ref min0, + ref min1, + ref min2); + } + + output[x] = + (0.45f * min0) + + (0.3f * min1) + + (0.25f * min2); + } + } + } + + public static bool Mask( + Configuration configuration, + JxlImageF mask0, + JxlImageF mask1, + in ButteraugliParameters parameters, + JxlBlurTemp blurTemp, + JxlImageF? diffAc, + out JxlImageF mask) + { + int width = mask0.XSize; + int height = mask0.YSize; + + mask = new(configuration, width, height); + + const float mul = 6.19424080439f; + const float bias = 12.61050594197f; + const float radius = 2.7f; + + JxlImageF diff0 = new(configuration, width, height); + JxlImageF diff1 = new(configuration, width, height); + JxlImageF blurred0 = new(configuration, width, height); + JxlImageF blurred1 = new(configuration, width, height); + + DiffPrecompute(mask0, mul, bias, diff0); + DiffPrecompute(mask1, mul, bias, diff1); + + if (!Blur(diff0, radius, parameters, blurTemp, blurred0)) + { + return false; + } + + FuzzyErosion(blurred0, diff0); + + if (!Blur(diff1, radius, parameters, blurTemp, blurred1)) + { + return false; + } + + for (int y = 0; y < height; y++) + { + Span maskRow = mask.GetRow(y); + Span diffRow = diffAc is not null ? diffAc.GetRow(y) : []; + + ReadOnlySpan diff0Row = diff0.GetRow(y); + ReadOnlySpan blur0Row = blurred0.GetRow(y); + ReadOnlySpan blur1Row = blurred1.GetRow(y); + + for (int x = 0; x < width; x++) + { + maskRow[x] = diff0Row[x]; + + if (diffRow != null) + { + const float maskToErrorMul = 10.0f; + float diff = blur0Row[x] - blur1Row[x]; + diffRow[x] += maskToErrorMul * diff * diff; + } + } + } + + return true; + } + + public static bool MaskPsychoImage( + Configuration configuration, + ButteraugliPsychoImage pi0, + ButteraugliPsychoImage pi1, + int width, + int height, + in ButteraugliParameters parameters, + BlurTemp blurTemp, + JxlImageF mask, + JxlImageF? diffAc) + { + JxlImageF mask0 = new(configuration, width, height); + JxlImageF mask1 = new(configuration, width, height); + + CombineChannelsForMasking( + pi0.Hf, + pi0.Uhf, + mask0); + + CombineChannelsForMasking( + pi1.Hf, + pi1.Uhf, + mask1); + + return Mask( + mask0, + mask1, + parameters, + blurTemp, + mask, + diffAc); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float MaskY(float delta) + { + const float offset = 0.829591754942f; + const float scaler = 0.451936922203f; + const float mul = 2.5485944793f; + + float c = mul / ((scaler * delta) + offset); + float result = GlobalScale * (1.0f + c); + + return result * result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float MaskDcY(float delta) + { + const float offset = 0.20025578522f; + const float scaler = 3.87449418804f; + const float mul = 0.505054525019f; + + float c = mul / ((scaler * delta) + offset); + float result = GlobalScale * (1.0f + c); + + return result * result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float MaskColor( + ReadOnlySpan color, + float mask) + => (color[0] * mask) + (color[1] * mask) + (color[2] * mask); + + public static bool CombineChannelsToDiffmap( + JxlImageF mask, + JxlImage3F blockDiffDc, + JxlImage3F blockDiffAc, + float xmul, + JxlImageF result) + { + if (!SameSize(mask, result)) + { + return false; + } + + Span diffDc = stackalloc float[3]; + Span diffAc = stackalloc float[3]; + + int xsize = mask.XSize; + int ysize = mask.YSize; + + for (int y = 0; y < ysize; ++y) + { + Span rowOut = result.GetRow(y); + + for (int x = 0; x < xsize; ++x) + { + float val = mask.GetRow(y)[x]; + + float maskVal = MaskY(val); + float dcMaskVal = MaskDcY(val); + + for (int i = 0; i < 3; ++i) + { + diffDc[i] = blockDiffDc.PlaneRow(i, y)[x]; + diffAc[i] = blockDiffAc.PlaneRow(i, y)[x]; + } + + diffAc[0] *= xmul; + diffDc[0] *= xmul; + + rowOut[x] = MathF.Sqrt( + MaskColor(diffDc, dcMaskVal) + + MaskColor(diffAc, maskVal)); + } + } + + return true; + } + + public static void L2Diff( + JxlImageF i0, + JxlImageF i1, + float w, + JxlImageF diffmap) + { + if (w == 0) + { + return; + } + + for (int y = 0; y < i0.YSize; ++y) + { + ReadOnlySpan row0 = i0.GetRow(y); + ReadOnlySpan row1 = i1.GetRow(y); + Span rowDiff = diffmap.GetRow(y); + + for (int x = 0; x < i0.XSize; ++x) + { + float diff = row0[x] - row1[x]; + rowDiff[x] += diff * diff * w; + } + } + } + + public static void SetL2Diff( + JxlImageF i0, + JxlImageF i1, + float w, + JxlImageF diffmap) + { + if (w == 0) + { + return; + } + + for (int y = 0; y < i0.YSize; ++y) + { + ReadOnlySpan row0 = i0.GetRow(y); + ReadOnlySpan row1 = i1.GetRow(y); + Span rowDiff = diffmap.GetRow(y); + + for (int x = 0; x < i0.XSize; ++x) + { + float diff = row0[x] - row1[x]; + rowDiff[x] = diff * diff * w; + } + } + } + + public static void L2DiffAsymmetric( + JxlImageF i0, + JxlImageF i1, + float w0gt1, + float w0lt1, + JxlImageF diffmap) + { + if (w0gt1 == 0 && w0lt1 == 0) + { + return; + } + + Vector vw0gt1 = new(w0gt1 * 0.8f); + Vector vw0lt1 = new(w0lt1 * 0.8f); + + int lanes = Vector.Count; + + for (int y = 0; y < i0.YSize; ++y) + { + ReadOnlySpan row0 = i0.GetRow(y); + ReadOnlySpan row1 = i1.GetRow(y); + Span rowDiff = diffmap.GetRow(y); + + for (int x = 0; x < i0.XSize; x += lanes) + { + Vector val0 = new(row0[x..]); + Vector val1 = new(row1[x..]); + + // Primary symmetric quadratic objective. + Vector diff = val0 - val1; + + Vector total = + (diff * diff * vw0gt1) + new Vector(rowDiff[x..]); + + Vector fabs0 = Vector.Abs(val0); + + Vector tooSmall = fabs0 * new Vector(0.4f); + + Vector tooBig = fabs0; + + Vector ifNeg = + Vector.ConditionalSelect( + Vector.GreaterThan(val1, -tooSmall), + val1 + tooSmall, + Vector.ConditionalSelect( + Vector.LessThan(val1, -tooBig), + -val1 - tooBig, + Vector.Zero)); + + Vector ifPos = + Vector.ConditionalSelect( + Vector.LessThan(val1, tooSmall), + tooSmall - val1, + Vector.ConditionalSelect( + Vector.GreaterThan(val1, tooBig), + val1 - tooBig, + Vector.Zero)); + + Vector v = + Vector.ConditionalSelect( + Vector.LessThan(val0, Vector.Zero), + ifNeg, + ifPos); + + total += vw0lt1 * v * v; + + total.CopyTo(rowDiff[x..]); + } + } + } + + public static void OpsinAbsorbance( + bool clamp, + Vector in0, + Vector in1, + Vector in2, + out Vector out0, + out Vector out1, + out Vector out2) + { + Vector mix0 = new(0.29956550340058319f); + Vector mix1 = new(0.63373087833825936f); + Vector mix2 = new(0.077705617820981968f); + Vector mix3 = new(1.7557483643287353f); + + Vector mix4 = new(0.22158691104574774f); + Vector mix5 = new(0.69391388044116142f); + Vector mix6 = new(0.0987313588422f); + Vector mix7 = new(1.7557483643287353f); + + Vector mix8 = new(0.02f); + Vector mix9 = new(0.02f); + Vector mix10 = new(0.20480129041026129f); + Vector mix11 = new(12.226454707163354f); + + out0 = (mix0 * in0) + ((mix1 * in1) + ((mix2 * in2) + mix3)); + out1 = (mix4 * in0) + ((mix5 * in1) + ((mix6 * in2) + mix7)); + out2 = (mix8 * in0) + ((mix9 * in1) + ((mix10 * in2) + mix11)); + + if (clamp) + { + out0 = Vector.Max(out0, mix3); + out1 = Vector.Max(out1, mix7); + out2 = Vector.Max(out2, mix11); + } + } + + public static bool OpsinDynamicsImage( + JxlImage3F rgb, + in ButteraugliParameters parameters, + JxlImage3F blurred, + BlurTemp blurTemp, + JxlImage3F xyb) + { + if (blurred == null) + { + return false; + } + + const double sigma = 1.2; + + if (!Blur(rgb.Plane(0), sigma, parameters, blurTemp, blurred.Plane(0))) + { + return false; + } + + if (!Blur(rgb.Plane(1), sigma, parameters, blurTemp, blurred.Plane(1))) + { + return false; + } + + if (!Blur(rgb.Plane(2), sigma, parameters, blurTemp, blurred.Plane(2))) + { + return false; + } + + Vector intensityMultiplier = new((float)parameters.IntensityTarget); + + int lanes = Vector.Count; + + Vector minValue = new(1e-4f); + + for (int y = 0; y < rgb.YSize; ++y) + { + ReadOnlySpan rowR = rgb.PlaneRow(0, y); + ReadOnlySpan rowG = rgb.PlaneRow(1, y); + ReadOnlySpan rowB = rgb.PlaneRow(2, y); + + ReadOnlySpan blurredR = blurred.PlaneRow(0, y); + ReadOnlySpan blurredG = blurred.PlaneRow(1, y); + ReadOnlySpan blurredB = blurred.PlaneRow(2, y); + + Span outX = xyb.PlaneRow(0, y); + Span outY = xyb.PlaneRow(1, y); + Span outB = xyb.PlaneRow(2, y); + + for (int x = 0; x < rgb.XSize; x += lanes) + { + Vector sensitivity0; + Vector sensitivity1; + Vector sensitivity2; + { + OpsinAbsorbance( + true, + new Vector(blurredR[x..]) * intensityMultiplier, + new Vector(blurredG[x..]) * intensityMultiplier, + new Vector(blurredB[x..]) * intensityMultiplier, + out Vector pre0, + out Vector pre1, + out Vector pre2); + + pre0 = Vector.Max(pre0, minValue); + pre1 = Vector.Max(pre1, minValue); + pre2 = Vector.Max(pre2, minValue); + + sensitivity0 = Gamma(pre0) / pre0; + sensitivity1 = Gamma(pre1) / pre1; + sensitivity2 = Gamma(pre2) / pre2; + + sensitivity0 = Vector.Max(sensitivity0, minValue); + sensitivity1 = Vector.Max(sensitivity1, minValue); + sensitivity2 = Vector.Max(sensitivity2, minValue); + } + + OpsinAbsorbance( + false, + new Vector(rowR[x..]) * intensityMultiplier, + new Vector(rowG[x..]) * intensityMultiplier, + new Vector(rowB[x..]) * intensityMultiplier, + out Vector cur0, + out Vector cur1, + out Vector cur2); + + cur0 *= sensitivity0; + cur1 *= sensitivity1; + cur2 *= sensitivity2; + + Vector min01 = new(1.7557483643287353f); + Vector min2 = new(12.226454707163354f); + + cur0 = Vector.Max(cur0, min01); + cur1 = Vector.Max(cur1, min01); + cur2 = Vector.Max(cur2, min2); + + (cur0 - cur1).CopyTo(outX[x..]); + (cur0 + cur1).CopyTo(outY[x..]); + cur2.CopyTo(outB[x..]); + } + } + + return true; + } + + public static bool ButteraugliDiffmapInPlace( + Configuration configuration, + JxlImage3F image0, + JxlImage3F image1, + in ButteraugliParameters parameters, + JxlImageF diffmap) + { + int xSize = image0.XSize; + int ySize = image0.YSize; + + using var blurTemp = new JxlBlurTemp(); + + using (JxlImage3F temp = new(configuration, xSize, ySize)) + { + if (!OpsinDynamicsImage(image0, parameters, temp, blurTemp, image0)) + { + return false; + } + + if (!OpsinDynamicsImage(image1, parameters, temp, blurTemp, image1)) + { + return false; + } + } + + using JxlPlane blockDiffDc = JxlImageF.Create(configuration, xSize, ySize); + blockDiffDc.ZeroFill(); + + // LF/DC + using (JxlImage3F lf0 = new(configuration, xSize, ySize)) + using (JxlImage3F lf1 = new(configuration, xSize, ySize)) + { + if (!SeparateLFAndMF(parameters, image0, lf0, image0, blurTemp)) + { + return false; + } + + if (!SeparateLFAndMF(parameters, image1, lf1, image1, blurTemp)) + { + return false; + } + + for (int c = 0; c < 3; c++) + { + L2Diff( + lf0.Plane(c), + lf1.Plane(c), + Wmul[6 + c], + blockDiffDc); + } + } + + JxlImageF[] hf0 = new JxlImageF[2]; + JxlImageF[] hf1 = new JxlImageF[2]; + + if (!SeparateMfAndHf(parameters, image0, hf0, blurTemp)) + { + return false; + } + + if (!SeparateMfAndHf(parameters, image1, hf1, blurTemp)) + { + return false; + } + + using JxlImageF blockDiffAc = new(configuration, xSize, ySize); + blockDiffAc.ZeroFill(); + + using (JxlImageF diffs = new(configuration, xSize, ySize)) + { + if (!MaltaDiffMap( + true, + image0.Plane(1), + image1.Plane(1), + WMfMalta, + WMfMalta, + Norm1Mf, + diffs, + blockDiffAc)) + { + return false; + } + + if (!MaltaDiffMap( + true, + image0.Plane(0), + image1.Plane(0), + WMfMaltaX, + WMfMaltaX, + Norm1MfX, + diffs, + blockDiffAc)) + { + return false; + } + } + + for (int c = 0; c < 3; c++) + { + L2Diff( + image0.Plane(c), + image1.Plane(c), + Wmul[3 + c], + blockDiffAc); + } + + // Free MF images + image0.Dispose(); + image1.Dispose(); + + JxlImageF[] uhf0 = new JxlImageF[2]; + JxlImageF[] uhf1 = new JxlImageF[2]; + + if (!SeparateHFAndUHF(parameters, hf0, uhf0, blurTemp)) + { + return false; + } + + if (!SeparateHFAndUHF(parameters, hf1, uhf1, blurTemp)) + { + return false; + } + + float hfAsymmetry = parameters.HfAsymmetry; + + using (JxlImageF diffs = new(configuration, xSize, ySize)) + { + MaltaDiffMap( + false, + uhf0[1], + uhf1[1], + WUhfMalta * hfAsymmetry, + WUhfMalta / hfAsymmetry, + Norm1Uhf, + diffs, + blockDiffAc); + + MaltaDiffMap( + false, + uhf0[0], + uhf1[0], + wUhfMaltaX * hfAsymmetry, + wUhfMaltaX / hfAsymmetry, + norm1UhfX, + diffs, + blockDiffAc); + + float sqrtAsym = MathF.Sqrt(hfAsymmetry); + + MaltaDiffMap( + true, + hf0[1], + hf1[1], + WHfMalta * sqrtAsym, + WHfMalta / sqrtAsym, + Norm1Hf, + diffs, + blockDiffAc); + + MaltaDiffMap( + true, + hf0[0], + hf1[0], + WHfMaltaX * sqrtAsym, + WHfMaltaX / sqrtAsym, + Norm1HfX, + diffs, + blockDiffAc); + } + + for (int c = 0; c < 2; c++) + { + L2DiffAsymmetric( + hf0[c], + hf1[c], + Wmul[c] * hfAsymmetry, + Wmul[c] / hfAsymmetry, + blockDiffAc); + } + + // Mask + using JxlImageF mask = new(configuration, xSize, ySize); + using JxlImageF mask0 = new(configuration, xSize, ySize); + using JxlImageF mask1 = new(configuration, xSize, ySize); + + CombineChannelsForMasking(hf0, uhf0, mask0); + CombineChannelsForMasking(hf1, uhf1, mask1); + + DeallocateHFAndUHF(hf0, uhf0); + DeallocateHFAndUHF(hf1, uhf1); + + if (!Mask(mask0, mask1, parameters, blurTemp, mask, blockDiffAc)) + { + return false; + } + + for (int y = 0; y < ySize; y++) + { + ReadOnlySpan dc = blockDiffDc.GetRow(y); + ReadOnlySpan ac = blockDiffAc.GetRow(y); + Span output = diffmap.GetRow(y); + ReadOnlySpan maskRow = mask.GetRow(y); + + for (int x = 0; x < xSize; x++) + { + float m = maskRow[x]; + + output[x] = + MathF.Sqrt( + (dc[x] * (float)MaskDcY(m)) + + (ac[x] * (float)MaskY(m))); + } + } + + return true; + } + + // Calculate a 2x2 subsampled image for purposes of recursive butteraugli at + // multiresolution. + public static JxlImage3F SubSample2x(Configuration configuration, JxlImage3F input) + { + int xs = (input.XSize + 1) / 2; + int ys = (input.YSize + 1) / 2; + + JxlImage3F retval = new(Configuration, xs, ys); + + for (int c = 0; c < 3; ++c) + { + for (int y = 0; y < ys; ++y) + { + for (int x = 0; x < xs; ++x) + { + retval.PlaneRow(c, y)[x] = 0.0f; + } + } + } + + for (int c = 0; c < 3; ++c) + { + for (int y = 0; y < input.YSize; ++y) + { + ReadOnlySpan srcRow = input.PlaneRow(c, y); + + for (int x = 0; x < input.XSize; ++x) + { + retval.PlaneRow(c, y / 2)[x / 2] += + 0.25f * srcRow[x]; + } + } + + if ((input.XSize & 1) != 0) + { + for (int y = 0; y < retval.YSize; ++y) + { + int lastColumn = retval.XSize - 1; + retval.PlaneRow(c, y)[lastColumn] *= 2.0f; + } + } + + if ((input.YSize & 1) != 0) + { + for (int x = 0; x < retval.XSize; ++x) + { + int lastRow = retval.YSize - 1; + retval.PlaneRow(c, lastRow)[x] *= 2.0f; + } + } + } + + return retval; + } + + public static void AddSupersampled2x(JxlImageF src, float w, JxlImageF dest) + { + const float heuristicMixingValue = 0.3f; + + for (int y = 0; y < dest.YSize; ++y) + { + Span destRow = dest.GetRow(y); + + for (int x = 0; x < dest.XSize; ++x) + { + destRow[x] *= 1.0f - (heuristicMixingValue * w); + destRow[x] += w * src.GetRow(y / 2)[x / 2]; + } + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs new file mode 100644 index 0000000000..be5837534b --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs @@ -0,0 +1,46 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; + +internal sealed class ButteraugliComparator +{ + private int xSize; + private int ySize; + private ButteraugliParameters parameters; + private readonly ButteraugliPsychoImage pi0; + private readonly JxlImage3F temp; + private bool tempInUse; + private readonly ButteraugliBlurTemp blurTemp; + + /// + /// Computes the butteraugli map between the original image given in the constructor and the distorted image given here. + /// + public bool Diffmap(JxlImage3F rgb1, JxlImageF result) + { + throw new NotImplementedException(); + } + + /// + /// Same as Diffmap but OpsinDynamicsImage() was already applied. + /// + public bool DiffmapOpsinDynamicsImage(JxlImage3F xyb1, JxlImageF result) + { + throw new NotImplementedException(); + } + + /// + /// Same as above but the frequency decomposition was already applied. + /// + public bool DiffmapPsychoImage(ButteraugliPsychoImage pi1, JxlImageF diffmap) + { + throw new NotImplementedException(); + } + + public bool Mask(JxlImageF mask) + { + throw new NotImplementedException(); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliParameters.cs index 9b5fd57618..7af303c8ba 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliParameters.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliParameters.cs @@ -9,18 +9,18 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; internal struct ButteraugliParameters() { /// - /// Multiplier for penalizing new HF artifacts more than + /// Gets or sets the multiplier for penalizing new HF artifacts more than /// blurring away features. Value of 1.0 represents neutral. /// - public float HfAsymmetry = 1f; + public float HfAsymmetry { get; set; } = 1f; /// - /// Multiplier for the psychovisual difference in the X channel. + /// Gets or sets the multiplier for the psychovisual difference in the X channel. /// - public float XMultiplier = 1f; + public float XMultiplier { get; set; } = 1f; /// - /// Number of nits that correspond to 1.0f input values. + /// Gets or sets the number of nits that correspond to 1.0f input values. /// - public float IntensityTarget = 80f; + public float IntensityTarget { get; set; } = 80f; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs index 5a1405b350..4230952860 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs @@ -4,7 +4,6 @@ using System.Buffers; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Fields; -using SixLabors.ImageSharp.Formats.Jxl.IO; using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs index 249d1ec9a7..7fdec43b03 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs @@ -3,9 +3,11 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing; -internal sealed class JxlWeightsSeparable5 +internal struct JxlWeightsSeparable5 { - public InlineArray12 Horizontal { get; set; } + // Don't make these a property so we can ref into them. - public InlineArray12 Vertical { get; set; } + public InlineArray12 Horizontal; + + public InlineArray12 Vertical; } From 0d159b9477c3bc4b563ad5f02774dd38f6e96fda Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 1 Aug 2026 23:44:39 +0400 Subject: [PATCH 056/328] Add CMS* prototype, XYB decoding, TOC, convolution and coefficient ordering * CMS - Color Management System --- .../Formats/Jxl/Cms/JxlCieXyPrimaries.cs | 27 ++ .../Formats/Jxl/Cms/JxlColorEncoding.cs | 77 ++++ .../Formats/Jxl/Cms/JxlColorSpace.cs | 31 ++ .../Jxl/Cms/JxlCustomTransferFunction.cs | 109 +++++ src/ImageSharp/Formats/Jxl/Cms/JxlCustomXy.cs | 53 +++ .../Formats/Jxl/Cms/JxlPrimaries.cs | 27 ++ .../Formats/Jxl/Cms/JxlRenderingIntent.cs | 13 + .../Formats/Jxl/Cms/JxlTransferFunction.cs | 45 ++ .../Formats/Jxl/Cms/JxlWhitePoint.cs | 33 ++ src/ImageSharp/Formats/Jxl/Cms/README.md | 4 + .../Jxl/IO/Metadata/JxlImageMetadata.cs | 22 + .../Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs | 8 +- src/ImageSharp/Formats/Jxl/InlineArrays.cs | 9 + .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 157 +++++++ .../Jxl/Processing/Decoder/JxlNoiseDecoder.cs | 63 +++ .../Processing/Decoder/JxlOpsinParameters.cs | 18 + .../Decoder/JxlOutputEncodingInfo.cs | 78 ++++ .../Jxl/Processing/Decoder/JxlXybDecoder.cs | 180 ++++++++ .../Jxl/Processing/JxlCoefficientOrder.cs | 91 ++++ .../Formats/Jxl/Processing/JxlConvolve.cs | 424 ++++++++++++++++++ .../Formats/Jxl/Processing/JxlDctScales.cs | 2 +- .../Formats/Jxl/Processing/JxlToc.cs | 191 ++++++++ .../Formats/Jxl/Processing/JxlXorShift.cs | 2 + 23 files changed, 1661 insertions(+), 3 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlCieXyPrimaries.cs create mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs create mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlColorSpace.cs create mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlCustomTransferFunction.cs create mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlCustomXy.cs create mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlPrimaries.cs create mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlRenderingIntent.cs create mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlTransferFunction.cs create mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlWhitePoint.cs create mode 100644 src/ImageSharp/Formats/Jxl/Cms/README.md create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOpsinParameters.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOutputEncodingInfo.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlToc.cs diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlCieXyPrimaries.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlCieXyPrimaries.cs new file mode 100644 index 0000000000..7e5a4367e1 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlCieXyPrimaries.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.ColorProfiles; + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms; + +/// +/// RGB primaries for CIEXY +/// +internal struct JxlCieXyPrimaries +{ + /// + /// Gets or sets the R component + /// + public CieXyChromaticityCoordinates R { get; set; } + + /// + /// Gets or sets the G component + /// + public CieXyChromaticityCoordinates G { get; set; } + + /// + /// Gets or sets the B component + /// + public CieXyChromaticityCoordinates B { get; set; } +} diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs new file mode 100644 index 0000000000..9a87dccc2d --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs @@ -0,0 +1,77 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms; + +internal sealed class JxlColorEncoding +{ + public JxlWhitePoint WhitePoint { get; set; } = JxlWhitePoint.D65; + + public JxlPrimaries Primaries { get; set; } = JxlPrimaries.SRgb; + + public JxlRenderingIntent RenderingIntent { get; set; } = JxlRenderingIntent.Relative; + + public bool HaveFields { get; set; } = true; + + public JxlIccBytes? Icc { get; set; } + + public JxlColorSpace ColorSpace { get; set; } = JxlColorSpace.Rgb; + + public bool Cmyk { get; set; } + + public JxlCustomTransferFunction TransferFunction { get; set; } + + public JxlCustomXy White { get; set; } + + public JxlCustomXy Red { get; set; } + + public JxlCustomXy Green { get; set; } + + public JxlCustomXy Blue { get; set; } + + public bool HasPrimaries => this.ColorSpace is not (JxlColorSpace.Gray or JxlColorSpace.Xyb); + + public int Channels => (this.ColorSpace == JxlColorSpace.Gray) ? 1 : 3; + + public bool TryGetPrimaries(out JxlCieXyPrimaries xy) + { + xy = default; + + if (!this.HasPrimaries || !this.HasPrimaries) + { + return false; + } + + switch (this.Primaries) + { + case JxlPrimaries.Custom: + xy.R = this.Red.GetValue(); + xy.G = this.Green.GetValue(); + xy.B = this.Blue.GetValue(); + break; + + case JxlPrimaries.SRgb: + xy.R = new(0.639998686f, 0.330010138f); + xy.G = new(0.300003784f, 0.600003357f); + xy.B = new(0.150002046f, 0.059997204f); + break; + + case JxlPrimaries.Bt2020: + xy.R = new(0.708f, 0.292f); + xy.G = new(0.170f, 0.797f); + xy.B = new(0.131f, 0.046f); + break; + + case JxlPrimaries.P3: + xy.R = new(0.680f, 0.320f); + xy.G = new(0.265f, 0.690f); + xy.B = new(0.150f, 0.060f); + break; + + default: + throw new InvalidOperationException("Invalid primaries: " + this.Primaries); + } + + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlColorSpace.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlColorSpace.cs new file mode 100644 index 0000000000..b65cbbef98 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlColorSpace.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms; + +/// +/// Supported, JPEG XL-specific color space types. +/// +internal enum JxlColorSpace : byte +{ + /// + /// Trichromatic color data. This also includes CMYK if Black + /// ExtraChannelInfo is present. + /// + Rgb, + + /// + /// Single-channel data. + /// + Gray, + + /// + /// Like Rgb but fixed values for primaries. + /// + Xyb, + + /// + /// Unknown color space + /// + Unknown +} diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlCustomTransferFunction.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlCustomTransferFunction.cs new file mode 100644 index 0000000000..acf049d4a7 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlCustomTransferFunction.cs @@ -0,0 +1,109 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms; + +internal struct JxlCustomTransferFunction +{ + private const uint MaxGamma = 8192; + private const uint GammaMultiplier = 10000000; + + public JxlCustomTransferFunction() + { + } + + public bool HaveGamma { get; set; } + + public uint Gamma { get; set; } + + public JxlTransferFunction TransferFunction { get; set; } = JxlTransferFunction.SRgb; + + public readonly bool IsUnknown => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.Unknown; + + public readonly bool IsSrgb => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.SRgb; + + public readonly bool IsLinear => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.Linear; + + public readonly bool IsPq => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.Pq; + + public readonly bool IsHlg => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.Hlg; + + public readonly bool Is709 => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.Bt709; + + public readonly bool IsDci => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.Dci; + + public readonly JxlTransferFunction GetTransferFunction() + { + if (this.HaveGamma) + { + return JxlTransferFunction.Unknown; + } + + return this.TransferFunction; + } + + public void SetTransferFunction(JxlTransferFunction tf) + { + this.HaveGamma = false; + this.TransferFunction = tf; + } + + public readonly float GetGamma() + { + if (!this.HaveGamma) + { + return 0.0f; + } + + return this.Gamma * (1.0f / GammaMultiplier); + } + + public void SetGamma(float newGamma) + { + if (newGamma is < 1.0f / MaxGamma or > 1.0f) + { + throw new InvalidOperationException($"Invalid gamma {newGamma}"); + } + + this.HaveGamma = false; + + if (IsAlmostEqual(newGamma, 1.0f)) + { + this.TransferFunction = JxlTransferFunction.Linear; + return; + } + + if (IsAlmostEqual(newGamma, 1.0f / 2.6f)) + { + this.TransferFunction = JxlTransferFunction.Dci; + return; + } + + // Don't translate 0.45.. to kSRGB nor k709 - that might change pixel + // values because those curves also have a linear part. + this.HaveGamma = true; + this.Gamma = (uint)MathF.Round((float)(newGamma * GammaMultiplier)); + this.TransferFunction = JxlTransferFunction.Unknown; + } + + public readonly bool IsSame(JxlCustomTransferFunction other) + { + if (this.HaveGamma != other.HaveGamma) + { + return false; + } + + if (this.HaveGamma) + { + return this.Gamma == other.Gamma; + } + + return this.TransferFunction == other.TransferFunction; + } + + private static bool IsAlmostEqual(float a, float b) + { + const float dist = 1e-3f; + return MathF.Abs(a - b) < dist; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlCustomXy.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlCustomXy.cs new file mode 100644 index 0000000000..95652cdaf4 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlCustomXy.cs @@ -0,0 +1,53 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.ColorProfiles; + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms; + +/// +/// A serializable form of CieXyChromaticityCoordinates +/// +internal struct JxlCustomXy +{ + private const uint Multiplier = 1000000; + private const float RoughLimit = 4.0f; + private const int Min = -0x200000; + private const int Max = 0x1FFFFF; + + public int X { get; set; } + + public int Y { get; set; } + + public readonly CieXyChromaticityCoordinates GetValue() => new( + x: this.X * (1.0f / Multiplier), + y: this.Y * (1.0f / Multiplier)); + + public bool SetValue(CieXyChromaticityCoordinates xy) + { + bool ok = (Math.Abs(xy.X) < RoughLimit) && (Math.Abs(xy.Y) < RoughLimit); + + if (!ok) + { + throw new InvalidOperationException("X or Y is out of bounds"); + } + + this.X = (int)MathF.Round((float)(xy.X * Multiplier)); + + if (this.X is < Min or > Max) + { + throw new InvalidOperationException("X is out of bounds"); + } + + this.Y = (int)MathF.Round((float)(xy.Y * Multiplier)); + + if (this.Y is < Min or > Max) + { + throw new InvalidOperationException("Y is out of bounds"); + } + + return true; + } + + public readonly bool IsSame(CieXyChromaticityCoordinates other) => this.X == other.X && this.Y == other.Y; +} diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlPrimaries.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlPrimaries.cs new file mode 100644 index 0000000000..48d4766fa6 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlPrimaries.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms; + +/// +/// JPEG XL primaries +/// +internal enum JxlPrimaries : byte +{ + /// + /// Same as ITU-R BT.709 + /// + SRgb = 1, + + /// + /// Values encoded in separate fields + /// + Custom = 2, + + /// + /// ITU-R BT.2020 + /// + Bt2020 = 9, + + P3 = 11, +} diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlRenderingIntent.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlRenderingIntent.cs new file mode 100644 index 0000000000..739dddcccd --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlRenderingIntent.cs @@ -0,0 +1,13 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms; + +internal enum JxlRenderingIntent : byte +{ + // Values match ICC sRGB encodings + Perceptual, // Good for photos, requires a profile with LUT + Relative, // Good for logos + Saturation, // Perhaps useful for CG with fully saturated colors + Absolute, // Leaves white point unchanged; good for proofing +} diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlTransferFunction.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlTransferFunction.cs new file mode 100644 index 0000000000..cfee681765 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlTransferFunction.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms; + +/// +/// JPEG XL transfer function type +/// +internal enum JxlTransferFunction : byte +{ + /// + /// ITU-R BT.709 + /// + Bt709 = 1, + + /// + /// Unknown transfer function + /// + Unknown = 2, + + /// + /// Linear transfer function + /// + Linear = 8, + + /// + /// sRGB + /// + SRgb = 13, + + /// + /// From ITU-R BT.2100 + /// + Pq = 16, + + /// + /// From SMPTE RP 431-2 reference projector + /// + Dci = 17, + + /// + /// From ITU-R BT.2100 + /// + Hlg = 18, +} diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlWhitePoint.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlWhitePoint.cs new file mode 100644 index 0000000000..c6c7e019f5 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlWhitePoint.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms; + +/// +/// White point from CICP Color Primaries. +/// +// Note that we define a separate enum instead of using the ColorPrimaries +// enum from CICP code because JPEG XL doesn't support all color primaries defined +// by CICP. +internal enum JxlWhitePoint : byte +{ + /// + /// sRGB/ITU-R BT.709/Display P3/ITU-R BT.2020 + /// + D65 = 1, + + /// + /// Actual values encoded in separate fields + /// + Custom = 2, + + /// + /// XYZ + /// + E = 10, + + /// + /// DCI-P3 + /// + Dci = 11, +} diff --git a/src/ImageSharp/Formats/Jxl/Cms/README.md b/src/ImageSharp/Formats/Jxl/Cms/README.md new file mode 100644 index 0000000000..557a4c8b91 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/README.md @@ -0,0 +1,4 @@ +# CMS +This is the JPEG XL Color Management System component. + +Not to be confused with Content Management System. diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs index d9bc419e97..09400f9a83 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Diagnostics; +using SixLabors.ImageSharp.Formats.Jxl.Cms; using SixLabors.ImageSharp.Formats.Jxl.Fields; namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; @@ -124,5 +125,26 @@ public void SetUIntSamples(int bits) this.Modular16BitBufferSufficient = bits <= 12; } + public void SetIntensityTarget() + { + JxlCustomTransferFunction? tf = this.ColorEncoding?.TransferFunction; + + if (tf is not null) + { + if (tf.Value.IsPq) + { + this.SetIntensityTarget(10000); + } + else if (tf.Value.IsHlg) + { + this.SetIntensityTarget(1000); + } + else + { + this.SetIntensityTarget(DefaultIntensityTarget); + } + } + } + public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); } diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs index 357ce27c39..dc44594d9a 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs @@ -12,9 +12,13 @@ internal sealed class JxlOpsinInverseMatrix : IJxlFields public JxlMatrix3x3F InverseMatrix { get; set; } - public InlineArray3 OpsinBiases { get; set; } + // Prefer arrays so we can set values like this: + // JxlOpsinInverseMatrix m = ...; + // m.OpsinBiases[0] = 1f; + // An InlineArray can't do that. + public float[] OpsinBiases { get; set; } = new float[3]; - public InlineArray4 QuantBiases { get; set; } + public float[] QuantBiases { get; set; } = new float[4]; public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); } diff --git a/src/ImageSharp/Formats/Jxl/InlineArrays.cs b/src/ImageSharp/Formats/Jxl/InlineArrays.cs index 9c19e9266c..f006d2fd09 100644 --- a/src/ImageSharp/Formats/Jxl/InlineArrays.cs +++ b/src/ImageSharp/Formats/Jxl/InlineArrays.cs @@ -13,6 +13,15 @@ internal struct InlineArray3 private T first; } +/// +/// Used by JxlOpsinParameters +/// +[InlineArray(36)] +internal struct InlineArray36 +{ + private T first; +} + /// /// Used by JxlCustomTransformData /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs new file mode 100644 index 0000000000..45c7463248 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -0,0 +1,157 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.IO; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +internal sealed class JxlDecoderCore : ImageDecoderCore +{ + /// + /// Identifies the signature of the JPEG XL file. + /// + private enum JxlSignature : byte + { + /// + /// Error status indicating not enough bytes to detect the signature. + /// + NotEnoughBytes, + + /// + /// A JPEG XL code stream. + /// + CodeStream, + + /// + /// The signature is invalid. + /// + Invalid, + + /// + /// Container format. + /// + Container + } + + /// + /// Represents a data type. + /// + private enum JxlDataType : byte + { + /// + /// + /// + UInt8, + + /// + /// + /// + UInt16, + + /// + /// + /// + Float, + + /// + /// + /// + Float16 + } + + public JxlDecoderCore(DecoderOptions options) + : base(options) + { + } + + /// + /// Ensures that the coordinates are not out of bounds. + /// + /// First coordinate + /// Second coordinate + /// Image width + /// Boolean indicating whether the coordinates are out of bounds + private static bool IsOutOfBounds(int a, int b, int size) + { + int position = a + b; + + return position > size || position < a; + } + + private static int InitialBasicInfoSizeHint() + { + const int containerHeaderSize = 48; + const int maxCodestreamBasicInfoSize = 50; + return containerHeaderSize + maxCodestreamBasicInfoSize; + } + + private static JxlSignature DetectSignature(ReadOnlySpan buffer, int length, ref int position) + { + if (position >= length) + { + return JxlSignature.NotEnoughBytes; + } + + buffer = buffer[position..]; + length -= position; + + // 0xFF 0x0A represents a codestream + if (length >= 1 && buffer[0] == 0xFF) + { + if (length < 2) + { + // We need at least two bytes for a valid codestream signature + return JxlSignature.NotEnoughBytes; + } + else if (buffer[1] == CodestreamMarker) + { + position += 2; + return JxlSignature.CodeStream; + } + else + { + return JxlSignature.Invalid; + } + } + + // Container? + if (length >= 1 && buffer[0] == 0) + { + if (length < SignatureBox.Length) + { + return JxlSignature.NotEnoughBytes; + } + else if (buffer[SignatureBox.Length..].SequenceEqual(SignatureBox)) + { + position += SignatureBox.Length; + return JxlSignature.Container; + } + else + { + return JxlSignature.Invalid; + } + } + + // Signature is invalid + return JxlSignature.Invalid; + } + + private static JxlSignature DetectSignature(ReadOnlySpan buffer, int length) + { + int position = 0; + return DetectSignature(buffer, length, ref position); + } + + private static int BitsPerChannel(JxlDataType dataType) + => dataType switch + { + JxlDataType.UInt8 => 8, + JxlDataType.UInt16 or JxlDataType.Float16 => 16, + JxlDataType.Float => 32, + _ => 0 + }; + + protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) => throw new NotImplementedException(); + + protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) => throw new NotImplementedException(); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs new file mode 100644 index 0000000000..819517551a --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs @@ -0,0 +1,63 @@ +// 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.Memory.ImageTypes; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +internal static class JxlNoiseDecoder +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void BitsToFloatingPoint(ReadOnlySpan randomBits, Span floats) + { + Vector bits = new(randomBits); + Vector rand12 = ((bits >> 9) | new Vector(0x3F800000u)).As(); + rand12.StoreUnsafe(ref MemoryMarshal.GetReference(floats)); + } + + public static void GenerateRandomImage(JxlXorShift rng, Rectangle rectangle, JxlImageF noise) + { + const int floatsPerBatch = JxlXorShift.Generators * sizeof(ulong) / sizeof(float); + + int xSize = rectangle.Width; + int ySize = rectangle.Height; + + Span batch64 = stackalloc ulong[JxlXorShift.Generators]; + Span batch32 = stackalloc uint[JxlXorShift.Generators * 2]; + + // stackalloc doesn't zero-initialize, so clear values + batch64.Clear(); + batch32.Clear(); + + int n = Vector.Count; + + for (int y = 0; y < ySize; y++) + { + Span row = noise.GetRow(rectangle, y); + int x = 0; + for (; x + floatsPerBatch < xSize; x += floatsPerBatch) + { + rng.Fill(batch64); + MemoryMarshal.Cast(batch32).CopyTo(batch64); + for (int i = 0; i < floatsPerBatch; i += n) + { + BitsToFloatingPoint(batch32[i..], row[(x + i)..]); + } + } + + rng.Fill(batch64); + MemoryMarshal.Cast(batch32).CopyTo(batch64); + + int batchPos = 0; + + for (; x < xSize; x += n) + { + BitsToFloatingPoint(batch32[batchPos..], row[x..]); + batchPos += n; + } + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOpsinParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOpsinParameters.cs new file mode 100644 index 0000000000..c18a18ef0b --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOpsinParameters.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +internal sealed class JxlOpsinParameters +{ + // Use arrays instead of InlineArrays because, with inline arrays we can't do: + // JxlOpsinParameters parameters = ...; + // parameters.OpsinBiasesCbrt[0] /* <-- error */ = 1.25f; + public float[] InverseOpsinMatrix { get; set; } = new float[36]; + + public float[] OpsinBiases { get; set; } = new float[4]; + + public float[] OpsinBiasesCbrt { get; set; } = new float[4]; + + public float[] QuantBiases { get; set; } = new float[4]; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOutputEncodingInfo.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOutputEncodingInfo.cs new file mode 100644 index 0000000000..bdec666432 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOutputEncodingInfo.cs @@ -0,0 +1,78 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Formats.Jxl.Cms; +using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +// Prefer class instead of struct because it's too large for a struct +// Note that this struct wasn't well documented, so it's not that easy to add +// XML documentation here. +internal sealed class JxlOutputEncodingInfo +{ + public JxlColorEncoding? OriginalColorEncoding { get; set; } + + public float OriginalIntensityTarget { get; set; } + + public JxlMatrix3x3F OriginalInverseMatrix { get; set; } + + public bool DefaultTransform { get; set; } + + public bool XybEncoded { get; set; } + + /// + /// Gets or sets the requested color encoding. + /// + public JxlColorEncoding ColorEncoding { get; set; } = new(); + + public JxlColorEncoding LinearColorEncoding { get; set; } = new(); + + public bool ColorEncodingIsOriginal { get; set; } + + public JxlOpsinParameters OpsinParameters { get; set; } = new(); + + public bool AllDefaultOpsin { get; set; } + + public float InverseGamma { get; set; } + + public Vector3 Luminances { get; set; } + + public float DesiredIntensityTarget { get; set; } + + public bool CmsSet { get; set; } + + public JxlCmsInterface Cms { get; set; } + + public void SetFromMetadata(JxlCodecMetadata metadata) + { + JxlImageMetadata imageMetadata = metadata.ImageMetadata ?? throw new InvalidOperationException("Missing image metadata"); + + this.OriginalColorEncoding = imageMetadata.ColorEncoding; + this.OriginalIntensityTarget = imageMetadata.IntensityTarget; + this.DesiredIntensityTarget = this.OriginalIntensityTarget; + + JxlOpsinInverseMatrix inverseMatrix = metadata.CustomTransformData?.OpsinInverseMatrix ?? throw new InvalidOperationException("Missing Opsin inverse matrix or transform data"); + this.OriginalInverseMatrix = inverseMatrix.InverseMatrix; + this.DefaultTransform = inverseMatrix.AllDefault; + this.XybEncoded = imageMetadata.XybEncoded; + + JxlOpsinParameters parameters = this.OpsinParameters; + + imageMetadata.OpsinBiases.CopyTo(parameters.OpsinBiases); + parameters.OpsinBiasesCbrt[0] = MathF.Cbrt(parameters.OpsinBiases[0]); + parameters.OpsinBiasesCbrt[1] = MathF.Cbrt(parameters.OpsinBiases[1]); + parameters.OpsinBiasesCbrt[2] = MathF.Cbrt(parameters.OpsinBiases[2]); + + parameters.OpsinBiasesCbrt[3] = 1; + parameters.OpsinBiases[3] = 1; + + inverseMatrix.QuantBiases.AsSpan().CopyTo(parameters.QuantBiases); + + bool origOK = JxlXybDecoder.CanOutputToColorEncoding(this.OriginalColorEncoding ?? throw new InvalidCastException("Missing color encoding")); + bool origGrey = this.OriginalColorEncoding.IsGray; + + return this.SetColorEncoding(!this.XybEncoded || origOK ? this.OriginalColorEncoding : JxlColorEncoding.LinearSrgb(origGrey)); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs new file mode 100644 index 0000000000..940b01170a --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs @@ -0,0 +1,180 @@ +// 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.Cms; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +/// +/// Decodes the XYB color format (which JPEG XL uses) into RGB. +/// +internal static class JxlXybDecoder +{ + /// + /// Converts XYB to RGB using SIMD, one vector at a time. + /// + /// X channel + /// Y channel + /// B channel + /// Opsin parameters & configuration + /// Output R + /// Output G + /// Output B + public static void ConvertXybToRgb( + Vector opsinX, + Vector opsinY, + Vector opsinB, + JxlOpsinParameters opsinParameters, + ref Vector linearR, + ref Vector linearG, + ref Vector linearB) + { + Vector negBiasR = new(opsinParameters.OpsinBiaases[0]); + Vector negBiasG = new(opsinParameters.OpsinBiaases[1]); + Vector negBiasB = new(opsinParameters.OpsinBiaases[2]); + + Vector gammaR = opsinX + opsinY; + Vector gammaG = opsinY - opsinX; + Vector gammaB = opsinB; + + Vector gammaR2 = gammaR * gammaR; + Vector gammaG2 = gammaG * gammaG; + Vector gammaB2 = gammaB * gammaB; + + Vector mixedR = (gammaR2 * gammaR) + negBiasR; + Vector mixedG = (gammaG2 * gammaG) + negBiasG; + Vector mixedB = (gammaB2 * gammaB) + negBiasB; + + Span inverseMatrix = opsinParameters.GetInverseOpsinMatrixSpan(); + + linearR = LoadDuplicate128(ref inverseMatrix[0 * 4]) * mixedR; + linearG = LoadDuplicate128(ref inverseMatrix[3 * 4]) * mixedR; + linearB = LoadDuplicate128(ref inverseMatrix[6 * 4]) * mixedR; + + linearR = (LoadDuplicate128(ref inverseMatrix[1 * 4]) * mixedG) + linearR; + linearG = (LoadDuplicate128(ref inverseMatrix[4 * 4]) * mixedG) + linearG; + linearB = (LoadDuplicate128(ref inverseMatrix[7 * 4]) * mixedG) + linearB; + + linearR = (LoadDuplicate128(ref inverseMatrix[2 * 4]) * mixedB) + linearR; + linearG = (LoadDuplicate128(ref inverseMatrix[5 * 4]) * mixedB) + linearG; + linearB = (LoadDuplicate128(ref inverseMatrix[8 * 4]) * mixedB) + linearB; + } + + public static bool OpsinToLinear(JxlImage3F opsin, Rectangle rect, JxlImage3F linear, JxlOpsinParameters opsinParameters) + { + if (!SameSize(rect, linear)) + { + return false; + } + + if (Vector.Count < 4) + { + // TODO: support 64bit vectors or no SIMD? + throw new PlatformNotSupportedException("XYB to RGB conversion requires at least 128-bit SIMD"); + } + + // Reuse variables instead of creating them over + // and over again + Unsafe.SkipInit(out Vector linearR); + Unsafe.SkipInit(out Vector linearG); + Unsafe.SkipInit(out Vector linearB); + + for (int y = 0; y < rect.Height; y++) + { + ReadOnlySpan rowOpsin0 = opsin.PlaneRow(rect, 0, y); + ReadOnlySpan rowOpsin1 = opsin.PlaneRow(rect, 1, y); + ReadOnlySpan rowOpsin2 = opsin.PlaneRow(rect, 2, y); + + ref float rowOpsin0Reference = ref MemoryMarshal.GetReference(rowOpsin0); + ref float rowOpsin1Reference = ref MemoryMarshal.GetReference(rowOpsin1); + ref float rowOpsin2Reference = ref MemoryMarshal.GetReference(rowOpsin2); + + Span rowLinear0 = linear.PlaneRow(0, y); + Span rowLinear1 = linear.PlaneRow(1, y); + Span rowLinear2 = linear.PlaneRow(2, y); + + ref float rowLinear0Reference = ref MemoryMarshal.GetReference(rowLinear0); + ref float rowLinear1Reference = ref MemoryMarshal.GetReference(rowLinear1); + ref float rowLinear2Reference = ref MemoryMarshal.GetReference(rowLinear2); + + for (int x = 0; x < rect.Height; x += Vector.Count) + { + Vector inOpsinX = Vector.LoadUnsafe(ref Unsafe.Add(ref rowOpsin0Reference, x)); + Vector inOpsinY = Vector.LoadUnsafe(ref Unsafe.Add(ref rowOpsin1Reference, x)); + Vector inOpsinB = Vector.LoadUnsafe(ref Unsafe.Add(ref rowOpsin2Reference, x)); + + ConvertXybToRgb(inOpsinX, inOpsinY, inOpsinB, opsinParameters, ref linearR, ref linearG, ref linearB); + + linearR.StoreUnsafe(ref Unsafe.Add(ref rowLinear0Reference, x)); + linearG.StoreUnsafe(ref Unsafe.Add(ref rowLinear1Reference, x)); + linearB.StoreUnsafe(ref Unsafe.Add(ref rowLinear2Reference, x)); + } + } + + return true; + } + + /// + /// A SIMD utility method which reads next 128 bits + /// (which in this case happens to be next 4 floats), + /// and duplicates them to fit in the CPU vector size. + /// For example, + /// + /// 128 bit vectors: A B C D (as-is) + /// 256 bit vectors: A B C D A B C D (duplicate once) + /// 512 bit vectors: A B C D A B C D A B C D A B C D (duplicate three times) + /// + /// Vector<T> has support for arbitrarily large + /// vector sizes. For example, some ARM CPUs support 2048-bit + /// vectors through Vector<T>. In that specific case, this + /// method can be used for future-proofing. + /// + /// Note that this method, albeit future-proof, may be considered + /// slow for smaller vector sizes (think CPUs with 128bit or 256bit vectors). + /// + /// Reference to first element to load & duplicate. + /// Vector with first 128 bits duplicated across the vector width. + private static Vector LoadDuplicate128(ref float reference) + { + Span value = stackalloc float[Vector.Count]; + Span values128 = [ + reference, + Unsafe.Add(ref reference, 1), + Unsafe.Add(ref reference, 2), + Unsafe.Add(ref reference, 3) + ]; + + for (int i = 0; i < Vector.Count; i += 4) + { + values128[i..].CopyTo(value[i..]); + } + + return new(value); + } + + public static bool CanOutputToColorEncoding(JxlColorEncoding colorEncoding) + { + if (!colorEncoding.HaveFields) + { + return false; + } + + JxlCustomTransferFunction tf = colorEncoding.TransferFunction; + + if (!tf.IsPq && !tf.IsSrgb && !tf.HaveGamma && !tf.IsLinear && !tf.IsHlg && !tf.IsDci && !tf.Is709) + { + return false; + } + + if (colorEncoding.IsGray && colorEncoding.WhitePoint != JxlWhitePoint.D65) + { + return false; + } + + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs new file mode 100644 index 0000000000..ccea59b2c8 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs @@ -0,0 +1,91 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Security.Principal; +using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Helps with reordering coefficients. +/// +internal static class JxlCoefficientOrder +{ + public const int Limit = 6156; + + public const int CoefficientOrderMaxSize = Limit * JxlFrameDimensions.DctBlockSize; + + public const int PermutationContexts = 8; + + /// + /// Gets the pattern which coefficients must follow to compute offsets. + /// + public static ReadOnlySpan CoefficientOrderOffsets => + [ + 0, 1, 2, 3, 4, 5, 6, 10, 14, 18, + 34, 50, 66, 68, 70, 72, 76, 80, 84, 92, + 100, 108, 172, 236, 300, 332, 364, 396, 652, 908, + 1164, 1292, 1420, 1548, 2572, 3596, 4620, 5132, 5644, Limit + ]; + + /// + /// Gets the pattern which coefficients must follow to compute offsets. + /// + public static ReadOnlySpan StrategyOrder => + [ + 0, 1, 1, 1, 2, 3, 4, 4, 5, 5, 6, 6, 1, 1, + 1, 1, 1, 1, 7, 8, 8, 9, 10, 10, 11, 12, 12, + ]; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int CoeffOrderOffset(int o, int c) => CoefficientOrderOffsets[(3 * o) + c] * JxlFrameDimensions.DctBlockSize; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint CoeffOrderContext(uint value) + { + uint token = 0; + uint nbits = 0; + uint bits = 0; + + new JxlAnsHybridUIntConfiguration(0, 0, 0).Encode(value, ref token, ref nbits, ref bits); + + return Math.Min(token, PermutationContexts - 1u); + } + + public static bool ReadPermutation(int skip, int size, Span order, JxlBitReader bitReader, JxlAnsSymbolReader reader, Span contextMap) + { + Span lehmer = stackalloc uint[size]; + lehmer.Clear(); + + Span temp = stackalloc uint[size * 2]; + temp.Clear(); + + uint end = reader.ReadHybridUnsignedInteger(CoeffOrderContext((int)size), bitReader, contextMap) + skip; + + if (end > size) + { + throw new InvalidOperationException("Invalid permutation size"); + } + + uint last = 0; + + for (int i = skip; i < end; i++) + { + lehmer[i] = reader.ReadHybridUnsignedInteger(CoeffOrderContext(last), bitReader, contextMap); + last = lehmer[i]; + if (lehmer[i] >= size - i) + { + throw new InvalidOperationException("Invalid lehmer code"); + } + } + + if (order.IsEmpty) + { + return true; + } + + return JxlLehmerCode.DecodeLehmerCode(lehmer, temp, size, order); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs new file mode 100644 index 0000000000..9bf6a7dcb0 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs @@ -0,0 +1,424 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Convolution filters +/// +internal static class JxlConvolve +{ + /// + /// Weighted sum of 1x5 pixels around ix, iy with [wx2, wx1, wx0, wx1, wx2]. + /// + public static float WeightedSumBorder( + JxlImageF input, + Func wrapY, + long ix, + long iy, + int width, + int height, + float wx0, + float wx1, + float wx2) + { + ReadOnlySpan row = input.GetRow(wrapY(iy, height)); + + float inM2 = row[WrapMirror(ix - 2, width)]; + float inP2 = row[WrapMirror(ix + 2, width)]; + float inM1 = row[WrapMirror(ix - 1, width)]; + float inP1 = row[WrapMirror(ix + 1, width)]; + float in00 = row[(int)ix]; + + float sum2 = wx2 * (inM2 + inP2); + float sum1 = wx1 * (inM1 + inP1); + float sum0 = wx0 * in00; + + return sum2 + (sum1 + sum0); + } + + public static Vector WeightedSum( + JxlImageF input, + Func wrapY, + int ix, + long iy, + int height, + Vector wx0, + Vector wx1, + Vector wx2) + { + ReadOnlySpan center = input.GetRow(wrapY(iy, height))[ix..]; + ref float centerRef = ref MemoryMarshal.GetReference(center); + + Vector inM2 = Vector.LoadUnsafe(ref Unsafe.Subtract(ref centerRef, 2)); + Vector inP2 = Vector.LoadUnsafe(ref Unsafe.Add(ref centerRef, 2)); + Vector inM1 = Vector.LoadUnsafe(ref Unsafe.Subtract(ref centerRef, 1)); + Vector inP1 = Vector.LoadUnsafe(ref Unsafe.Add(ref centerRef, 1)); + Vector in00 = Vector.LoadUnsafe(ref centerRef); + + Vector sum2 = wx2 * (inM2 + inP2); + Vector sum1 = wx1 * (inM1 + inP1); + Vector sum0 = wx0 * in00; + + return sum2 + (sum1 + sum0); + } + + public static float Symmetric5Border(JxlImageF input, Func wrapY, long ix, long iy, JxlWeightsSymmetric5 weights) + { + float w0 = weights.GetCVector()[0]; + float w1 = weights.GetRVector()[0]; + float w2 = weights.GetR2Vector()[0]; + float w4 = weights.GetDVector()[0]; + float w5 = weights.GetCVector()[0]; + float w8 = weights.GetD2Vector()[0]; + + int width = input.XSize; + int height = input.YSize; + + float sum0 = WeightedSumBorder(input, wrapY, ix, iy, width, height, w0, w1, w2) + + WeightedSumBorder(input, wrapY, ix, iy - 2, width, height, w2, w5, w8); + + float sum1 = WeightedSumBorder(input, wrapY, ix, iy + 2, width, height, w2, w5, w8); + + sum0 += WeightedSumBorder(input, wrapY, ix, iy + 1, width, height, w1, w4, w5); + sum1 += WeightedSumBorder(input, wrapY, ix, iy - 1, width, height, w1, w4, w5); + + return sum0 + sum1; + } + + public static void Symmetric5Interior( + JxlImageF image, + int ix, + Func wrapY, + int rix, + long iy, + JxlWeightsSymmetric5 weights, + Span rowOut) + { + Vector w0 = LoadDuplicate128(weights.GetCVector()); // c + Vector w1 = LoadDuplicate128(weights.GetRVector()); // r + Vector w2 = LoadDuplicate128(weights.GetR2Vector()); // R + Vector w4 = LoadDuplicate128(weights.GetDVector()); // d + Vector w5 = LoadDuplicate128(weights.GetLVector()); // L + Vector w8 = LoadDuplicate128(weights.GetD2Vector()); // D + + int height = image.YSize; + Vector sum0 = WeightedSum(image, wrapY, ix, iy, height, w0, w1, w2) + + WeightedSum(image, wrapY, ix, iy - 2, height, w2, w5, w8); + + Vector sum1 = WeightedSum(image, wrapY, ix, iy + 2, height, w2, w5, w8); + + sum0 += WeightedSum(image, wrapY, ix, iy - 1, height, w1, w4, w5); + sum1 += WeightedSum(image, wrapY, ix, iy + 1, height, w1, w4, w5); + + (sum0 + sum1).StoreUnsafe(ref Unsafe.Add(ref MemoryMarshal.GetReference(rowOut), rix)); + } + + public static void Symmetric5Row( + JxlImageF image, + Func wrapY, + in Rectangle rect, + long iy, + JxlWeightsSymmetric5 weights, + Span rowOut) + { + const int radius = 2; + int xEnd = rect.Right; + + int rix = 0; + int ix = rect.X; + + int n = Vector.Count; + int alignedX = RoundUpTo(radius, n); + + for (; ix < Math.Min(alignedX, xEnd); ix++, rix++) + { + rowOut[rix] = Symmetric5Border(image, wrapY, ix, iy, weights); + } + + for (; ix + n + radius <= xEnd; ix += n, rix += n) + { + Symmetric5Interior(image, ix, wrapY, rix, iy, weights, rowOut); + } + + for (; ix < xEnd; ix++, rix++) + { + rowOut[rix] = Symmetric5Border(image, wrapY, ix, iy, weights); + } + } + + public static bool Symmetric5( + JxlImageF input, + in Rectangle rectangle, + JxlWeightsSymmetric5 weights, + JxlImageF output, + Rectangle outputRect) + { + if (rectangle.Width != outputRect.Width || rectangle.Height != outputRect.Height) + { + return false; + } + + int height = rectangle.Height; + + for (int riy = 0; riy < height; riy++) + { + int iy = rectangle.Y + riy; + + if (iy < 2 || iy >= rectangle.Height - 2) + { + Symmetric5Row(input, WrapMirror, in rectangle, iy, weights, output.GetRow(outputRect, riy)); + } + else + { + Symmetric5Row(input, in rectangle, iy, weights, output.GetRow(outputRect, riy)); + } + } + + return true; + } + + public static float SlowSymmetric3Pixel( + JxlImageF image, + int x, + int y, + int width, + int height, + JxlWeightsSymmetric3 weights, + Func wrapX, + Func wrapY) + { + float sum = 0.0f; + + float c0 = weights.GetCVector()[0]; + float r0 = weights.GetRVector()[0]; + float d0 = weights.GetDVector()[0]; + + for (int ky = -1; ky <= 1; ky++) + { + int yy = wrapY(y + ky, height); + ReadOnlySpan row = image.GetRow(yy); + + float wc = (ky == 0) ? c0 : r0; + float wlr = (ky == 0) ? r0 : d0; + + int xm1 = wrapX(x - 1, width); + int xp1 = wrapX(x + 1, width); + + sum += (row[x] * wc) + ((row[xm1] + row[xp1]) * wlr); + } + + return sum; + } + + public static void SlowSymmetric3Row( + JxlImageF image, + int y, + int width, + int height, + JxlWeightsSymmetric3 weights, + Span outputRow, + Func wrapY) + { + outputRow[0] = SlowSymmetric3Pixel( + image, + 0, + y, + width, + height, + weights, + WrapMirror, + wrapY); + + for (int x = 1; x < width - 1; x++) + { + outputRow[x] = SlowSymmetric3Pixel( + image, + x, + y, + width, + height, + weights, + WrapUnchanged, + wrapY); + } + + outputRow[width - 1] = SlowSymmetric3Pixel( + image, + width - 1, + y, + width, + height, + weights, + WrapMirror, + wrapY); + } + + public static void SlowSymmetric3( + JxlImageF input, + Rectangle rect, + JxlWeightsSymmetric3 weights, + JxlImageF output) + { + int width = rect.Width; + int height = rect.Height; + + const int radius = 1; + + for (int y = 0; y < height; y++) + { + Span rowOut = output.GetRow(y); + + if (y < radius || y >= height - radius) + { + SlowSymmetric3Row( + input, + y, + width, + height, + weights, + rowOut, + WrapMirror); + } + else + { + SlowSymmetric3Row( + input, + y, + width, + height, + weights, + rowOut, + WrapUnchanged); + } + } + } + + public static float SlowSeparablePixel( + JxlImageF image, + Rectangle rect, + int x, + int y, + int radius, + ReadOnlySpan horzWeights, + ReadOnlySpan vertWeights) + { + int width = image.XSize; + int height = image.YSize; + + float sum = 0; + + for (int dy = -radius; dy <= radius; dy++) + { + float wy = vertWeights[Math.Abs(dy) * 4]; + int sy = WrapMirror(rect.Y + y + dy, height); + ReadOnlySpan row = image.GetRow(sy); + + for (int dx = -radius; dx <= radius; dx++) + { + float wx = horzWeights[Math.Abs(dx) * 4]; + int sx = WrapMirror(rect.X + x + dx, width); + sum += row[sx] * wx * wy; + } + } + + return sum; + } + + public static void SlowSeparable( + JxlImageF input, + Rectangle inputRect, + JxlWeightsSeparable5 weights, + JxlImageF output, + Rectangle outputRect, + int radius) + { + ReadOnlySpan horz = weights.Horizontal; + ReadOnlySpan vert = weights.Vertical; + + for (int y = 0; y < inputRect.Height; y++) + { + Span rowOut = output.GetRow(outputRect, y); + + for (int x = 0; x < inputRect.Width; x++) + { + rowOut[x] = SlowSeparablePixel( + input, + inputRect, + x, + y, + radius, + horz, + vert); + } + } + } + + public static void SlowSeparable5( + JxlImageF input, + Rectangle inputRect, + JxlWeightsSeparable5 weights, + JxlImageF output, + Rectangle outputRect) + => SlowSeparable(input, inputRect, weights, output, outputRect, 2); + + public static void FirstL1(ReadOnlySpan c, Span dst) + { + dst[0] = c[0]; + for (int i = 1; i < dst.Length; i++) + { + dst[i] = c[i - 1]; + } + } + + public static void FirstL2(ReadOnlySpan c, Span dst) + { + dst[0] = c[1]; + dst[1] = c[0]; + + for (int i = 2; i < dst.Length; i++) + { + dst[i] = c[i - 2]; + } + } + + /// + /// A SIMD utility method which takes in the 128 bit vector + /// and duplicates its values to fit in the CPU vector size. + /// For example, + /// + /// 128 bit vectors: A B C D (as-is) + /// 256 bit vectors: A B C D A B C D (duplicate once) + /// 512 bit vectors: A B C D A B C D A B C D A B C D (duplicate three times) + /// + /// Vector<T> has support for arbitrarily large + /// vector sizes. For example, some ARM CPUs support 2048-bit + /// vectors through Vector<T>. In that specific case, this + /// method can be used for future-proofing. + /// + /// Note that this method, albeit future-proof, may be considered + /// slow for smaller vector sizes (think CPUs with 256bit vectors). + /// + /// Vector to duplicate. + /// New vector that is duplicated across the width. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector LoadDuplicate128(Vector128 vec) + { + Span value = stackalloc float[Vector.Count]; + for (int i = 0; i < Vector.Count; i += 4) + { + vec.CopyTo(value[i..]); + } + + return new(value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int RoundUpTo(int value, int multiple) => ((value + multiple - 1) / multiple) * multiple; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctScales.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlDctScales.cs index 84e7819bc5..26bc7ee8e3 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlDctScales.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlDctScales.cs @@ -5,7 +5,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing; /// /// Read-only cosine lookups for the Discrete Cosine Transform (DCT), -/// a mathematical function used for quantization. +/// a mathematical function used for quantization and coefficient reordering. /// internal static class JxlDctScales { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlToc.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlToc.cs new file mode 100644 index 0000000000..21fa5d23b2 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlToc.cs @@ -0,0 +1,191 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Table of Contents encoding +/// +internal static class JxlToc +{ + private static readonly JxlU32Enc TocDistribution = new( + JxlFieldExpressions.Bits(10), + JxlFieldExpressions.BitsOffset(14, 2024), + JxlFieldExpressions.BitsOffset(22, 17408), + JxlFieldExpressions.BitsOffset(30, 4211712)); + + public static int AcGroupIndex(int pass, int group, int numGroups, int numDcGroups) + => 2 + numDcGroups + (pass * numGroups) + group; + + public static int NumberOfTocEntries(int numGroups, int numDcGroups, int numPasses) + { + if (numGroups == 1 && numPasses == 1) + { + return 1; + } + + return AcGroupIndex(0, 0, numGroups, numDcGroups) + (numGroups * numPasses); + } + + private const int BitsPerByte = 8; + private const int MaxTocEntries = 65536; + + public static bool ReadToc( + Configuration configuration, + int tocEntries, + JxlBitReader reader, + List sizes, + List permutation) + { + if (tocEntries > MaxTocEntries) + { + return false; // too many TOC entries + } + + sizes.Clear(); + sizes.Capacity = tocEntries; + + for (int i = 0; i < tocEntries; i++) + { + sizes.Add(0); + } + + if (reader.TotalBitsConsumed >= reader.TotalBytes * BitsPerByte) + { + return false; // not enough bytes + } + + bool CheckBitBudget(int numEntries) + { + long minimalBitCost = numEntries * (2 + 10); + long bitBudget = reader.TotalBytes * BitsPerByte; + long expenses = reader.TotalBitsConsumed; + + return expenses <= bitBudget && + minimalBitCost <= bitBudget - expenses; + } + + if (tocEntries <= 0) + { + return false; + } + + if (reader.ReadBits32(1) == 1) + { + if (!CheckBitBudget(tocEntries)) + { + return false; + } + + permutation.Clear(); + + for (int i = 0; i < tocEntries; i++) + { + permutation.Add(default); + } + + if (!DecodePermutation( + configuration, + 0, + tocEntries, + permutation, + reader)) + { + return false; + } + } + + if (!reader.JumpToByteBoundary()) + { + return false; + } + + if (!CheckBitBudget(tocEntries)) + { + return false; + } + + for (int i = 0; i < tocEntries; i++) + { + sizes[i] = JxlU32Coder.Read(TocDistribution, reader); + } + + if (!reader.JumpToByteBoundary()) + { + return false; + } + + return CheckBitBudget(0); + } + + public static bool ReadGroupOffsets( + Configuration configuration, + int tocEntries, + JxlBitReader reader, + List offsets, + List sizes, + out ulong totalSize) + { + totalSize = 0; + + List permutation = []; + + if (!ReadToc( + configuration, + tocEntries, + reader, + sizes, + permutation)) + { + return false; + } + + offsets.Clear(); + offsets.Capacity = tocEntries; + + for (int i = 0; i < tocEntries; i++) + { + offsets.Add(0); + } + + ulong offset = 0; + + for (int i = 0; i < tocEntries; i++) + { + ulong size = sizes[i]; + + if (offset + size < offset) + { + return false; + } + + offsets[i] = offset; + offset += size; + } + + totalSize = offset; + + if (permutation.Count != 0) + { + List permutedOffsets = new(tocEntries); + List permutedSizes = new(tocEntries); + + foreach (byte index in permutation) + { + permutedOffsets.Add(offsets[index]); + permutedSizes.Add(sizes[index]); + } + + offsets.Clear(); + offsets.AddRange(permutedOffsets); + + sizes.Clear(); + sizes.AddRange(permutedSizes); + } + + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlXorShift.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlXorShift.cs index b3849d69f0..78d8900a52 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlXorShift.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlXorShift.cs @@ -7,6 +7,8 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing; internal sealed class JxlXorShift { + public const int Generators = 8; + private readonly ulong[] s0 = new ulong[8]; private readonly ulong[] s1 = new ulong[8]; From 083385bde44688700d2332fa1201fb205b22b88d Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 1 Aug 2026 23:46:24 +0400 Subject: [PATCH 057/328] Use inline arrays in quantizer --- src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs index 4230952860..01723479bb 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs @@ -51,12 +51,12 @@ internal sealed class JxlQuantizer /// /// Represents the multipliers for the DC coefficients. /// - private readonly float[] mulDc = new float[4]; + private readonly InlineArray4 mulDc; /// /// Represents the inverse multipliers for the DC coefficients. /// - private readonly float[] inverseMulDc = new float[4]; + private readonly InlineArray4 inverseMulDc; /// /// Global scale @@ -86,7 +86,7 @@ internal sealed class JxlQuantizer /// /// The zero bias. /// - private readonly float[] zeroBias = new float[3]; + private readonly InlineArray3 zeroBias; /// /// The dequant matrices. From a1cae71e4d8e1fe6182236b1004901aed798ce7d Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 1 Aug 2026 23:52:40 +0400 Subject: [PATCH 058/328] Prefer const --- .../Formats/Jxl/Processing/Butteraugli/Butteraugli.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs index eb25583ecf..87ac5855a8 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs @@ -36,10 +36,10 @@ internal static class Butteraugli private const float IntensityTargetNormalizationHack = 0.79079917404f; - private static readonly float InternalGoodQualityThreshold = + private const float InternalGoodQualityThreshold = 17.83f * IntensityTargetNormalizationHack; - private static readonly float GlobalScale = + private const float GlobalScale = 1.0f / InternalGoodQualityThreshold; public static ReadOnlySpan Wmul => From eb80187037968dd04393235d0df4fc6a0acf7bf9 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sun, 2 Aug 2026 00:32:55 +0400 Subject: [PATCH 059/328] Don't use System.Diagnostics.Debug --- .../Formats/Jxl/Fields/JxlF16Coder.cs | 1 - .../Formats/Jxl/Fields/JxlReadVisitor.cs | 2 +- .../Formats/Jxl/Fields/JxlVisitorBase.cs | 22 ++++++----- .../Formats/Jxl/IO/Entropy/JxlAnsHelper.cs | 13 +++---- .../Entropy/JxlAnsHybridUIntConfiguration.cs | 6 ++- .../Jxl/IO/Metadata/JxlImageMetadata.cs | 6 ++- .../Jxl/Memory/ImageTypes/JxlImageB.cs | 4 +- .../Formats/Jxl/Memory/JxlImage3{T}.cs | 9 +++-- .../Formats/Jxl/Memory/JxlPlaneBase.cs | 8 ++-- .../Jxl/Processing/Decoder/JxlAnsReader.cs | 19 +++------- .../Jxl/Processing/Decoder/JxlBitReader.cs | 5 +-- .../Formats/Jxl/Processing/JxlAcContext.cs | 18 +++++---- .../Formats/Jxl/Processing/JxlAcStrategy.cs | 5 --- .../Jxl/Processing/JxlAcStrategyImage.cs | 14 +++---- .../Jxl/Processing/JxlAcStrategyRow.cs | 3 +- .../Formats/Jxl/Processing/JxlMatrix3x3F.cs | 2 +- .../Processing/Splines/JxlQuantizedSpline.cs | 38 +++++-------------- 17 files changed, 71 insertions(+), 104 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs index d45b0968af..b930e5dc49 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; namespace SixLabors.ImageSharp.Formats.Jxl.Fields; diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs index b0396579cc..35be5d15e0 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; namespace SixLabors.ImageSharp.Formats.Jxl.Fields; diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlVisitorBase.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlVisitorBase.cs index 7d2dffffbf..ff3ee5a3d5 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlVisitorBase.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlVisitorBase.cs @@ -1,10 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Diagnostics; - -#pragma warning disable SA1405 // Debug.Assert should provide message text - namespace SixLabors.ImageSharp.Formats.Jxl.Fields; internal class JxlVisitorBase : JxlVisitor @@ -26,14 +22,18 @@ public override bool Visit(IJxlFields fields) if (visited) { - // TODO: use DebugGuard - Debug.Assert(!this.extensionStates.IsBegun || this.extensionStates.IsEnded); + if (!(!this.extensionStates.IsBegun || this.extensionStates.IsEnded)) + { + throw new InvalidOperationException("Invalid extension state"); + } } this.extensionStates.Pop(); - // TODO: use DebugGuard - Debug.Assert(this.depth != 0); + if (this.depth == 0) + { + throw new InvalidOperationException("Depth must not be 0"); + } this.depth--; return visited; @@ -47,8 +47,10 @@ public override bool Boolean(bool defaultValue, ref bool value) return false; } - // TODO: use DebugGuard - Debug.Assert(bits <= 1u); + if (bits > 1u) + { + throw new InvalidOperationException("Invalid bits"); + } value = bits == 1u; diff --git a/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHelper.cs b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHelper.cs index 8e775050e1..7feda2848d 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHelper.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHelper.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Buffers; -using System.Diagnostics; using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; @@ -16,8 +15,8 @@ public static int GetPopulationCountPrecision(int logCount, int shift) // NOTE: The result may potentially be large, so prefer using a memory allocator public static IMemoryOwner CreateFlatHistogram(Configuration configuration, int length, int totalCount) { - Debug.Assert(length <= 0, "Length should be >= 0"); - Debug.Assert(length > totalCount, "Length should be <= totalCount"); + DebugGuard.MustBeLessThanOrEqualTo(length, 0, nameof(length)); + DebugGuard.MustBeGreaterThan(length, totalCount, nameof(length)); int count = totalCount / length; IMemoryOwner result = configuration.MemoryAllocator.Allocate(length); @@ -66,11 +65,11 @@ public static JxlAnsSymbol Lookup(ReadOnlySpan table, int value, in public static bool InitAliasTable(Span preDistribution, uint logRange, int logAlphaSize, Span entries) { + DebugGuard.MustBeLessThan(logAlphaSize, (int)logRange, nameof(logAlphaSize)); + int range = 1 << (int)logRange; int tableSize = 1 << logAlphaSize; - Debug.Assert(tableSize <= range, "table_size must be <= range"); - int distributionPointer = preDistribution.Length - 1; while (distributionPointer >= 0 && preDistribution[distributionPointer] == 0) @@ -88,9 +87,7 @@ public static bool InitAliasTable(Span preDistribution, uint logRange, int if (distribution.Length > tableSize) { - Debug.Fail("Too many items in the distribution"); - - return false; + throw new InvalidOperationException("Too many items in the distribution"); } int entrySize = range >> logAlphaSize; diff --git a/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHybridUIntConfiguration.cs b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHybridUIntConfiguration.cs index b8609f9163..6c2d8cc2ba 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHybridUIntConfiguration.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHybridUIntConfiguration.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Diagnostics; using SixLabors.ImageSharp.Formats.Jxl.Fields; namespace SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; @@ -15,7 +14,10 @@ public JxlAnsHybridUIntConfiguration(uint splitExponent = 4, uint msbInToken = 2 this.MsbInToken = msbInToken; this.LsbInToken = lsbInToken; - Debug.Assert(splitExponent >= msbInToken + lsbInToken, "Split exponent should be < msbInToken + lsbInToken"); + if (splitExponent < msbInToken + lsbInToken) + { + throw new InvalidOperationException("Split exponent should be < msbInToken + lsbInToken"); + } } public uint SplitExponent { get; set; } diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs index 09400f9a83..1fcf00b6fe 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Diagnostics; using SixLabors.ImageSharp.Formats.Jxl.Cms; using SixLabors.ImageSharp.Formats.Jxl.Fields; @@ -49,7 +48,10 @@ public float IntensityTarget { float intensityTarget = this.ToneMapping?.IntensityTarget ?? 0f; - Debug.Assert(intensityTarget != 0f, "Intensity target should be present"); + if (intensityTarget == 0f) + { + throw new InvalidOperationException("Intensity target should be present"); + } return intensityTarget; } diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageB.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageB.cs index 0faba189ad..2d36f5b668 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageB.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageB.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Diagnostics; - namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; /// @@ -25,7 +23,7 @@ public JxlImageB(Configuration configuration, int xSize, int ySize, int prePaddi public Memory GetRowBytesMemory(int y) { - Debug.Assert(y < this.YSize, "Attempted to access out-of-bounds Y coordinate"); + DebugGuard.MustBeLessThan(y, this.YSize, nameof(y)); Memory row = this.Bytes[(y * this.BytesPerRow)..]; diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs index 37707b7ba4..ab637308f9 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Diagnostics; using System.Runtime.InteropServices; namespace SixLabors.ImageSharp.Formats.Jxl.Memory; @@ -79,9 +78,11 @@ public bool ShrinkTo(int x, int y) return true; } - [Conditional("DEBUG")] - private void PlaneRowBoundsCheck(int c, int y) => - Debug.Assert(c < PlaneCount && y < this.YSize, "The bounds check has failed"); + private void PlaneRowBoundsCheck(int c, int y) + { + DebugGuard.MustBeLessThan(c, PlaneCount, nameof(c)); + DebugGuard.MustBeLessThan(y, this.YSize, nameof(y)); + } public void Dispose() { diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs index 9948ae790f..367b3fc300 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Buffers; -using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -76,8 +75,8 @@ public bool ShrinkTo(int x, int y) return false; } - Debug.Assert(x <= this.OriginalXSize, "ShrinkTo cannot expand memory"); - Debug.Assert(y <= this.OriginalYSize, "ShrinkTo cannot expand memory"); + DebugGuard.MustBeLessThanOrEqualTo(x, this.OriginalXSize, nameof(x)); + DebugGuard.MustBeLessThanOrEqualTo(y, this.OriginalYSize, nameof(y)); this.XSize = x; this.YSize = y; @@ -88,10 +87,9 @@ public bool ShrinkTo(int x, int y) protected Span GetRowBase(int y) where T : unmanaged { - Debug.Assert(y < this.YSize, "Attempted to access out-of-bounds Y coordinate"); + DebugGuard.MustBeLessThan(y, this.YSize, nameof(y)); Span row = this.Bytes.Span[(y * this.BytesPerRow)..]; - return MemoryMarshal.Cast(row); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs index 5320f5f171..3795f092b8 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs @@ -2,8 +2,7 @@ // Licensed under the Six Labors Split License. using System.Buffers; -using System.Diagnostics; -using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; @@ -100,9 +99,8 @@ public static uint DecodeVariableLengthUint16(JxlBitReader reader) { if (symbols[0] == symbols[1]) { - Debug.Fail("Corrupt data"); counts.Dispose(); - return null; + throw new InvalidOperationException("The data is corrupt"); } countsSpan[(int)symbols[0]] = reader.ReadBits32((uint)precisionBits); @@ -142,9 +140,7 @@ public static uint DecodeVariableLengthUint16(JxlBitReader reader) if (shift > JxlAnsConstants.AnsLogTableSize + 1) { - Debug.Fail("Invalid shift"); - - return null; + throw new InvalidOperationException("Invalid shift"); } uint length = DecodeVariableLengthUint8(reader) + 3u; @@ -192,16 +188,14 @@ public static uint DecodeVariableLengthUint16(JxlBitReader reader) if (omitPos < 0) { - Debug.Fail("The histogram is corrupt or invalid."); counts.Dispose(); - return null; + throw new InvalidOperationException("The histogram is corrupt or invalid."); } if (omitPos + 1 < length && logCounts[omitPos + 1] == JxlAnsConstants.AnsLogTableSize) { - Debug.Fail("The histogram is corrupt or invalid."); counts.Dispose(); - return null; + throw new InvalidOperationException("The histogram is corrupt or invalid."); } int previous = 0; @@ -246,9 +240,8 @@ public static uint DecodeVariableLengthUint16(JxlBitReader reader) if (countsSpan[omitPos] <= 0) { - Debug.Fail("The histogram count is incorrect."); counts.Dispose(); - return null; + throw new InvalidOperationException("The histogram count is incorrect."); } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs index 87c1408f8e..cdc69838df 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Buffers.Binary; -using System.Diagnostics; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; @@ -72,7 +71,7 @@ private void MaybeRefill() private ulong ReadBits64Core(uint n, bool peek = false) { - Debug.Assert(n <= 64, "Too many bits to pack into ulong"); + DebugGuard.MustBeLessThanOrEqualTo(n, 64u, nameof(n)); this.MaybeRefill(); if (this.IsEndOfStream) @@ -117,7 +116,7 @@ private ulong ReadBits64Core(uint n, bool peek = false) private uint ReadBits32Core(uint n, bool peek = false) { - Debug.Assert(n <= 32, "Too many bits to pack into uint"); + DebugGuard.MustBeLessThanOrEqualTo(n, 32u, nameof(n)); this.MaybeRefill(); if (this.IsEndOfStream) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAcContext.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAcContext.cs index 88c1e88f7b..58e5c8b4f4 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlAcContext.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAcContext.cs @@ -1,11 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Diagnostics; using System.Runtime.CompilerServices; -#pragma warning disable SA1405 // Debug.Assert should provide message text - namespace SixLabors.ImageSharp.Formats.Jxl.Processing; /// @@ -38,15 +35,20 @@ internal static class JxlAcContext [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int ZeroDensityContext(int nonZeroesLeft, int k, int coveredBlocks, int log2CoveredBlocks, int prev) { - Debug.Assert((1 << log2CoveredBlocks) == coveredBlocks); + DebugGuard.IsTrue((1 << log2CoveredBlocks) == coveredBlocks, "log2CoveredBlocks must be equal to Log2(coveredBlocks)"); nonZeroesLeft = (nonZeroesLeft + coveredBlocks - 1) >> log2CoveredBlocks; k >>= log2CoveredBlocks; - Debug.Assert(k > 0); - Debug.Assert(k < 64); - Debug.Assert(nonZeroesLeft > 0); - Debug.Assert(nonZeroesLeft < 64); + if (k is < 0 or >= 64) + { + throw new InvalidOperationException("k must be within range of 0..63 inclusive"); + } + + if (nonZeroesLeft is <= 0 or >= 64) + { + throw new InvalidOperationException("nonZeroesLeft must be within range of 1..63 inclusive"); + } return ((CoefficientNumNonzeroContext[nonZeroesLeft] + CoefficientFrequencyContext[k]) * 2) + prev; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs index 32c7fe3e18..e7a518224c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs @@ -1,13 +1,10 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using static SixLabors.ImageSharp.Formats.Jxl.Processing.JxlFrameDimensions; -#pragma warning disable SA1405 // Debug.Assert should provide message text - namespace SixLabors.ImageSharp.Formats.Jxl.Processing; [StructLayout(LayoutKind.Sequential, Pack = 8)] @@ -37,8 +34,6 @@ public JxlAcStrategy(JxlAcStrategyType strategy, bool isFirst) { this.Strategy = strategy; this.isFirst = isFirst; - - Debug.Assert(this.IsMultiblock); } public JxlAcStrategy(JxlAcStrategyType strategy) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs index 6070db54c9..a33485df5d 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Diagnostics; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; @@ -81,9 +80,7 @@ public bool SetNoBoundsChecks(int x, int y, JxlAcStrategyType type, bool check = if (check && rowSpan[pos] != Invalid) { - Debug.Fail("Invalid AC strategy. Blocks overlap."); - - return false; + throw new InvalidOperationException("Invalid AC strategy. Blocks overlap."); } rowSpan[pos] = (byte)(rawTypeTimes2 | ((iy | ix) == 0 ? 1 : 0)); @@ -95,12 +92,13 @@ public bool SetNoBoundsChecks(int x, int y, JxlAcStrategyType type, bool check = public bool Set(int x, int y, JxlAcStrategyType type) { -#if DEBUG JxlAcStrategy strategy = new(type); - Debug.Assert(y + strategy.CoveredBlocksY <= this.layers!.YSize, "Invalid range"); - Debug.Assert(x + strategy.CoveredBlocksX <= this.layers.XSize, "Invalid range"); -#endif + if (y + strategy.CoveredBlocksX > this.layers!.YSize || + x + strategy.CoveredBlocksX > this.layers.XSize) + { + throw new InvalidOperationException("Invalid range"); + } return this.SetNoBoundsChecks(x, y, type, check: false); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyRow.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyRow.cs index a99b3654ac..b94c565e7b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyRow.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyRow.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -19,7 +18,7 @@ public JxlAcStrategy this[int x] { ReadOnlySpan span = this.row.Span; - Debug.Assert(x * 8 < span.Length, "Too many bytes of memory were requested"); + DebugGuard.MustBeLessThan(x * 8, span.Length, "x overflows"); ref byte first = ref MemoryMarshal.GetReference(span); JxlAcStrategyType strategy = (JxlAcStrategyType)(Unsafe.Add(ref Unsafe.As(ref first), x) >> 1); diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3F.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3F.cs index 0a2d59a90f..b8620c2dd4 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3F.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3F.cs @@ -4,7 +4,7 @@ using System.Runtime.InteropServices; #pragma warning disable IDE0044 // Add readonly modifier -#pragma warning disable IDE0051 // Remove unused private members +#pragma warning disable IDE0052 // Remove unread private members namespace SixLabors.ImageSharp.Formats.Jxl.Processing; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs index 0e0135c175..76b3e24b4a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Buffers; -using System.Diagnostics; using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; @@ -139,9 +138,7 @@ public bool Dequantize( if (!this.ValidateSplinePointPos(px, py)) { - Debug.Fail("Spline points out of range"); - - return false; + throw new InvalidOperationException("Spline points out of range"); } int currentX = (int)px; @@ -165,18 +162,15 @@ public bool Dequantize( currentDx += point.First; currentDy += point.Second; manhattanDistance = Math.Abs(currentDx) + Math.Abs(currentDy); + if (manhattanDistance > areaLimit) { - Debug.Fail("Manhattan distance is too large"); - - return false; + throw new InvalidOperationException("Manhattan distance is too large"); } if (!ValidateSplinePointPos(currentDx, currentDy)) { - Debug.Fail("Delta points out of range"); - - return false; + throw new InvalidOperationException("Delta points out of range"); } currentX += currentDx; @@ -184,9 +178,7 @@ public bool Dequantize( if (!ValidateSplinePointPos(currentX, currentY)) { - Debug.Fail("Current points out of range"); - - return false; + throw new InvalidOperationException("Current points out of range"); } controlPoints[i + 1] = new(currentX, currentY); @@ -239,9 +231,7 @@ public bool Dequantize( totalEstimatedAreaReached = widthEstimate * manhattanDistance; if (totalEstimatedAreaReached > areaLimit) { - Debug.Fail("Total estimated area is too large"); - - return false; + throw new InvalidOperationException("Total estimated area is too large"); } return true; @@ -258,18 +248,14 @@ public bool Decode( int numControlPoints = decoder.ReadHybridUnsignedInteger(NumControlPointsContext, br, contextMap); if (numControlPoints > maxControlPoints) { - Debug.Fail("Too many control points"); - - return false; + throw new InvalidOperationException("Too many control points"); } totalControlPoints += numControlPoints; if (totalControlPoints >= maxControlPoints) { - Debug.Fail("Too many control points"); - - return false; + throw new InvalidOperationException("Too many control points"); } this.ResizeControlPoints(configuration, numControlPoints); @@ -289,9 +275,7 @@ public bool Decode( if (controlPoint.First >= deltaLimit || controlPoint.First <= -deltaLimit || controlPoint.Second >= deltaLimit || controlPoint.Second <= -deltaLimit) { - Debug.Fail("Spline delta-delta is out of bounds"); - - return false; + throw new InvalidOperationException("Spline delta-delta is out of bounds"); } } @@ -319,9 +303,7 @@ bool TryDecodeDct(ReadOnlySpan contextMap, Span dct) dct[i] = UnpackSigned(decoder.ReadHybridUnsignedInteger(DctContext, br, contextMap)); if (dct[i] == invalidCoefficient) { - Debug.Fail("The DCT coefficient is invalid"); - - return false; + throw new InvalidOperationException("The DCT coefficient is invalid"); } } From 79e8ce7b1367e2c8ae3cc2c3ed1cdddfafc2be2b Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 6 Aug 2026 13:15:07 +0400 Subject: [PATCH 060/328] Add Move to Front transform See inverse_mtf-inl.h --- .../Formats/Jxl/Processing/JxlInverseMtf.cs | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlInverseMtf.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlInverseMtf.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlInverseMtf.cs new file mode 100644 index 0000000000..6638f8600a --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlInverseMtf.cs @@ -0,0 +1,91 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Inverse Move to Front implementation +/// +internal static class JxlInverseMtf +{ + // NOTE: here we use Vector512 to store 64 bytes in a + // more efficient manner. However, it doesn't necessarily + // require 512-bit CPU vector support. + // If the user's CPU has 256-bit vectors, the JIT will emit + // such instructions for each half. Likewise, if the user's + // CPU only goes up to 128-bit vectors, the JIT will emit + // 128-bit vector code for each quarter. And if the CPU + // doesn't support SIMD at all, the JIT will emit scalar + // instructions. + public static void MoveToFront(Span v, byte index) + { + byte value = v[index]; + byte i = index; + + ref byte vR = ref MemoryMarshal.GetReference(v); + + if (i < 4) + { + for (; i != 0; --i) + { + v[i] = v[i - 1]; + } + } + else + { + int tail = i & 63; + + if (tail != 0) + { + i -= (byte)tail; + Vector512 vec = Vector512.LoadUnsafe(ref Unsafe.Add(ref vR, i)); + Vector512 prev = Vector512.LoadUnsafe(ref Unsafe.Add(ref vR, i + 1)); + + // TODO: optimize this? + Span maskBytes = stackalloc byte[64]; + + for (int j = 0; j < 64; j++) + { + maskBytes[j] = (byte)(j < tail ? 0xFF : 0); + } + + Vector512 mask = Vector512.Create(maskBytes); + Vector512 filter = Vector512.ConditionalSelect(mask, vec, prev); + filter.StoreUnsafe(ref Unsafe.Add(ref vR, i + 1)); + } + + while (i != 0) + { + i -= 64; + Vector512 vec = Vector512.LoadUnsafe(ref Unsafe.Add(ref vR, i)); + vec.StoreUnsafe(ref Unsafe.Add(ref vR, i + 1)); + } + } + + v[0] = value; + } + + public static void InverseMoveToFrontTransform(Span v, int vLength) + { + Span mtf = stackalloc byte[256 + 64]; + for (int i = 0; i < 256; i++) + { + mtf[i] = (byte)i; + } + + for (int i = 0; i < vLength; i++) + { + byte index = v[i]; + v[i] = mtf[index]; + + if (index != 0) + { + MoveToFront(mtf, index); + } + } + } +} From d1f66463bc299dba93c1ce66cc5f961668421f2e Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 6 Aug 2026 13:18:39 +0400 Subject: [PATCH 061/328] Prefer inline arrays --- .../Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs | 14 ++++++-------- .../Jxl/Processing/Decoder/JxlOpsinParameters.cs | 13 ++++++------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs index dc44594d9a..ef74d80122 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +#pragma warning disable SA1401 // Fields should be private + using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.Processing; @@ -8,17 +10,13 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; internal sealed class JxlOpsinInverseMatrix : IJxlFields { - public bool AllDefault { get; set; } + public InlineArray3 OpsinBiases; - public JxlMatrix3x3F InverseMatrix { get; set; } + public InlineArray3 QuantBiases; - // Prefer arrays so we can set values like this: - // JxlOpsinInverseMatrix m = ...; - // m.OpsinBiases[0] = 1f; - // An InlineArray can't do that. - public float[] OpsinBiases { get; set; } = new float[3]; + public bool AllDefault { get; set; } - public float[] QuantBiases { get; set; } = new float[4]; + public JxlMatrix3x3F InverseMatrix { get; set; } public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOpsinParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOpsinParameters.cs index c18a18ef0b..6fdfa28f9f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOpsinParameters.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOpsinParameters.cs @@ -1,18 +1,17 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +#pragma warning disable SA1401 // Fields should be private + namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; internal sealed class JxlOpsinParameters { - // Use arrays instead of InlineArrays because, with inline arrays we can't do: - // JxlOpsinParameters parameters = ...; - // parameters.OpsinBiasesCbrt[0] /* <-- error */ = 1.25f; - public float[] InverseOpsinMatrix { get; set; } = new float[36]; + public InlineArray36 InverseOpsinMatrix; - public float[] OpsinBiases { get; set; } = new float[4]; + public InlineArray4 OpsinBiases; - public float[] OpsinBiasesCbrt { get; set; } = new float[4]; + public InlineArray4 OpsinBiasesCbrt; - public float[] QuantBiases { get; set; } = new float[4]; + public InlineArray4 QuantBiases; } From fe41799b9f4c9169e30a8c92410ff545500a60f6 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 6 Aug 2026 13:21:07 +0400 Subject: [PATCH 062/328] Prevent potential overflow --- src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index 45c7463248..fbb7a22d02 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -73,7 +73,7 @@ public JxlDecoderCore(DecoderOptions options) /// Boolean indicating whether the coordinates are out of bounds private static bool IsOutOfBounds(int a, int b, int size) { - int position = a + b; + long position = a + b; return position > size || position < a; } From 8209aa1f197ea059d7c5f17759f1a6bc61daba22 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 6 Aug 2026 13:22:37 +0400 Subject: [PATCH 063/328] Refactor --- src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs index ccea59b2c8..2bded64ac0 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; -using System.Security.Principal; using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; @@ -62,7 +61,7 @@ public static bool ReadPermutation(int skip, int size, Span order, JxlBitRe Span temp = stackalloc uint[size * 2]; temp.Clear(); - uint end = reader.ReadHybridUnsignedInteger(CoeffOrderContext((int)size), bitReader, contextMap) + skip; + uint end = reader.ReadHybridUnsignedInteger(CoeffOrderContext((uint)size), bitReader, contextMap) + skip; if (end > size) { From 074ff1b231576b622c89eca82a339ad36f01ec80 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 6 Aug 2026 13:37:16 +0400 Subject: [PATCH 064/328] Validate the size parameter in JxlCoefficientOrder.ReadPermutation --- src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs index 2bded64ac0..4ee1a99587 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs @@ -55,6 +55,8 @@ public static uint CoeffOrderContext(uint value) public static bool ReadPermutation(int skip, int size, Span order, JxlBitReader bitReader, JxlAnsSymbolReader reader, Span contextMap) { + DebugGuard.MustBeLessThanOrEqualTo(size, 65536, nameof(size)); + Span lehmer = stackalloc uint[size]; lehmer.Clear(); From f19c8797ec804b48602b6188e29a7478b61a1f37 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 8 Aug 2026 18:17:35 +0400 Subject: [PATCH 065/328] Add more implementations and prototypes - Add decoding of Huffman Codes (see dec_huffman.cc and dec_huffman.h) - Add constructors to JxlImage3* classes - Make JxlColorCorrelationMap.Create 'xyb' parameter use true as a default value - Prototype of DCT quant weight parameters - Add passes shared state (see passes_state.cc and passes_state.h) - Add prototype for image operations (see image_ops.cc and image_ops.h) - Simplify inverse MTF (Move to Front) transform - Add patch context (see patch_dictionary_internal.h) - Add prototype of quantizer weights - Add 2nd prototype of ANS entropy decoding (see dec_ans.cc and dec_ans.h) - Add prototype of patch dictionary decoding (see dec_patch_dictionary.cc and dec_patch_dictionary.h) --- .../Jxl/IO/Entropy/JxlAnsLz77Parameters.cs | 70 +++- .../Formats/Jxl/IO/JxlHuffmanCode.cs | 6 +- .../Jxl/Memory/ImageTypes/JxlImage3B.cs | 5 + .../Jxl/Memory/ImageTypes/JxlImage3F.cs | 5 + .../Jxl/Memory/ImageTypes/JxlImage3I.cs | 5 + .../Jxl/Memory/ImageTypes/JxlImage3S.cs | 5 + .../Jxl/Memory/ImageTypes/JxlImage3U.cs | 5 + .../Formats/Jxl/Memory/JxlImage3{T}.cs | 11 +- .../Jxl/Processing/Decoder/JxlAnsCode.cs | 50 +++ .../Jxl/Processing/Decoder/JxlAnsReader.cs | 32 ++ .../Processing/Decoder/JxlAnsSymbolReader.cs | 30 ++ .../Processing/Decoder/JxlHuffmanDecoder.cs | 331 ++++++++++++++++++ .../Processing/Decoder/JxlPatchBlendMode.cs | 16 + .../Processing/Decoder/JxlPatchBlending.cs | 11 + .../Processing/Decoder/JxlPatchDictionary.cs | 256 ++++++++++++++ .../Processing/Decoder/JxlPatchPosition.cs | 11 + .../Decoder/JxlPatchReferencePosition.cs | 13 + .../Jxl/Processing/JxlColorCorrelationMap.cs | 2 +- .../Processing/JxlDctQuantWeightParameters.cs | 28 ++ .../Jxl/Processing/JxlImageFeatures.cs | 27 ++ .../Jxl/Processing/JxlImageOperations.cs | 51 +++ .../Formats/Jxl/Processing/JxlInverseMtf.cs | 82 +---- .../Jxl/Processing/JxlPassesSharedState.cs | 111 ++++++ .../Formats/Jxl/Processing/JxlPatchContext.cs | 32 ++ .../Formats/Jxl/Processing/JxlQuantWeights.cs | 15 + 25 files changed, 1132 insertions(+), 78 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsCode.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsSymbolReader.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlHuffmanDecoder.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchBlendMode.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchBlending.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchDictionary.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchPosition.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchReferencePosition.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlDctQuantWeightParameters.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlImageFeatures.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlPatchContext.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlQuantWeights.cs diff --git a/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsLz77Parameters.cs b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsLz77Parameters.cs index cbf6d3a4e4..01016f4dd6 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsLz77Parameters.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsLz77Parameters.cs @@ -7,15 +7,75 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; internal sealed class JxlAnsLz77Parameters : IJxlFields { - public bool Enabled { get; set; } + private bool enabled; + private uint minimumSymbol; + private uint minimumLength; + private JxlAnsHybridUIntConfiguration lengthUintConfig = new(0, 0, 0); - public uint MinimumSymbol { get; set; } + public JxlAnsLz77Parameters() => JxlBundle.Init(this); - public uint MinimumLength { get; set; } + public bool Enabled + { + get => this.enabled; + set => this.enabled = value; + } - public JxlAnsHybridUIntConfiguration LengthUintConfig { get; set; } = new(0, 0, 0); + public uint MinimumSymbol + { + get => this.minimumSymbol; + set => this.minimumSymbol = value; + } + + public uint MinimumLength + { + get => this.minimumLength; + set => this.minimumLength = value; + } + + public JxlAnsHybridUIntConfiguration LengthUintConfig + { + get => this.lengthUintConfig; + set => this.lengthUintConfig = value; + } public int NonserializedDistanceContext { get; set; } - public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); + public ref JxlAnsHybridUIntConfiguration GetLengthUIntConfigReference() => ref this.lengthUintConfig; + + public bool Visit(JxlVisitor visitor) + { + if (!visitor.Boolean(false, ref this.enabled)) + { + return false; + } + + if (!visitor.Conditional(this.enabled)) + { + return true; + } + + if (!visitor.U32( + JxlFieldExpressions.Value(224u), + JxlFieldExpressions.Value(512u), + JxlFieldExpressions.Value(4096u), + JxlFieldExpressions.BitsOffset(15u, 8u), + 224u, + ref this.minimumSymbol)) + { + return false; + } + + if (!visitor.U32( + JxlFieldExpressions.Value(3u), + JxlFieldExpressions.Value(4u), + JxlFieldExpressions.BitsOffset(2u, 5u), + JxlFieldExpressions.BitsOffset(8u, 9u), + 3u, + ref this.minimumLength)) + { + return false; + } + + return true; + } } diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlHuffmanCode.cs b/src/ImageSharp/Formats/Jxl/IO/JxlHuffmanCode.cs index 30bca3446d..2f95f7226c 100644 --- a/src/ImageSharp/Formats/Jxl/IO/JxlHuffmanCode.cs +++ b/src/ImageSharp/Formats/Jxl/IO/JxlHuffmanCode.cs @@ -6,15 +6,15 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO; /// /// A single Huffman code. /// -internal struct JxlHuffmanCode +internal struct JxlHuffmanCode(byte bits, ushort value) { /// /// Number of bits for this symbol. /// - public byte Bits; + public byte Bits = bits; /// /// Symbol value/offset. /// - public ushort Value; + public ushort Value = value; } diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3B.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3B.cs index 63a5c59c9f..8c3b2ae36a 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3B.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3B.cs @@ -11,4 +11,9 @@ internal sealed class JxlImage3B : JxlImage3 public JxlImage3B() { } + + public JxlImage3B(Configuration configuration, int xSize, int ySize) + : base(configuration, xSize, ySize) + { + } } diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3F.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3F.cs index b456dfd9a7..8387f63194 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3F.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3F.cs @@ -11,4 +11,9 @@ internal sealed class JxlImage3F : JxlImage3 public JxlImage3F() { } + + public JxlImage3F(Configuration configuration, int xSize, int ySize) + : base(configuration, xSize, ySize) + { + } } diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3I.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3I.cs index 0d9f6c8202..f3486b95a7 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3I.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3I.cs @@ -11,4 +11,9 @@ internal sealed class JxlImage3I : JxlImage3 public JxlImage3I() { } + + public JxlImage3I(Configuration configuration, int xSize, int ySize) + : base(configuration, xSize, ySize) + { + } } diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3S.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3S.cs index 00615ff846..94b88056a1 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3S.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3S.cs @@ -11,4 +11,9 @@ internal sealed class JxlImage3S : JxlImage3 public JxlImage3S() { } + + public JxlImage3S(Configuration configuration, int xSize, int ySize) + : base(configuration, xSize, ySize) + { + } } diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3U.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3U.cs index 1921e86d33..8d118c048c 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3U.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3U.cs @@ -11,4 +11,9 @@ internal sealed class JxlImage3U : JxlImage3 public JxlImage3U() { } + + public JxlImage3U(Configuration configuration, int xSize, int ySize) + : base(configuration, xSize, ySize) + { + } } diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs index ab637308f9..9e9b10c099 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs @@ -17,6 +17,9 @@ public JxlImage3() { } + public JxlImage3(Configuration configuration, int xSize, int ySize) + => this.Allocate(configuration, xSize, ySize); + public JxlImage3(JxlImage3 other) { for (int i = 0; i < PlaneCount; i++) @@ -54,15 +57,15 @@ public void Swap(JxlImage3 other) } public static JxlImage3 Create(Configuration configuration, int xSize, int ySize) + => new(configuration, xSize, ySize); + + public void Allocate(Configuration configuration, int xSize, int ySize) { JxlPlane plane0 = JxlPlane.Create(configuration, xSize, ySize); JxlPlane plane1 = JxlPlane.Create(configuration, xSize, ySize); JxlPlane plane2 = JxlPlane.Create(configuration, xSize, ySize); - return new JxlImage3() - { - planes = [plane0, plane1, plane2] - }; + this.planes = [plane0, plane1, plane2]; } public bool ShrinkTo(int x, int y) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsCode.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsCode.cs new file mode 100644 index 0000000000..56dbab4ed3 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsCode.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +internal sealed class JxlAnsCode +{ + public List HuffmanData { get; set; } = []; + + public List UIntConfig { get; set; } = []; + + public List DegenerateSymbols { get; set; } = []; + + public bool UsePrefixCode { get; set; } + + public byte LogAlphaSize { get; set; } + + public JxlAnsLz77Parameters Lz77 { get; set; } = new(); + + public int MaxNumBits { get; set; } + + public void UpdateMaxNumBits(int ctx, int symbol) + { + Span configs = CollectionsMarshal.AsSpan(this.UIntConfig); + ref JxlAnsHybridUIntConfiguration cfg = ref configs[ctx]; + if (this.Lz77.Enabled && this.Lz77.NonserializedDistanceContext != ctx && symbol >= this.Lz77.MinimumSymbol) + { + symbol -= (int)this.Lz77.MinimumSymbol; + cfg = ref this.Lz77.GetLengthUIntConfigReference(); + } + + uint splitToken = cfg.SplitToken; + uint msbInToken = cfg.MsbInToken; + uint lsbInToken = cfg.LsbInToken; + uint splitExponent = cfg.SplitExponent; + + if (symbol < splitToken) + { + this.MaxNumBits = Math.Max(this.MaxNumBits, (int)splitExponent); + return; + } + + uint nExtra = splitExponent - (msbInToken + lsbInToken) + (((uint)symbol - splitToken) >> (int)(msbInToken + lsbInToken)); + uint total = msbInToken + lsbInToken + nExtra + 1; + this.MaxNumBits = Math.Max(this.MaxNumBits, (int)total); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs index 3795f092b8..51e014d80d 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs @@ -2,12 +2,17 @@ // Licensed under the Six Labors Split License. using System.Buffers; +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; internal static class JxlAnsReader { + private const int WindowSize = 1 << 20; + + private const int NumSpecialDistances = 120; + // Prefer jagged arrays over multidimensional arrays // for performance. Collection expressions help represent // jagged arrays easily. @@ -31,6 +36,33 @@ internal static class JxlAnsReader [3, 10], [4, 4], [3, 7], [4, 1], [3, 6], [3, 8], [3, 9], [4, 2], ]; + private static readonly sbyte[][] SpecialDistances = + [ + [0, 1], [1, 0], [1, 1], [-1, 1], [0, 2], [2, 0], [1, 2], [-1, 2], + [2, 1], [-2, 1], [2, 2], [-2, 2], [0, 3], [3, 0], [1, 3], [-1, 3], + [3, 1], [-3, 1], [2, 3], [-2, 3], [3, 2], [-3, 2], [0, 4], [4, 0], + [1, 4], [-1, 4], [4, 1], [-4, 1], [3, 3], [-3, 3], [2, 4], [-2, 4], + [4, 2], [-4, 2], [0, 5], [3, 4], [-3, 4], [4, 3], [-4, 3], [5, 0], + [1, 5], [-1, 5], [5, 1], [-5, 1], [2, 5], [-2, 5], [5, 2], [-5, 2], + [4, 4], [-4, 4], [3, 5], [-3, 5], [5, 3], [-5, 3], [0, 6], [6, 0], + [1, 6], [-1, 6], [6, 1], [-6, 1], [2, 6], [-2, 6], [6, 2], [-6, 2], + [4, 5], [-4, 5], [5, 4], [-5, 4], [3, 6], [-3, 6], [6, 3], [-6, 3], + [0, 7], [7, 0], [1, 7], [-1, 7], [5, 5], [-5, 5], [7, 1], [-7, 1], + [4, 6], [-4, 6], [6, 4], [-6, 4], [2, 7], [-2, 7], [7, 2], [-7, 2], + [3, 7], [-3, 7], [7, 3], [-7, 3], [5, 6], [-5, 6], [6, 5], [-6, 5], + [8, 0], [4, 7], [-4, 7], [7, 4], [-7, 4], [8, 1], [8, 2], [6, 6], + [-6, 6], [8, 3], [5, 7], [-5, 7], [7, 5], [-7, 5], [8, 4], [6, 7], + [-6, 7], [7, 6], [-7, 6], [8, 5], [7, 7], [-7, 7], [8, 6], [8, 7] + ]; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int SpecialDistance(int index, int multiplier) + { + Span indexDistance = SpecialDistances[index]; + int dist = indexDistance[0] + (multiplier * indexDistance[1]); + return dist > 1 ? dist : 1; + } + public static uint DecodeVariableLengthUint8(JxlBitReader reader) { if (reader.ReadBoolean()) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsSymbolReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsSymbolReader.cs new file mode 100644 index 0000000000..8c4a83ca2f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsSymbolReader.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +internal sealed class JxlAnsSymbolReader +{ + private const int MaxCheckpointInterval = 512; + + // Use class because the Lz77Window property uses 2KB memory + private sealed class Checkpoint + { + public uint State { get; set; } + + public uint NumToCopy { get; set; } + + public uint CopyPos { get; set; } + + public uint NumDecoded { get; set; } + + public uint[] Lz77Window { get; set; } = new uint[MaxCheckpointInterval]; + } + + private readonly JxlAnsEntry[] aliasTables = []; + private JxlHuffmanDecodingData huffmanData; + private bool usePrefixCode; + private uint state = AnsSignature << 16u; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlHuffmanDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlHuffmanDecoder.cs new file mode 100644 index 0000000000..991d9c6159 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlHuffmanDecoder.cs @@ -0,0 +1,331 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +/// +/// Decodes Huffman codes. +/// +internal sealed class JxlHuffmanDecoder +{ + /// + /// Number of bits that a huffman table uses. + /// + private const int HuffmanTableBits = 8; + + private const int GoalSize = 1 << HuffmanTableBits; + + public const int CodeLengthCodes = 18; + + public const int DefaultCodeLength = 8; + + public const int CodeLengthRepeatCode = 16; + + /// + /// Static Huffman codes for code length code lengths. + /// + private static readonly JxlHuffmanCode[] CodeLengthCodeLengthsCodes = + [ + new(2, 0), new(2, 4), new(2, 3), new(3, 2), new(2, 0), new(2, 4), new(2, 3), new(4, 1), + new(2, 0), new(2, 4), new(2, 3), new(3, 2), new(2, 0), new(2, 4), new(2, 3), new(4, 5), + ]; + + private static ReadOnlySpan CodeLengthCodeOrder => + [ + 1, 2, 3, 4, 0, 5, 17, 6, 16, 7, 8, 9, 10, 11, 12, 13, 14, 15, + ]; + + /// + /// Gets or sets the list of huffman codes. + /// + public JxlHuffmanCode[] Table { get; set; } = []; + + public static bool ReadHuffmanCodeLengths(Span codeLengthCodeLengths, int numSymbols, Span codeLengths, JxlBitReader br) + { + int symbol = 0; + int prevCodeLen = DefaultCodeLength; + int repeat = 0; + int repeatCodeLen = 0; + int space = 32768; + + Span table = stackalloc JxlHuffmanCode[32]; + Span counts = stackalloc ushort[16]; + table.Clear(); + counts.Clear(); + + for (int i = 0; i < CodeLengthCodes; i++) + { + counts[codeLengthCodeLengths[i]]++; + } + + if (JxlHuffman.BuildHuffmanTable(table, 5, codeLengthCodeLengths, counts) == 0) + { + return false; + } + + while (symbol < numSymbols && space > 0) + { + JxlHuffmanCode code = table[(int)br.PeekBits32(5u)]; + br.SkipBits32(code.Bits); + byte codeLength = (byte)code.Value; // It is indeed converted from ushort to byte + + if (codeLength < CodeLengthRepeatCode) + { + repeat = 0; + codeLengths[symbol++] = codeLength; + if (codeLength != 0) + { + prevCodeLen = codeLength; + space -= 32768 >> codeLength; + } + } + else + { + int extraBits = codeLength - 14; + byte newLength = 0; + if (codeLength == CodeLengthRepeatCode) + { + newLength = (byte)prevCodeLen; + } + + if (repeatCodeLen != newLength) + { + repeat = 0; + repeatCodeLen = newLength; + } + + int oldRepeat = repeat; + + if (repeat > 0) + { + repeat -= 2; + repeat <<= extraBits; + } + + repeat += (int)br.ReadBits32((uint)extraBits) + 3; + int repeatDelta = repeat - oldRepeat; + + if (symbol + repeatDelta > numSymbols) + { + return false; + } + + codeLengths.Slice(symbol, repeatDelta).Fill((byte)repeatCodeLen); + symbol += repeatDelta; + if (repeatCodeLen != 0) + { + space -= repeatDelta << (15 - repeatCodeLen); + } + } + } + + if (space != 0) + { + return false; + } + + codeLengths[symbol..].Clear(); + return true; + } + + /// + /// Reads a simple Huffman code. + /// + /// Alphabet size (256 at most) + /// Bit-stream reader + /// Output table (must have at most 8 items) + /// Status of the operation + public static bool ReadSimpleCode(int alphabetSize, JxlBitReader br, Span table) + { + int maxBits = (alphabetSize > 1) ? FloorLog2Nonzero(alphabetSize - 1) + 1 : 0; + uint symbolCount = br.ReadBits32(2u) + 1u; + + Span symbols = stackalloc ushort[4]; + symbols.Clear(); // Clearing is necessary. Not every value will be initialized. + + for (int i = 0; i < symbolCount; i++) + { + uint symbol = br.ReadBits32((uint)maxBits); + if (symbol >= alphabetSize) + { + return false; + } + + symbols[i] = (ushort)symbol; + } + + for (int i = 0; i < symbolCount - 1; i++) + { + for (int j = i + 1; j < symbolCount; j++) + { + if (symbols[i] == symbols[j]) + { + return false; + } + } + } + + if (symbolCount == 4) + { + symbolCount += br.ReadBits32(1u); + } + + int tableSize = 1; + switch (symbolCount) + { + case 1: + table[0] = new(0, symbols[0]); + break; + + case 2: + if (symbols[0] > symbols[1]) + { + SwapSymbols(0, 1, symbols); + } + + table[0] = new(1, symbols[0]); + table[1] = new(1, symbols[1]); + tableSize = 2; + break; + + case 3: + if (symbols[1] > symbols[2]) + { + SwapSymbols(1, 2, symbols); + } + + table[0] = new(1, symbols[0]); + table[2] = new(1, symbols[0]); + table[1] = new(2, symbols[1]); + table[3] = new(2, symbols[2]); + tableSize = 4; + break; + + case 4: + for (int i = 0; i < 3; i++) + { + for (int j = i + 1; j < 4; j++) + { + if (symbols[i] > symbols[j]) + { + SwapSymbols(i, j, symbols); + } + } + } + + table[0] = new(2, symbols[0]); + table[2] = new(2, symbols[1]); + table[1] = new(2, symbols[2]); + table[3] = new(2, symbols[3]); + tableSize = 4; + break; + + case 5: + if (symbols[2] > symbols[3]) + { + SwapSymbols(2, 3, symbols); + } + + table[0] = new(1, symbols[0]); + table[1] = new(2, symbols[1]); + table[2] = new(1, symbols[0]); + table[3] = new(3, symbols[2]); + table[4] = new(1, symbols[0]); + table[5] = new(2, symbols[1]); + table[6] = new(1, symbols[0]); + table[7] = new(3, symbols[3]); + tableSize = 8; + break; + + default: + // This should be unreachable. + return false; + } + + while (tableSize != GoalSize) + { + table[tableSize..].CopyTo(table); + tableSize <<= 1; + } + + return true; + } + + public bool ReadFromBitStream(int alphabetSize, JxlBitReader br) + { + if (alphabetSize > (1 << JxlAnsConstants.PrefixMaxBits)) + { + return false; + } + + uint simpleCodeOrSkip = br.ReadBits32(2u); + if (simpleCodeOrSkip == 1u) + { + this.Table = new JxlHuffmanCode[GoalSize]; + return ReadSimpleCode(alphabetSize, br, this.Table); + } + + // The alphabet size is at most 256 + Span codeLengths = stackalloc byte[alphabetSize]; + codeLengths.Clear(); // Zero-initialized in reference software + + Span codeLengthCodeLengths = stackalloc byte[CodeLengthCodes]; + codeLengthCodeLengths.Clear(); // Zero-initialized in reference software + + int space = 32; + int numCodes = 0; + + for (uint i = simpleCodeOrSkip; i < CodeLengthCodes && space > 0; i++) + { + int codeLengthIndex = CodeLengthCodeOrder[(int)i]; + JxlHuffmanCode huff = CodeLengthCodeLengthsCodes[(int)br.PeekBits32(4u)]; + br.SkipBits32(huff.Bits); + byte value = (byte)huff.Value; // It's indeed converted from ushort to byte + codeLengthCodeLengths[codeLengthIndex] = value; + + if (value != 0) + { + space -= 32 >> value; + numCodes++; + } + } + + bool ok = (numCodes == 1 || space == 0) && ReadHuffmanCodeLengths(codeLengthCodeLengths, alphabetSize, codeLengths, br); + + if (!ok) + { + return false; + } + + Span counts = stackalloc ushort[16]; + counts.Clear(); // Zero-initialized + + this.Table = new JxlHuffmanCode[alphabetSize + 376]; + uint tableSize = JxlHuffman.BuildHuffmanTable(this.Table, HuffmanTableBits, codeLengths, counts); + + this.Table = this.Table[..(int)tableSize]; + + return tableSize > 0; + } + + public ushort ReadSymbol(JxlBitReader br) + { + Span table = this.Table.AsSpan()[(int)br.PeekBits32(HuffmanTableBits)..]; + int bitCount = table[0].Bits; + if (bitCount > HuffmanTableBits) + { + br.SkipBits32(HuffmanTableBits); + bitCount -= HuffmanTableBits; + table = table[(int)(table[0].Value + br.PeekBits32((uint)bitCount))..]; + } + + br.SkipBits32(table[0].Bits); + return table[0].Value; + } + + private static void SwapSymbols(int i, int j, Span symbols) => RuntimeUtility.Swap(ref symbols[i], ref symbols[j]); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchBlendMode.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchBlendMode.cs new file mode 100644 index 0000000000..755cfe4eb9 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchBlendMode.cs @@ -0,0 +1,16 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +internal enum JxlPatchBlendMode : byte +{ + None, + Replace, + Add, + Multiply, + BlendAbove, + BlendBelow, + AlphaWeightedAddAbove, + AlphaWeightedAddBelow +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchBlending.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchBlending.cs new file mode 100644 index 0000000000..7cedfc3053 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchBlending.cs @@ -0,0 +1,11 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +internal struct JxlPatchBlending +{ + public JxlPatchBlendMode Mode; + public int AlphaChannel; + public bool Clamp; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchDictionary.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchDictionary.cs new file mode 100644 index 0000000000..1b72325667 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchDictionary.cs @@ -0,0 +1,256 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +internal sealed class JxlPatchDictionary +{ + private struct PatchTreeNode + { + public long LeftChild; + public long RightChild; + public int YCenter; + public int Start; + public int Count; + } + + private struct SortedPatch + { + public int First; + public int Second; + } + + private readonly JxlReferenceFrame[] referenceFrames = new JxlReferenceFrame[4]; + private readonly List positions = []; + private readonly List referencePositions = []; + private readonly List blendings = []; + private int blendingsStride; + private readonly List patchTree = []; + private readonly List numPatches = []; + private readonly List sortedPatchesY0 = []; + private readonly List sortedPatchesY1 = []; + + public bool HasAny => this.positions.Count > 0; + + public void Clear() + { + this.positions.Clear(); + ComputePatchTree(); + } + + public void Decode( + JxlMemoryManager memoryManager, + JxlBitReader br, + ulong xsize, + ulong ysize, + ulong numExtraChannels, + ref bool usesExtraChannels) + { + this.positions.Clear(); + this.blendingsStride = (int)(numExtraChannels + 1); + + List contextMap = []; + var code = new JxlAnsCode(); + + var status = DecodeHistograms( + memoryManager, + br, + PatchDictionaryContexts, + code, + contextMap); + + JxlAnsSymbolReader decoder = JxlAnsSymbolReader.Create(code, br); + + ulong ReadNum(int context) + => decoder.ReadHybridUint(context, br, contextMap); + + ulong numRefPatch = ReadNum(kNumRefPatchContext); + + ulong numPixels = xsize * ysize; + ulong maxRefPatches = 1024 + (numPixels / 4); + ulong maxPatches = maxRefPatches * 4; + ulong maxBlendingInfos = maxPatches * 4; + + if (numRefPatch > maxRefPatches) + { + throw new InvalidOperationException("Too many patches in dictionary"); + } + + ulong totalPatches = 0; + ulong nextSize = 1; + + for (ulong id = 0; id < numRefPatch; id++) + { + JxlPatchReferencePosition refPos = new() + { + Ref = ReadNum(kReferenceFrameContext) + }; + + if (refPos.Ref >= kMaxNumReferenceFrames || this.referenceFrames[(int)refPos.Ref].Frame.XSize == 0) + { + throw new InvalidOperationException("Invalid reference frame ID"); + } + + if (!this.referenceFrames[refPos.Ref].IsInXYB) + { + throw new InvalidOperationException("Patches cannot use frames saved post color transforms"); + } + + JxlImageBundle ib = this.referenceFrames[refPos.Ref].Frame; + + refPos.X0 = ReadNum(kPatchReferencePositionContext); + refPos.Y0 = ReadNum(kPatchReferencePositionContext); + refPos.XSize = ReadNum(kPatchSizeContext) + 1; + refPos.YSize = ReadNum(kPatchSizeContext) + 1; + + if (refPos.X0 + refPos.XSize > ib.XSize) + { + throw new InvalidOperationException("Invalid position specified in reference frame"); + } + + if (refPos.Y0 + refPos.YSize > ib.YSize) + { + throw new InvalidOperationException("Invalid position specified in reference frame"); + } + + ulong idCount = ReadNum(kPatchCountContext); + + if (idCount > maxPatches) + { + throw new InvalidOperationException("Too many patches in dictionary"); + } + + idCount++; + + totalPatches += idCount; + + if (totalPatches > maxPatches) + { + throw new InvalidOperationException("Too many patches in dictionary"); + } + + if (nextSize < totalPatches) + { + nextSize *= 2; + nextSize = Math.Min(nextSize, maxPatches); + } + + if (nextSize * (ulong)this.blendingsStride > maxBlendingInfos) + { + throw new InvalidOperationException("Too many patches in dictionary"); + } + + _ = this.blendings.EnsureCapacity((int)nextSize); + _ = this.blendings.EnsureCapacity((int)(nextSize * (ulong)this.blendingsStride)); + + bool chooseAlpha = numExtraChannels > 1; + + for (ulong i = 0; i < idCount; i++) + { + JxlPatchPosition pos = new() + { + ReferencePositionIndex = this.referencePositions.Count + }; + + if (i == 0) + { + pos.X = ReadNum(kPatchPositionContext); + pos.Y = ReadNum(kPatchPositionContext); + } + else + { + long deltaX = JxlPackSigned.UnpackSigned(ReadNum(kPatchOffsetContext)); + + if (deltaX < 0 && (int)(-deltaX) > this.positions[^1].X) + { + throw new InvalidOperationException($"Invalid patch: negative x coordinate ({this.positions[^1].X}, delta {deltaX})"); + } + + pos.X = (int)(this.positions[^1].X + deltaX); + + long deltaY = JxlPackSigned.UnpackSigned(ReadNum(kPatchOffsetContext)); + + if (deltaY < 0 && (int)(-deltaY) > this.positions[^1].Y) + { + throw new InvalidOperationException($"Invalid patch: negative y coordinate ({this.positions[^1].Y}, delta {deltaY})"); + } + + pos.Y = (int)(this.positions[^1].Y + deltaY); + } + + if (pos.X + refPos.XSize > (int)xsize) + { + throw new InvalidOperationException($"Invalid patch x: {pos.X} + {refPos.XSize} > {xsize}"); + } + + if (pos.Y + refPos.YSize > (int)ysize) + { + throw new InvalidOperationException($"Invalid patch y: {pos.Y} + {refPos.YSize} > {ysize}"); + } + + for (int j = 0; j < this.blendingsStride; j++) + { + uint blendMode = (uint)ReadNum(kPatchBlendModeContext); + + if (blendMode >= kNumPatchBlendModes) + { + throw new InvalidOperationException($"Invalid patch blend mode: {blendMode}"); + } + + JxlPatchBlending info = new() + { + Mode = (JxlPatchBlendMode)blendMode + }; + + if (UsesAlpha(info.Mode)) + { + usesExtraChannels = true; + } + + if (info.Mode != JxlPatchBlendMode.None && j > 0) + { + usesExtraChannels = true; + } + + if (UsesAlpha(info.Mode) && chooseAlpha) + { + info.AlphaChannel = (uint)ReadNum(kPatchAlphaChannelContext); + + if (info.AlphaChannel >= (int)numExtraChannels) + { + throw new InvalidOperationException($"Invalid alpha channel for blending: {info.AlphaChannel} out of {numExtraChannels}"); + } + } + else + { + info.AlphaChannel = 0; + } + + if (UsesClamp(info.Mode)) + { + info.Clamp = ReadNum(kPatchClampContext) != 0; + } + else + { + info.Clamp = false; + } + + this.blendings.Add(info); + } + + this.positions.Add(pos); + } + + this.positions.Add(refPos); + } + + this.positions.TrimExcess(); + + if (!decoder.CheckAnsFinalState()) + { + throw new InvalidOperationException("ANS checksum failure."); + } + + this.ComputePatchTree(); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchPosition.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchPosition.cs new file mode 100644 index 0000000000..264d694e85 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchPosition.cs @@ -0,0 +1,11 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +internal struct JxlPatchPosition +{ + public int X; + public int Y; + public int ReferencePositionIndex; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchReferencePosition.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchReferencePosition.cs new file mode 100644 index 0000000000..8e11eb55ad --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchReferencePosition.cs @@ -0,0 +1,13 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +internal struct JxlPatchReferencePosition +{ + public int Ref; + public int X0; + public int Y0; + public int XSize; + public int YSize; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelationMap.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelationMap.cs index 89ae3f7296..b2e2581f72 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelationMap.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelationMap.cs @@ -19,7 +19,7 @@ internal sealed class JxlColorCorrelationMap public bool DecodeDc(JxlBitReader reader) => this.Base.DecodeDc(reader); - public static JxlColorCorrelationMap Create(Configuration configuration, int width, int height, bool xyb) + public static JxlColorCorrelationMap Create(Configuration configuration, int width, int height, bool xyb = true) { JxlColorCorrelationMap map = new(); diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctQuantWeightParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlDctQuantWeightParameters.cs new file mode 100644 index 0000000000..288d129956 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlDctQuantWeightParameters.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal sealed class JxlDctQuantWeightParameters +{ + private const int Log2MaxDistanceBands = 4; + private const int MaxDistanceBands = 1 + (1 << Log2MaxDistanceBands); + + private int numDistanceBands; + private readonly float[][] distanceBands; + + public JxlDctQuantWeightParameters() + { + this.distanceBands = new float[3][]; + for (int i = 0; i < 3; i++) + { + this.distanceBands[i] = new float[MaxDistanceBands]; + } + } + + public JxlDctQuantWeightParameters(float[][] distanceBands, int numDistanceBands) + { + this.numDistanceBands = numDistanceBands; + this.distanceBands = distanceBands; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlImageFeatures.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlImageFeatures.cs new file mode 100644 index 0000000000..142edd0710 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlImageFeatures.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Image features for the JPEG XL passes decoder +/// +internal sealed class JxlImageFeatures +{ + /// + /// Gets or sets noise parameters for the passes decoder + /// + public JxlNoiseParameters NoiseParameters { get; set; } = new(); + + /// + /// Gets or sets patch dictionary for the passes decoder + /// + public JxlPatchDictionary PatchDictionary { get; set; } = new(); + + /// + /// Gets or sets splines for the passes decoder + /// + public JxlSplines Splines { get; set; } = new(); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs new file mode 100644 index 0000000000..f1b50596f7 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs @@ -0,0 +1,51 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal static class JxlImageOperations +{ + /// + /// Returns true if first image has same width and height as the second image. + /// + /// First image + /// Second image + /// True if width and height is equal. + public static bool SameSize(JxlPlaneBase a, JxlPlaneBase b) => a.XSize == b.XSize && a.YSize == b.YSize; + + public static bool CopyImage(JxlPlane from, JxlPlane to) + where T : unmanaged + { + if (!SameSize(from, to)) + { + return false; + } + + if (from.XSize == 0 || from.YSize == 0) + { + return true; + } + + for (int y = 0; y < from.YSize; y++) + { + Span rowFrom = from.GetRow(y); + Span rowTo = to.GetRow(y); + rowFrom.CopyTo(rowTo); + } + + return true; + } + + public static bool CopyImageTo(Rectangle rectFrom, JxlPlane from, Rectangle rectTo, JxlPlane to) + where T : unmanaged + { + if (rectFrom != rectTo) + { + return false; + } + + + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlInverseMtf.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlInverseMtf.cs index 6638f8600a..8016ec51c0 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlInverseMtf.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlInverseMtf.cs @@ -1,10 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; - namespace SixLabors.ImageSharp.Formats.Jxl.Processing; /// @@ -12,79 +8,35 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing; /// internal static class JxlInverseMtf { - // NOTE: here we use Vector512 to store 64 bytes in a - // more efficient manner. However, it doesn't necessarily - // require 512-bit CPU vector support. - // If the user's CPU has 256-bit vectors, the JIT will emit - // such instructions for each half. Likewise, if the user's - // CPU only goes up to 128-bit vectors, the JIT will emit - // 128-bit vector code for each quarter. And if the CPU - // doesn't support SIMD at all, the JIT will emit scalar - // instructions. - public static void MoveToFront(Span v, byte index) + public static void MoveToFront(Span values, byte index) { - byte value = v[index]; - byte i = index; - - ref byte vR = ref MemoryMarshal.GetReference(v); - - if (i < 4) - { - for (; i != 0; --i) - { - v[i] = v[i - 1]; - } - } - else - { - int tail = i & 63; - - if (tail != 0) - { - i -= (byte)tail; - Vector512 vec = Vector512.LoadUnsafe(ref Unsafe.Add(ref vR, i)); - Vector512 prev = Vector512.LoadUnsafe(ref Unsafe.Add(ref vR, i + 1)); + byte value = values[index]; - // TODO: optimize this? - Span maskBytes = stackalloc byte[64]; - - for (int j = 0; j < 64; j++) - { - maskBytes[j] = (byte)(j < tail ? 0xFF : 0); - } - - Vector512 mask = Vector512.Create(maskBytes); - Vector512 filter = Vector512.ConditionalSelect(mask, vec, prev); - filter.StoreUnsafe(ref Unsafe.Add(ref vR, i + 1)); - } - - while (i != 0) - { - i -= 64; - Vector512 vec = Vector512.LoadUnsafe(ref Unsafe.Add(ref vR, i)); - vec.StoreUnsafe(ref Unsafe.Add(ref vR, i + 1)); - } - } - - v[0] = value; + // CopyTo supports overlapping source and destination regions. + values[..index].CopyTo(values[1..]); + values[0] = value; } - public static void InverseMoveToFrontTransform(Span v, int vLength) + public static void InverseMoveToFrontTransform(Span values) { - Span mtf = stackalloc byte[256 + 64]; - for (int i = 0; i < 256; i++) + Span table = stackalloc byte[256]; + + for (int i = 0; i < table.Length; i++) { - mtf[i] = (byte)i; + table[i] = (byte)i; } - for (int i = 0; i < vLength; i++) + for (int i = 0; i < values.Length; i++) { - byte index = v[i]; - v[i] = mtf[index]; + byte index = values[i]; + byte value = table[index]; + values[i] = value; if (index != 0) { - MoveToFront(mtf, index); + // CopyTo handles the overlap and shifts the preceding entries. + table[..index].CopyTo(table[1..]); + table[0] = value; } } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs new file mode 100644 index 0000000000..806c302f51 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs @@ -0,0 +1,111 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; +using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal class JxlPassesSharedState +{ + public JxlCodecMetadata CodecMetadata { get; set; } = new(); + + public JxlFrameDimensions FrameDimensions { get; set; } + + public JxlAcStrategyImage AcStrategy { get; set; } + + public JxlDequantMatrices Matrices { get; set; } = new(); + + public JxlQuantizer Quantizer { get; set; } + + public JxlImageI RawQuantField { get; set; } + + public JxlImageB EpfSharpness { get; set; } + + public JxlColorCorrelationMap ColorMap { get; set; } + + public JxlImageFeatures ImageFeatures { get; set; } = new(); + + public int CoeffOrderSize { get; set; } + + public List CoeffOrders { get; set; } = []; + + public JxlImageB QuantDc { get; set; } + + public JxlImage3F DcStorage { get; set; } + + public JxlImage3F Dc { get; set; } + + public JxlBlockContextMap BlockContextMap { get; set; } = new(); + + public JxlImage3F[] DcFrames { get; set; } = new JxlImage3F[4]; + + public JxlReferenceFrame[] ReferenceFrames { get; set; } = new JxlReferenceFrame[4]; + + public int NumHistograms { get; set; } + + public JxlPassesSharedState(Configuration configuration, JxlFrameHeader frameHeader, bool encoder) + { + if (frameHeader.Metadata is null) + { + throw new InvalidOperationException("The frame header metadata is missing"); + } + + this.CodecMetadata = frameHeader.Metadata; + this.FrameDimensions = frameHeader.FrameDimensions; + this.ImageFeatures.PatchDictionary.SetShared(this.ImageFeatures.ReferenceFrames); + + JxlFrameDimensions dimensions = frameHeader.FrameDimensions; + + this.AcStrategy = JxlAcStrategyImage.Create(configuration, dimensions.XSizeBlocks, dimensions.YSizeBlocks); + this.RawQuantField = new JxlImageI(configuration, dimensions.XSizeBlocks, dimensions.YSizeBlocks); + this.EpfSharpness = new JxlImageB(configuration, dimensions.XSizeBlocks, dimensions.YSizeBlocks); + this.ColorMap = JxlColorCorrelationMap.Create(configuration, dimensions.XSize, dimensions.YSize); + + this.CoeffOrderSize = JxlCoefficientOrder.CoefficientOrderMaxSize; + + if (encoder && + this.CoeffOrders.Count < (frameHeader.Passes.NumPasses & JxlCoefficientOrder.CoefficientOrderMaxSize) && + frameHeader.Encoding == JxlFrameEncoding.VarDct) + { + // we add the padding to CoeffOrders so its length is equal to the variable upperBound + int upperBound = frameHeader.Passes.NumPasses & JxlCoefficientOrder.CoefficientOrderMaxSize; + int length = this.CoeffOrders.Count; + int delta = upperBound - length; + + for (int i = 0; i < delta; i++) + { + this.CoeffOrders.Add(0); // default constant + } + } + + this.QuantDc = new JxlImageB(configuration, dimensions.XSizeBlocks, dimensions.YSizeBlocks); + + bool useDcFrame = (frameHeader.Flags & (ulong)JxlFrameHeaderFlags.Dc) != 0; + if (!encoder && useDcFrame) + { + if (frameHeader.DcLevel == 4) + { + throw new InvalidOperationException("DC level for DC frames cannot be equal to 4"); + } + + this.DcStorage = new JxlImage3F(); + this.Dc = this.DcFrames[(int)frameHeader.DcLevel]; + + if (this.Dc.XSize == 0) + { + throw new InvalidOperationException("DC frame was specified for DC Level = " + frameHeader.DcLevel + ", but frame wasn't decoded with level " + frameHeader.DcLevel + 1); + } + + this.QuantDc.Clear(); + } + else + { + this.DcStorage = new JxlImage3F(configuration, dimensions.XSizeBlocks, dimensions.YSizeBlocks); + this.Dc = this.DcStorage; + } + + this.Quantizer = new(this.Matrices); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlPatchContext.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlPatchContext.cs new file mode 100644 index 0000000000..a79a4720ef --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlPatchContext.cs @@ -0,0 +1,32 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Context numbers for patch decoding +/// +internal enum JxlPatchContext : byte +{ + NumRefPatch = 0, + + ReferenceFrame = 1, + + PatchSize = 2, + + PatchReferencePosition = 3, + + PatchPosition = 4, + + PatchBlendMode = 5, + + PatchOffset = 6, + + PatchCount = 7, + + PatchAlphaChannel = 8, + + PatchClamp = 9, + + NumPatchDictionaryContexts +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantWeights.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantWeights.cs new file mode 100644 index 0000000000..2c50d92afd --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantWeights.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal static class JxlQuantWeights +{ + public const int MaxQuantTableSize = JxlAcStrategy.MaximumCoefficientArea; + + public const int NumPredefinedTables = 1; + + public const int CeilLog2NumPredefinedTables = 0; + + public const int Log2NumQuantModes = 3; +} From ff98ffd0f74778f0abe7b44851e1586490449589 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 8 Aug 2026 18:26:07 +0400 Subject: [PATCH 066/328] Add image copy operations --- .../Jxl/Processing/JxlImageOperations.cs | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs index f1b50596f7..c62f6a4030 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs @@ -46,6 +46,47 @@ public static bool CopyImageTo(Rectangle rectFrom, JxlPlane from, Rectangl return false; } - + if (!from.IsRectangleInside(rectFrom)) + { + return false; + } + + if (!to.IsRectangleInside(rectTo)) + { + return false; + } + + if (rectFrom.Width == 0) + { + return true; + } + + for (int y = 0; y < rectFrom.Height; y++) + { + Span rowFrom = from.GetRow(rectFrom, y); + Span rowTo = to.GetRow(rectTo, y); + rowFrom.CopyTo(rowTo); + } + + return true; + } + + public static bool CopyImageTo(Rectangle rectFrom, JxlImage3 from, Rectangle rectTo, JxlImage3 to) + where T : unmanaged + { + if (rectFrom != rectTo) + { + return false; + } + + for (int plane = 0; plane < 3; plane++) + { + if (!CopyImageTo(rectFrom, from.Plane(plane), rectTo, to.Plane(plane))) + { + return false; + } + } + + return true; } } From e718a4ad31782c10a6f0d5b9c5e92536a009f327 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 8 Aug 2026 19:53:58 +0400 Subject: [PATCH 067/328] Implement image operations --- .../Formats/Jxl/Memory/JxlPlane{T}.cs | 7 + .../Jxl/Processing/JxlImageOperations.cs | 457 ++++++++++++++++++ 2 files changed, 464 insertions(+) diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs index 6bb09c6002..7326768f09 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.Processing; + namespace SixLabors.ImageSharp.Formats.Jxl.Memory; // NOTE: Do not seal this class. @@ -33,4 +35,9 @@ public static JxlPlane Create(Configuration configuration, int xSize, int ySi } public Span GetRow(int y) => this.GetRowBase(y); + + /// + /// Fills everything in this image with 0. + /// + public void Clear() => JxlImageOperations.ZeroFillImage(this); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs index c62f6a4030..b6c8a3eb93 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs @@ -1,10 +1,17 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Memory; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +/// +/// Provides methods for processing 2D views of memory used by the +/// JPEG XL codec. +/// internal static class JxlImageOperations { /// @@ -15,6 +22,15 @@ internal static class JxlImageOperations /// True if width and height is equal. public static bool SameSize(JxlPlaneBase a, JxlPlaneBase b) => a.XSize == b.XSize && a.YSize == b.YSize; + /// + /// Copies everything from one plane to another. + /// + /// The type of planes to copy. + /// Source plane (read-only) + /// Destination plane (write-only) + /// + /// Status of the copy operation + /// public static bool CopyImage(JxlPlane from, JxlPlane to) where T : unmanaged { @@ -38,6 +54,20 @@ public static bool CopyImage(JxlPlane from, JxlPlane to) return true; } + /// + /// Within bounds specified by input rectangles, copies everything from one plane to another. + /// + /// The type of planes to copy. + /// Area in the source plane. + /// Source plane to copy (read-only) + /// Area in the destination plane. + /// Destination plane to copy (write-only) + /// + /// Status of the copy operation. + /// + /// + /// Rectangles MUST be within plane bounds and both should be same. + /// public static bool CopyImageTo(Rectangle rectFrom, JxlPlane from, Rectangle rectTo, JxlPlane to) where T : unmanaged { @@ -71,6 +101,21 @@ public static bool CopyImageTo(Rectangle rectFrom, JxlPlane from, Rectangl return true; } + /// + /// Within bounds specified by input rectangles, copies everything within every plane + /// from one image to another. + /// + /// The type of image to copy. + /// Area in the source image. + /// Source image to copy (read-only) + /// Area in the destination image. + /// Destination image to copy (write-only) + /// + /// Status of the copy operation. + /// + /// + /// Rectangles MUST be within image bounds and both should be same. + /// public static bool CopyImageTo(Rectangle rectFrom, JxlImage3 from, Rectangle rectTo, JxlImage3 to) where T : unmanaged { @@ -89,4 +134,416 @@ public static bool CopyImageTo(Rectangle rectFrom, JxlImage3 from, Rectang return true; } + + /// + /// Converts a plane from one type to another within specified bounds and ensures to clamp values if the + /// minimum and maximum limits of the result type are lower than the input type (e.g., input is + /// and destination is ). + /// + /// Type of the source plane + /// Type of the destination plane + /// The area of the source plane + /// The source plane (read-only) + /// The area of the destination plane + /// The destination plane (write-only) + /// + /// Status of the copy operation. + /// + public static bool ConvertPlaneAndClamp(Rectangle rectFrom, JxlPlane from, Rectangle rectTo, JxlPlane to) + where TFrom : unmanaged, INumber + where TTo : unmanaged, INumber + { + if (rectFrom != rectTo) + { + return false; + } + + for (int y = 0; y < rectTo.Height; y++) + { + Span rowFrom = from.GetRow(rectFrom, y); + Span rowTo = to.GetRow(rectTo, y); + + for (int x = 0; x < rectTo.Width; x++) + { + rowTo[x] = TTo.CreateSaturating(rowFrom[x]); + } + } + + return true; + } + + /// + /// + /// Copies an image region from to , + /// including up to pixels of surrounding source data + /// on each side of the region. + /// + /// + /// Padding is taken from the neighboring pixels in the source plane and is + /// limited by the source plane boundaries. The destination rectangle is + /// expanded by the same amount to preserve the relative pixel positions. + /// Returns if the destination does not have enough + /// space for the required left or top padding. + /// + /// + /// The source plane area + /// The source plane (read-only) + /// The maximum number of pixels to include around the source region. + /// The destination plane area. + /// The destination place (write-only) + /// + /// if the region was copied successfully; + /// otherwise, if the destination cannot accommodate + /// the required padding. + /// + public static bool CopyImageToWithPadding(Rectangle fromRect, JxlPlane from, int padding, Rectangle toRect, JxlPlane to) + where T : unmanaged + { + int xExtra0 = Math.Min(padding, fromRect.X); + int xExtra1 = Math.Min( + padding, + from.XSize - fromRect.X - fromRect.Width); + + int yExtra0 = Math.Min(padding, fromRect.Y); + int yExtra1 = Math.Min( + padding, + from.YSize - fromRect.Y - fromRect.Height); + + if (toRect.X < xExtra0 || toRect.Y < yExtra0) + { + return false; + } + + return CopyImageTo( + new Rectangle( + fromRect.X - xExtra0, + fromRect.Y - yExtra0, + fromRect.Width + xExtra0 + xExtra1, + fromRect.Height + yExtra0 + yExtra1), + from, + new Rectangle( + toRect.X - xExtra0, + toRect.Y - yExtra0, + toRect.Width + xExtra0 + xExtra1, + toRect.Height + yExtra0 + yExtra1), + to); + } + + /// + /// Performs linear combination of two grayscale images, and allocates & returns the + /// image with the linear combination. The returned image can later be disposed. + /// + /// The type of the plane. This type should be numeric as linear combination involves multiplication. + /// The configuration which includes a memory allocator for the return value. + /// The lambda for image 1. + /// The first image. + /// The lambda for image 2. + /// The second image. + /// A new image with linear combination, or null if it failed. + public static JxlPlane? LinComb(Configuration configuration, T lambda1, JxlPlane image1, T lambda2, JxlPlane image2) + where T : unmanaged, INumber + { + int xSize = image1.XSize; + int ySize = image1.YSize; + + if (xSize != image2.XSize || ySize != image2.YSize) + { + return null; + } + + JxlPlane result = JxlPlane.Create(configuration, xSize, ySize); + + for (int y = 0; y < ySize; y++) + { + Span row1 = image1.GetRow(y); + Span row2 = image2.GetRow(y); + Span rowOut = result.GetRow(y); + + for (int x = 0; x < xSize; x++) + { + rowOut[x] = (lambda1 * row1[x]) + (lambda2 * row2[x]); + } + } + + return result; + } + + /// + /// Multiplies all image values by the lambda in-place. + /// + /// The type of the plane. Should be numeric. + /// The lambda for multiplication. + /// The image to multiply. + public static void ScaleImage(T lambda, JxlPlane image) + where T : unmanaged, INumber + { + for (int y = 0; y < image.YSize; y++) + { + // TODO: SIMD + Span row = image.GetRow(y); + for (int x = 0; x < image.XSize; x++) + { + row[x] = lambda * row[x]; + } + } + } + + /// + /// Multiplies all image values within all planes by the lambda in-place. + /// + /// The type of the image. Should be numeric. + /// The lambda for multiplication. + /// The image to multiply. + public static void ScaleImage(T lambda, JxlImage3 image) + where T : unmanaged, INumber + { + for (int plane = 0; plane < 3; plane++) + { + ScaleImage(lambda, image.Plane(plane)); + } + } + + /// + /// Fills every value in the plane with . + /// + /// The type of the plane. + /// The value to fill everything with. + /// The plane to fill. + public static void FillImage(T value, JxlPlane image) + where T : unmanaged + { + for (int y = 0; y < image.YSize; y++) + { + Span row = image.GetRow(y); + row.Fill(value); + } + } + + /// + /// Sets every value in the plane to 0. See also . + /// + /// The type of the plane. + /// The image to clear. + public static void ZeroFillImage(JxlPlane image) + where T : unmanaged + { + if (image.XSize == 0) + { + return; + } + + for (int y = 0; y < image.YSize; y++) + { + image.GetRow(y).Clear(); + } + } + + /// + /// Core method for the WrapMirror function. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Mirror(long x, long xSize) + { + DebugGuard.MustBeGreaterThan(xSize, 0, nameof(xSize)); + + while (x < 0 || x >= xSize) + { + if (x < 0) + { + x = -x - 1; + } + else + { + x = (2 * xSize) - 1 - x; + } + } + + return (int)x; + } + + /// + /// Searches the entire plane to find the smallest and largest value. + /// + /// The type of the plane. + /// The plane where to search for the minimum & maximum values. + /// Lowest value found in the plane. + /// Largest value found in the plane. + public static void ImageMinMax(JxlPlane image, out T min, out T max) + where T : unmanaged, INumber, IMinMaxValue + { + min = T.MaxValue; // Start with opposite + max = T.MinValue; // Start with opposite + + for (int y = 0; y < image.YSize; y++) + { + Span row = image.GetRow(y); + + for (int x = 0; x < image.XSize; x++) + { + min = T.Min(min, row[x]); + max = T.Max(max, row[x]); + } + } + } + + /// + /// Within the bounds specified by the rectangle , sets every + /// value within the area to be . + /// + /// The type of the plane. + /// The value to fill the area with. + /// The input plane. + /// The area of the plane to fill everything with. + public static void FillPlane(T value, JxlPlane image, Rectangle rect) + where T : unmanaged + { + for (int y = 0; y < rect.Height; y++) + { + Span row = image.GetRow(rect, y); + row.Fill(value); + } + } + + /// + /// Clears every plane within the image. + /// + /// The type of the image + /// The image whose all planes will be cleared. + public static void ZeroFillImage(JxlImage3 image) + where T : unmanaged + { + for (int plane = 0; plane < 3; plane++) + { + ZeroFillImage(image.Plane(plane)); + } + } + + private static bool DownsampleImageCore(JxlPlane input, int factor, JxlPlane output) + { + if (factor == 1) + { + return false; + } + + if (!output.ShrinkTo(JxlMath.DivCeil(input.XSize, factor), JxlMath.DivCeil(input.YSize, factor))) + { + return false; + } + + int inStride = input.PixelsPerRow; + for (int y = 0; y < output.YSize; y++) + { + Span rowOut = output.GetRow(y); + Span rowIn = input.GetRow(factor * y); + for (int x = 0; x < output.XSize; x++) + { + int count = 0; + float sum = 0; + for (int iy = 0; iy < factor && iy + (factor * y) < input.YSize; iy++) + { + for (int ix = 0; ix < factor && ix + (factor * x) < input.XSize; ix++) + { + sum += rowIn[(iy * inStride) + (x * factor) + ix]; + count++; + } + } + + rowOut[x] = sum / count; + } + } + + return true; + } + + /// + /// Downsamples the image. The resulting image can later be disposed. + /// + /// Configuration which has a memory allocator which is used to allocate the result image. + /// The image to downsample. + /// Downsampling factor. + /// A new downsampled image. It can later be disposed. + public static JxlImageF? DownsampleImage(Configuration configuration, JxlImageF image, int factor) + { + JxlImageF downsampled = JxlImageF.Create( + configuration, + JxlMath.DivCeil(image.XSize, factor) + JxlFrameDimensions.BlockDimensions, + JxlMath.DivCeil(image.YSize, factor) + JxlFrameDimensions.BlockDimensions); + + if (!DownsampleImageCore(image, factor, downsampled)) + { + downsampled.Dispose(); + return null; + } + + return downsampled; + } + + /// + /// Downsamples all planes within the image. The resulting image can later be disposed. + /// + /// Configuration which has a memory allocator which is used to allocate the result image. + /// The image to downsample. + /// Downsampling factor. + /// A new downsampled image. It can later be disposed. + public static JxlImage3F? DownsampleImage(Configuration configuration, JxlImage3F opsin, int factor) + { + if (factor == 1) + { + return null; + } + + JxlImage3F downsampled = JxlImage3F.Create( + configuration, + JxlMath.DivCeil(opsin.XSize, factor) + JxlFrameDimensions.BlockDimensions, + JxlMath.DivCeil(opsin.YSize, factor) + JxlFrameDimensions.BlockDimensions); + + if (!downsampled.ShrinkTo( + downsampled.XSize - JxlFrameDimensions.BlockDimensions, + downsampled.YSize - JxlFrameDimensions.BlockDimensions)) + { + return null; + } + + for (int plane = 0; plane < 3; plane++) + { + if (!DownsampleImageCore(opsin.Plane(plane), factor, downsampled.Plane(plane))) + { + downsampled.Dispose(); + return null; + } + } + + return downsampled; + } + + public static bool PadImageToBlockMultipleInPlace(JxlImage3 input, int blockDimensions) + { + int xSizeOriginal = input.XSize; + int ySizeOriginal = input.YSize; + + int xSize = JxlMath.RoundUpTo(xSizeOriginal, blockDimensions); + int ySize = JxlMath.RoundUpTo(ySizeOriginal, blockDimensions); + + if (!input.ShrinkTo(xSize, ySize)) + { + return false; + } + + for (int plane = 0; plane < 3; plane++) + { + for (int y = 0; y < ySizeOriginal; y++) + { + Span row = input.PlaneRow(plane, y); + row[xSizeOriginal..].Fill(row[xSizeOriginal - 1]); + } + + Span sourceRow = input.PlaneRow(plane, ySizeOriginal - 1); + for (int y = ySizeOriginal; y < ySize; y++) + { + sourceRow.CopyTo(input.PlaneRow(plane, y)); + } + } + + return true; + } } From 048945c5d83ab2ccfd1369f13897e09e296fb439 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 8 Aug 2026 19:57:50 +0400 Subject: [PATCH 068/328] Reduce number of errors --- .../Formats/Jxl/Processing/Butteraugli/Butteraugli.cs | 4 ++-- .../Formats/Jxl/Processing/JxlColorCorrelationMap.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs index 87ac5855a8..c0322e91c1 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs @@ -2069,7 +2069,7 @@ public static bool ButteraugliDiffmapInPlace( } using JxlPlane blockDiffDc = JxlImageF.Create(configuration, xSize, ySize); - blockDiffDc.ZeroFill(); + blockDiffDc.Clear(); // LF/DC using (JxlImage3F lf0 = new(configuration, xSize, ySize)) @@ -2109,7 +2109,7 @@ public static bool ButteraugliDiffmapInPlace( } using JxlImageF blockDiffAc = new(configuration, xSize, ySize); - blockDiffAc.ZeroFill(); + blockDiffAc.Clear(); using (JxlImageF diffs = new(configuration, xSize, ySize)) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelationMap.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelationMap.cs index b2e2581f72..03dd8af454 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelationMap.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelationMap.cs @@ -28,8 +28,8 @@ public static JxlColorCorrelationMap Create(Configuration configuration, int wid map.YToXMap = new JxlImageSB(configuration, xBlocks, yBlocks); map.YToBMap = new JxlImageSB(configuration, xBlocks, yBlocks); - ZeroFillImage(map.YToXMap); - ZeroFillImage(map.YToBMap); + map.YToXMap.Clear(); + map.YToBMap.Clear(); if (!xyb) { From 4f71c526a0d0bba8291e38c60628e8bac9e3f7ed Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 8 Aug 2026 21:11:24 +0400 Subject: [PATCH 069/328] Implement JPEG XL numerics functions & reduce errors --- .../Formats/Jxl/Fields/JxlReadVisitor.cs | 7 +- .../Formats/Jxl/Fields/JxlU32Coder.cs | 3 +- .../Entropy/JxlAnsHybridUIntConfiguration.cs | 3 +- .../Jxl/Processing/Decoder/JxlAnsReader.cs | 2 +- .../Processing/Decoder/JxlHuffmanDecoder.cs | 4 +- .../Formats/Jxl/Processing/JxlAcStrategy.cs | 3 +- .../Jxl/Processing/JxlColorCorrelationMap.cs | 2 +- .../Jxl/Processing/JxlFrameDimensions.cs | 21 +- .../Jxl/Processing/JxlImageOperations.cs | 4 +- .../Formats/Jxl/Processing/JxlLehmerCode.cs | 2 +- .../Formats/Jxl/Processing/JxlMath.cs | 682 ++++++++++++++++++ .../Processing/Splines/JxlQuantizedSpline.cs | 2 +- 12 files changed, 707 insertions(+), 28 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs index 35be5d15e0..c60e5985ee 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.Processing; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; namespace SixLabors.ImageSharp.Formats.Jxl.Fields; @@ -56,13 +57,13 @@ public override bool BeginExtensions(ref ulong extensions) for (ulong remainingExtensions = extensions; remainingExtensions != 0; remainingExtensions &= remainingExtensions - 1) { - int idxExtension = Num0BitsBelowLS1BitNonzero(remainingExtensions); + ulong idxExtension = JxlMath.Num0BitsBelowLS1Bit_Nonzero(remainingExtensions); if (!this.U64(0, ref this.extensionBits[idxExtension])) { return false; } - if (!SafeAdd(this.totalExtensionBits, this.extensionBits[idxExtension], ref this.totalExtensionBits)) + if (!JxlMath.SafeAdd(this.totalExtensionBits, this.extensionBits[idxExtension], ref this.totalExtensionBits)) { DebugGuard.IsTrue(false, "Extension bits overflow; the codestream is not valid"); @@ -94,7 +95,7 @@ public override bool EndExtensions() long bitsRead = reader.TotalBitsConsumed; long end = 0; - if (!SafeAdd(this.posAfterExtSize, this.totalExtensionBits, ref end)) + if (!JxlMath.SafeAdd(this.posAfterExtSize, this.totalExtensionBits, ref end)) { DebugGuard.IsTrue(false, "Invalid extension size."); diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs index 8cd2fe7ea3..bdfb456673 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.Processing; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; namespace SixLabors.ImageSharp.Formats.Jxl.Fields; @@ -72,7 +73,7 @@ public static uint Read(in JxlU32Enc enc, JxlBitReader reader) /// public static bool ChooseSelector(in JxlU32Enc enc, uint value, ref uint selector, ref int totalBits) { - int bitsRequired = 32 - Num0BitsAboveMS1Bit(value); + int bitsRequired = 32 - JxlMath.Num0BitsAboveMS1Bit(value); if (bitsRequired > 32) { diff --git a/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHybridUIntConfiguration.cs b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHybridUIntConfiguration.cs index 6c2d8cc2ba..c1d6409076 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHybridUIntConfiguration.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHybridUIntConfiguration.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.Processing; namespace SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; @@ -40,7 +41,7 @@ public void Encode(uint value, ref uint token, ref uint bitCount, ref uint bits) } else { - uint n = FloorLog2Nonzero(value); + uint n = JxlMath.FloorLog2Nonzero(value); uint m = value - (1u << (int)n); unchecked diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs index 51e014d80d..78707b7027 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs @@ -155,7 +155,7 @@ public static uint DecodeVariableLengthUint16(JxlBitReader reader) return counts; } - int upperBoundLog = FloorLog2Nonzero(JxlAnsConstants.AnsLogTableSize + 1); + int upperBoundLog = JxlMath.FloorLog2Nonzero(JxlAnsConstants.AnsLogTableSize + 1); int log = 0; for (; log < upperBoundLog; log++) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlHuffmanDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlHuffmanDecoder.cs index 991d9c6159..216d5e7795 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlHuffmanDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlHuffmanDecoder.cs @@ -141,10 +141,10 @@ public static bool ReadHuffmanCodeLengths(Span codeLengthCodeLengths, int /// Status of the operation public static bool ReadSimpleCode(int alphabetSize, JxlBitReader br, Span table) { - int maxBits = (alphabetSize > 1) ? FloorLog2Nonzero(alphabetSize - 1) + 1 : 0; + int maxBits = (alphabetSize > 1) ? JxlMath.FloorLog2Nonzero(alphabetSize - 1) + 1 : 0; uint symbolCount = br.ReadBits32(2u) + 1u; - Span symbols = stackalloc ushort[4]; + scoped Span symbols = stackalloc ushort[4]; symbols.Clear(); // Clearing is necessary. Not every value will be initialized. for (int i = 0; i < symbolCount; i++) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs index e7a518224c..c6ecc8f16f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs @@ -94,7 +94,6 @@ public JxlAcStrategy(int rawStrategy) private static void CoefficientOrderAndLookup(JxlAcStrategy strategy, bool isLookup, Span output) { // TODO: CoefficientLayout - // TODO: CeilLog2Nonzero int cx = strategy.CoveredBlocksX; int cy = strategy.CoveredBlocksY; @@ -102,7 +101,7 @@ private static void CoefficientOrderAndLookup(JxlAcStrategy strategy, bool isLoo int xs = cx / cy; int xsm = xs - 1; - int xss = CeilLog2Nonzero(xs); + int xss = JxlMath.CeilLog2Nonzero(xs); int cur = cx * cy; for (int i = 0; i < cx * BlockDimensions; i++) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelationMap.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelationMap.cs index 03dd8af454..9ce8e2e86a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelationMap.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelationMap.cs @@ -23,7 +23,7 @@ public static JxlColorCorrelationMap Create(Configuration configuration, int wid { JxlColorCorrelationMap map = new(); - (int xBlocks, int yBlocks) = (DivCeil(width, JxlChromaFromLuma.ColorTileDimension), DivCeil(height, JxlChromaFromLuma.ColorTileDimension)); + (int xBlocks, int yBlocks) = (JxlMath.DivCeil(width, JxlChromaFromLuma.ColorTileDimension), JxlMath.DivCeil(height, JxlChromaFromLuma.ColorTileDimension)); map.YToXMap = new JxlImageSB(configuration, xBlocks, yBlocks); map.YToBMap = new JxlImageSB(configuration, xBlocks, yBlocks); diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlFrameDimensions.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlFrameDimensions.cs index 4ec265f523..70a3bff8bb 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlFrameDimensions.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlFrameDimensions.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Runtime.CompilerServices; - namespace SixLabors.ImageSharp.Formats.Jxl.Processing; internal sealed class JxlFrameDimensions @@ -18,10 +16,10 @@ public JxlFrameDimensions(int xSizePixel, int ySizePixel, int groupSizeShift, in this.DcGroupDimension = this.GroupDimension * BlockDimensions; this.XSizeUpsampled = xSizePixel; this.YSizeUpsampled = ySizePixel; - this.XSize = DivCeil(xSizePixel, upsampling); - this.YSize = DivCeil(ySizePixel, upsampling); - this.XSizeBlocks = DivCeil(this.XSize, BlockDimensions << maxHorizontalShift) << maxHorizontalShift; - this.YSizeBlocks = DivCeil(this.YSize, BlockDimensions << maxVerticalShift) << maxVerticalShift; + this.XSize = JxlMath.DivCeil(xSizePixel, upsampling); + this.YSize = JxlMath.DivCeil(ySizePixel, upsampling); + this.XSizeBlocks = JxlMath.DivCeil(this.XSize, BlockDimensions << maxHorizontalShift) << maxHorizontalShift; + this.YSizeBlocks = JxlMath.DivCeil(this.YSize, BlockDimensions << maxVerticalShift) << maxVerticalShift; this.XSizePadded = this.XSizeBlocks * BlockDimensions; this.YSizePadded = this.YSizeBlocks * BlockDimensions; @@ -33,10 +31,10 @@ public JxlFrameDimensions(int xSizePixel, int ySizePixel, int groupSizeShift, in this.XSizeUpsampledPadded = this.XSizePadded * upsampling; this.YSizeUpsampledPadded = this.YSizePadded * upsampling; - this.XSizeGroups = DivCeil(this.XSize, GroupDimensions); - this.YSizeGroups = DivCeil(this.YSize, GroupDimensions); - this.XSizeDcGroups = DivCeil(this.XSizeBlocks, GroupDimensions); - this.YSizeDcGroups = DivCeil(this.YSizeBlocks, GroupDimensions); + this.XSizeGroups = JxlMath.DivCeil(this.XSize, GroupDimensions); + this.YSizeGroups = JxlMath.DivCeil(this.YSize, GroupDimensions); + this.XSizeDcGroups = JxlMath.DivCeil(this.XSizeBlocks, GroupDimensions); + this.YSizeDcGroups = JxlMath.DivCeil(this.YSizeBlocks, GroupDimensions); this.NumGroups = this.XSizeGroups * this.YSizeGroups; this.NumDcGroups = this.XSizeDcGroups * this.YSizeDcGroups; } @@ -76,7 +74,4 @@ public JxlFrameDimensions(int xSizePixel, int ySizePixel, int groupSizeShift, in public int GroupDimension { get; set; } public int DcGroupDimension { get; set; } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int DivCeil(int x, int y) => x / y; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs index b6c8a3eb93..b356b9aab1 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs @@ -464,7 +464,7 @@ private static bool DownsampleImageCore(JxlPlane input, int factor, JxlPl /// A new downsampled image. It can later be disposed. public static JxlImageF? DownsampleImage(Configuration configuration, JxlImageF image, int factor) { - JxlImageF downsampled = JxlImageF.Create( + JxlImageF downsampled = new( configuration, JxlMath.DivCeil(image.XSize, factor) + JxlFrameDimensions.BlockDimensions, JxlMath.DivCeil(image.YSize, factor) + JxlFrameDimensions.BlockDimensions); @@ -492,7 +492,7 @@ private static bool DownsampleImageCore(JxlPlane input, int factor, JxlPl return null; } - JxlImage3F downsampled = JxlImage3F.Create( + JxlImage3F downsampled = new( configuration, JxlMath.DivCeil(opsin.XSize, factor) + JxlFrameDimensions.BlockDimensions, JxlMath.DivCeil(opsin.YSize, factor) + JxlFrameDimensions.BlockDimensions); diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlLehmerCode.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlLehmerCode.cs index 095e936d32..4afbcb34fd 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlLehmerCode.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlLehmerCode.cs @@ -52,7 +52,7 @@ public static bool DecodeLehmerCode(ReadOnlySpan code, Span temp, in return false; } - int log2n = CeilLog2Nonzero(n); + int log2n = JxlMath.CeilLog2Nonzero(n); int paddedN = 1 << log2n; for (int i = 0; i < paddedN; i++) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs new file mode 100644 index 0000000000..79eee30226 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs @@ -0,0 +1,682 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Common math functions used by the JPEG XL codec. +/// +internal static class JxlMath +{ + /// + /// Number of bits in a single byte. + /// + public const int BitsPerByte = 8; // This makes it more clear than just typing the number 8 + + /// + /// Default intensity target constant. + /// + public const float DefaultIntensityTarget = 255f; + + /// + /// Multiplier for conversion of log2(x) result to ln(x). The + /// value is derived by 1.0f / MathF.Log2(MathF.E). + /// + public const float InverseLog2E = 0.6931471805599453f; + + /// + /// Integer division by default is truncated toward zero. This + /// function performs division and ceiling without any floating-point + /// operations. + /// + /// Dividend + /// Divisor + /// + /// Result of the division with ceiling. It is equivalent to + /// MathF.Ceiling(a / b) without any floating-point usage. + /// + /// + /// This function only works for positive values. Division will be + /// incorrect for negative values. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int DivCeil(int a, int b) + { + unchecked + { + // This is a wrapper for Numerics.DivideCeil but for + // int input values. + return (int)Numerics.DivideCeil((uint)a, (uint)b); + } + } + + /// + /// Ensures that rounded to the multiple of . + /// + /// The value to round. + /// The allowed step size. + /// + /// , transformed to ensure that it stays within the step size of . + /// + /// + /// + /// If value=4, align=6, the result is 6. If value=7 and align=3 the result is 9. + /// + /// + /// This will not work correctly for negative values. + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int RoundUpTo(int value, int align) => DivCeil(value, align) * align; + + /// + /// Ensures that rounded to the multiple of . + /// + /// The value to round. + /// The allowed step size. + /// + /// , transformed to ensure that it stays within the step size of . + /// + /// + /// + /// If value=4, align=6, the result is 6. If value=7 and align=3 the result is 9. + /// + /// + /// This will not work correctly for negative values. + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint RoundUpTo(uint value, uint align) => Numerics.DivideCeil(value, align) * align; + + /// + /// Performs subtraction & returns a boolean indicating whether or not did the subtraction + /// result in an overflow. + /// + /// The minuend. + /// The subtrahend. + /// Subtracted value. + /// True if the subtraction led to an overflow. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool SubOverflow(uint a, uint b, out uint c) + { + c = a - b; + return (((a ^ b) & (a ^ c)) >> 31) != 0; + } + + /// + /// Performs subtraction & returns a boolean indicating whether or not did the subtraction + /// result in an overflow. + /// + /// The minuend. + /// The subtrahend. + /// Subtracted value. + /// True if the subtraction led to an overflow. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool SubOverflow(int a, int b, out int c) + { + c = 0; + unchecked + { + return SubOverflow((uint)a, (uint)b, out Unsafe.As(ref c)); + } + } + + /// + /// A slow, but safe multiplication method which returns false on an overflow. + /// + /// The multiplier + /// The multiplicand + /// The resulting value (or 0 on error) + /// + /// If the multiplication leads to an overflow returns false, + /// otherwise returns true and places the output in . + /// + /// + /// This method is meant for unsigned multiplication only. Negative values + /// won't work properly. + /// + public static bool SafeMultiply(int a, int b, out int product) + { + product = 0; + + if (a == 0 || b == 0) + { + return true; + } + + if (b > (int.MaxValue / a)) + { + return false; + } + + product = a * b; + + return true; + } + + /// + /// A slow, but safe multiplication method which returns false on an overflow. + /// + /// The multiplier + /// The multiplicand + /// The resulting value (or 0 on error) + /// + /// If the multiplication leads to an overflow returns false, + /// otherwise returns true and places the output in . + /// + /// + /// This method is meant for unsigned multiplication only. Negative values + /// won't work properly. + /// + public static bool SafeMultiply(uint a, uint b, out uint product) + { + product = 0; + + if (a == 0 || b == 0) + { + return true; + } + + if (b > (uint.MaxValue / a)) + { + return false; + } + + product = a * b; + + return true; + } + + /// + /// Performs addition and returns a boolean indicating whether the addition led to + /// an overflow. + /// + /// The augend + /// The addend + /// The resulting sum of addition. + /// A boolean indicating whether the addition led to an overflow. + /// + /// This method is meant for unsigned addition only. Negative values + /// won't work properly. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool SafeAdd(int a, int b, out int sum) + { + unchecked + { + sum = a + b; + return sum >= a; + } + } + + /// + /// Performs addition and returns a boolean indicating whether the addition led to + /// an overflow. + /// + /// The augend + /// The addend + /// The resulting sum of addition. + /// A boolean indicating whether the addition led to an overflow. + /// + /// This method is meant for unsigned addition only. Negative values + /// won't work properly. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool SafeAdd(long a, long b, out long sum) + { + unchecked + { + sum = a + b; + return sum >= a; + } + } + + /// + /// Performs addition and returns a boolean indicating whether the addition led to + /// an overflow. + /// + /// The augend + /// The addend + /// The resulting sum of addition. + /// A boolean indicating whether the addition led to an overflow. + /// + /// This method is meant for unsigned addition only. Negative values + /// won't work properly. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool SafeAdd(uint a, uint b, out uint sum) + { + unchecked + { + sum = a + b; + return sum >= a; + } + } + + /// + /// Performs addition and returns a boolean indicating whether the addition led to + /// an overflow. + /// + /// The augend + /// The addend + /// The resulting sum of addition. + /// A boolean indicating whether the addition led to an overflow. + /// + /// This method is meant for unsigned addition only. Negative values + /// won't work properly. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool SafeAdd(ulong a, ulong b, out ulong sum) + { + unchecked + { + sum = a + b; + return sum >= a; + } + } + + /// + /// Ensures that the value stays within the specified range. + /// + /// The input value + /// The lower bound + /// The upper bound + /// + /// if value is lower than . + /// if value is higher than . Otherwise, if it's within + /// the range. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Clamp1(int value, int low, int high) => Numerics.Clamp(value, low, high); + + /// + /// Rounds the dimensions up to block dimensions. + /// + /// The dimensions to round up to block dimensions. + /// The rounded dimensions. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int RoundUpToBlockDimensions(int dim) + { + unchecked + { + return (dim + 7) & ~7; + } + } + + /// + /// Rounds the dimensions up to block dimensions. + /// + /// The dimensions to round up to block dimensions. + /// The rounded dimensions. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint RoundUpToBlockDimensions(uint dim) + { + unchecked + { + return (dim + 7u) & ~7u; + } + } + + /// + /// Rounds the specified number of bits to a multiple of bytes. + /// + /// The input bits. + /// Bits rounded to the multiples of bytes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int RoundUpBitsToByteMultiple(int bits) + { + unchecked + { + return (bits + 7) & ~7; + } + } + + /// + /// Rounds the specified number of bits to a multiple of bytes. + /// + /// The input bits. + /// Bits rounded to the multiples of bytes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint RoundUpBitsToByteMultiple(uint bits) + { + unchecked + { + return (bits + 7u) & ~7u; + } + } + + /// + /// Multiplies by π. + /// + /// The value to multiply. + /// + /// multiplier * π + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Pi(float multiplier) => multiplier * MathF.PI; + + /// + /// Multiplies by π. + /// + /// The value to multiply. + /// + /// multiplier * π + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double Pi(double multiplier) => multiplier * Math.PI; + + /// + /// Returns the amount of zero bits before the highest 1 bit. + /// + /// The input value. + /// + /// Number of zero bits before the highest 1 bit. This is the equivalent + /// of leading zero count. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Num0BitsAboveMS1Bit_Nonzero(uint x) + { + DebugGuard.MustBeGreaterThan(x, 0u, nameof(x)); + + return BitOperations.LeadingZeroCount(x); + } + + /// + /// Returns the amount of zero bits before the highest 1 bit. + /// + /// The input value. + /// + /// Number of zero bits before the highest 1 bit. This is the equivalent + /// of leading zero count. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Num0BitsAboveMS1Bit_Nonzero(int x) + { + unchecked + { + return Num0BitsAboveMS1Bit_Nonzero((uint)x); + } + } + + /// + /// Returns the amount of zero bits before the highest 1 bit. + /// + /// The input value. + /// + /// Number of zero bits before the highest 1 bit. This is the equivalent + /// of leading zero count. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Num0BitsAboveMS1Bit_Nonzero(ulong x) + { + DebugGuard.MustBeGreaterThan(x, 0uL, nameof(x)); + + return BitOperations.LeadingZeroCount(x); + } + + /// + /// Returns the amount of zero bits before the highest 1 bit. + /// + /// The input value. + /// + /// Number of zero bits before the highest 1 bit. This is the equivalent + /// of leading zero count. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Num0BitsAboveMS1Bit_Nonzero(long x) + { + unchecked + { + return Num0BitsAboveMS1Bit_Nonzero((ulong)x); + } + } + + /// + /// Returns the number of 0 bits to the right of the lowest 1 bit. + /// + /// The input value. + /// + /// Number of zero bits after the lowest 1 bit. This is the equivalent + /// of trailing zero count. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint Num0BitsBelowLS1Bit_Nonzero(uint x) + { + DebugGuard.MustBeGreaterThan(x, 0u, nameof(x)); + + return (uint)BitOperations.TrailingZeroCount(x); + } + + /// + /// Returns the number of 0 bits to the right of the lowest 1 bit. + /// + /// The input value. + /// + /// Number of zero bits after the lowest 1 bit. This is the equivalent + /// of trailing zero count. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint Num0BitsBelowLS1Bit_Nonzero(ulong x) + { + DebugGuard.MustBeGreaterThan(x, 0uL, nameof(x)); + + return (uint)BitOperations.TrailingZeroCount(x); + } + + /// + /// Returns the number of 0 bits to the right of the lowest 1 bit. + /// + /// The input value. + /// + /// Number of zero bits after the lowest 1 bit. This is the equivalent + /// of trailing zero count. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint Num0BitsBelowLS1Bit_Nonzero(int x) => Num0BitsBelowLS1Bit_Nonzero((uint)x); + + /// + /// Returns the number of 0 bits to the right of the lowest 1 bit. + /// + /// The input value. + /// + /// Number of zero bits after the lowest 1 bit. This is the equivalent + /// of trailing zero count. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint Num0BitsBelowLS1Bit_Nonzero(long x) => Num0BitsBelowLS1Bit_Nonzero((ulong)x); + + /// + /// Returns the number of 0 bits to the right of the lowest 1 bit. + /// + /// The input value. + /// + /// Number of zero bits after the lowest 1 bit. This is the equivalent + /// of trailing zero count. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint Num0BitsBelowLS1Bit(uint x) => x == 0 ? 32u : Num0BitsBelowLS1Bit_Nonzero(x); + + /// + /// Returns the number of 0 bits to the right of the lowest 1 bit. + /// + /// The input value. + /// + /// Number of zero bits after the lowest 1 bit. This is the equivalent + /// of trailing zero count. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint Num0BitsBelowLS1Bit(int x) => Num0BitsBelowLS1Bit_Nonzero((uint)x); + + /// + /// Returns the number of 0 bits to the right of the lowest 1 bit. + /// + /// The input value. + /// + /// Number of zero bits after the lowest 1 bit. This is the equivalent + /// of trailing zero count. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint Num0BitsBelowLS1Bit(ulong x) => x == 0 ? 64u : Num0BitsBelowLS1Bit_Nonzero(x); + + /// + /// Returns the number of 0 bits to the right of the lowest 1 bit. + /// + /// The input value. + /// + /// Number of zero bits after the lowest 1 bit. This is the equivalent + /// of trailing zero count. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint Num0BitsBelowLS1Bit(long x) => Num0BitsBelowLS1Bit_Nonzero((ulong)x); + + /// + /// Returns the amount of zero bits before the highest 1 bit. + /// + /// The input value. + /// + /// Number of zero bits before the highest 1 bit. This is the equivalent + /// of leading zero count. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Num0BitsAboveMS1Bit(int x) => x == 0 ? sizeof(int) * 8 : Num0BitsAboveMS1Bit_Nonzero(x); + + /// + /// Returns the amount of zero bits before the highest 1 bit. + /// + /// The input value. + /// + /// Number of zero bits before the highest 1 bit. This is the equivalent + /// of leading zero count. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint Num0BitsAboveMS1Bit(uint x) => x == 0 ? sizeof(uint) * 8u : unchecked((uint)Num0BitsAboveMS1Bit_Nonzero((int)x)); + + /// + /// Returns the amount of zero bits before the highest 1 bit. + /// + /// The input value. + /// + /// Number of zero bits before the highest 1 bit. This is the equivalent + /// of leading zero count. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long Num0BitsAboveMS1Bit(long x) => x == 0 ? sizeof(long) * 8L : Num0BitsAboveMS1Bit_Nonzero(x); + + /// + /// Returns the amount of zero bits before the highest 1 bit. + /// + /// The input value. + /// + /// Number of zero bits before the highest 1 bit. This is the equivalent + /// of leading zero count. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong Num0BitsAboveMS1Bit(ulong x) => x == 0 ? sizeof(ulong) * 8uL : unchecked((ulong)Num0BitsAboveMS1Bit_Nonzero((long)x)); + + /// + /// Integer equivalent of MathF.Floor(MathF.Log2(x)). + /// + /// The input value. + /// Floor(Log2(x)) in integer form. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint FloorLog2Nonzero(uint x) => (uint)(((sizeof(uint) * 8) - 1) ^ Num0BitsAboveMS1Bit_Nonzero(x)); + + /// + /// Integer equivalent of MathF.Floor(MathF.Log2(x)). + /// + /// The input value. + /// Floor(Log2(x)) in integer form. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int FloorLog2Nonzero(int x) => ((sizeof(int) * 8) - 1) ^ Num0BitsAboveMS1Bit_Nonzero(x); + + /// + /// Integer equivalent of MathF.Floor(MathF.Log2(x)). + /// + /// The input value. + /// Floor(Log2(x)) in integer form. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long FloorLog2Nonzero(long x) => ((sizeof(long) * 8) - 1) ^ Num0BitsAboveMS1Bit_Nonzero(x); + + /// + /// Integer equivalent of MathF.Floor(MathF.Log2(x)). + /// + /// The input value. + /// Floor(Log2(x)) in integer form. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong FloorLog2Nonzero(ulong x) => (ulong)(((sizeof(ulong) * 8) - 1) ^ Num0BitsAboveMS1Bit_Nonzero(x)); + + /// + /// Integer equivalent of MathF.Ceiling(MathF.Log2(x)). + /// + /// The input value. + /// Ceil(Log2(x)) in integer form. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int CeilLog2Nonzero(int x) + { + int floorLog2 = FloorLog2Nonzero(x); + + if ((x & (x - 1)) == 0) + { + return floorLog2; + } + + return floorLog2 + 1; + } + + /// + /// Integer equivalent of MathF.Ceiling(MathF.Log2(x)). + /// + /// The input value. + /// Ceil(Log2(x)) in integer form. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint CeilLog2Nonzero(uint x) + { + uint floorLog2 = FloorLog2Nonzero(x); + + if ((x & (x - 1)) == 0) + { + return floorLog2; + } + + return floorLog2 + 1; + } + + /// + /// Integer equivalent of MathF.Ceiling(MathF.Log2(x)). + /// + /// The input value. + /// Ceil(Log2(x)) in integer form. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long CeilLog2Nonzero(long x) + { + long floorLog2 = FloorLog2Nonzero(x); + + if ((x & (x - 1)) == 0) + { + return floorLog2; + } + + return floorLog2 + 1; + } + + /// + /// Integer equivalent of MathF.Ceiling(MathF.Log2(x)). + /// + /// The input value. + /// Ceil(Log2(x)) in integer form. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong CeilLog2Nonzero(ulong x) + { + ulong floorLog2 = FloorLog2Nonzero(x); + + if ((x & (x - 1)) == 0) + { + return floorLog2; + } + + return floorLog2 + 1; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs index 76b3e24b4a..ed5729067c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs @@ -216,7 +216,7 @@ public bool Dequantize( color[2] += (long)MathF.Ceiling(MathF.Abs(yToB)) * color[1]; long maxColor = Math.Max(color[1], Math.Max(color[0], color[2])); - long logColor = Math.Max(1L, (long)CeilLog2Nonzero(1L + maxColor)); + long logColor = Math.Max(1L, JxlMath.CeilLog2Nonzero(1L + maxColor)); float weightLimit = MathF.Ceiling(MathF.Sqrt((float)areaLimit / logColor) / MathF.Max(1, manhattanDistance)); for (int i = 0; i < 32; i++) From 4d0a46863902cd3968214bd30de37e0631947c88 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 8 Aug 2026 21:22:58 +0400 Subject: [PATCH 070/328] Add rectangle support to images This massively reduces number of syntax errors --- src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs | 7 +++++++ src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs | 11 +++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs index 9e9b10c099..91aed2e97a 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs @@ -46,6 +46,13 @@ public Span PlaneRow(int plane, int row) return rowSpan; } + public Span PlaneRow(Rectangle rectangle, int c, int y) + { + DebugGuard.MustBeGreaterThanOrEqualTo(y + rectangle.Top, 0, nameof(y)); + + return this.PlaneRow(c, y + rectangle.Top)[rectangle.Left..]; + } + public JxlPlane Plane(int index) => this.planes[index]; public void Swap(JxlImage3 other) diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs index 7326768f09..e61381b194 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs @@ -36,6 +36,17 @@ public static JxlPlane Create(Configuration configuration, int xSize, int ySi public Span GetRow(int y) => this.GetRowBase(y); + public Span GetRow(Rectangle rectangle, int y) + { + DebugGuard.MustBeGreaterThanOrEqualTo(y + rectangle.Top, 0, nameof(y)); + + return this.GetRow(y + rectangle.Top)[rectangle.Left..]; + } + + public bool IsRectangleInside(Rectangle rectangle) => rectangle.Contains(this.GetRectangle()); + + public Rectangle GetRectangle() => new(0, 0, this.XSize, this.YSize); + /// /// Fills everything in this image with 0. /// From 2c517a2fbcf2864c58ed36c33b22c5cda40bd7a5 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 8 Aug 2026 21:36:39 +0400 Subject: [PATCH 071/328] Document JxlPlane and JxlPlaneBase --- .../Formats/Jxl/Memory/JxlPlaneBase.cs | 75 ++++++++++++++++++- .../Formats/Jxl/Memory/JxlPlane{T}.cs | 45 ++++++++++- 2 files changed, 116 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs index 367b3fc300..1b63eb43f2 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs @@ -7,11 +7,22 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Memory; -// NOTE: Do not seal this type. +/// +/// Base class for a single-plane image. +/// internal class JxlPlaneBase : IDisposable { + /// + /// Underlying bytes + /// private IMemoryOwner? bytes; + /// + /// Initializes a new instance of the class. + /// + /// Plane width + /// Plane height + /// The size of each pixel in bytes. public JxlPlaneBase(int xSize, int ySize, int sizeOfT) { this.XSize = xSize; @@ -22,17 +33,32 @@ public JxlPlaneBase(int xSize, int ySize, int sizeOfT) this.Size = sizeOfT; } + /// + /// Initializes a new instance of the class with empty values. + /// public JxlPlaneBase() : this(0, 0, 0) { } + /// + /// Gets the number of bytes per row. + /// public int BytesPerRow { get; private set; } + /// + /// Gets the width of the image. + /// public int XSize { get; private set; } + /// + /// Gets the height of the image. + /// public int YSize { get; private set; } + /// + /// Gets the underlying bytes of this image as a Memory<T>. + /// public Memory Bytes => #if DEBUG this.bytes?.Memory ?? throw new InvalidOperationException("Bytes are missing"); @@ -40,14 +66,31 @@ public JxlPlaneBase() return this.bytes!.Memory; #endif + /// + /// Gets the underlying bytes of this image as a Span<T>. + /// public Span BytesSpan => this.Bytes.Span; protected int Size { get; set; } + /// + /// Gets or sets the width that was initially assigned. For example, if the image gets shrinked, + /// the XSize YSize properties get changed while this property will stay same. + /// protected int OriginalXSize { get; set; } + /// + /// Gets or sets the height that was initially assigned. For example, if the image gets shrinked, + /// the XSize YSize properties get changed while this property will stay same. + /// protected int OriginalYSize { get; set; } + /// + /// Allocates the underlying memory for the plane. + /// + /// The configuration which has a memory allocator used to allocate memory. + /// Padding + /// Status of allocation. public bool Allocate(Configuration configuration, int prePadding) { if (this.bytes != null || this.BytesPerRow != 0) @@ -67,6 +110,21 @@ public bool Allocate(Configuration configuration, int prePadding) return true; } + /// + /// Shrinks the image so its width is equal to and its height is + /// equal to . + /// + /// The output width + /// The output height + /// Status of the shrinking operation. + /// + /// + /// This method can only shrink memory. It cannot expand it. + /// + /// + /// When shrinking, the underlying memory does not get resized. + /// + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool ShrinkTo(int x, int y) { @@ -84,6 +142,12 @@ public bool ShrinkTo(int x, int y) return true; } + /// + /// Base function to return the span for a specified row as a generic <T>. + /// + /// The type of the row. + /// The index of the row to get the span for. + /// A span which covers the row memory. protected Span GetRowBase(int y) where T : unmanaged { @@ -93,8 +157,10 @@ protected Span GetRowBase(int y) return MemoryMarshal.Cast(row); } - protected void SetBytes(IMemoryOwner bytes) => this.bytes = bytes; - + /// + /// Swaps properties & data of this image with the specified image. + /// + /// The other image to swap with. public void Swap(JxlPlaneBase other) { (this.XSize, other.XSize) = (other.XSize, this.XSize); @@ -105,6 +171,9 @@ public void Swap(JxlPlaneBase other) (this.bytes, other.bytes) = (other.bytes, this.bytes); } + /// + /// Releases all underlying memory used by this plane. + /// public void Dispose() { this.bytes?.Dispose(); diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs index e61381b194..2d96ab636f 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs @@ -5,21 +5,44 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Memory; -// NOTE: Do not seal this class. +/// +/// A generic version of a 2D single-plane JPEG XL image. +/// +/// The type of each pixel. internal class JxlPlane : JxlPlaneBase where T : unmanaged { + /// + /// Initializes a new instance of the class. + /// public JxlPlane() { } + /// + /// Initializes a new instance of the class with the specified width and height. + /// + /// Plane width. + /// Plane height public unsafe JxlPlane(int width, int height) : base(width, height, sizeof(T)) { } + /// + /// Gets the number of pixels per row. + /// public unsafe int PixelsPerRow => this.BytesPerRow / sizeof(T); + /// + /// Allocates a new plane. + /// + /// The configuration which contains a memory allocator. + /// Plane width + /// Plane height + /// Padding + /// A new allocated plane + /// Thrown when allocation fails. public static JxlPlane Create(Configuration configuration, int xSize, int ySize, int prePadding = 0) { JxlPlane plane = new(xSize, ySize); @@ -34,8 +57,19 @@ public static JxlPlane Create(Configuration configuration, int xSize, int ySi return plane; } + /// + /// Returns a span for the specified row. + /// + /// The row index. + /// A span which covers memory for the specified row. public Span GetRow(int y) => this.GetRowBase(y); + /// + /// Returns a span for the specified row within the specified rectangle bounds. + /// + /// The bounds. + /// The row index. + /// A span which covers memory for the specified row with the rectangle offsets. public Span GetRow(Rectangle rectangle, int y) { DebugGuard.MustBeGreaterThanOrEqualTo(y + rectangle.Top, 0, nameof(y)); @@ -43,8 +77,17 @@ public Span GetRow(Rectangle rectangle, int y) return this.GetRow(y + rectangle.Top)[rectangle.Left..]; } + /// + /// Checks if the specified rectangle is within the bounds image. + /// + /// The input rectangle. + /// Boolean indicating whether the rectangle is inside. public bool IsRectangleInside(Rectangle rectangle) => rectangle.Contains(this.GetRectangle()); + /// + /// Returns the rectangle for this image bounds. + /// + /// A rectangle with x,y=0,0 width,height=XSize,YSize. public Rectangle GetRectangle() => new(0, 0, this.XSize, this.YSize); /// From 50813c1cbbde2cb901d941ff90b5bc76395e85eb Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 8 Aug 2026 22:12:01 +0400 Subject: [PATCH 072/328] Add passes decoder state prototype & reduce errors in JxlQuantizedSpline --- .../Decoder/JxlGroupDecoderCache.cs | 12 +++ .../Decoder/JxlPassesDecoderState.cs | 77 +++++++++++++++++++ .../Processing/Splines/JxlQuantizedSpline.cs | 7 +- 3 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlGroupDecoderCache.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlGroupDecoderCache.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlGroupDecoderCache.cs new file mode 100644 index 0000000000..f82e332d8d --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlGroupDecoderCache.cs @@ -0,0 +1,12 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +/// +/// Cache for the JPEG XL Group Decoder. +/// +internal sealed class JxlGroupDecoderCache +{ + +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs new file mode 100644 index 0000000000..5b534ef773 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs @@ -0,0 +1,77 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +internal sealed class JxlPassesDecoderState +{ + public JxlPassesDecoderState(JxlFrameHeader header, Configuration configuration) + { + this.XDmMultiplier = MathF.Pow(1 / 1.25f, header.XQmScale - 2f); + this.BDmMultiplier = MathF.Pow(1 / 1.25f, header.BQmScale - 2f); + + this.MainOutput.Callback = PixelCallback; + this.MainOutput.Buffer = null; + + this.UndoOrientation = JxlOrientation.Identity; + this.Upsampler8x = GetUpsamplingImage(configuration, this.Shared.CodecMetadata.CustomTransformData, 0, 3); + + if (header.LoopFilter?.EpfIterations > 0) + { + this.Sigma = new JxlImageF( + configuration, + (this.Shared.FrameDimensions.XSizeBlocks + 2) * SigmaPadding, + (this.Shared.FrameDimensions.YSizeBlocks + 2) * SigmaPadding); + } + + this.SharedStorage = new(configuration, header, false); + this.Shared = this.SharedStorage; + } + + public JxlPassesSharedState SharedStorage { get; set; } + + public JxlPassesSharedState Shared { get; set; } + + public JxlRenderPipelineStage[] Upsampler8x { get; set; } = []; + + public List Code { get; set; } = []; + + public List> ContextMap { get; set; } = []; + + public float XDmMultiplier { get; set; } + + public float BDmMultiplier { get; set; } + + public JxlImageF? Sigma { get; set; } + + public int Width { get; set; } + + public int Height { get; set; } + + public JxlImageOutput MainOutput { get; set; } + + public List ExtraOutput { get; set; } = []; + + public bool FastXybSRgb8Conversion { get; set; } + + public bool UnpremultiplyAlpha { get; set; } + + public JxlOrientation UndoOrientation { get; set; } + + public int VisibleFrameIndex { get; set; } + + public int NonvisibleFrameIndex { get; set; } + + public int UsedAcs { get; set; } + + public JxlDctAcImage Coefficients { get; set; } = []; + + public JxlRenderPipeline RenderPipeline { get; set; } + + public JxlImageBundle FrameStorageForReferencing { get; set; } + + public JxlOutputEncodingInfo OutputEncodingInfo { get; set; } = new(); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs index ed5729067c..40dff40e2b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; @@ -269,8 +270,8 @@ public bool Decode( { ref JxlControlPoint controlPoint = ref controlPoints[i]; - controlPoint.First = UnpackSigned(decoder.ReadHybridUnsignedInteger(ControlPointsContext, br, contextMap)); - controlPoint.Second = UnpackSigned(decoder.ReadHybridUnsignedInteger(ControlPointsContext, br, contextMap)); + controlPoint.First = JxlPackSigned.UnpackSigned(decoder.ReadHybridUnsignedInteger(ControlPointsContext, br, contextMap)); + controlPoint.Second = JxlPackSigned.UnpackSigned(decoder.ReadHybridUnsignedInteger(ControlPointsContext, br, contextMap)); if (controlPoint.First >= deltaLimit || controlPoint.First <= -deltaLimit || controlPoint.Second >= deltaLimit || controlPoint.Second <= -deltaLimit) @@ -300,7 +301,7 @@ bool TryDecodeDct(ReadOnlySpan contextMap, Span dct) for (int i = 0; i < 32; i++) { - dct[i] = UnpackSigned(decoder.ReadHybridUnsignedInteger(DctContext, br, contextMap)); + dct[i] = JxlPackSigned.UnpackSigned(decoder.ReadHybridUnsignedInteger(DctContext, br, contextMap)); if (dct[i] == invalidCoefficient) { throw new InvalidOperationException("The DCT coefficient is invalid"); From 27a848cd256ed439752b92d1ff3ed7280e034d34 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 12 Aug 2026 16:06:41 +0400 Subject: [PATCH 073/328] Add DCT, transpose, and performance improvements - Add a slice & assert to JxlHuffmanDecoder alphabetSize to allocate at most 256 items - Use [0, 0] instead of stackalloc[2] followed by Clear() in JxlAnsReader - Add assert to Butteraugli ComputeKernel method & use float for Butteraugli Wmul & use InlineArray - Add transpose. - Note: transpose is scalar, it doesn't support SIMD yet - Add shared constants & file signature - Improve while loop in JxlImageOperations.Mirror - Floating-point Discrete Cosine Transform (1D and 2D) - Add an inline array of 2 items Source files implemented from libjxl with this commit: - dct-inl.h - dct_block-inl.h - transpose-inl.h --- src/ImageSharp/Formats/Jxl/InlineArrays.cs | 9 + .../Jxl/Processing/Butteraugli/Butteraugli.cs | 20 +- .../Jxl/Processing/Decoder/JxlAnsReader.cs | 3 +- .../Processing/Decoder/JxlHuffmanDecoder.cs | 5 +- .../Formats/Jxl/Processing/JxlDct.cs | 305 ++++++++++++++++++ .../Formats/Jxl/Processing/JxlDctOutput.cs | 52 +++ .../Formats/Jxl/Processing/JxlDctScales.cs | 27 ++ .../Formats/Jxl/Processing/JxlDctSource.cs | 55 ++++ .../Jxl/Processing/JxlImageOperations.cs | 8 +- .../Formats/Jxl/Processing/JxlShared.cs | 30 ++ .../Formats/Jxl/Processing/JxlTranspose.cs | 22 ++ 11 files changed, 522 insertions(+), 14 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlDct.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlDctOutput.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlDctSource.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlShared.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlTranspose.cs diff --git a/src/ImageSharp/Formats/Jxl/InlineArrays.cs b/src/ImageSharp/Formats/Jxl/InlineArrays.cs index f006d2fd09..6276051700 100644 --- a/src/ImageSharp/Formats/Jxl/InlineArrays.cs +++ b/src/ImageSharp/Formats/Jxl/InlineArrays.cs @@ -7,6 +7,15 @@ namespace SixLabors.ImageSharp.Formats.Jxl; +/// +/// Used by Butteraugli +/// +[InlineArray(2)] +internal struct InlineArray2 +{ + private T first; +} + [InlineArray(3)] internal struct InlineArray3 { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs index c0322e91c1..73420e86d7 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs @@ -42,11 +42,11 @@ internal static class Butteraugli private const float GlobalScale = 1.0f / InternalGoodQualityThreshold; - public static ReadOnlySpan Wmul => + public static ReadOnlySpan Wmul => [ - 400.0, 1.50815703118, 0, - 2150.0, 10.6195433239, 16.2176043152, - 29.2353797994, 0.844626970982, 0.703646627719, + 400.0f, 1.50815703118f, 0f, + 2150.0f, 10.6195433239f, 16.2176043152f, + 29.2353797994f, 0.844626970982f, 0.703646627719f, ]; public static ReadOnlySpan ComputeKernel(float sigma) @@ -55,7 +55,11 @@ public static ReadOnlySpan ComputeKernel(float sigma) float scaler = -1.0f / (2.0f * sigma * sigma); int diff = Math.Max(1, (int)(m * MathF.Abs(sigma))); - // Use new because there's only up to 3 elements + // If sigma is very large we should not return a 'new float[]' allocation. + // This guard is temporary so we can verify the range of the number of elements. + // TODO: remove guard if the value doesn't exceed the limit for many JXL files + DebugGuard.MustBeLessThanOrEqualTo(sigma, 32f, nameof(sigma)); + float[] kernel = new float[(2 * diff) + 1]; for (int i = -diff; i <= diff; i++) @@ -536,7 +540,7 @@ public static bool SeparateMfAndHf( Configuration configuration, in ButteraugliParameters parameters, JxlImage3F mf, - JxlImageF[] hf, + ref InlineArray2 hf, BlurTemp blurTemp) { const float sigmaHf = 3.22489901262f; @@ -2095,8 +2099,8 @@ public static bool ButteraugliDiffmapInPlace( } } - JxlImageF[] hf0 = new JxlImageF[2]; - JxlImageF[] hf1 = new JxlImageF[2]; + InlineArray2 hf0 = default; + InlineArray2 hf1 = default; if (!SeparateMfAndHf(parameters, image0, hf0, blurTemp)) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs index 78707b7027..180dbd3c1c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs @@ -103,8 +103,7 @@ public static uint DecodeVariableLengthUint16(JxlBitReader reader) if (isSimpleCode) { - Span symbols = stackalloc uint[2]; - symbols.Clear(); + Span symbols = [0, 0]; uint maxSymbol = 0u; uint symCount = reader.ReadBits32(1u) + 1u; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlHuffmanDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlHuffmanDecoder.cs index 216d5e7795..7201a511aa 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlHuffmanDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlHuffmanDecoder.cs @@ -269,8 +269,9 @@ public bool ReadFromBitStream(int alphabetSize, JxlBitReader br) return ReadSimpleCode(alphabetSize, br, this.Table); } - // The alphabet size is at most 256 - Span codeLengths = stackalloc byte[alphabetSize]; + DebugGuard.MustBeLessThanOrEqualTo(alphabetSize, 256, nameof(alphabetSize)); + + Span codeLengths = stackalloc byte[256].Slice(0, alphabetSize); codeLengths.Clear(); // Zero-initialized in reference software Span codeLengthCodeLengths = stackalloc byte[CodeLengthCodes]; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDct.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlDct.cs new file mode 100644 index 0000000000..5e3e76355f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlDct.cs @@ -0,0 +1,305 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Discrete Cosine Transform with SIMD support. +/// +internal static class JxlDct +{ + /// + /// Creates a new coefficient bundle. + /// + /// Number of items. + /// Coefficient size. + /// A new coefficient bundle. + public static CoefficientBundle CoeffBundle(int n, int sz) => new(n, sz); + + public static void Dct1DCore(int n, int sz, Span mem, Span tmp) + { + if (n == 2) + { + Vector in1 = new(mem); + Vector in2 = new(mem[sz..]); + (in1 + in2).CopyTo(mem); + (in1 - in2).CopyTo(mem[sz..]); + } + else + { + CoefficientBundle cb = CoeffBundle(n / 2, sz); + + cb.AddReverse(mem, mem[(n / 2 * sz)..], tmp); + Dct1DCore(n / 2, sz, tmp, tmp[(n * sz)..]); + + cb.SubReverse(mem, mem[(n / 2 * sz)..], tmp[(n / 2 * sz)..]); + cb.Multiply(tmp); + + Dct1DCore(n / 2, sz, tmp[(n / 2 * sz)..], tmp[(n * sz)..]); + cb.B(tmp[(n / 2 * sz)..]); + + CoeffBundle(n, sz).InverseEvenOdd(tmp, mem); + } + } + + public static void InverseDct1DCore(int n, int sz, Span from, int fromStride, Span to, int toStride, Span tmp) + { + if (n == 1) + { + from.CopyTo(to); + } + else if (n == 2) + { + Vector in1 = new(from); + Vector in2 = new(from[fromStride..]); + (in1 + in2).CopyTo(to); + (in1 + in2).CopyTo(to[toStride..]); + } + else + { + CoefficientBundle cbDiv2 = CoeffBundle(n / 2, sz); + CoefficientBundle cb = CoeffBundle(n, sz); + + cb.ForwardEvenOdd(from, fromStride, tmp); + InverseDct1DCore(n / 2, sz, tmp, sz, tmp, sz, tmp[(n * sz)..]); + + cbDiv2.BTranspose(tmp[((n / 2) * sz)..]); + InverseDct1DCore(n / 2, sz, tmp[((n / 2) * sz)..], sz, tmp[((n / 2) * sz)..], sz, tmp[(n * sz)..]); + + cb.MultiplyAndAdd(tmp, to, toStride); + } + } + + public static void Dct1DWrapper(int n, int m, bool fit, JxlDctSource from, JxlDctOutput to, int mp, Span tmp) + { + CoefficientBundle cb = CoeffBundle(n, m); + + for (int i = 0; i < mp; i += m) + { + cb.LoadFromBlock(from, i, tmp); + Dct1DCore(n, m, tmp, tmp[(n * m)..]); + cb.StoreToBlockAndScale(tmp, ref to, i); + + if (fit) + { + return; + } + } + } + + public static void InverseDct1DWrapper(int n, int m, bool fit, JxlDctSource from, JxlDctOutput to, int mp, Span tmp) + { + for (int i = 0; i < mp; i += m) + { + InverseDct1DCore(n, m, from.Address(0, i), from.Stride, to.Address(0, i), to.Stride, tmp); + + if (fit) + { + return; + } + } + } + + public static void Dct1DCapped(int n, int m, int l, JxlDctSource from, JxlDctOutput to, Span tmp) + { + bool fit = m <= l; + Dct1DWrapper(n, m, fit, from, to, m, tmp); + } + + public static void InverseDct1DCapped(int n, int m, int l, JxlDctSource from, JxlDctOutput to, Span tmp) + { + bool fit = m <= l; + InverseDct1DWrapper(n, m, fit, from, to, m, tmp); + } + + public static void Dct1D(int n, int m, JxlDctSource from, JxlDctOutput to, Span tmp) + { + int lanes = Vector.Count; + Dct1DCapped(n, m, lanes, from, to, tmp); + } + + public static void InverseDct1D(int n, int m, JxlDctSource source, JxlDctOutput output, Span tmp) + { + int lanes = Vector.Count; + InverseDct1DCapped(n, m, lanes, source, output, tmp); + } + + public static void ComputeScaledDct(int rows, int columns, JxlDctSource from, Span to, Span scratchSpace) + { + Span block = scratchSpace; + Span tmp = scratchSpace[(rows * columns)..]; + + if (rows < columns) + { + Dct1D(rows, columns, from, new JxlDctOutput(block, columns), tmp); + JxlTranspose.Transpose(rows, columns, new JxlDctSource(block, columns), new JxlDctOutput(to, rows)); + Dct1D(columns, rows, new JxlDctSource(to, rows), new JxlDctOutput(block, rows), tmp); + JxlTranspose.Transpose(columns, rows, new JxlDctSource(block, rows), new JxlDctOutput(to, columns)); + } + else + { + Dct1D(rows, columns, from, new JxlDctOutput(to, columns), tmp); + JxlTranspose.Transpose(rows, columns, new JxlDctSource(to, columns), new JxlDctOutput(block, rows)); + Dct1D(columns, rows, new JxlDctSource(block, rows), new JxlDctOutput(to, rows), tmp); + } + } + + public static void ComputeScaledInverseDct(int rows, int columns, Span from, JxlDctOutput to, Span scratchSpace) + { + Span block = scratchSpace; + Span tmp = scratchSpace[(rows * columns)..]; + + if (rows < columns) + { + JxlTranspose.Transpose(rows, columns, new JxlDctSource(from, columns), new JxlDctOutput(block, rows)); + InverseDct1D(columns, rows, new JxlDctSource(block, rows), new JxlDctOutput(from, rows), tmp); + JxlTranspose.Transpose(columns, rows, new JxlDctSource(from, rows), new JxlDctOutput(block, columns)); + InverseDct1D(rows, columns, new JxlDctSource(block, columns), to, tmp); + } + else + { + InverseDct1D(columns, rows, new JxlDctSource(from, rows), new JxlDctOutput(block, rows), tmp); + JxlTranspose.Transpose(columns, rows, new JxlDctSource(block, rows), new JxlDctOutput(from, columns)); + InverseDct1D(rows, columns, new JxlDctSource(from, columns), to, tmp); + } + } + + /// + /// Core methods for the Discrete Cosine Transform (DCT). + /// + public readonly struct CoefficientBundle(int n, int sz) + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddReverse(Span aIn1, Span aIn2, Span aOut) + { + for (int i = 0; i < n; i++) + { + Vector in1 = new(aIn1[(i * sz)..]); + Vector in2 = new(aIn2[((n - i - 1) * sz)..]); + (in1 + in2).CopyTo(aOut[(i * sz)..]); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SubReverse(Span aIn1, Span aIn2, Span aOut) + { + for (int i = 0; i < n; i++) + { + Vector in1 = new(aIn1[(i * sz)..]); + Vector in2 = new(aIn2[((n - i - 1) * sz)..]); + (in1 - in2).CopyTo(aOut[(i * sz)..]); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void B(Span coeff) + { + Vector sqrt2 = new(JxlDctScales.Sqrt2); + Vector in10 = new(coeff); + Vector in20 = new(coeff[sz..]); + ((in10 * sqrt2) + in20).CopyTo(coeff); + + for (int i = 1; i + 1 < n; i++) + { + Vector in1 = new(coeff[(i * sz)..]); + Vector in2 = new(coeff[((i + 1) * sz)..]); + (in1 + in2).CopyTo(coeff[(i * sz)..]); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void BTranspose(Span coeff) + { + for (int i = n - 1; i > 0; i--) + { + Vector in1 = new(coeff[(i * sz)..]); + Vector in2 = new(coeff[((i - 1) * sz)..]); + (in1 + in2).CopyTo(coeff[(i * sz)..]); + } + + Vector sqrt2 = new(JxlDctScales.Sqrt2); + Vector in1x = new(coeff); + (in1x * sqrt2).CopyTo(coeff); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void InverseEvenOdd(Span aIn, Span aOut) + { + for (int i = 0; i < n / 2; i++) + { + new Vector(aIn[(i * sz)..]).CopyTo(aOut[((2 * i) * sz)..]); + } + + for (int i = n / 2; i < n; i++) + { + new Vector(aIn[(i * sz)..]).CopyTo(aOut[(((2 * (i - (n / 2))) + 1) * sz)..]); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ForwardEvenOdd(Span aIn, int aInStride, Span aOut) + { + for (int i = 0; i < n / 2; i++) + { + new Vector(aIn[(2 * i * aInStride)..]).CopyTo(aOut[(i * sz)..]); + } + + for (int i = n / 2; i < n; i++) + { + new Vector(aIn[(((2 * (i - (n / 2))) + 1) * aInStride)..]).CopyTo(aOut[(i * sz)..]); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Multiply(Span coeff) + { + ReadOnlySpan multipliers = JxlDctScales.GetMultipliers(n); + + for (int i = 0; i < n / 2; i++) + { + Vector in1 = new(coeff[(((n / 2) + i) * sz)..]); + Vector mul = new(multipliers[i]); + (in1 * mul).CopyTo(coeff[((n / (2 + i)) * sz)..]); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void MultiplyAndAdd(Span coeff, Span output, int outStride) + { + ReadOnlySpan multipliers = JxlDctScales.GetMultipliers(n); + + for (int i = 0; i < n / 2; i++) + { + Vector mul = new(multipliers[i]); + Vector in1 = new(coeff[(i * sz)..]); + Vector in2 = new(coeff[((n / (2 + i)) * sz)..]); + Vector out1 = (mul * in2) * in1; + Vector out2 = -(mul * in2) + in1; + out1.CopyTo(output[(i * outStride)..]); + out2.CopyTo(output[((n - i - 1) * outStride)..]); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void LoadFromBlock(in JxlDctSource input, int offset, Span coeff) + { + for (int i = 0; i < n; i++) + { + input.LoadPart(i, offset).CopyTo(coeff[(i * sz)..]); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void StoreToBlockAndScale(Span coeff, ref JxlDctOutput output, int offset) + { + Vector mul = new(1.0f / n); + for (int i = 0; i < n; i++) + { + output.StorePart(mul * new Vector(coeff[(i * sz)..]), i, offset); + } + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctOutput.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlDctOutput.cs new file mode 100644 index 0000000000..1be3b9adfe --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlDctOutput.cs @@ -0,0 +1,52 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Output DCT block. +/// +internal ref struct JxlDctOutput(Span data, int stride) +{ + /// + /// Raw block data. + /// + public Span Data = data; + + /// + /// Stride size. + /// + public readonly int Stride = stride; + + /// + /// Returns the span to the start of a row and offset. + /// + /// The row index. + /// The offset. + /// + /// Span for that row & offset. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Span Address(int row, int i) => this.Data[((row * this.Stride) + i)..]; + + /// + /// Writes a single value to the block at the row and offset. + /// + /// The value to write. + /// The row index. + /// The offset. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(float value, int row, int i) => this.Data[(row * this.Stride) + i] = value; + + /// + /// Stores the vector into the data at the specified row and offset. + /// + /// The vector to write. + /// The row index. + /// The offset. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly void StorePart(Vector value, int row, int index) => value.CopyTo(this.Address(row, index)); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctScales.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlDctScales.cs index 26bc7ee8e3..0ebcb7a1fe 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlDctScales.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlDctScales.cs @@ -9,6 +9,16 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing; /// internal static class JxlDctScales { + /// + /// Square root of 2. + /// + public const float Sqrt2 = 1.41421356237f; + + /// + /// Square root of 0.5. + /// + public const float Sqrt05 = 0.70710678118f; + /// /// Gets 8x1 DCT resample scales. /// @@ -358,4 +368,21 @@ internal static class JxlDctScales 9.058751453879703f, 11.644627325175037f, 16.300023088031555f, 27.163977662448232f, 81.48784219222516f, ]; + + /// + /// Returns DCT multipliers for size . + /// + /// The multiplier size + /// DCT multipliers for the given size. + public static ReadOnlySpan GetMultipliers(int n) => n switch + { + 4 => Multipliers4, + 8 => Multipliers8, + 16 => Multipliers16, + 32 => Multipliers32, + 64 => Multipliers64, + 128 => Multipliers128, + 256 => Multipliers256, + _ => [] + }; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctSource.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlDctSource.cs new file mode 100644 index 0000000000..08dafc11e7 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlDctSource.cs @@ -0,0 +1,55 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Source DCT block. +/// +internal readonly ref struct JxlDctSource(Span data, int stride) +{ + /// + /// Raw block data. + /// + public readonly Span Data = data; + + /// + /// Stride size. + /// + public readonly int Stride = stride; + + /// + /// Returns the span to the start of a row and offset. + /// + /// The row index. + /// The offset. + /// + /// Span for that row & offset. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span Address(int row, int i) => this.Data[((row * this.Stride) + i)..]; + + /// + /// Returns the coefficient at the row and offset. + /// + /// The row index. + /// The offset. + /// + /// Coefficient at that row and offset. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float Read(int row, int i) => this.Data[(row * this.Stride) + i]; + + /// + /// Loads a vector at the specified row and offset. + /// + /// The row index. + /// The offset. + /// + /// Vector at that row and offset. + /// + public Vector LoadPart(int row, int i) => new(this.Address(row, i)); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs index b356b9aab1..e5ddf834f7 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs @@ -346,16 +346,20 @@ private static int Mirror(long x, long xSize) { DebugGuard.MustBeGreaterThan(xSize, 0, nameof(xSize)); - while (x < 0 || x >= xSize) + while (true) { if (x < 0) { x = -x - 1; } - else + else if (x >= xSize) { x = (2 * xSize) - 1 - x; } + else + { + break; + } } return (int)x; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlShared.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlShared.cs new file mode 100644 index 0000000000..711dbc27ea --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlShared.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Shared JPEG XL constants +/// +internal static class JxlShared +{ + /// + /// Maximum number of passes in an image. + /// + public const int MaximumNumberOfPasses = 11; + + /// + /// Maximum number of reference frames. + /// + public const int MaximumNumberOfReferenceFrames = 4; + + /// + /// Gets the 12-byte signature (a.k.a. magic) for JPEG XL files. + /// + public static ReadOnlySpan SignatureBox => + [ + 0x00, 0x00, 0x00, 0x0C, + (byte)'J', (byte)'X', (byte)'L', (byte)' ', + 0x0D, 0x0A, 0x87, 0x0A + ]; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlTranspose.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlTranspose.cs new file mode 100644 index 0000000000..4b05150a77 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlTranspose.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Performs transpose on JPEG XL DCT blocks. +/// +internal static class JxlTranspose +{ + // TODO: SIMD + public static void Transpose(int r, int c, JxlDctSource from, JxlDctOutput to) + { + for (int n = 0; n < r; n++) + { + for (int m = 0; m < c; m++) + { + to.Write(from.Read(n, m), m, n); + } + } + } +} From 0d9aea4f127a0f599fe1d3d74638373c543f2d06 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 12 Aug 2026 16:08:39 +0400 Subject: [PATCH 074/328] Remove GC.SuppressFinalize(this) --- .../Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs index 40dff40e2b..7e46f49b05 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs @@ -38,7 +38,6 @@ public void Dispose() { this.memoryOwner?.Dispose(); this.ControlPoints = Memory.Empty; - GC.SuppressFinalize(this); } public void ReserveControlPoints(Configuration configuration, int n) From 20e3a3452a4add118c29c79f368164cbcacdcdfb Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 12 Aug 2026 17:35:38 +0400 Subject: [PATCH 075/328] Add image bundle --- .../Formats/Jxl/Processing/JxlImageBundle.cs | 506 ++++++++++++++++++ 1 file changed, 506 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlImageBundle.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlImageBundle.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlImageBundle.cs new file mode 100644 index 0000000000..48f84175b3 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlImageBundle.cs @@ -0,0 +1,506 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Cms; +using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; +using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// An image bundle. +/// +internal sealed class JxlImageBundle +{ + /// + /// Image data for additional channels. + /// + private List extraChannels = []; + + /// + /// Initializes a new instance of the class. + /// + public JxlImageBundle() + { + } + + /// + /// Initializes a new instance of the class with the specified image metadata. + /// + /// Initial image metadata. + public JxlImageBundle(JxlImageMetadata? metadata) => this.Metadata = metadata; + + /// + /// Gets or sets the optional name of the bundle. + /// + public string? Name { get; set; } + + /// + /// Gets or sets the blend mode. Default is Blend. + /// + public JxlBlendMode BlendMode { get; set; } = JxlBlendMode.Blend; + + /// + /// Gets or sets a value indicating whether blending should be done. (Default: false) + /// + public bool Blend { get; set; } + + /// + /// Gets or sets a value indicating whether this a reference frame. + /// + public bool UseForNextFrame { get; set; } + + /// + /// Gets or sets the duration for animation. + /// + public uint Duration { get; set; } + + /// + /// Gets or sets the timecode for animation. + /// + public uint Timecode { get; set; } + + /// + /// Gets or sets the frame origin. + /// + public Point Origin { get; set; } + + /// + /// Gets or sets the chroma subsampling for Y'Cb'Cr images. + /// + public JxlYCbCrChromaSubsampling? ChromaSubsampling { get; set; } + + /// + /// Gets or sets the color transform mode for this image, the default is None. + /// + public JxlColorTransform ColorTransform { get; set; } = JxlColorTransform.None; + + /// + /// Gets or sets the JPEG data if the input image was converted to JPEG XL from a JPEG. + /// + public JxlJpegData? JpegData { get; set; } + + /// + /// Gets a value indicating whether returns the image does or will represent quantized DCT-8 coefficients + /// stored in the 8x8 pixel regions. + /// + public bool IsJpeg => this.JpegData is not null; + + /// + /// Gets or sets the number of bytes that were actually read. + /// + public long DecodedBytes { get; set; } + + /// + /// Gets a value indicating whether the black extra channel is present. + /// + public bool ContainsBlack => this.Metadata?.FindExtraChannel(JxlExtraChannel.Black) is not null; + + /// + /// Gets a value indicating whether the alpha extra channel is present. + /// + public bool ContainsAlpha => this.Metadata?.FindExtraChannel(JxlExtraChannel.Alpha) is not null; + + /// + /// Gets a value indicating whether the alpha channel is premultiplied. + /// + public bool IsAlphaPremultiplied => this.Metadata?.FindExtraChannel(JxlExtraChannel.Alpha)?.AlphaAssociated == true; + + /// + /// Gets a value indicating whether the color encoding specifies Gray. + /// + public bool IsGray => this.CurrentColorEncoding?.IsGray == true; + + /// + /// Gets a value indicating whether the color encoding specifies sRGB. + /// + public bool IsSrgb => this.CurrentColorEncoding?.IsSrgb == true; + + /// + /// Gets a value indicating whether the color encoding specifies linear sRGB. + /// + public bool IsLinearSrgb => this.CurrentColorEncoding?.IsLinearSrgb == true; + + /// + /// Gets the current color encoding for this image. + /// + public JxlColorEncoding? CurrentColorEncoding { get; private set; } + + /// + /// Gets the image metadata for this image bundle. + /// + public JxlImageMetadata? Metadata { get; } + + /// + /// Gets the color data. + /// + public JxlImage3F? Color { get; private set; } + + /// + /// Gets a value indicating whether the color data is present and usable. + /// + public bool HasColor => this.Color?.XSize != 0; + + /// + /// Gets the width. + /// + public int XSize + { + get + { + if (this.IsJpeg) + { + return this.JpegData!.Width; + } + + if (this.Color?.XSize != 0) + { + return this.Color!.XSize; + } + + return this.extraChannels?.Count > 0 ? 0 : this.extraChannels![0].XSize; + } + } + + /// + /// Gets the height. + /// + public int YSize + { + get + { + if (this.IsJpeg) + { + return this.JpegData!.Height; + } + + if (this.Color?.YSize != 0) + { + return this.Color!.YSize; + } + + return this.extraChannels?.Count > 0 ? 0 : this.extraChannels![0].YSize; + } + } + + /// + /// Gets the black extra channel. + /// + public JxlImageF? Black + { + get + { + if (!this.ContainsBlack || this.Metadata is null) + { + return null; + } + + int ec = this.Metadata!.FindExtraChannel(JxlExtraChannel.Black) - this.Metadata.ExtraChannelInfo.Data; + return this.extraChannels[ec]; + } + } + + /// + /// Gets the alpha extra channel. + /// + public JxlImageF? Alpha + { + get + { + if (!this.ContainsAlpha || this.Metadata is null) + { + return null; + } + + int ec = this.Metadata!.FindExtraChannel(JxlExtraChannel.Alpha) - this.Metadata.ExtraChannelInfo.Data; + return this.extraChannels[ec]; + } + } + + /// + /// Gets the oriented X size. + /// + public int OrientedXSize => this.Metadata?.Orientation > 4 ? this.YSize : this.XSize; + + /// + /// Gets the oriented Y size. + /// + public int OrientedYSize => this.Metadata?.Orientation > 4 ? this.XSize : this.YSize; + + /// + /// Gets the real bit depth. + /// + public uint RealBitDepth => this.Metadata!.BitDepth!.BitsPerSample; + + /// + /// Returns false if the width or height is 0 and any extra channel + /// does not match this image bundle's width or height; returns true if + /// the sizes are otherwise correct. + /// + /// + /// True if the sizes are correct. False if they aren't. + /// + public bool VerifySizes() + { + if (this.ContainsExtraChannels()) + { + int xs = this.XSize; + int ys = this.YSize; + + if (xs == 0 || ys == 0) + { + return false; + } + + foreach (JxlImageF ec in this.extraChannels) + { + if (ec.XSize != xs || ec.YSize != ys) + { + return false; + } + } + } + + return true; + } + + /// + /// Overrides the color encoding for this image bundle. + /// + /// The new color encoding. + public void OverrideProfile(JxlColorEncoding encoding) => this.CurrentColorEncoding = encoding; + + /// + /// If the color data is present, assigns it to and returns true, + /// otherwise returns false and assigns null. + /// + /// Output color data. + /// True if color isn't null. + public bool TryGetColor(out JxlImage3F? color) + { + color = null; + if (this.Color is not null) + { + color = this.Color; + } + + return color is not null; + } + + /// + /// Removes the color data, replacing it with a new Image3F with 0 as width and height. + /// + public void RemoveColor() => this.Color = new JxlImage3F(); + + /// + /// Removes all extra channels, if any. + /// + public void ClearExtraChannels() => this.extraChannels.Clear(); + + /// + /// Returns true if there is at least 1 extra channel. + /// + /// Boolean indicating if extra channels are present. + public bool ContainsExtraChannels() => this.extraChannels.Count > 0; + + /// + /// Returns an enumerable for extra channels. + /// + /// Extra channels enumerable. + public IEnumerable EnumerateExtraChannels() => this.extraChannels; + + /// + /// Sets the extra channels. + /// + /// The extra channels. + /// + /// True if each plane had width and height greater than 0 and sizes are correct + /// after changing the extra channels; false otherwise. + /// + public bool TrySetExtraChannels(List extraChannels) + { + foreach (JxlImageF plane in extraChannels) + { + if (plane.XSize == 0 || plane.YSize == 0) + { + return false; + } + } + + this.extraChannels = extraChannels; + + return this.VerifySizes(); + } + + /// + /// Attempts to set the alpha channel. + /// + /// The alpha channel to set. + /// True if it was set successfully; false otherwise. + /// Thrown if the corresponding extra channel has incorrect info. + public bool TrySetAlpha(JxlImageF alpha) + { + if (this.Metadata is null) + { + return false; + } + + JxlExtraChannelInfo? eci = this.Metadata!.FindExtraChannel(JxlExtraChannel.Alpha); + + if (eci is null) + { + return false; + } + + if (alpha.XSize == 0 || alpha.YSize == 0) + { + return false; + } + + int eciIndex = this.Metadata.ExtraChannelInfo.Data; + + if (eciIndex != this.extraChannels.Count) + { + throw new InvalidOperationException("The SetAlpha parameter is incorrect"); + } + + this.extraChannels.Add(alpha); + + return this.VerifySizes(); + } + + /// + /// Ensures that the metadata of this image is valid. + /// + /// True if metadata is correct. False if it isn't. + /// Rare. + public bool VerifyMetadata() + { + if (this.CurrentColorEncoding?.Icc?.IsEmpty == true) + { + return false; + } + + if (this.Metadata?.ColorEncoding?.IsGray != this.IsGray) + { + return false; + } + + if (this.Metadata?.HasAlpha == true) + { + JxlImageF? img = this.Alpha; + if (img?.XSize == 0) + { + throw new InvalidOperationException("Alpha should not have width equal to 0"); + } + } + + int alphaBits = this.Metadata?.AlphaBits ?? 0; + + if (alphaBits > 32) + { + return false; + } + + return true; + } + + /// + /// Updates the bundle from the sepcified image. + /// + /// Color data. + /// Current color encoding. + /// True if setting the image succeeded; otherwise false. + public bool SetFromImage(JxlImage3F color, JxlColorEncoding current) + { + if (color.XSize == 0 || color.YSize == 0) + { + return false; + } + + if (this.Metadata?.ColorEncoding?.IsGray == this.IsGray) + { + return false; + } + + this.Color = color; + this.CurrentColorEncoding = current; + + return this.VerifySizes(); + } + + /// + /// Shrinks this image and all of its extra channels to the specified + /// width and height. + /// + /// The desired width. + /// The desired height. + /// + /// If this bundle color data or any of the extra channels + /// happens to have a smaller width or height than the specified + /// width or height, that is considered expanding, which will immediately + /// return false. If this method returns true, all colors and + /// extra channels have successfully been shrunk. + /// + public bool ShrinkTo(int width, int height) + { + if (this.HasColor) + { + if (this.Color?.ShrinkTo(width, height) != true) + { + return false; + } + } + + foreach (JxlImageF extraChannel in this.extraChannels) + { + if (!extraChannel.ShrinkTo(width, height)) + { + return false; + } + } + + return true; + } + + /// + /// Copies this image bundle to a new bundle. + /// + /// + /// A configuration with a memory allocator. + /// + /// + /// A new copy of this image bundle. + /// + /// + /// Thrown if some extra channels cannot be copied. + /// + public JxlImageBundle Copy(Configuration configuration) + { + JxlImageBundle copy = new(this.Metadata); + + if (this.Color is not null) + { + copy.Color = new JxlImage3F(configuration, this.Color.XSize, this.Color.YSize); + } + + copy.CurrentColorEncoding = this.CurrentColorEncoding; + copy.JpegData = this.JpegData; + copy.ColorTransform = this.ColorTransform; + copy.ChromaSubsampling = this.ChromaSubsampling; + + foreach (JxlImageF plane in this.extraChannels) + { + JxlImageF ec = new(configuration, plane.XSize, plane.YSize); + if (!JxlImageOperations.CopyImage(plane, ec)) + { + throw new InvalidOperationException("Cannot copy extra channel"); + } + + copy.extraChannels.Add(ec); + } + + return copy; + } +} From 6e432ca50bc411fa9c896300080cf3fced437b34 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 12 Aug 2026 17:45:39 +0400 Subject: [PATCH 076/328] Use inline arrays in Butteraugli --- .../Formats/Jxl/Processing/Butteraugli/Butteraugli.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs index 73420e86d7..2011c186b3 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs @@ -726,7 +726,7 @@ public static bool SeparateHFAndUHF( return true; } - public static void DeallocateHFAndUHF(JxlImageF[] hf, JxlImageF[] uhf) + public static void DeallocateHFAndUHF(InlineArray2 hf, InlineArray2 uhf) { for (int i = 0; i < 2; i++) { @@ -1424,8 +1424,8 @@ public static bool MaltaDiffMapLf( } public static void CombineChannelsForMasking( - JxlImageF[] hf, - JxlImageF[] uhf, + InlineArray2 hf, + InlineArray2 uhf, JxlImageF output) { // Only X and Y components are involved in masking. @@ -2157,8 +2157,8 @@ public static bool ButteraugliDiffmapInPlace( image0.Dispose(); image1.Dispose(); - JxlImageF[] uhf0 = new JxlImageF[2]; - JxlImageF[] uhf1 = new JxlImageF[2]; + InlineArray2 uhf0 = default; + InlineArray2 uhf1 = default; if (!SeparateHFAndUHF(parameters, hf0, uhf0, blurTemp)) { From 8b4ff41b9ae24206032e158866eb041f02f81afe Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 15 Aug 2026 15:46:33 +0400 Subject: [PATCH 077/328] First prototype of JxlDecoderCore - Implemented decode.cc - Added prototype of JXL image info - Made JxlBitReader use ReadOnlyMemory - Made changes to ICC codecs, changing accessibility of a few methods from private to internal and exposing IccDataReader's index - Added JxlMemoryWriter as a MemoryAllocator alternative to MemoryStream --- .../Formats/Jxl/Fields/JxlBundle.cs | 2 +- .../Formats/Jxl/IO/JxlMemoryWriter.cs | 82 + src/ImageSharp/Formats/Jxl/JxlImageInfo.cs | 22 + .../Jxl/Processing/Decoder/JxlBitReader.cs | 14 +- .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 2938 ++++++++++++++++- .../Jxl/Processing/JxlBoxCodingMode.cs | 20 + .../Profiles/ICC/DataReader/IccDataReader.cs | 7 + .../Metadata/Profiles/ICC/IccReader.cs | 4 +- 8 files changed, 2988 insertions(+), 101 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/IO/JxlMemoryWriter.cs create mode 100644 src/ImageSharp/Formats/Jxl/JxlImageInfo.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlBoxCodingMode.cs diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs index e6e673f9c8..7202932539 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; namespace SixLabors.ImageSharp.Formats.Jxl.Fields; diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlMemoryWriter.cs b/src/ImageSharp/Formats/Jxl/IO/JxlMemoryWriter.cs new file mode 100644 index 0000000000..77058802b3 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/JxlMemoryWriter.cs @@ -0,0 +1,82 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO; + +/// +/// A disposable writer for bytes in memory. It is highly similar to +/// , but its buffer relies on +/// . +/// +internal sealed class JxlMemoryWriter(MemoryAllocator allocator) : IDisposable +{ + /// + /// The initial capacity in bytes. + /// + private const int InitialCapacity = 1024; + + /// + /// Core buffer. + /// + private IMemoryOwner buffer = allocator.Allocate(InitialCapacity); + + /// + /// Gets the length of the written data in bytes. + /// + public int Length { get; private set; } + + /// + /// Gets the capacity of the buffer in bytes. + /// + public int Capacity => this.buffer.Memory.Length; + + /// + /// Releases the underlying buffer. + /// + public void Dispose() => this.buffer.Dispose(); + + /// + /// Writes the specified bytes into the writer. + /// + /// The bytes to write. + public void Write(ReadOnlySpan bytes) + { + int requiredCapacity = checked(this.Length + bytes.Length); + this.EnsureCapacity(requiredCapacity); + + bytes.CopyTo(this.buffer.Memory.Span[this.Length..]); + this.Length = requiredCapacity; + } + + /// + /// Returns a span containing the bytes written to the writer. + /// + /// A span containing the written bytes. + public Span AsSpan() => this.buffer.Memory.Span[..this.Length]; + + /// + /// Returns memory containing the bytes written to the writer. + /// + /// Memory containing the written bytes. + public Memory AsMemory() => this.buffer.Memory[..this.Length]; + + private void EnsureCapacity(int requiredCapacity) + { + if (requiredCapacity <= this.Capacity) + { + return; + } + + int newCapacity = Math.Max(requiredCapacity, checked(this.Capacity * 2)); + + IMemoryOwner previousBuffer = this.buffer; + this.buffer = allocator.Allocate(newCapacity); + + previousBuffer.Memory.Span[..this.Length].CopyTo(this.buffer.Memory.Span); + + previousBuffer.Dispose(); + } +} diff --git a/src/ImageSharp/Formats/Jxl/JxlImageInfo.cs b/src/ImageSharp/Formats/Jxl/JxlImageInfo.cs new file mode 100644 index 0000000000..47f5e4e954 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/JxlImageInfo.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Metadata; + +namespace SixLabors.ImageSharp.Formats.Jxl; + +/// +/// Image information specific to the JPEG XL format. +/// +public class JxlImageInfo : ImageInfo +{ + /// + /// Initializes a new instance of the class. + /// + /// Image size + /// Image metadata + public JxlImageInfo(Size size, ImageMetadata metadata) + : base(size, metadata) + { + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs index cdc69838df..9dbc090aec 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs @@ -8,8 +8,10 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; /// /// Represents a bitstream reader. /// -internal sealed class JxlBitReader(ReadOnlyMemory bytes) +internal ref struct JxlBitReader(ReadOnlySpan bytes) { + private readonly ReadOnlySpan data = bytes; + private ulong buffer; private uint bufferRemainingBits; private int pointer; @@ -22,16 +24,14 @@ internal sealed class JxlBitReader(ReadOnlyMemory bytes) /// /// Gets the total number of bits consumed. /// - public long TotalBitsConsumed => ((long)this.pointer * 8) + (64 - this.bufferRemainingBits); + public readonly long TotalBitsConsumed => ((long)this.pointer * 8) + (64 - this.bufferRemainingBits); /// /// Fetches a new buffer. /// private void RefillCore() { - ReadOnlySpan samplesSpan = bytes.Span; - - int remaining = samplesSpan.Length - this.pointer; + int remaining = this.data.Length - this.pointer; if (remaining <= 0) { // we don't have any more data... mark an end of stream @@ -43,7 +43,7 @@ private void RefillCore() if (remaining >= 8) { - this.buffer = BinaryPrimitives.ReadUInt64LittleEndian(samplesSpan[this.pointer..]); + this.buffer = BinaryPrimitives.ReadUInt64LittleEndian(this.data[this.pointer..]); this.bufferRemainingBits = 64u; this.pointer += 8; } @@ -52,7 +52,7 @@ private void RefillCore() ulong value = 0; for (int i = 0; i < remaining; i++) { - value |= (ulong)samplesSpan[this.pointer + i] << (8 * i); + value |= (ulong)this.data[this.pointer + i] << (8 * i); } this.buffer = value; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index fbb7a22d02..e984b36b93 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -1,155 +1,2911 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; +using System.Buffers.Binary; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; +using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; -internal sealed class JxlDecoderCore : ImageDecoderCore +/// +/// Internal decoder for JPEG XL. +/// +internal sealed class JxlDecoderCore : ImageDecoderCore, IDisposable { + private const long NumBuffersLimit = 1 << 20; + + /// + /// Current stage of the decoding pipeline. + /// + private JxlDecoderStage decoderStage; + + /// + /// Status of whether or not has the signature been parsed. + /// + private bool gotSignature; + + /// + /// Did we parse the final code stream? + /// + private bool lastCodestreamSeen; + + /// + /// Did we parse the signature of the code stream? + /// + private bool gotCodestreamSignature; + + /// + /// Did we parse basic JXL information? + /// + private bool gotBasicInfo; + + /// + /// Did we parse the transform data? + /// + private bool gotTransformData; + + /// + /// Did we parse all the codestream metadata headers? + /// + private bool gotAllHeaders; + + /// + /// Are we decoding pixels now? + /// + private bool postHeaders; + + /// + /// ICC profile for JPEG XL metadata, if present. + /// + private IccProfile? iccProfile; + + /// + /// The frame index box, if present. + /// + private JxlDecoderFrameIndexBox? frameIndexBox; + + /// + /// Did we get the preview image, or determined we cannot get it or there isn't any? + /// + private bool gotPreviewImage; + + /// + /// Is this a preview frame? + /// + private bool previewFrame; + + private long filePosition; + + /// + /// Offset where box contents start. + /// + private long boxContentsBegin; + + /// + /// Offset where box contents end. + /// + private long boxContentsEnd; + + /// + /// boxContentsEnd - boxContentsBegin + /// + private long boxContentsSize; + + /// + /// Total size of the box in bytes. + /// + private long boxSize; + + /// + /// Size of the headers in bytes. + /// + private long headerSize; + + /// + /// Are box contents unbounded? + /// + private bool boxContentsUnbounded; + + /// + /// Type of the box currently being decoded. + /// + private JxlBoxType boxType; + + /// + /// Underlying type for brob boxes. + /// + private JxlBoxType boxDecodedType; + + private bool boxEvent; + + /// + /// Should box contents be decompressed (using Brotli)? + /// + private bool decompressBoxes; + + /// + /// Should the output buffer for the box be set? + /// + private bool boxOutBufferSet; + + /// + /// Should the output buffer for the current box be set? + /// + private bool boxOutBufferSetCurrentBox; + + /// + /// Output buffer for the current box. + /// + private IMemoryOwner? boxOutputBuffer; + + /// + /// Size of the output buffer. + /// + private long boxOutBufferSize; + + /// + /// Offset of start of box output buffer. + /// + private long boxOutBufferBegin; + + /// + /// Current offset of start of box output buffer. + /// + private long boxOutBufferPos; + + /// + /// Should orientation be preserved? + /// + private bool keepOrientation; + + /// + /// Should alpha channel be unpremultiplied? + /// + private bool unpremultiplyAlpha; + + private bool renderSpotcolors; + + private bool coalescing; + + /// + /// Custom intensity target. + /// + private float desiredIntensityTarget; + + private int eventsWanted; + + private int originalEventsWanted; + + private long basicInfoSizeHint; + + /// + /// Is container format present? + /// + private bool haveContainer; + + /// + /// Total number of boxes. + /// + private long boxCount; + + /// + /// The level of progressive detail in frame coding. + /// + private JxlProgressiveDetail progressiveDetail = JxlProgressiveDetail.Dc; + + /// + /// Progressive detail of current frame. + /// + private JxlProgressiveDetail frameProgressiveDetail; + + /// + /// The intended downsampling ratio for the current progression step. + /// + private long downsamplingTarget; + + /// + /// True if the image output buffer or callback was set. + /// + private bool imageOutBufferSet; + + /// + /// Size of the image output buffer. + /// + private long imageOutputSize; + + /// + /// Output data for extra channels. + /// + private List extraChannelOutputs = []; + + /// + /// Codec metadata if present. + /// + private JxlCodecMetadata? metadata; + + /// + /// Image metadata if present. + /// + private JxlImageMetadata? imageMetadata; + + /// + /// The image bundle. + /// + private JxlImageBundle? imageBundle; + + /// + /// State for passes decoder. + /// + private JxlPassesDecoderState? passesState; + + /// + /// State for frame decoder. + /// + private JxlFrameDecoder? frameDecoder; + + /// + /// The next section. + /// + private long nextSection; + + private List sectionProcessed = []; + + /// + /// The frame header, if present. + /// + private JxlFrameHeader? frameHeader; + + /// + /// Remaining frame size. + /// + private long remainingFrameSize; + + /// + /// Stage of the decoding pipeline. + /// + private JxlFrameStage frameStage; + + /// + /// Has progression for DC frames been completed? + /// + private bool dcFrameProgressionDone; + + private bool isLastOfStill; + + /// + /// Is the currently processed frame the last of the codestream? + /// + private bool isLastTotal; + + /// + /// How many frames should be skipped? + /// + private int skipFrames; + + /// + /// Is active frame being skipped? + /// + private bool skippingFrame; + + private int internalFrames; + + private int externalFrames; + + /// + /// All frame reference.s + /// + private List frameReferences = []; + + private List frameExternalToInternal = []; + + private List frameRequired = []; + + /// + /// Codestream input data is temporarily copied here. + /// + private JxlMemoryWriter? codestreamCopy; + + private long codestreamUnconsumed; + + /// + /// Position in the codestreamCopy vector. + /// + private long codestreamPos; + + /// + /// Number of remaining bits in the codestream copy. + /// + private long codestreamBitsAhead; + + /// + /// Stage of the box parsing pipeline. + /// + private JxlBoxStage boxStage; + + /// + /// FTYP minor-version. + /// + /// 0 - jxlp must be in order + /// 1 - OOO jxlp allowed + /// + /// + private int jxlFileFormatVersion; + + /// + /// Counter of next expected jxlp box. + /// + private int nextJxlpIndex; + + /// + /// OOO jxlp payloads keyed by counter. Keys are: codestream bytes without + /// 4byte header, and is_last. + /// + private Dictionary jxlpOooBuffer = []; + + private long jxlpOooBufferTotal; + + private int bufferingJxlpIndex; + + private bool bufferingJxlpIsLast; + + /// + /// Decompresses box contents. + /// + private JxlBoxContentDecoder? boxContentDecoder; + + /// + /// Decodes JPEG XL to JPEG. + /// + private JxlToJpegDecoder? jpegDecoder; + + private JxlBoxContentDecoder? metadataDecoder; + + /// + /// Raw bytes for EXIF metadata. + /// + private IMemoryOwner? exifMetadata; + + /// + /// Raw bytes for XMP metadata. + /// + private IMemoryOwner? xmpMetadata; + + /// + /// State of EXIF storage. 0 - not stored, + /// 1 - currently stored, 2 - finished. + /// + private int storeExif; + + /// + /// State of XMP storage. 0 - not stored, + /// 1 - currently stored, 2 - finished. + /// + private int storeXmp; + + /// + /// Position in the output buffer for JPEG + /// reconstruction. + /// + private long reconstructionOutputBufferPos; + + /// + /// EXIF size for JPEG reconstruction. + /// + private long reconstructionExifSize; + + /// + /// XMP size for JPEG reconstruction. + /// + private long reconstructionXmpSize; + + /// + /// Stage of reconstruction pipeline. + /// + private JpegReconstructionStage reconstructionOutputJpeg; + + /// + /// Next input data. + /// + private IMemoryOwner? nextInput; + + private long availableInput; + + private bool inputClosed; + + /// + /// Output image buffer. + /// + private Stream? imageOutBuffer; + + /// + /// Callback to initialize image output. + /// + private JxlImageOutputInitializerCallback? imageOutputInitCallback; + + /// + /// Callback to run image output. + /// + private JxlImageOutputRunCallback? imageOutputRunCallback; + + /// + /// Callback to dispose image output. + /// + private JxlImageOutputDestroyCallback? imageOutputDestroyCallback; + + /// + /// Bit depth for image output. + /// + private JxlBitDepth imageOutputBitDepth = new(); + + public JxlDecoderCore(DecoderOptions options) + : base(options) + => this.Reset(); + + public long SizeHintBasicInfo => this.gotBasicInfo ? 0 : this.basicInfoSizeHint; + + /// + /// Gets or sets a value indicating whether orientation should be kept. + /// + public bool KeepOrientation + { + get => this.keepOrientation; + set + { + this.BeforeUpdateState(nameof(this.KeepOrientation)); + this.keepOrientation = value; + } + } + + /// + /// Gets or sets a value indicating whether to unpremultiply RGB values + /// by the alpha channel. + /// + public bool UnpremultiplyAlpha + { + get => this.unpremultiplyAlpha; + set + { + this.BeforeUpdateState(nameof(this.UnpremultiplyAlpha)); + this.unpremultiplyAlpha = value; + } + } + + /// + /// Gets or sets a value indicating whether spotcolors (special inks used in printing) + /// are rendered in the output. + /// + public bool RenderSpotcolors + { + get => this.renderSpotcolors; + set + { + this.BeforeUpdateState(nameof(this.RenderSpotcolors)); + this.renderSpotcolors = value; + } + } + + /// + /// Gets or sets a value indicating whether multiple frames (especially zero-duration frames) + /// have to be merged into a single image. + /// + public bool Coalescing + { + get => this.coalescing; + set + { + this.BeforeUpdateState(nameof(this.Coalescing)); + this.coalescing = value; + } + } + + /// + /// Gets the dimensions of the current image buffer. + /// + public Size CurrentDimensions + { + get + { + int width; + int height; + + if (this.frameHeader?.IsPreviewFrame == true) + { + width = this.metadata!.GetOrientedPreviewXSize(this.keepOrientation); + height = this.metadata!.GetOrientedPreviewYSize(this.keepOrientation); + } + else + { + width = this.metadata!.GetOrientedXSize(this.keepOrientation); + height = this.metadata!.GetOrientedYSize(this.keepOrientation); + + if (!this.coalescing) + { + JxlFrameDimensions dim = this.frameHeader!.FrameDimensions; + + width = dim.XSizeUpsampled; + height = dim.YSizeUpsampled; + + if (!this.keepOrientation && this.metadata.ImageMetadata!.Orientation > 4) + { + RuntimeUtility.Swap(ref width, ref height); + } + } + } + + return new Size(width, height); + } + } + + /// + /// Stage of the decoder pipeline. + /// + private enum JxlDecoderStage : byte + { + /// + /// Initialized but hasn't decoded yet. + /// + Initialized, + + /// + /// Decoding right now. + /// + Started, + + /// + /// Code stream done, but other boxes could still occur. + /// + CodeStreamFinished, + + /// + /// Decoding failed and the decoder is no longer usable. + /// + Error + } + + /// + /// Identifies the signature of the JPEG XL file. + /// + private enum JxlSignature : byte + { + /// + /// Error status indicating not enough bytes to detect the signature. + /// + NotEnoughBytes, + + /// + /// A JPEG XL code stream. + /// + CodeStream, + + /// + /// The signature is invalid. + /// + Invalid, + + /// + /// Container format. + /// + Container + } + + /// + /// Represents a data type. + /// + private enum JxlDataType : byte + { + /// + /// + /// + UInt8, + + /// + /// + /// + UInt16, + + /// + /// + /// + Float, + + /// + /// + /// + Float16 + } + + /// + /// Frame stage for this decoder. + /// + private enum JxlFrameStage : byte + { + /// + /// Frame header should be parsed. + /// + Header, + + /// + /// TOC should be parsed. + /// + Toc, + + /// + /// Full pixels should be parsed. + /// + Full + } + + /// + /// Stage of the box parsing pipeline. + /// + private enum JxlBoxStage : byte + { + /// + /// Box header of the next box. + /// + Header, + + /// + /// File type box. + /// + Ftyp, + + /// + /// Box with skipped contents. + /// + Skip, + + /// + /// Code stream boxes. + /// + CodeStream, + + /// + /// Extra header of partial code stream box. + /// + PartialCodeStream, + + /// + /// Out-of-order jxlp box payload. + /// + BufferingJxlp, + + /// + /// Jpeg reconstruction box. + /// + JpegReconstruction + } + + /// + /// Reconstruction stage for JPEG images. + /// + private enum JpegReconstructionStage : byte + { + /// + /// Don't output anything. + /// + None, + + /// + /// Set metadata to the JPEG data. + /// + SetMetadata, + + /// + /// Outputting the JPEG bytes. + /// + Output + } + + /// + /// A single frame index box entry. See . + /// + private struct JxlDecoderFrameIndexBoxEntry + { + /// + /// Offset of start byte of this frame compared to start + /// byte of previous frame. + /// + public long Offset; + + /// + /// Duration in ticks between the start of this frame and the start of the next frame. + /// + public int DurationInTicks; + + /// + /// Amount of frames. + /// + public int AmountOfFrames; + } + + // This is a class not a struct. This is so we can + // assign its values from an array access. Like this: + // this.frameReferences[(int)internalIndex].Reference = ... + // where this.frameReferences = JxlFrameReference[]. + private sealed class JxlFrameReference(int reference, int savedAs) + { + public int Reference = reference; + public int SavedAs = savedAs; + } + + /// + /// A frame index box. + /// + private sealed class JxlDecoderFrameIndexBox + { + /// + /// Gets or sets all entries within this frame index box. + /// + public List Entries { get; set; } = []; + + /// + /// Gets the number of entries. + /// + public int Count => this.Entries.Count; + + /// + /// Gets or sets the numerator. (Default: 1) + /// + public int Numerator { get; set; } = 1; + + /// + /// Gets or sets the denominator. (Default: 1000) + /// + public int Denominator { get; set; } = 1000; + + /// + /// Adds a new frame. + /// + /// Offset to first byte. + /// Duration in ticks. + /// Amount of frames. + public void AddFrame(long offset, int ticks, int frames) => this.Entries.Add(new JxlDecoderFrameIndexBoxEntry() + { + Offset = offset, + AmountOfFrames = frames, + DurationInTicks = ticks + }); + } + + private sealed record JxlExtraChannelOutput(JxlPixelFormat Format, object? Buffer, long BufferSize); + + private sealed record JxlOooEntry(byte[] CodestreamBytes, bool IsLast); + + public void Dispose() + { + // RewindDecodingState resets everything, + // including disposal of streams. + this.RewindDecodingState(); + + // Streams like input and output streams may + // be unmanaged. + GC.SuppressFinalize(this); + } + + /// + /// Ensures that the coordinates are not out of bounds. + /// + /// First coordinate + /// Second coordinate + /// Image width + /// Boolean indicating whether the coordinates are out of bounds + private static bool IsOutOfBounds(int a, int b, int size) + { + long position = a + b; + + return position > size || position < a; + } + + private static int InitialBasicInfoSizeHint() + { + const int containerHeaderSize = 48; + const int maxCodestreamBasicInfoSize = 50; + return containerHeaderSize + maxCodestreamBasicInfoSize; + } + + private static JxlSignature DetectSignature(ReadOnlySpan buffer, int length, ref int position) + { + if (position >= length) + { + return JxlSignature.NotEnoughBytes; + } + + buffer = buffer[position..]; + length -= position; + + // 0xFF 0x0A represents a codestream + if (length >= 1 && buffer[0] == 0xFF) + { + if (length < 2) + { + // We need at least two bytes for a valid codestream signature + return JxlSignature.NotEnoughBytes; + } + else if (buffer[1] == CodestreamMarker) + { + position += 2; + return JxlSignature.CodeStream; + } + else + { + return JxlSignature.Invalid; + } + } + + // Container? + if (length >= 1 && buffer[0] == 0) + { + if (length < SignatureBox.Length) + { + return JxlSignature.NotEnoughBytes; + } + else if (buffer[SignatureBox.Length..].SequenceEqual(SignatureBox)) + { + position += SignatureBox.Length; + return JxlSignature.Container; + } + else + { + return JxlSignature.Invalid; + } + } + + // Signature is invalid + return JxlSignature.Invalid; + } + + private static JxlSignature DetectSignature(ReadOnlySpan buffer, int length) + { + int position = 0; + return DetectSignature(buffer, length, ref position); + } + + private static int BitsPerChannel(JxlDataType dataType) + => dataType switch + { + JxlDataType.UInt8 => 8, + JxlDataType.UInt16 or JxlDataType.Float16 => 16, + JxlDataType.Float => 32, + _ => 0 + }; + + private static uint GetBitDepth(JxlBitDepth bitDepth, JxlImageMetadata metadata, JxlPixelFormat pixelFormat) + { + if (bitDepth.Type == JxlBitDepthType.FromPixelFormat) + { + return BitsPerChannel(pixelFormat.DataType); + } + else if (bitDepth.Type == JxlBitDepthType.FromCodeStream) + { + return metadata.BitDepth!.BitsPerSample; + } + else if (bitDepth.Type == JxlBitDepthType.Custom) + { + return bitDepth.BitsPerSample; + } + + return 0; + } + + private List GetFrameDependencies(int index, Span references) + { + DebugGuard.MustBeLessThan(index, references.Length, nameof(index)); + + const int storageNum = 8; + + List result = []; + int invalid = references.Length; + List[] storage = new List[storageNum]; + + for (int s = 0; s < storageNum; s++) + { + storage[s] = new List(references.Length); + int mask = 1 << s; + int id = invalid; + + for (int i = 0; i < references.Length; i++) + { + if ((references[i].SavedAs & mask) != 0) + { + id = i; + } + + storage[s][i] = id; + } + } + + Span seen = stackalloc byte[index + 1]; + seen.Clear(); // All values are explicitly cleared in reference source + + Stack stack = []; + stack.Push(index); + seen[index] = 1; + + for (int s = 0; s < storageNum; s++) + { + int frameRef = storage[s][index]; + + if (frameRef == invalid) + { + continue; + } + + if (seen[frameRef] != 0) + { + continue; + } + + stack.Push(frameRef); + seen[frameRef] = 1; + result.Add(frameRef); + } + + while (stack.Count > 0) + { + int frameIndex = stack.Pop(); + if (frameIndex == 0) + { + continue; + } + + for (int s = 0; s < storageNum; s++) + { + int mask = 1 << s; + + if ((references[frameIndex].Reference & mask) == 0) + { + continue; + } + + int frameRef = storage[s][frameIndex - 1]; + if (frameRef == invalid) + { + continue; + } + + if (seen[frameRef] != 0) + { + continue; + } + + stack.Push(frameRef); + seen[frameRef] = 1; + result.Add(frameRef); + } + } + + return result; + } + + /// + /// Checks if the buffer of specified length can be added. + /// + /// Length of the desired buffer. + /// + /// True if buffers with such lengths can be added; false if they + /// exceed the size limit. + /// + public bool CanAddBuffer(long length) + { + const long bufferLimit = 1 << 48; + return length < bufferLimit && + (length + this.jxlpOooBufferTotal + (this.codestreamCopy?.Memory.Length ?? 0)) < bufferLimit; + } + + public bool TryInjectNextBufferedJxlpBox() + { + if (!this.jxlpOooBuffer.TryGetValue(this.nextJxlpIndex, out JxlOooEntry? value)) + { + return false; + } + + if (value == this.jxlpOooBuffer.Last().Value) + { + return true; + } + + value.Deconstruct(out byte[] data, out bool isLast); + int length = data.Length; + + this.codestreamCopy!.Write(data); + + if (isLast) + { + this.lastCodestreamSeen = true; + } + + _ = this.jxlpOooBuffer.Remove(this.nextJxlpIndex++); + + this.jxlpOooBufferTotal -= length; + + return true; + } + + /// + /// Returns true if the jbrd box needs exif or xmp. + /// + /// JBRD needs more boxes - true, otherwise false. + public bool JbrdNeedsMoreBoxes() => + (this.storeExif < 2 && this.reconstructionExifSize > 0) + || (this.storeXmp < 2 && this.reconstructionXmpSize > 0); + + /// + /// Moves the input data forward by size bytes. + /// + /// Number of bytes to advance. + /// Thrown if advancing out of bounds. + public void AdvanceInput(long size) + { + if (this.availableInput < size) + { + throw new InvalidOperationException("Attempting to advance out of bounds"); + } + + this.nextInput += size; + this.filePosition += size; + this.availableInput -= size; + } + + /// + /// Returns number of available bytes in the code stream. + /// + /// + /// Number of available code stream bytes. + /// + public long AvailableCodeStream() + { + long avail = this.availableInput; + + if (!this.boxContentsUnbounded) + { + avail = Math.Min(avail, this.boxContentsEnd - this.filePosition); + } + + return avail; + } + + /// + /// Ensures that the copy of the code stream is present. + /// + /// + /// Thrown if the copy is missing or null. + /// + private void EnsureCodeStreamCopy() + { + if (this.codestreamCopy is null) + { + throw new InvalidOperationException("Copy of the code stream is missing"); + } + } + + /// + /// Moves forward by 'size' bytes in the code stream. + /// + /// Number of bytes to advance. + public void AdvanceCodeStream(long size) + { + this.EnsureCodeStreamCopy(); + long avail = this.AvailableCodeStream(); + + if (this.codestreamCopy!.Length == 0) + { + if (size <= avail) + { + // We have >= size bytes available, so + // advancing won't be out of bounds. + this.AdvanceInput(size); + } + else + { + // We have a limited amount of bytes for the + // code stream, and advancing by size would be + // out of bounds. So limit the value. + this.codestreamPos = size - avail; + this.AdvanceInput(avail); + } + } + else + { + this.codestreamPos += size; + if (this.codestreamPos + this.codestreamUnconsumed >= this.codestreamCopy.Length) + { + long advance = Math.Min( + this.codestreamUnconsumed, + this.codestreamUnconsumed + this.codestreamPos - this.codestreamCopy.Length); + + this.AdvanceInput(advance); + + this.codestreamPos -= Math.Min(this.codestreamPos, this.codestreamCopy.Length); + this.codestreamUnconsumed = 0; + + // Now we want to clear the code stream copy... + this.codestreamCopy.Dispose(); + this.codestreamCopy = new(this.Options.Configuration.MemoryAllocator); + } + } + } + + /// + /// Attempts to expand the buffer. + /// + /// Status of requesting more input. + public bool TryRequestMoreInput() + { + this.EnsureCodeStreamCopy(); + + if (this.codestreamCopy!.Length > 0) + { + long avail = this.AvailableCodeStream(); + + if (!this.CanAddBuffer(avail)) + { + return false; + } + + this.codestreamCopy.Write(this.nextInput!.Memory.Span[..(int)avail]); + + this.AdvanceInput(avail); + } + else + { + this.AdvanceInput(this.codestreamUnconsumed); + this.codestreamUnconsumed = 0; + } + + return true; + } + + public Memory? TryGetCodestreamInput() + { + if (this.codestreamCopy is null) + { + return null; + } + + if (this.codestreamCopy.Length == 0 && this.codestreamPos > 0) + { + long avail = this.AvailableCodeStream(); + long skip = Math.Min(this.codestreamPos, avail); + this.AdvanceInput(skip); + this.codestreamPos -= skip; + + if (this.codestreamPos > 0) + { + _ = this.TryRequestMoreInput(); + return null; + } + } + + if (this.codestreamPos > this.codestreamCopy.Length) + { + throw new InvalidOperationException("Codestream position > length of codestream copy"); + } + + if (this.codestreamUnconsumed > this.codestreamCopy.Length) + { + throw new InvalidOperationException("Codestream unconsumed > length of codestream copy"); + } + + long availCodestream = this.AvailableCodeStream(); + + if (this.codestreamCopy.Length == 0) + { + if (availCodestream == 0) + { + _ = this.TryRequestMoreInput(); + return null; + } + + return this.nextInput!.Memory[..(int)availCodestream]; + } + else + { + if (!this.CanAddBuffer(availCodestream)) + { + return null; + } + + this.codestreamCopy.Write(this.nextInput!.Memory.Span.Slice((int)this.codestreamUnconsumed, (int)(availCodestream - this.codestreamUnconsumed))); + + this.codestreamUnconsumed = availCodestream; + + return this.codestreamCopy.AsMemory(); + } + } + + /// + /// Returns true if the decoder can continue using code stream input. + /// + /// True if the decoder can use code stream input. Otherwise false. + public bool CanUseMoreCodestreamInput() => this.decoderStage != JxlDecoderStage.CodeStreamFinished; + + /// + /// Checks if width * height can be represented safely as a + /// positive integer after rounding the width up to the next + /// multiple of 32. + /// + /// Input width. + /// Input height. + /// + /// Boolean indicating whether the padded image dimensions fit + /// within a signed 32-bit integer when calculating the total + /// pixel count. + /// + /// + /// Negative values aren't rejected, but will produce incorrect + /// results. This method is meant to be used with positive values only. + /// + public static bool CheckSizeLimit(int width, int height) + { + if (width == 0 || height == 0) + { + return true; + } + + int paddedWidth = JxlMath.DivCeil(width, 32) * 32; + + if (paddedWidth < width) + { + // Overflow + return false; + } + + int pixelCount = paddedWidth * height; + + if (pixelCount / paddedWidth != height) + { + // Overflow + return false; + } + + return true; + } + + /// + /// Resets the decoder state to its default values, and, + /// additionally, releases memory used by buffers and replaces + /// them with new fresh copies. + /// + public void RewindDecodingState() + { + this.decoderStage = JxlDecoderStage.Initialized; + + this.gotSignature = false; + this.lastCodestreamSeen = false; + this.gotCodestreamSignature = false; + this.gotBasicInfo = false; + this.gotTransformData = false; + this.gotAllHeaders = false; + this.postHeaders = false; + + this.iccProfile = null; + + this.gotPreviewImage = false; + this.previewFrame = false; + this.filePosition = 0; + + this.boxContentsBegin = 0; + this.boxContentsEnd = 0; + this.boxContentsSize = 0; + this.boxSize = 0; + this.headerSize = 0; + this.boxContentsUnbounded = false; + + this.boxType = null; + this.boxDecodedType = null; + + this.boxEvent = false; + this.boxStage = JxlBoxStage.Header; + + this.jxlFileFormatVersion = 0; + this.nextJxlpIndex = 0; + this.jxlpOooBuffer.Clear(); + this.jxlpOooBufferTotal = 0; + this.bufferingJxlpIndex = 0; + this.bufferingJxlpIsLast = false; + + this.boxOutBufferSet = false; + this.boxOutBufferSetCurrentBox = false; + this.boxOutputBuffer?.Dispose(); + this.boxOutputBuffer = null; + this.boxOutBufferSize = 0; + this.boxOutBufferBegin = 0; + this.boxOutBufferPos = 0; + + this.exifMetadata?.Dispose(); + this.exifMetadata = null; + this.xmpMetadata?.Dispose(); + this.xmpMetadata = null; + this.storeExif = 0; + this.storeXmp = 0; + + this.reconstructionOutputBufferPos = 0; + this.reconstructionExifSize = 0; + this.reconstructionXmpSize = 0; + this.reconstructionOutputJpeg = JpegReconstructionStage.None; + + this.eventsWanted = this.originalEventsWanted; + this.basicInfoSizeHint = InitialBasicInfoSizeHint(); + this.haveContainer = false; + this.boxCount = 0; + this.downsamplingTarget = 8; + + this.imageOutBufferSet = false; + this.imageOutBuffer?.Dispose(); + this.imageOutBuffer = null; + this.imageOutputInitCallback = null; + this.imageOutputRunCallback = null; + this.imageOutputDestroyCallback = null; + this.imageOutputSize = 0; + + this.imageOutputBitDepth = new() + { + Type = JxlBitDepthType.FromPixelFormat + }; + + this.extraChannelOutputs.Clear(); + + this.nextInput?.Dispose(); + this.nextInput = null; + + this.availableInput = 0; + this.inputClosed = false; + + this.passesState?.Reset(); + this.frameDecoder?.Reset(); + this.nextSection = 0; + this.sectionProcessed.Clear(); + + this.imageBundle.Reset(); + this.metadata = new JxlCodecMetadata(); + this.imageMetadata = this.metadata.ImageMetadata; + + this.frameHeader = new() + { + Metadata = this.metadata + }; + + this.codestreamCopy?.Dispose(); + this.codestreamCopy = new(this.Options.Configuration.MemoryAllocator); + this.codestreamUnconsumed = 0; + this.codestreamPos = 0; + this.codestreamBitsAhead = 0; + + this.frameStage = JxlFrameStage.Header; + this.remainingFrameSize = 0; + this.isLastOfStill = false; + this.isLastTotal = false; + this.skipFrames = 0; + this.skippingFrame = false; + this.internalFrames = 0; + this.externalFrames = 0; + } + + /// + /// Resets the decoder to its default values. + /// + public void Reset() + { + this.RewindDecodingState(); + + this.keepOrientation = false; + this.unpremultiplyAlpha = false; + this.renderSpotcolors = true; + this.coalescing = true; + this.desiredIntensityTarget = 0f; + this.originalEventsWanted = 0; + this.eventsWanted = 0; + + this.frameReferences.Clear(); + this.frameExternalToInternal.Clear(); + this.frameRequired.Clear(); + + this.decompressBoxes = false; + } + /// - /// Identifies the signature of the JPEG XL file. + /// Returns the code stream as a Span. /// - private enum JxlSignature : byte + /// A Span representing the code stream. + /// Thrown if the code stream cannot be retrieved. + private Span GetCodeStreamSpan() { - /// - /// Error status indicating not enough bytes to detect the signature. - /// - NotEnoughBytes, + Memory codestreamInput = this.TryGetCodestreamInput() + ?? throw new InvalidOperationException("Cannot retrieve codestream input"); - /// - /// A JPEG XL code stream. - /// - CodeStream, + Span span = codestreamInput.Span; - /// - /// The signature is invalid. - /// - Invalid, + return span; + } + + /// + /// Skips frames without decoding them. + /// + /// Number of frames to skip. + public void SkipFrames(int amount) + { + this.skipFrames += amount; + this.frameRequired.Clear(); + + int nextFrame = this.externalFrames + this.skipFrames; + + if (nextFrame < this.frameExternalToInternal.Count) + { + int internalIndex = this.frameExternalToInternal[nextFrame]; + if (internalIndex < this.frameReferences.Count) + { + List deps = this.GetFrameDependencies(internalIndex, CollectionsMarshal.AsSpan(this.frameReferences)); + this.ResizeFrameRequired(internalIndex + 1); + + foreach (int index in deps) + { + if (index < this.frameRequired.Count) + { + this.frameRequired[index] = 1; + } + } + } + } + } + + /// + /// Ensures that frameRequired's count reaches . + /// + /// Max. number of items that frameRequired must have. + private void ResizeFrameRequired(int upperBound) + { + while (this.frameRequired.Count < upperBound) + { + this.frameRequired.Add(0); + } + } + + /// + /// Skips the current frame without having to decode it. + /// + /// + /// Thrown if the frame cannot be skipped. + /// + public void SkipCurrentFrame() + { + if (this.frameStage == JxlFrameStage.Full) + { + throw new InvalidOperationException("The decoder is ready to parse the frame, so the frame cannot be skipped"); + } + + this.frameStage = JxlFrameStage.Header; + this.AdvanceCodeStream(this.remainingFrameSize); + + if (this.isLastOfStill) + { + this.imageOutBufferSet = false; + } + } + + /// + /// Ensures that the decoder state doesn't change while + /// it's busy decoding an image. + /// + /// Name of the parameter that was changed/ + /// Thrown if the state doesn't match initialization. + private void BeforeUpdateState(string propertyName) + { + if (this.decoderStage != JxlDecoderStage.Initialized) + { + throw new InvalidOperationException("The decoder is already processing the image, so " + propertyName + " cannot be changed"); + } + } + + /// + /// Reads a single bundle into . + /// + /// Type of the bundle to read. + /// Bundle binary data. + /// Bit reader to continue from. + /// The bundle to parse. + /// Status of parsing the bundle. + private bool ReadBundle(Span data, JxlBitReader br, T bundle) + where T : IJxlFields + { + JxlBitReader reader = new(data); + reader.SkipBits64((ulong)br.TotalBitsConsumed); + + bool canRead = JxlBundle.CanRead(reader, bundle); + + if (!canRead) + { + return this.TryRequestMoreInput(); + } + + if (!JxlBundle.Read(reader, bundle)) + { + return false; + } + + return true; + } + + /// + /// Reads all basic metadata and headers. + /// + /// Status of the parsing. + /// Thrown if the data is incorrect. + /// Thrown if the data is malformed. + public bool ReadBasicInfo() + { + if (!this.gotCodestreamSignature) + { + Span span = this.GetCodeStreamSpan(); + + if (span.Length < 2) + { + return this.TryRequestMoreInput(); + } + + if (span[0] != 0xFF || span[1] != CodestreamMarker) + { + throw new InvalidOperationException("The file signature is invalid"); + } + + this.gotCodestreamSignature = true; + this.AdvanceCodeStream(2); + } + + Span sp = this.GetCodeStreamSpan(); + + JxlBitReader bitReader = new(sp); + + if (!this.ReadBundle(sp, bitReader, this.metadata!.Size!)) + { + throw new InvalidDataException("Could not parse the size header"); + } + + if (!this.ReadBundle(sp, bitReader, this.metadata!.ImageMetadata!)) + { + throw new InvalidDataException("Could not parse the image metadata"); + } + + long totalBits = bitReader.TotalBitsConsumed; + + this.AdvanceCodeStream(totalBits / JxlMath.BitsPerByte); + + this.codestreamBitsAhead = totalBits % JxlMath.BitsPerByte; + this.gotBasicInfo = true; + this.basicInfoSizeHint = 0; + this.imageMetadata = this.metadata.ImageMetadata; + + if (!CheckSizeLimit(this.metadata.Size!.XSize, this.metadata.Size.YSize)) + { + throw new InvalidOperationException("The image is too large"); + } + + return true; + } + + /// + /// Parses all necessary headers. + /// + /// Status of the parsing. + /// Thrown if data is incorrect. + public bool ReadAllHeaders() + { + if (!this.gotTransformData) + { + Span span = this.GetCodeStreamSpan(); + + JxlBitReader reader = new(span); + reader.SkipBits64((ulong)this.codestreamBitsAhead); + + this.metadata!.CustomTransformData!.NonserializedXybEncoded = this.metadata.ImageMetadata!.XybEncoded; + + if (!this.ReadBundle(span, reader, this.metadata.CustomTransformData)) + { + throw new InvalidOperationException("Cannot read custom transform data bundle"); + } + + long totalBits = reader.TotalBitsConsumed; + this.AdvanceCodeStream(totalBits / JxlMath.BitsPerByte); + this.codestreamBitsAhead = totalBits % JxlMath.BitsPerByte; + this.gotTransformData = true; + } + + Span sp = this.GetCodeStreamSpan(); + + JxlBitReader bitReader = new(sp); + bitReader.SkipBits64((ulong)this.codestreamBitsAhead); + + if (this.metadata!.ImageMetadata!.ColorEncoding!.NeedsIcc) + { + // TODO: optimize this? ImageSharp ICC doesn't support spans + // so we need to allocate an array. + IccDataReader reader = new(sp.ToArray()); + IccProfileHeader header = IccReader.ReadHeader(reader); + IccTagDataEntry[] tagData = IccReader.ReadTagData(reader); + + IccProfile icc = new(header, tagData); + this.iccProfile = icc; + sp = sp[reader.Index..]; + + byte[] iccRawData = icc.ToByteArray(); + this.metadata.ImageMetadata.ColorEncoding.SetIccRaw(iccRawData); + } + + this.gotAllHeaders = true; + bitReader.JumpToByteBoundary(); + + this.AdvanceCodeStream(bitReader.TotalBitsConsumed / JxlMath.BitsPerByte); + this.codestreamBitsAhead = 0; + + this.passesState ??= new(this.frameHeader!, this.Options.Configuration); + this.passesState.OutputEncodingInfo.SetFromMetadata(this.metadata); + + if (this.desiredIntensityTarget > 0f) + { + this.passesState.OutputEncodingInfo.DesiredIntensityTarget = this.desiredIntensityTarget; + } + + this.imageMetadata = this.metadata.ImageMetadata; + + return true; + } + + /// + /// Processes all sections in this JPEG XL images and invokes + /// the frame decoder. + /// + /// Thrown if the data is invalid or malformed. + public void ProcessSections() + { + Span span = this.GetCodeStreamSpan(); + + var toc = this.frameDecoder!.Toc; + + long pos = 0; + List sectionInfo = []; + List sectionStatus = []; + + for (long i = this.nextSection; i < toc.Size; i++) + { + if (this.sectionProcessed[(int)i] != 0) + { + pos += toc[i].Size; + continue; + } + + long id = toc[i].Id; + long size = toc[i].Size; + + if (IsOutOfBounds((int)pos, (int)size, span.Length)) + { + break; + } + + JxlBitReader br = new(span.Slice((int)pos, (int)size)); + sectionInfo.Add(new(br, id, i)); + sectionStatus.Add(default); + pos += size; + } + + this.frameDecoder.ProcessSections(sectionInfo, sectionStatus); + + bool outOfBounds = false; + + foreach (JxlFrameDecoder.SectionInfo info in sectionInfo) + { + if (!info.BitReader.AllReadsWithinBounds) + { + outOfBounds = true; + break; + } + } + + if (outOfBounds) + { + throw new InvalidOperationException("Frame out of bounds"); + } + + for (int i = 0; i < sectionStatus.Count; i++) + { + JxlFrameDecoder.SectionStatus ss = sectionStatus[i]; + + if (ss == JxlFrameDecoder.Done) + { + this.sectionProcessed[sectionInfo[i].Index] = 1; + } + else if (ss != JxlFrameDecoder.Skipped) + { + throw new InvalidOperationException("Unexpected section status"); + } + } + + long completedPrefixBytes = 0; + + while (this.nextSection < this.sectionProcessed.Count && this.sectionProcessed[(int)this.nextSection] == 1) + { + completedPrefixBytes += toc[(int)this.nextSection].Size; + this.nextSection++; + } + + this.remainingFrameSize -= completedPrefixBytes; + this.AdvanceCodeStream(completedPrefixBytes); + } + + /// + /// Processes all codestream contents. + /// + /// Status of codestream processing. + /// Thrown when data is corrupt, malformed, or incorrect. + public int ProcessCodestream() + { + if (!this.gotBasicInfo) + { + bool status = this.ReadBasicInfo(); + + if (!status) + { + throw new InvalidOperationException("Could not parse basic info"); + } + } + + if ((this.eventsWanted & BasicInfo) != 0) + { + this.eventsWanted &= ~BasicInfo; + return JxlCodestreamType.BasicInfo; + } + + if (this.eventsWanted == 0) + { + this.decoderStage = JxlDecoderStage.CodeStreamFinished; + return JxlCodestreamType.Success; + } + + if (!this.gotAllHeaders) + { + bool status = this.ReadAllHeaders(); + + if (!status) + { + throw new InvalidOperationException("Could not parse headers"); + } + } + + if ((this.eventsWanted & ColorEncoding) != 0) + { + this.eventsWanted &= ~ColorEncoding; + return JxlCodestreamType.ColorEncoding; + } + + if (this.eventsWanted == 0) + { + this.decoderStage = JxlDecoderStage.CodeStreamFinished; + return JxlCodestreamType.Success; + } + + this.postHeaders = true; + + if (!this.gotPreviewImage && this.metadata!.ImageMetadata!.HavePreview) + { + this.previewFrame = true; + } + + while (true) + { + bool parseFrames = (this.eventsWanted & (PreviewImage | DecodedFrame | FullImage)) != 0; + if (!parseFrames) + { + break; + } + + if (this.frameStage == JxlFrameStage.Header && this.isLastTotal) + { + break; + } + + if (this.frameStage == JxlFrameStage.Header) + { + if (this.reconstructionOutputJpeg is JpegReconstructionStage.SetMetadata or JpegReconstructionStage.Output) + { + throw new InvalidOperationException("Cannot decode frames following a JPEG reconstruction frame"); + } + + this.imageBundle ??= new(this.imageMetadata!); + + if (!this.jpegDecoder.SetImageBundleJpegData(this.imageBundle!)) + { + throw new InvalidOperationException("Cannot set JXL->JPEG decoder image bundle"); + } + + this.frameDecoder = new(this.passesState!, this.metadata!, useSlowRenderingPipeline: false); + this.frameHeader = new() + { + Metadata = this.metadata + }; + + Span span = this.GetCodeStreamSpan(); + JxlBitReader reader = new(span); + + this.frameDecoder.InitializeFrame(reader, this.imageBundle!, this.previewFrame); + + if (!reader.AllReadsWithinBounds) + { + return this.TryRequestMoreInput() ? 1 : 0; + } + + this.AdvanceCodeStream(reader.TotalBitsConsumed / JxlMath.BitsPerByte); + this.frameHeader = this.frameDecoder.GetFrameHeader(); + + JxlFrameDimensions dim = this.frameHeader.FrameDimensions; + + if (!CheckSizeLimit(dim.XSizeUpsampledPadded, dim.YSizeUpsampledPadded)) + { + throw new InvalidOperationException("Frame is too large"); + } + + int outputType = this.previewFrame ? PreviewImage : FullImage; + bool outputNeeded = (this.eventsWanted & outputType) != 0; + + if (outputNeeded) + { + this.frameDecoder.InitializeFrameOutput(); + } + + this.remainingFrameSize = this.frameDecoder.SumSectionSizes(); + + this.frameStage = JxlFrameStage.Toc; + if (this.previewFrame) + { + if ((this.eventsWanted & PreviewImage) == 0) + { + this.frameStage = JxlFrameStage.Header; + this.AdvanceCodeStream(this.remainingFrameSize); + this.gotPreviewImage = true; + this.previewFrame = false; + } + + continue; + } + + int savedAs = JxlFrameDecoder.SavedAs(this.frameHeader); + this.isLastTotal = this.frameHeader.IsLast; + this.isLastOfStill = this.isLastTotal || this.frameHeader.AnimationFrame!.Duration > 0; + this.isLastOfStill |= !this.coalescing && this.frameHeader.FrameType == JxlFrameType.RegularFrame; + + int internalFrameIndex = this.internalFrames; + int externalFrameIndex = this.externalFrames; + + if (this.isLastOfStill) + { + this.externalFrames++; + } + + this.internalFrames++; + + if (this.skipFrames > 0) + { + this.skippingFrame = true; + + if (this.isLastOfStill) + { + this.skipFrames--; + } + } + else + { + this.skippingFrame = false; + } + + if (externalFrameIndex >= this.frameExternalToInternal.Count) + { + this.frameExternalToInternal.Add(internalFrameIndex); + + if (this.frameExternalToInternal.Count != externalFrameIndex + 1) + { + throw new InvalidOperationException("Internal error"); + } + } + + if (internalFrameIndex >= this.frameReferences.Count) + { + this.frameReferences.Add(new JxlFrameReference(0xFF, savedAs)); + + if (this.frameReferences.Count != internalFrameIndex + 1) + { + throw new InvalidOperationException("Internal error"); + } + } + + if (this.skippingFrame) + { + bool referenceable = this.frameHeader.CanBeReferenced + || this.frameHeader.FrameType == JxlFrameType.DcFrame; + + if (internalFrameIndex < this.frameRequired.Count && this.frameRequired[internalFrameIndex] == 0) + { + referenceable = false; + } + + if (!referenceable) + { + this.frameStage = JxlFrameStage.Header; + this.AdvanceCodeStream(this.remainingFrameSize); + continue; + } + } + + if ((this.eventsWanted & Frame) != 0 && this.isLastOfStill) + { + if (!this.skippingFrame) + { + return Frame; + } + } + + if (this.frameStage == JxlFrameStage.Toc) + { + this.frameDecoder.SetRenderSpotcolors(this.renderSpotcolors); + this.frameDecoder.SetCoalescing(this.coalescing); + + if (!this.previewFrame && + (this.eventsWanted & FrameProgression) != 0) + { + this.frameProgressiveDetail = this.frameDecoder.SetPauseAtProgressive(this.progressiveDetail); + } + else + { + this.frameProgressiveDetail = JxlProgressiveDetail.Frames; + } + + this.dcFrameProgressionDone = false; + this.nextSection = 0; + this.sectionProcessed.Clear(); + ResizeSectionProcessed(this.frameDecoder.Toc.Size); + + if (this.previewFrame || (this.eventsWanted & FullImage) != 0) + { + this.frameStage = JxlFrameStage.Full; + } + else if (!this.isLastTotal) + { + this.frameStage = JxlFrameStage.Header; + this.AdvanceCodeStream(this.remainingFrameSize); + continue; + } + else + { + break; + } + } + + if (this.frameStage == JxlFrameStage.Full) + { + if (!this.imageOutBufferSet) + { + if (this.previewFrame) + { + return NeedPreviewOutBuffer; + } + + if ((!this.jpegDecoder.IsOutputSet || this.imageBundle!.JpegData is null) + && this.isLastOfStill + && !this.skippingFrame) + { + return NeedImageOutputBuffer; + } + } + + if (this.imageOutBufferSet) + { + Size dimensions = this.CurrentDimensions; + int bitsPerSample = GetBitDepth(this.imageOutputBitDepth, this.metadata!.ImageMetadata!, this.imageOutputFormat); + + this.frameDecoder.SetImageOutput( + new PixelCallback( + this.imageOutputInitCallback, + this.imageOutputRunCallback, + this.imageOutputDestroyCallback, + this.imageOutputInitOpaque), + this.imageOutBuffer, + this.imageOutputSize, + dimensions.Width, + dimensions.Height, + this.imageOutputFormat, + bitsPerSample, + this.unpremultiplyAlpha, + !this.keepOrientation); + + for (int i = 0; i < this.extraChannelOutputs.Count; i++) + { + JxlExtraChannelOutput extra = this.extraChannelOutputs[i]; + int ecBitsPerSample = GetBitDepth(this.imageOutputBitDepth, this.metadata.ImageMetadata!.ExtraChannels[i], extra.Format); + + this.frameDecoder.AddExtraChannelOutput( + extra.Buffer, + extra.BufferSize, + dimensions.Width, + extra.Format, + ecBitsPerSample); + } + } + + long nextNumPassesToPause = this.frameDecoder.NextNumPassesToPause; + + this.ProcessSections(); + + bool allSectionsDone = this.frameDecoder.DecodedAll; + bool gotDcOnly = !allSectionsDone && this.frameDecoder.HasDecodedDc; + + if (this.frameProgressiveDetail >= JxlProgressiveDetail.Dc && + !this.dcFrameProgressionDone && + gotDcOnly) + { + this.dcFrameProgressionDone = true; + this.downsamplingTarget = 8; + return Progression; + } + + bool newProgressionStepDone = this.frameDecoder.NumCompletePasses >= nextNumPassesToPause; + + if (!allSectionsDone && + this.frameProgressiveDetail >= JxlProgressiveDetail.LastPasses && + newProgressionStepDone) + { + this.downsamplingTarget = this.frameHeader.Passes.GetDownsamplingTargetForCompletedPasses(this.frameDecoder.NumCompletePasses); + return Progression; + } + + if (!allSectionsDone) + { + return this.TryRequestMoreInput() ? 1 : 0; + } + + if (!this.previewFrame) + { + long internalIndex = this.internalFrames - 1; + if (this.frameReferences.Count <= internalIndex) + { + throw new InvalidOperationException("Internal error"); + } + + this.frameReferences[(int)internalIndex].Reference = this.frameDecoder.References; + } + + this.frameDecoder.FinalizeFrame(); + + if (this.jpegDecoder.IsOutputSet && this.imageBundle!.JpegData is not null) + { + this.frameStage = JxlFrameStage.Header; + this.reconstructionOutputJpeg = JpegReconstructionStage.SetMetadata; + + return FullImage; + } - /// - /// Container format. - /// - Container + if (this.previewFrame || this.isLastOfStill) + { + this.imageOutBufferSet = false; + this.extraChannelOutputs.Clear(); + } + } + + this.frameStage = JxlFrameStage.Header; + this.imageBundle.Reset(); + + if (this.previewFrame) + { + this.gotPreviewImage = true; + this.previewFrame = false; + this.eventsWanted &= ~PreviewImage; + return PreviewImage; + } + else if (this.isLastOfStill && (this.eventsWanted & FullImage) != 0 && !this.skippingFrame) + { + return FullImage; + } + } + } + + this.decoderStage = JxlDecoderStage.CodeStreamFinished; + return 1; } /// - /// Represents a data type. + /// Sets the input JPEG XL data to . /// - private enum JxlDataType : byte + /// The input data to parse JPEG XL. + /// Thrown if the input data cannot be changed at this moment. + public void SetInput(IMemoryOwner data) { - /// - /// - /// - UInt8, - - /// - /// - /// - UInt16, + if (this.nextInput is not null) + { + throw new InvalidOperationException("Input is already present. Use DisposeInput first"); + } - /// - /// - /// - Float, + if (this.inputClosed) + { + throw new InvalidOperationException("Input is closed"); + } - /// - /// - /// - Float16 + this.nextInput = data; + this.availableInput = data.Memory.Length; } - public JxlDecoderCore(DecoderOptions options) - : base(options) + /// + /// Disposes the input data. + /// + /// + /// Number of available bytes left in the input data before disposal. + /// + public long DisposeInput() { + long previousAvailableBytes = this.availableInput; + + this.nextInput?.Dispose(); + this.nextInput = null; + this.availableInput = 0; + + return previousAvailableBytes; } /// - /// Ensures that the coordinates are not out of bounds. + /// Closes the input stream so it can't be read anymore. /// - /// First coordinate - /// Second coordinate - /// Image width - /// Boolean indicating whether the coordinates are out of bounds - private static bool IsOutOfBounds(int a, int b, int size) + public void CloseInput() => this.inputClosed = true; + + /// + /// Sets the output buffer for JPEG reconstruction. + /// + /// Buffer for JPEG reconstruction. + /// Thrown when the buffer can't be set. + public void SetJpegBuffer(Memory data) { - long position = a + b; + if (this.internalFrames > 1) + { + throw new InvalidOperationException("JPEG reconstruction only works for first frames"); + } - return position > size || position < a; - } + if (this.jpegDecoder.IsOutputSet) + { + throw new InvalidOperationException("Already set JPEG buffer"); + } - private static int InitialBasicInfoSizeHint() - { - const int containerHeaderSize = 48; - const int maxCodestreamBasicInfoSize = 50; - return containerHeaderSize + maxCodestreamBasicInfoSize; + this.jpegDecoder.SetOutputBuffer(data); } - private static JxlSignature DetectSignature(ReadOnlySpan buffer, int length, ref int position) + /// + /// Parses the start of a box. + /// + /// Input bytes to parse from. + /// Size of remaining input bytes. + /// Offset of input bytes. + /// File offset. + /// Type of the parsed box. + /// Output box size. + /// Output header size. + /// + /// True if the parsing went fine. False if the parsing requests + /// more input bytes. + /// + /// + /// Thrown when data is invalid. + /// + private static bool ParseBoxHeader(Span input, long size, long pos, long filePos, JxlBoxType type, out long boxSize, out long headerSize) { - if (position >= length) + boxSize = 0; + headerSize = 0; + + if (IsOutOfBounds((int)pos, 8, (int)size)) { - return JxlSignature.NotEnoughBytes; + headerSize = 8; + return false; } - buffer = buffer[position..]; - length -= position; + long boxStart = pos; + boxSize = BinaryPrimitives.ReadInt32BigEndian(input[(int)pos..]); + pos += 4; + type = (JxlBoxType)BitConverter.ToInt32(input.Slice((int)pos, 4)); + pos += 4; - // 0xFF 0x0A represents a codestream - if (length >= 1 && buffer[0] == 0xFF) + if (boxSize == 1) { - if (length < 2) + headerSize = 16; + + if (IsOutOfBounds((int)pos, 8, (int)size)) { - // We need at least two bytes for a valid codestream signature - return JxlSignature.NotEnoughBytes; + return false; } - else if (buffer[1] == CodestreamMarker) + + long boxSize64 = BinaryPrimitives.ReadInt64BigEndian(input[(int)pos..]); + pos += 8; + boxSize = boxSize64; + } + + headerSize = pos - boxStart; + + if (boxSize > 0 && boxSize < headerSize) + { + throw new InvalidOperationException("Invalid box size"); + } + + if (filePos + boxSize < filePos) + { + throw new InvalidOperationException("Box size overflow"); + } + + return true; + } + + /// + /// Processes all boxes and their contents if this is a container format. + /// + /// Status of processing. + /// Thrown when data is invalid. + public int ProcessBoxes() + { + // We have a box handling loop here. + while (true) + { + if (this.boxStage != JxlBoxStage.Header) { - position += 2; - return JxlSignature.CodeStream; + this.AdvanceInput(this.headerSize); + this.headerSize = 0; + + if ((this.eventsWanted & Box) != 0 && this.boxEvent && !this.boxOutBufferSetCurrentBox) + { + this.boxEvent = false; + } + + if ((this.eventsWanted & Box) != 0 && this.boxOutBufferSetCurrentBox) + { + Memory nextOut = this.boxOutputBuffer!.Memory[(int)this.boxOutBufferPos..]; + long availOut = this.boxOutBufferSize - this.boxOutBufferPos; + + Span bufferSpan = this.boxOutputBuffer.Memory.Span; + Span startSlice = bufferSpan[(int)this.boxOutBufferPos..]; + + int status = this.boxContentDecoder!.Process( + this.nextInput, + this.availableInput, + this.filePosition - this.boxContentsBegin, + nextOut, + ref availOut); + + long produced = startSlice.Length - availOut; + this.boxOutBufferPos += produced; + + if (status == Complete && (this.eventsWanted & Complete) == 0) + { + status = Success; + } + + if (status is not (Success or NeedMoreInput)) + { + return status; + } + } + + if (this.storeExif == 1 || this.storeXmp == 1) + { + IMemoryOwner metadata = (this.storeExif == 1 ? this.exifMetadata : this.xmpMetadata) ?? throw new InvalidOperationException("Metadata is missing, but should be present"); + + // Boxes should not contain more than 64MiB data. + const long blockSizeLimit = 64L << 20; + + // Use array version of metadata so we + // can resize the array. + byte[] md = metadata.Memory.ToArray(); + + while (true) + { + if (md.Length == 0) + { + Array.Resize(ref md, 64); + } + + Span originalNextOutput = md.AsSpan()[(int)this.reconstructionOutputBufferPos..]; + Span nextOutput = originalNextOutput; + long availableOutput = md.Length - this.reconstructionOutputBufferPos; + + int boxResult = this.metadataDecoder.Decode( + this.nextInput, + this.availableInput, + this.filePosition - this.boxContentsBegin, + ref nextOutput, + ref availableOutput); + + long produced = originalNextOutput.Length - nextOutput.Length; + this.reconstructionOutputBufferPos += produced; + + if (boxResult == NeedMoreOutput) + { + if (md.Length >= blockSizeLimit) + { + throw new InvalidOperationException("Box with EXIF or XMP metadata is too large"); + } + + Array.Resize(ref md, md.Length * 2); + } + else if (boxResult == NeedMoreInput) + { + break; + } + else if (boxResult == Complete) + { + long neededSize = this.storeExif == 1 ? this.reconstructionExifSize : this.reconstructionXmpSize; + + if (this.boxContentsUnbounded && this.reconstructionOutputBufferPos < neededSize) + { + break; + } + else + { + Array.Resize(ref md, (int)this.reconstructionOutputBufferPos); + + if (this.storeExif == 1) + { + this.storeExif = 2; + } + + if (this.storeXmp == 1) + { + this.storeXmp = 2; + } + + break; + } + } + else + { + // Error + return boxResult; + } + } + } + } + + if (this.reconstructionOutputJpeg == JpegReconstructionStage.SetMetadata && this.JbrdNeedsMoreBoxes()) + { + JxlJpegData jpegData = this.imageBundle!.JpegData.GetData(); + + if (this.reconstructionExifSize > 0) + { + int status = JxlToJpegDecoder.SetExif(this.exifMetadata!.Memory, jpegData); + if (status != Success) + { + return status; + } + } + + if (this.reconstructionXmpSize > 0) + { + int status = JxlToJpegDecoder.SetXmp(this.xmpMetadata!.Memory, jpegData); + if (status != Success) + { + return status; + } + } + + this.reconstructionOutputJpeg = JpegReconstructionStage.Output; + } + + if (this.reconstructionOutputJpeg == JpegReconstructionStage.Output && !this.JbrdNeedsMoreBoxes()) + { + int status = this.jpegDecoder!.WriteOutput(this.imageBundle!.JpegData); + if (status != Success) + { + return status; + } + + this.reconstructionOutputJpeg = JpegReconstructionStage.None; + this.imageBundle.Reset(); + + if ((this.eventsWanted & FullImage) != 0) + { + return FullImage; + } + } + + if (this.boxStage == JxlBoxStage.Header) + { + if (!this.haveContainer) + { + if (this.decoderStage == JxlDecoderStage.CodeStreamFinished) + { + return Success; + } + + this.boxStage = JxlBoxStage.CodeStream; + this.boxContentsUnbounded = true; + + continue; + } + + if (this.availableInput == 0) + { + if (this.decoderStage != JxlDecoderStage.CodeStreamFinished) + { + return NeedMoreInput; + } + + if (this.JbrdNeedsMoreBoxes()) + { + return NeedMoreInput; + } + + if (this.inputClosed) + { + return Success; + } + + if ((this.eventsWanted & Box) != 0) + { + return Success; + } + + return NeedMoreInput; + } + + bool boxedCodestreamDone = ((this.eventsWanted & Box) != 0) + && this.decoderStage == JxlDecoderStage.CodeStreamFinished + && !this.JbrdNeedsMoreBoxes() + && this.lastCodestreamSeen; + + if (boxedCodestreamDone && + this.availableInput >= 2 && + this.nextInput!.Memory.Span[0] == 0xFF && + this.nextInput.Memory.Span[1] == CodestreamMarker) + { + return Success; + } + + int status = ParseBoxHeader(this.nextInput, this.availableInput, 0, this.filePosition, this.boxType, out long boxSize, out long headerSize); + + if (this.boxType == JxlBoxTypes.Brob) + { + if (this.availableInput < headerSize + 4) + { + return NeedMoreInput; + } + + this.boxDecodedType = BitConverter.ToInt32(this.nextInput!.Memory.Span[(int)headerSize..]); + } + else + { + this.boxDecodedType = this.boxType; + } + + this.boxCount++; + + if (boxedCodestreamDone && this.boxType == JxlBoxTypes.Jxl) + { + return Success; + } + + if (this.boxCount == 2 && this.boxType != JxlBoxType.FileType) + { + throw new InvalidOperationException("The second box must be a ftyp (File Type) box"); + } + + if (this.boxType == JxlBoxTypes.FileType && this.boxCount != 2) + { + throw new InvalidOperationException("The ftyp (File Type) box must be a second box"); + } + + this.boxContentsUnbounded = boxSize == 0; + this.boxContentsBegin = this.filePosition + headerSize; + this.boxContentsEnd = this.boxContentsUnbounded ? 0 : (this.filePosition + boxSize); + this.boxContentsSize = this.boxContentsUnbounded ? 0 : (boxSize - headerSize); + this.boxSize = boxSize; + this.headerSize = headerSize; + + if ((this.originalEventsWanted & JpegReconstruction) != 0) + { + if (this.storeExif == 0 && this.boxDecodedType == JxlBoxTypes.Exif) + { + this.storeExif = 1; + this.reconstructionOutputBufferPos = 0; + } + + if (this.storeXmp == 0 && this.boxDecodedType == JxlBoxTypes.Xml) + { + this.storeXmp = 1; + this.reconstructionOutputBufferPos = 0; + } + } + + if ((this.eventsWanted & Box) != 0) + { + bool decompress = this.decompressBoxes && this.boxType == JxlBoxTypes.Brob; + this.boxContentDecoder.StartBox(decompress, this.boxContentsUnbounded, this.boxContentsSize); + } + + if (this.storeExif == 1 || this.storeXmp == 1) + { + bool brob = this.boxType == JxlBoxTypes.Brob; + this.metadataDecoder.StartBox(brob, this.boxContentsUnbounded, this.boxContentsSize); + } + + if (this.boxType == JxlBoxTypes.FileType) + { + this.boxStage = JxlBoxStage.Ftyp; + } + else if (this.boxType == JxlBoxTypes.JxlCodeStream) + { + if (this.lastCodestreamSeen) + { + throw new InvalidOperationException("Only one jxlc (JPEG XL codestream) box can be present"); + } + + this.lastCodestreamSeen = true; + this.boxStage = JxlBoxStage.CodeStream; + } + else if (this.boxType == JxlBoxTypes.JxlPartialCodeStream) + { + this.boxStage = JxlBoxStage.PartialCodeStream; + } + else if ((this.originalEventsWanted & JpegReconstruction) != 0 && this.boxType == JxlBoxTypes.JpegReconstructionData) + { + if ((this.eventsWanted & JpegReconstruction) == 0) + { + throw new InvalidOperationException("Multiple JPEG reconstruction boxes detected"); + } + + this.boxStage = JxlBoxStage.JpegReconstruction; + } + else + { + this.boxStage = JxlBoxStage.Skip; + } + + if ((this.eventsWanted & Box) != 0) + { + this.boxEvent = true; + this.boxOutBufferSetCurrentBox = false; + return Box; + } + } + else if (this.boxStage == JxlBoxStage.Ftyp) + { + if (this.boxContentsSize < 12) + { + throw new InvalidOperationException("The file type box is too small"); + } + + if (this.availableInput < 8) + { + return NeedMoreInput; + } + + Span nextSpan = this.nextInput!.Memory.Span; + if (!(nextSpan[0] == 'j' && nextSpan[1] == 'x' && nextSpan[2] == 'l' && nextSpan[3] == ' ')) + { + throw new InvalidOperationException("File type box major brand must be \"jxl \""); + } + + uint version = BinaryPrimitives.ReadUInt32BigEndian(nextSpan[4..]); + if (version > 1) + { + throw new InvalidOperationException("Unknown JXL file format version " + version + ", known versions are 0 and 1"); + } + + this.jxlFileFormatVersion = (int)version; + this.AdvanceInput(8); + this.boxStage = JxlBoxStage.Skip; + } + else if (this.boxStage == JxlBoxStage.PartialCodeStream) + { + if (this.lastCodestreamSeen) + { + throw new InvalidOperationException("Cannot have jxlp box after last jxlp box"); + } + + if (this.availableInput < 4) + { + return NeedMoreInput; + } + + if (!this.boxContentsUnbounded && this.boxContentsSize < 4) + { + throw new InvalidOperationException("jxlp box is too small to contain an index"); + } + + uint jxlpIndex = BinaryPrimitives.ReadUInt32BigEndian(this.nextInput!.Memory.Span); + uint counter = jxlpIndex & 0x7FFFFFFFu; + bool isLast = (jxlpIndex & 0x80000000u) != 0; + + if (counter < this.nextJxlpIndex) + { + throw new InvalidOperationException("jxlp box index " + counter + " is a duplicate (already processed)"); + } + + this.AdvanceInput(4); + + if (counter == this.nextJxlpIndex) + { + this.nextJxlpIndex++; + + if (isLast) + { + this.lastCodestreamSeen = true; + } + + this.boxStage = JxlBoxStage.CodeStream; + } + else if (this.jxlFileFormatVersion >= 1) + { + if (this.jxlpOooBuffer.Count >= NumBuffersLimit) + { + return Error; + } + + // When creating a new OOO (Out-of-order) entry, + // the data is initially empty. + byte[] buffer = []; + JxlOooEntry entry = new(buffer, isLast); + this.jxlpOooBuffer.Add((int)counter, entry); + + this.bufferingJxlpIndex = (int)counter; + this.bufferingJxlpIsLast = isLast; + this.boxStage = JxlBoxStage.BufferingJxlp; + } + else + { + throw new InvalidOperationException("JXLP box with index " + counter + " is out of order (index " + this.nextJxlpIndex + " was expected). Out-of-order jxlp boxes require file format version 1 in the file type (ftyp) box."); + } + } + else if (this.boxStage == JxlBoxStage.CodeStream) + { + int status = this.ProcessCodestream(); + + if (status == FullImage) + { + if (this.reconstructionOutputJpeg != JpegReconstructionStage.None) + { + continue; + } + } + + if (status == NeedMoreInput) + { + if (this.filePosition == this.boxContentsEnd && !this.boxContentsUnbounded) + { + bool hasMoreData = this.TryInjectNextBufferedJxlpBox(); + + if (hasMoreData) + { + continue; + } + + this.boxStage = JxlBoxStage.Header; + continue; + } + } + + if (status == Success) + { + if (this.JbrdNeedsMoreBoxes()) + { + this.boxStage = JxlBoxStage.Skip; + continue; + } + + if (this.boxContentsUnbounded) + { + break; + } + + if ((this.eventsWanted & Box) != 0) + { + this.boxStage = JxlBoxStage.Skip; + continue; + } + } + + return status; + } + else if (this.boxStage == JxlBoxStage.BufferingJxlp) + { + long remaining = this.boxContentsUnbounded + ? this.availableInput + : Math.Min(this.availableInput, this.boxContentsEnd - this.filePosition); + + if (!this.CanAddBuffer(remaining) || !this.jxlpOooBuffer.TryGetValue(this.bufferingJxlpIndex, out JxlOooEntry? entry)) + { + return Error; + } + + entry!.CodestreamBytes.Write(this.nextInput!.Memory.Span[..(int)remaining]); + this.jxlpOooBufferTotal += remaining; + this.AdvanceInput(remaining); + + bool boxDone = !this.boxContentsUnbounded && this.filePosition >= this.boxContentsEnd; + + if (!boxDone) + { + return NeedMoreInput; + } + + this.boxStage = JxlBoxStage.Header; + } + else if (this.boxStage == JxlBoxStage.JpegReconstruction) + { + if (!this.jpegDecoder.IsParsingBox) + { + this.jpegDecoder.StartBox(this.boxContentsUnbounded, this.boxContentsSize); + } + + Span nextInput = this.nextInput!.Memory.Span; + long availableInput = this.availableInput; + + int reconstructionResult = this.jpegDecoder.Process(ref nextInput, ref availableInput); + + long consumed = this.nextInput.Memory.Length - nextInput.Length; + this.AdvanceInput(consumed); + + if (reconstructionResult == JpegReconstruction) + { + JxlJpegData jpegData = this.jpegDecoder!.GetJpegData(); + long numExif = JxlToJpegDecoder.NumExifMarkers(jpegData); + long numXmp = JxlToJpegDecoder.NumXmpMarkers(jpegData); + + if (numExif > 0) + { + if (numExif > 1) + { + throw new InvalidOperationException("Only one EXIF marker for JPEG reconstruction can be present"); + } + + if (JxlToJpegDecoder.ExifBoxContentSize(jpegData, ref this.reconstructionExifSize) != Success) + { + throw new InvalidOperationException("Invalid jbrd EXIF size"); + } + } + + if (numXmp > 0) + { + if (numXmp > 1) + { + throw new InvalidOperationException("Only one XMP marker for JPEG reconstruction can be present"); + } + + if (JxlToJpegDecoder.XmlBoxContentSize(jpegData, ref this.reconstructionXmpSize) != Success) + { + throw new InvalidOperationException("Invalid jbrd XMP size"); + } + } + + this.boxStage = JxlBoxStage.Header; + + if ((this.eventsWanted & JpegReconstruction) != 0) + { + this.eventsWanted &= ~JpegReconstruction; + return JpegReconstruction; + } + } + else + { + return reconstructionResult; + } + } + else if (this.boxStage == JxlBoxStage.Skip) + { + if (this.boxContentsUnbounded) + { + if (this.inputClosed) + { + return Success; + } + + if (!this.boxOutBufferSet) + { + return Success; + } + + this.AdvanceInput(this.availableInput); + return NeedMoreInput; + } + + long remaining = this.boxContentsEnd - this.filePosition; + if (this.availableInput < remaining) + { + this.basicInfoSizeHint = InitialBasicInfoSizeHint() + this.boxContentsEnd - this.filePosition; + this.AdvanceInput(this.availableInput); + return NeedMoreInput; + } + else + { + this.AdvanceInput(remaining); + this.boxStage = JxlBoxStage.Header; + } } else { - return JxlSignature.Invalid; + throw new InvalidOperationException("Unreachable"); } } - // Container? - if (length >= 1 && buffer[0] == 0) + return Success; + } + + /// + /// Releases memory used by the JPEG output buffer. + /// + public void DisposeJpegBuffer() => this.jpegDecoder.DisposeOutputBuffer(); + + /// + /// Main core decoding routine. + /// + /// Thrown when data or input parameters are invalid. + public void DecodeInput() + { + if (this.decoderStage == JxlDecoderStage.Initialized) { - if (length < SignatureBox.Length) + this.decoderStage = JxlDecoderStage.Started; + } + + if (this.decoderStage == JxlDecoderStage.Error) + { + // Should NEVER occur! If it does make sure to always reset the decoder + // in the Decode method. + throw new InvalidOperationException("The core decoder cannot be used because it contains an error. A reset must be made."); + } + + if (!this.gotSignature) + { + JxlSignatureCheck status = CheckSignature(this.nextInput, this.availableInput); + if (status == JxlSignatureCheck.InvalidSignature) { - return JxlSignature.NotEnoughBytes; + throw new InvalidOperationException("The signature is invalid."); } - else if (buffer[SignatureBox.Length..].SequenceEqual(SignatureBox)) + + if (status == JxlSignatureCheck.NotEnoughBytes) { - position += SignatureBox.Length; - return JxlSignature.Container; + if (this.inputClosed) + { + throw new InvalidOperationException("The input is closed"); + } + + ThrowNotEnoughData(); + } + + this.gotSignature = true; + + if (status == JxlSignatureCheck.Container) + { + this.haveContainer = true; } else { - return JxlSignature.Invalid; + this.lastCodestreamSeen = true; } } - // Signature is invalid - return JxlSignature.Invalid; - } + int status = this.ProcessBoxes(); - private static JxlSignature DetectSignature(ReadOnlySpan buffer, int length) - { - int position = 0; - return DetectSignature(buffer, length, ref position); - } + if (status == NeedMoreInput && this.inputClosed) + { + ThrowNotEnoughData(); + } - private static int BitsPerChannel(JxlDataType dataType) - => dataType switch + if (status == Success) { - JxlDataType.UInt8 => 8, - JxlDataType.UInt16 or JxlDataType.Float16 => 16, - JxlDataType.Float => 32, - _ => 0 - }; + if (this.CanUseMoreCodestreamInput()) + { + throw new InvalidOperationException("The code stream did not finish"); + } + + if (this.JbrdNeedsMoreBoxes()) + { + throw new InvalidOperationException("Missing metadata boxes for JPEG reconstruction"); + } + } + } protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) => throw new NotImplementedException(); diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlBoxCodingMode.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlBoxCodingMode.cs new file mode 100644 index 0000000000..89f5af5e3f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlBoxCodingMode.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Specifies the type of box content. +/// +internal enum JxlBoxCodingMode : byte +{ + /// + /// Compress using Brotli codec. + /// + Brotli, + + /// + /// No compression (raw contents). + /// + Uncompressed +} diff --git a/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.cs b/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.cs index c5464c8d72..faa94dea40 100644 --- a/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.cs +++ b/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.cs @@ -3,6 +3,8 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Icc; +#pragma warning disable IDE0032 // Use auto property + /// /// Provides methods to read ICC data types /// @@ -25,6 +27,11 @@ internal sealed partial class IccDataReader public IccDataReader(byte[] data) => this.data = data ?? throw new ArgumentNullException(nameof(data)); + /// + /// Gets the reading position. + /// + public int Index => this.currentIndex; + /// /// Gets the length in bytes of the raw data /// diff --git a/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs b/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs index 084ec388d6..d46a7332ba 100644 --- a/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs +++ b/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs @@ -53,7 +53,7 @@ public static IccTagDataEntry[] ReadTagData(byte[] data) return ReadTagData(reader); } - private static IccProfileHeader ReadHeader(IccDataReader reader) + internal static IccProfileHeader ReadHeader(IccDataReader reader) { reader.SetIndex(0); @@ -79,7 +79,7 @@ private static IccProfileHeader ReadHeader(IccDataReader reader) }; } - private static IccTagDataEntry[] ReadTagData(IccDataReader reader) + internal static IccTagDataEntry[] ReadTagData(IccDataReader reader) { IccTagTableEntry[] tagTable = ReadTagTable(reader); List entries = new(tagTable.Length); From e03a581fc73652ce503b5275f1e17211ec4d1df9 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 17 Aug 2026 18:31:55 +0400 Subject: [PATCH 078/328] Add JPEG XL container prototype & file type box JxlBoxHeader contains a header for boxes in the JPEG XL container. JxlFileTypeBox represents the ftyp box. BinaryUtils contains helper methods to read/write primitives from/to Stream in custom endianness --- .../Formats/Jxl/IO/BinaryUtils.Generated.cs | 321 ++++++++++++++++++ src/ImageSharp/Formats/Jxl/IO/BinaryUtils.tt | 86 +++++ .../Formats/Jxl/IO/Container/JxlBoxHeader.cs | 132 +++++++ .../Jxl/IO/Container/JxlFileTypeBox.cs | 58 ++++ 4 files changed, 597 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/IO/BinaryUtils.Generated.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/BinaryUtils.tt create mode 100644 src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/Container/JxlFileTypeBox.cs diff --git a/src/ImageSharp/Formats/Jxl/IO/BinaryUtils.Generated.cs b/src/ImageSharp/Formats/Jxl/IO/BinaryUtils.Generated.cs new file mode 100644 index 0000000000..055538df7a --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/BinaryUtils.Generated.cs @@ -0,0 +1,321 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO; + + +/// +/// Reads primitives from streams with correct endianness. +/// +// TODO: move this class into the IO or Common folder? +internal static class BinaryUtils +{ + /// + /// Reads a + /// from the specified stream in little-endian order. + /// + /// The stream where the will be read from. + /// + public static Int16 ReadInt16LittleEndian(Stream stream) + { + Span data = stackalloc byte[sizeof(Int16)]; + stream.ReadExactly(data); + return BinaryPrimitives.ReadInt16LittleEndian(data); + } + + /// + /// Reads a + /// from the specified stream in big-endian order. + /// + /// The stream where the will be read from. + /// + public static Int16 ReadInt16BigEndian(Stream stream) + { + Span data = stackalloc byte[sizeof(Int16)]; + stream.ReadExactly(data); + return BinaryPrimitives.ReadInt16BigEndian(data); + } + + /// + /// Writes a + /// into the specified stream in little-endian order. + /// + /// The stream where the will be written to. + /// Value which will be written to the stream. + public static void WriteInt16LittleEndian(Stream stream, Int16 value) + { + Span data = stackalloc byte[sizeof(Int16)]; + BinaryPrimitives.WriteInt16LittleEndian(data, value); + stream.Write(data); + } + + /// + /// Writes a + /// into the specified stream in big-endian order. + /// + /// The stream where the will be written to. + /// Value which will be written to the stream. + public static void WriteInt16BigEndian(Stream stream, Int16 value) + { + Span data = stackalloc byte[sizeof(Int16)]; + BinaryPrimitives.WriteInt16BigEndian(data, value); + stream.Write(data); + } + /// + /// Reads a + /// from the specified stream in little-endian order. + /// + /// The stream where the will be read from. + /// + public static UInt16 ReadUInt16LittleEndian(Stream stream) + { + Span data = stackalloc byte[sizeof(UInt16)]; + stream.ReadExactly(data); + return BinaryPrimitives.ReadUInt16LittleEndian(data); + } + + /// + /// Reads a + /// from the specified stream in big-endian order. + /// + /// The stream where the will be read from. + /// + public static UInt16 ReadUInt16BigEndian(Stream stream) + { + Span data = stackalloc byte[sizeof(UInt16)]; + stream.ReadExactly(data); + return BinaryPrimitives.ReadUInt16BigEndian(data); + } + + /// + /// Writes a + /// into the specified stream in little-endian order. + /// + /// The stream where the will be written to. + /// Value which will be written to the stream. + public static void WriteUInt16LittleEndian(Stream stream, UInt16 value) + { + Span data = stackalloc byte[sizeof(UInt16)]; + BinaryPrimitives.WriteUInt16LittleEndian(data, value); + stream.Write(data); + } + + /// + /// Writes a + /// into the specified stream in big-endian order. + /// + /// The stream where the will be written to. + /// Value which will be written to the stream. + public static void WriteUInt16BigEndian(Stream stream, UInt16 value) + { + Span data = stackalloc byte[sizeof(UInt16)]; + BinaryPrimitives.WriteUInt16BigEndian(data, value); + stream.Write(data); + } + /// + /// Reads a + /// from the specified stream in little-endian order. + /// + /// The stream where the will be read from. + /// + public static Int32 ReadInt32LittleEndian(Stream stream) + { + Span data = stackalloc byte[sizeof(Int32)]; + stream.ReadExactly(data); + return BinaryPrimitives.ReadInt32LittleEndian(data); + } + + /// + /// Reads a + /// from the specified stream in big-endian order. + /// + /// The stream where the will be read from. + /// + public static Int32 ReadInt32BigEndian(Stream stream) + { + Span data = stackalloc byte[sizeof(Int32)]; + stream.ReadExactly(data); + return BinaryPrimitives.ReadInt32BigEndian(data); + } + + /// + /// Writes a + /// into the specified stream in little-endian order. + /// + /// The stream where the will be written to. + /// Value which will be written to the stream. + public static void WriteInt32LittleEndian(Stream stream, Int32 value) + { + Span data = stackalloc byte[sizeof(Int32)]; + BinaryPrimitives.WriteInt32LittleEndian(data, value); + stream.Write(data); + } + + /// + /// Writes a + /// into the specified stream in big-endian order. + /// + /// The stream where the will be written to. + /// Value which will be written to the stream. + public static void WriteInt32BigEndian(Stream stream, Int32 value) + { + Span data = stackalloc byte[sizeof(Int32)]; + BinaryPrimitives.WriteInt32BigEndian(data, value); + stream.Write(data); + } + /// + /// Reads a + /// from the specified stream in little-endian order. + /// + /// The stream where the will be read from. + /// + public static UInt32 ReadUInt32LittleEndian(Stream stream) + { + Span data = stackalloc byte[sizeof(UInt32)]; + stream.ReadExactly(data); + return BinaryPrimitives.ReadUInt32LittleEndian(data); + } + + /// + /// Reads a + /// from the specified stream in big-endian order. + /// + /// The stream where the will be read from. + /// + public static UInt32 ReadUInt32BigEndian(Stream stream) + { + Span data = stackalloc byte[sizeof(UInt32)]; + stream.ReadExactly(data); + return BinaryPrimitives.ReadUInt32BigEndian(data); + } + + /// + /// Writes a + /// into the specified stream in little-endian order. + /// + /// The stream where the will be written to. + /// Value which will be written to the stream. + public static void WriteUInt32LittleEndian(Stream stream, UInt32 value) + { + Span data = stackalloc byte[sizeof(UInt32)]; + BinaryPrimitives.WriteUInt32LittleEndian(data, value); + stream.Write(data); + } + + /// + /// Writes a + /// into the specified stream in big-endian order. + /// + /// The stream where the will be written to. + /// Value which will be written to the stream. + public static void WriteUInt32BigEndian(Stream stream, UInt32 value) + { + Span data = stackalloc byte[sizeof(UInt32)]; + BinaryPrimitives.WriteUInt32BigEndian(data, value); + stream.Write(data); + } + /// + /// Reads a + /// from the specified stream in little-endian order. + /// + /// The stream where the will be read from. + /// + public static Int64 ReadInt64LittleEndian(Stream stream) + { + Span data = stackalloc byte[sizeof(Int64)]; + stream.ReadExactly(data); + return BinaryPrimitives.ReadInt64LittleEndian(data); + } + + /// + /// Reads a + /// from the specified stream in big-endian order. + /// + /// The stream where the will be read from. + /// + public static Int64 ReadInt64BigEndian(Stream stream) + { + Span data = stackalloc byte[sizeof(Int64)]; + stream.ReadExactly(data); + return BinaryPrimitives.ReadInt64BigEndian(data); + } + + /// + /// Writes a + /// into the specified stream in little-endian order. + /// + /// The stream where the will be written to. + /// Value which will be written to the stream. + public static void WriteInt64LittleEndian(Stream stream, Int64 value) + { + Span data = stackalloc byte[sizeof(Int64)]; + BinaryPrimitives.WriteInt64LittleEndian(data, value); + stream.Write(data); + } + + /// + /// Writes a + /// into the specified stream in big-endian order. + /// + /// The stream where the will be written to. + /// Value which will be written to the stream. + public static void WriteInt64BigEndian(Stream stream, Int64 value) + { + Span data = stackalloc byte[sizeof(Int64)]; + BinaryPrimitives.WriteInt64BigEndian(data, value); + stream.Write(data); + } + /// + /// Reads a + /// from the specified stream in little-endian order. + /// + /// The stream where the will be read from. + /// + public static UInt64 ReadUInt64LittleEndian(Stream stream) + { + Span data = stackalloc byte[sizeof(UInt64)]; + stream.ReadExactly(data); + return BinaryPrimitives.ReadUInt64LittleEndian(data); + } + + /// + /// Reads a + /// from the specified stream in big-endian order. + /// + /// The stream where the will be read from. + /// + public static UInt64 ReadUInt64BigEndian(Stream stream) + { + Span data = stackalloc byte[sizeof(UInt64)]; + stream.ReadExactly(data); + return BinaryPrimitives.ReadUInt64BigEndian(data); + } + + /// + /// Writes a + /// into the specified stream in little-endian order. + /// + /// The stream where the will be written to. + /// Value which will be written to the stream. + public static void WriteUInt64LittleEndian(Stream stream, UInt64 value) + { + Span data = stackalloc byte[sizeof(UInt64)]; + BinaryPrimitives.WriteUInt64LittleEndian(data, value); + stream.Write(data); + } + + /// + /// Writes a + /// into the specified stream in big-endian order. + /// + /// The stream where the will be written to. + /// Value which will be written to the stream. + public static void WriteUInt64BigEndian(Stream stream, UInt64 value) + { + Span data = stackalloc byte[sizeof(UInt64)]; + BinaryPrimitives.WriteUInt64BigEndian(data, value); + stream.Write(data); + } +} diff --git a/src/ImageSharp/Formats/Jxl/IO/BinaryUtils.tt b/src/ImageSharp/Formats/Jxl/IO/BinaryUtils.tt new file mode 100644 index 0000000000..c83e9e4c4b --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/BinaryUtils.tt @@ -0,0 +1,86 @@ +<#@ template language="C#" #> +<#@ import namespace="System" #> +<#@ import namespace="System.IO" #> +<#@ import namespace="System.Collections.Generic" #> +<#@ output extension=".Generated.cs" #> +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO; + +<# + List types = [ + typeof(short), + typeof(ushort), + typeof(int), + typeof(uint), + typeof(long), + typeof(ulong) + ]; +#> + +/// +/// Reads primitives from streams with correct endianness. +/// +// TODO: move this class into the IO or Common folder? +internal static class BinaryUtils +{ +<# + foreach (Type type in types) + { +#> + /// + /// Reads a + /// from the specified stream in little-endian order. + /// + /// The stream where the will be read from. + /// + public static <#= type.Name #> Read<#= type.Name#>LittleEndian(Stream stream) + { + Span data = stackalloc byte[sizeof(<#= type.Name #>)]; + stream.ReadExactly(data); + return BinaryPrimitives.Read<#= type.Name #>LittleEndian(data); + } + + /// + /// Reads a + /// from the specified stream in big-endian order. + /// + /// The stream where the will be read from. + /// + public static <#= type.Name #> Read<#= type.Name#>BigEndian(Stream stream) + { + Span data = stackalloc byte[sizeof(<#= type.Name #>)]; + stream.ReadExactly(data); + return BinaryPrimitives.Read<#= type.Name #>BigEndian(data); + } + + /// + /// Writes a + /// into the specified stream in little-endian order. + /// + /// The stream where the will be written to. + /// Value which will be written to the stream. + public static void Write<#= type.Name #>LittleEndian(Stream stream, <#= type.Name #> value) + { + Span data = stackalloc byte[sizeof(<#= type.Name #>)]; + BinaryPrimitives.Write<#= type.Name #>LittleEndian(data, value); + stream.Write(data); + } + + /// + /// Writes a + /// into the specified stream in big-endian order. + /// + /// The stream where the will be written to. + /// Value which will be written to the stream. + public static void Write<#= type.Name #>BigEndian(Stream stream, <#= type.Name #> value) + { + Span data = stackalloc byte[sizeof(<#= type.Name #>)]; + BinaryPrimitives.Write<#= type.Name #>BigEndian(data, value); + stream.Write(data); + } +<# } #> +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs b/src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs new file mode 100644 index 0000000000..d39f3c7e75 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs @@ -0,0 +1,132 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; +using System.Runtime.InteropServices; +using System.Text; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Container; + +/// +/// Header for JPEG XL container format. +/// +[StructLayout(LayoutKind.Sequential, Size = 16)] +internal struct JxlBoxHeader +{ + /// + /// Box size in bytes. + /// + public ulong Size; + + /// + /// Type of the box. + /// + public uint Type; + + /// + /// True if the size field extends until the end of the file. + /// + public bool SizeExtendsTillEnd; + + /// + /// Initializes a new instance of the struct. + /// + /// The size of the box. + /// The type of the box. + /// Does the box size extend till the end of the file? + public JxlBoxHeader(ulong size, uint type, bool sizeExtendsTillEnd) + { + this.Size = size; + this.Type = type; + this.SizeExtendsTillEnd = sizeExtendsTillEnd; + } + + /// + /// Converts a 4-character ASCII string (e.g. "jxlc") into a uint type code. + /// + /// Input type string to convert + /// Unsigned integer representation of the type string + public static uint TypeFromString(string typeString) + { + if (typeString.Length != 4) + { + throw new ArgumentException("Box type must be exactly 4 characters", nameof(typeString)); + } + + Span buffer = stackalloc byte[4]; + _ = Encoding.ASCII.GetBytes(typeString, buffer); + + return BinaryPrimitives.ReadUInt32BigEndian(buffer); + } + + /// + /// Converts a uint type code back into a 4-character ASCII string. + /// + /// Unsigned integer representation of the type string + /// The string representing the type code. + public static string TypeToString(uint typeCode) + { + Span buffer = stackalloc byte[4]; + BinaryPrimitives.WriteUInt32BigEndian(buffer, typeCode); + + return Encoding.ASCII.GetString(buffer); + } + + /// + /// Parses the JPEG XL box header. + /// + /// A stream to parse the header from. + /// The box header. + /// Thrown when the header is invalid. + public static JxlBoxHeader ReadHeader(Stream stream) + { + ulong size = BinaryUtils.ReadUInt32BigEndian(stream); + bool haveSize64 = false; + + if (size == 1) + { + // When the size value is equal to 1, a new 64-bit + // size field follows. + haveSize64 = true; + size = BinaryUtils.ReadUInt64BigEndian(stream); + } + + // Read the 4-byte type field. + uint type = BinaryUtils.ReadUInt32BigEndian(stream); + + if (haveSize64) + { + // When the 64-bit largesize was read, + // the size cannot proceed till the end of the file. + if (size is 0 or 1) + { + throw new InvalidOperationException("Large size cannot have another large size or extend till the end of the file"); + } + + return new JxlBoxHeader(size, type, sizeExtendsTillEnd: false); + } + else + { + return new JxlBoxHeader(size, type, sizeExtendsTillEnd: size == 0); + } + } + + /// + /// Writes the box header to the specified stream. + /// + /// The stream to write the box header to. + public readonly void WriteHeader(Stream writer) + { + if (this.Size is > uint.MaxValue or 1) + { + BinaryUtils.WriteUInt32BigEndian(writer, 1); // Indicates a large size is present + BinaryUtils.WriteUInt64BigEndian(writer, this.Size); + } + else + { + BinaryUtils.WriteUInt32BigEndian(writer, (uint)this.Size); + } + + BinaryUtils.WriteUInt32BigEndian(writer, this.Type); + } +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Container/JxlFileTypeBox.cs b/src/ImageSharp/Formats/Jxl/IO/Container/JxlFileTypeBox.cs new file mode 100644 index 0000000000..ca2adb1ca4 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Container/JxlFileTypeBox.cs @@ -0,0 +1,58 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Container; + +/// +/// A ftyp box payload. +/// +internal sealed class JxlFileTypeBox(string majorBrand, uint minorVersion) +{ + /// + /// Gets or sets the primary format. Has to be "jxl " for JPEG XL. + /// + public string MajorBrand { get; set; } = majorBrand; + + /// + /// Gets or sets the revision of the major brand. + /// + public uint MinorVersion { get; set; } = minorVersion; + + /// + /// Gets or sets the list of other brands the file is compatible with. + /// + public List CompatibleBrands { get; set; } = []; + + public int GetPayloadSize() => 8 + (this.CompatibleBrands.Count * 4); + + public static JxlFileTypeBox Parse(Stream stream, ulong boxSize) + { + string majorBrand = JxlBoxHeader.TypeToString(BinaryUtils.ReadUInt32BigEndian(stream)); + uint minorVersion = BinaryUtils.ReadUInt32BigEndian(stream); + boxSize -= 8; + + List compatibleBrands = []; + for (ulong i = 0; i < boxSize; i += 4) + { + compatibleBrands.Add(JxlBoxHeader.TypeToString(BinaryUtils.ReadUInt32BigEndian(stream))); + } + + JxlFileTypeBox ftyp = new(majorBrand, minorVersion) + { + CompatibleBrands = compatibleBrands + }; + + return ftyp; + } + + public void WritePayload(Stream stream) + { + BinaryUtils.WriteUInt32BigEndian(stream, JxlBoxHeader.TypeFromString(this.MajorBrand)); + BinaryUtils.WriteUInt32BigEndian(stream, this.MinorVersion); + + foreach (string compatibleBrand in this.CompatibleBrands) + { + BinaryUtils.WriteUInt32BigEndian(stream, JxlBoxHeader.TypeFromString(compatibleBrand)); + } + } +} From 2d12bc6b4aec1f3edb55634daa39e0299e27bb72 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 18 Aug 2026 20:31:34 +0400 Subject: [PATCH 079/328] Zero-copy input access (prototype 1) --- .../Formats/Jxl/IO/Container/JxlBoxHeader.cs | 13 ++- .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 102 +++++------------- 2 files changed, 34 insertions(+), 81 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs b/src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs index d39f3c7e75..dc5eb13513 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs @@ -28,17 +28,24 @@ internal struct JxlBoxHeader /// public bool SizeExtendsTillEnd; + /// + /// True if the size is 64-bit. + /// + public bool ContainsLargeSize; + /// /// Initializes a new instance of the struct. /// /// The size of the box. /// The type of the box. /// Does the box size extend till the end of the file? - public JxlBoxHeader(ulong size, uint type, bool sizeExtendsTillEnd) + /// Is there a 64-bit size field? + public JxlBoxHeader(ulong size, uint type, bool sizeExtendsTillEnd, bool containsLargeSize) { this.Size = size; this.Type = type; this.SizeExtendsTillEnd = sizeExtendsTillEnd; + this.ContainsLargeSize = containsLargeSize; } /// @@ -103,11 +110,11 @@ public static JxlBoxHeader ReadHeader(Stream stream) throw new InvalidOperationException("Large size cannot have another large size or extend till the end of the file"); } - return new JxlBoxHeader(size, type, sizeExtendsTillEnd: false); + return new JxlBoxHeader(size, type, sizeExtendsTillEnd: false, containsLargeSize: true); } else { - return new JxlBoxHeader(size, type, sizeExtendsTillEnd: size == 0); + return new JxlBoxHeader(size, type, sizeExtendsTillEnd: size == 0, containsLargeSize: false); } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index e984b36b93..be5f1d335a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -7,6 +7,7 @@ using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.IO.Container; using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.IO; @@ -1539,36 +1540,29 @@ private bool ReadBundle(Span data, JxlBitReader br, T bundle) /// Status of the parsing. /// Thrown if the data is incorrect. /// Thrown if the data is malformed. - public bool ReadBasicInfo() + public bool ReadBasicInfo(Stream stream) { if (!this.gotCodestreamSignature) { - Span span = this.GetCodeStreamSpan(); - - if (span.Length < 2) - { - return this.TryRequestMoreInput(); - } + Span fileSignature = stackalloc byte[2]; + stream.ReadExactly(fileSignature); - if (span[0] != 0xFF || span[1] != CodestreamMarker) + if (fileSignature[0] != 0xFF || fileSignature[1] != CodestreamMarker) { throw new InvalidOperationException("The file signature is invalid"); } this.gotCodestreamSignature = true; - this.AdvanceCodeStream(2); } - Span sp = this.GetCodeStreamSpan(); - - JxlBitReader bitReader = new(sp); + JxlBitReader bitReader = new(stream); - if (!this.ReadBundle(sp, bitReader, this.metadata!.Size!)) + if (!this.ReadBundle(stream, bitReader, this.metadata!.Size!)) { throw new InvalidDataException("Could not parse the size header"); } - if (!this.ReadBundle(sp, bitReader, this.metadata!.ImageMetadata!)) + if (!this.ReadBundle(stream, bitReader, this.metadata!.ImageMetadata!)) { throw new InvalidDataException("Could not parse the image metadata"); } @@ -1924,8 +1918,7 @@ public int ProcessCodestream() if (this.skippingFrame) { - bool referenceable = this.frameHeader.CanBeReferenced - || this.frameHeader.FrameType == JxlFrameType.DcFrame; + bool referenceable = this.frameHeader.CanBeReferenced || this.frameHeader.FrameType == JxlFrameType.DcFrame; if (internalFrameIndex < this.frameRequired.Count && this.frameRequired[internalFrameIndex] == 0) { @@ -2182,63 +2175,18 @@ public void SetJpegBuffer(Memory data) /// Parses the start of a box. /// /// Input bytes to parse from. - /// Size of remaining input bytes. - /// Offset of input bytes. - /// File offset. - /// Type of the parsed box. + /// Type of the box /// Output box size. /// Output header size. - /// - /// True if the parsing went fine. False if the parsing requests - /// more input bytes. - /// /// /// Thrown when data is invalid. /// - private static bool ParseBoxHeader(Span input, long size, long pos, long filePos, JxlBoxType type, out long boxSize, out long headerSize) + private static void ParseBoxHeader(Stream input, out JxlBoxType type, out long boxSize, out long headerSize) { - boxSize = 0; - headerSize = 0; - - if (IsOutOfBounds((int)pos, 8, (int)size)) - { - headerSize = 8; - return false; - } - - long boxStart = pos; - boxSize = BinaryPrimitives.ReadInt32BigEndian(input[(int)pos..]); - pos += 4; - type = (JxlBoxType)BitConverter.ToInt32(input.Slice((int)pos, 4)); - pos += 4; - - if (boxSize == 1) - { - headerSize = 16; - - if (IsOutOfBounds((int)pos, 8, (int)size)) - { - return false; - } - - long boxSize64 = BinaryPrimitives.ReadInt64BigEndian(input[(int)pos..]); - pos += 8; - boxSize = boxSize64; - } - - headerSize = pos - boxStart; - - if (boxSize > 0 && boxSize < headerSize) - { - throw new InvalidOperationException("Invalid box size"); - } - - if (filePos + boxSize < filePos) - { - throw new InvalidOperationException("Box size overflow"); - } - - return true; + JxlBoxHeader header = JxlBoxHeader.ReadHeader(input); + boxSize = (long)header.Size; + headerSize = (header.ContainsLargeSize ? 12 : 4) + 4; + type = (JxlBoxType)header.Type; } /// @@ -2246,14 +2194,14 @@ private static bool ParseBoxHeader(Span input, long size, long pos, long f /// /// Status of processing. /// Thrown when data is invalid. - public int ProcessBoxes() + public int ProcessBoxes(Stream stream) { // We have a box handling loop here. while (true) { if (this.boxStage != JxlBoxStage.Header) { - this.AdvanceInput(this.headerSize); + // this.AdvanceInput(this.headerSize); this.headerSize = 0; if ((this.eventsWanted & Box) != 0 && this.boxEvent && !this.boxOutBufferSetCurrentBox) @@ -2582,20 +2530,18 @@ public int ProcessBoxes() return NeedMoreInput; } - Span nextSpan = this.nextInput!.Memory.Span; - if (!(nextSpan[0] == 'j' && nextSpan[1] == 'x' && nextSpan[2] == 'l' && nextSpan[3] == ' ')) + if (BinaryUtils.ReadInt32BigEndian(stream) != 0x6A786C20) // Bytes "jxl " in Big Endian { throw new InvalidOperationException("File type box major brand must be \"jxl \""); } - uint version = BinaryPrimitives.ReadUInt32BigEndian(nextSpan[4..]); + uint version = BinaryUtils.ReadUInt32BigEndian(stream); if (version > 1) { throw new InvalidOperationException("Unknown JXL file format version " + version + ", known versions are 0 and 1"); } this.jxlFileFormatVersion = (int)version; - this.AdvanceInput(8); this.boxStage = JxlBoxStage.Skip; } else if (this.boxStage == JxlBoxStage.PartialCodeStream) @@ -2615,7 +2561,7 @@ public int ProcessBoxes() throw new InvalidOperationException("jxlp box is too small to contain an index"); } - uint jxlpIndex = BinaryPrimitives.ReadUInt32BigEndian(this.nextInput!.Memory.Span); + uint jxlpIndex = BinaryUtils.ReadUInt32BigEndian(stream); uint counter = jxlpIndex & 0x7FFFFFFFu; bool isLast = (jxlpIndex & 0x80000000u) != 0; @@ -2624,8 +2570,6 @@ public int ProcessBoxes() throw new InvalidOperationException("jxlp box index " + counter + " is a duplicate (already processed)"); } - this.AdvanceInput(4); - if (counter == this.nextJxlpIndex) { this.nextJxlpIndex++; @@ -2720,9 +2664,11 @@ public int ProcessBoxes() return Error; } - entry!.CodestreamBytes.Write(this.nextInput!.Memory.Span[..(int)remaining]); + // Now we want to write the 'remaining' number of bytes + // from input into the codestream. + using IMemoryOwner buffer = this.Options.Configuration.MemoryAllocator.Allocate((int)remaining); + entry!.CodestreamBytes.Write(buffer.Memory.Span); this.jxlpOooBufferTotal += remaining; - this.AdvanceInput(remaining); bool boxDone = !this.boxContentsUnbounded && this.filePosition >= this.boxContentsEnd; From 08c13fc028e406d4cef26979aba616204186990b Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 18 Aug 2026 20:51:03 +0400 Subject: [PATCH 080/328] Performance improvements --- .../Formats/Jxl/IO/BinaryUtils.Generated.cs | 1 - src/ImageSharp/Formats/Jxl/IO/BinaryUtils.tt | 1 - .../Formats/Jxl/IO/Container/JxlBoxHeader.cs | 28 ++++++++++++++++--- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/IO/BinaryUtils.Generated.cs b/src/ImageSharp/Formats/Jxl/IO/BinaryUtils.Generated.cs index 055538df7a..a5b07c45f6 100644 --- a/src/ImageSharp/Formats/Jxl/IO/BinaryUtils.Generated.cs +++ b/src/ImageSharp/Formats/Jxl/IO/BinaryUtils.Generated.cs @@ -5,7 +5,6 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO; - /// /// Reads primitives from streams with correct endianness. /// diff --git a/src/ImageSharp/Formats/Jxl/IO/BinaryUtils.tt b/src/ImageSharp/Formats/Jxl/IO/BinaryUtils.tt index c83e9e4c4b..ba98aca4ad 100644 --- a/src/ImageSharp/Formats/Jxl/IO/BinaryUtils.tt +++ b/src/ImageSharp/Formats/Jxl/IO/BinaryUtils.tt @@ -20,7 +20,6 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO; typeof(ulong) ]; #> - /// /// Reads primitives from streams with correct endianness. /// diff --git a/src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs b/src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs index dc5eb13513..93c0e992bc 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Buffers.Binary; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; @@ -13,6 +14,18 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO.Container; [StructLayout(LayoutKind.Sequential, Size = 16)] internal struct JxlBoxHeader { + private static readonly Dictionary KnownTypeCodes = new() + { + { 0x6A786C20, "jxl " }, + { 0x6A786C70, "jxlp" }, + { 0x6A786C63, "jxlc" }, + { 0x66747970, "ftyp" }, + { 0x6A627264, "jbrd" }, + { 0x45786966, "Exif" }, + { 0x786D6C20, "xml " }, + { 0x6A756D62, "jumb" } + }; + /// /// Box size in bytes. /// @@ -53,6 +66,7 @@ public JxlBoxHeader(ulong size, uint type, bool sizeExtendsTillEnd, bool contain /// /// Input type string to convert /// Unsigned integer representation of the type string + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static uint TypeFromString(string typeString) { if (typeString.Length != 4) @@ -60,10 +74,10 @@ public static uint TypeFromString(string typeString) throw new ArgumentException("Box type must be exactly 4 characters", nameof(typeString)); } - Span buffer = stackalloc byte[4]; - _ = Encoding.ASCII.GetBytes(typeString, buffer); - - return BinaryPrimitives.ReadUInt32BigEndian(buffer); + return ((uint)typeString[0] << 24) | + ((uint)typeString[1] << 16) | + ((uint)typeString[2] << 8) | + typeString[3]; } /// @@ -73,6 +87,12 @@ public static uint TypeFromString(string typeString) /// The string representing the type code. public static string TypeToString(uint typeCode) { + if (KnownTypeCodes.TryGetValue(typeCode, out string? str)) + { + return str; + } + + // The box type is not known Span buffer = stackalloc byte[4]; BinaryPrimitives.WriteUInt32BigEndian(buffer, typeCode); From 6ba0d37002506726f10c816c1c81add9439be242 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 18 Aug 2026 20:56:11 +0400 Subject: [PATCH 081/328] Zero-copy input access (prototype 2) --- .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 41 ++++++++----------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index be5f1d335a..6466c82264 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Buffers; -using System.Buffers.Binary; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Jxl.Fields; @@ -733,8 +732,9 @@ private struct JxlDecoderFrameIndexBoxEntry // where this.frameReferences = JxlFrameReference[]. private sealed class JxlFrameReference(int reference, int savedAs) { - public int Reference = reference; - public int SavedAs = savedAs; + public int Reference { get; set; } = reference; + + public int SavedAs { get; set; } = savedAs; } /// @@ -812,27 +812,24 @@ private static int InitialBasicInfoSizeHint() return containerHeaderSize + maxCodestreamBasicInfoSize; } - private static JxlSignature DetectSignature(ReadOnlySpan buffer, int length, ref int position) + private static JxlSignature DetectSignature(Stream buffer) { - if (position >= length) + int firstByte = buffer.ReadByte(); + if (firstByte == -1) { - return JxlSignature.NotEnoughBytes; + throw new EndOfStreamException(); } - buffer = buffer[position..]; - length -= position; - - // 0xFF 0x0A represents a codestream - if (length >= 1 && buffer[0] == 0xFF) + if (firstByte == 0xFF) { - if (length < 2) + int secondByte = buffer.ReadByte(); + if (secondByte == -1) { - // We need at least two bytes for a valid codestream signature - return JxlSignature.NotEnoughBytes; + throw new EndOfStreamException(); } - else if (buffer[1] == CodestreamMarker) + + if (secondByte == CodestreamMarker) { - position += 2; return JxlSignature.CodeStream; } else @@ -842,15 +839,13 @@ private static JxlSignature DetectSignature(ReadOnlySpan buffer, int lengt } // Container? - if (length >= 1 && buffer[0] == 0) + if (firstByte == 0) { - if (length < SignatureBox.Length) - { - return JxlSignature.NotEnoughBytes; - } - else if (buffer[SignatureBox.Length..].SequenceEqual(SignatureBox)) + Span signatureBox = stackalloc byte[JxlShared.SignatureBox.Length]; + buffer.ReadExactly(signatureBox); + + if (signatureBox.SequenceEqual(JxlShared.SignatureBox)) { - position += SignatureBox.Length; return JxlSignature.Container; } else From c53589347240c2ed797d8da3656686560a7b57e6 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 18 Aug 2026 22:34:14 +0400 Subject: [PATCH 082/328] Zero-copy input access (prototype 3) --- .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index 6466c82264..d1edd36b9d 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -858,12 +858,6 @@ private static JxlSignature DetectSignature(Stream buffer) return JxlSignature.Invalid; } - private static JxlSignature DetectSignature(ReadOnlySpan buffer, int length) - { - int position = 0; - return DetectSignature(buffer, length, ref position); - } - private static int BitsPerChannel(JxlDataType dataType) => dataType switch { @@ -1146,9 +1140,8 @@ public bool TryRequestMoreInput() return false; } - this.codestreamCopy.Write(this.nextInput!.Memory.Span[..(int)avail]); - - this.AdvanceInput(avail); + using IMemoryOwner codestreamPending = this.Options.Configuration.MemoryAllocator.Allocate((int)avail); + this.codestreamCopy.Write(codestreamPending.Memory.Span); } else { @@ -1170,7 +1163,7 @@ public bool TryRequestMoreInput() { long avail = this.AvailableCodeStream(); long skip = Math.Min(this.codestreamPos, avail); - this.AdvanceInput(skip); + this.Skip(skip); this.codestreamPos -= skip; if (this.codestreamPos > 0) @@ -2235,7 +2228,8 @@ public int ProcessBoxes(Stream stream) if (this.storeExif == 1 || this.storeXmp == 1) { - IMemoryOwner metadata = (this.storeExif == 1 ? this.exifMetadata : this.xmpMetadata) ?? throw new InvalidOperationException("Metadata is missing, but should be present"); + IMemoryOwner metadata = (this.storeExif == 1 ? this.exifMetadata : this.xmpMetadata) + ?? throw new InvalidOperationException("Metadata is missing, but should be present"); // Boxes should not contain more than 64MiB data. const long blockSizeLimit = 64L << 20; From 75f676d3ad84b902863ed1579b64384272add68e Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 24 Aug 2026 07:16:13 +0400 Subject: [PATCH 083/328] Add prototype of modular - Added Reversible Color Transform (RCT) - Added encoder ANS histograms - Added incomplete context predictor - Added speed tier - Added image format detector - Added missing types - Simplified JxlBoxHeader --- .../Formats/Jxl/IO/Container/JxlBoxHeader.cs | 39 ++-- src/ImageSharp/Formats/Jxl/JxlFormat.cs | 9 +- .../Formats/Jxl/JxlImageFormatDetector.cs | 49 +++++ .../Jxl/Processing/Decoder/JxlFrameDecoder.cs | 109 ++++++++++ .../Decoder/JxlProgressiveDetail.cs | 47 +++++ .../Jxl/Processing/Decoder/JxlSectionInfo.cs | 13 ++ .../Processing/Decoder/JxlSectionStatus.cs | 30 +++ .../Jxl/Processing/Decoder/JxlTocEntry.cs | 11 + .../Encoder/Ans/JxlAnsHistogramStrategy.cs | 25 +++ .../Encoder/Ans/JxlClusteringType.cs | 25 +++ .../Processing/Encoder/Ans/JxlHistogram.cs | 111 ++++++++++ .../Encoder/Ans/JxlHistogramParameters.cs | 113 +++++++++++ .../Encoder/Ans/JxlHybridUIntMethod.cs | 35 ++++ .../Processing/Encoder/Ans/JxlLz77Method.cs | 90 ++++++++ .../Jxl/Processing/JxlAcStrategyImage.cs | 2 +- .../Formats/Jxl/Processing/JxlOverride.cs | 26 +++ .../Jxl/Processing/JxlOverrideHelpers.cs | 43 ++++ .../Jxl/Processing/JxlPassesSharedState.cs | 7 +- .../Formats/Jxl/Processing/JxlQuantizer.cs | 40 ++-- .../Formats/Jxl/Processing/JxlSpeedTier.cs | 76 +++++++ .../ContextPrediction/JxlContextPrediction.cs | 97 +++++++++ .../ContextPrediction/JxlFlatDecisionNode.cs | 19 ++ .../ContextPrediction/JxlMaTreeLookup.cs | 29 +++ .../JxlMaTreeLookupResult.cs | 9 + .../ContextPrediction/JxlModularHeader.cs | 140 +++++++++++++ .../JxlModularMultiplierInfo.cs | 17 ++ .../ContextPrediction/JxlModularState.cs | 192 ++++++++++++++++++ .../ContextPrediction/JxlPredictor.cs | 34 ++++ .../ContextPrediction/JxlPredictorFacts.cs | 25 +++ .../Encoding/ContextPrediction/JxlTreeKind.cs | 20 ++ .../Encoding/ContextPrediction/JxlTreeMode.cs | 30 +++ .../Processing/Modular/JxlModularChannel.cs | 77 +++++++ .../Jxl/Processing/Modular/JxlModularImage.cs | 11 + .../Processing/Modular/Transforms/JxlRct.cs | 115 +++++++++++ .../Transforms/JxlSqueezeParameters.cs | 74 +++++++ .../Modular/Transforms/JxlTransform.cs | 11 + .../Modular/Transforms/JxlTransformType.cs | 30 +++ 37 files changed, 1779 insertions(+), 51 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/JxlImageFormatDetector.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlProgressiveDetail.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlSectionInfo.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlSectionStatus.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlTocEntry.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlAnsHistogramStrategy.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlClusteringType.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHistogram.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHistogramParameters.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHybridUIntMethod.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlLz77Method.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlOverride.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlOverrideHelpers.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlSpeedTier.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlContextPrediction.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlFlatDecisionNode.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlMaTreeLookup.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlMaTreeLookupResult.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularHeader.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularMultiplierInfo.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlPredictor.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlPredictorFacts.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlTreeKind.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlTreeMode.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularChannel.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularImage.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlRct.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueezeParameters.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransformType.cs diff --git a/src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs b/src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs index 93c0e992bc..8e3600bb5e 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs @@ -14,18 +14,6 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO.Container; [StructLayout(LayoutKind.Sequential, Size = 16)] internal struct JxlBoxHeader { - private static readonly Dictionary KnownTypeCodes = new() - { - { 0x6A786C20, "jxl " }, - { 0x6A786C70, "jxlp" }, - { 0x6A786C63, "jxlc" }, - { 0x66747970, "ftyp" }, - { 0x6A627264, "jbrd" }, - { 0x45786966, "Exif" }, - { 0x786D6C20, "xml " }, - { 0x6A756D62, "jumb" } - }; - /// /// Box size in bytes. /// @@ -87,16 +75,27 @@ public static uint TypeFromString(string typeString) /// The string representing the type code. public static string TypeToString(uint typeCode) { - if (KnownTypeCodes.TryGetValue(typeCode, out string? str)) + return typeCode switch { - return str; - } - - // The box type is not known - Span buffer = stackalloc byte[4]; - BinaryPrimitives.WriteUInt32BigEndian(buffer, typeCode); + 0x6A786C20 => "jxl ", + 0x6A786C70 => "jxlp", + 0x6A786C63 => "jxlc", + 0x66747970 => "ftyp", + 0x6A627264 => "jbrd", + 0x45786966 => "Exif", + 0x786D6C20 => "xml ", + 0x6A756D62 => "jumb", + _ => Fallback(typeCode) + }; + + static string Fallback(uint typeCode) + { + // The box type is not known + Span buffer = stackalloc byte[4]; + BinaryPrimitives.WriteUInt32BigEndian(buffer, typeCode); - return Encoding.ASCII.GetString(buffer); + return Encoding.ASCII.GetString(buffer); + } } /// diff --git a/src/ImageSharp/Formats/Jxl/JxlFormat.cs b/src/ImageSharp/Formats/Jxl/JxlFormat.cs index 00e7b66eb5..f5c23ad685 100644 --- a/src/ImageSharp/Formats/Jxl/JxlFormat.cs +++ b/src/ImageSharp/Formats/Jxl/JxlFormat.cs @@ -3,13 +3,20 @@ namespace SixLabors.ImageSharp.Formats.Jxl; -internal class JxlFormat : IImageFormat +/// +/// JPEG XL format +/// +public sealed class JxlFormat : IImageFormat { + /// public string Name => "JPEG XL"; + /// public string DefaultMimeType => "image/jxl"; + /// IEnumerable IImageFormat.MimeTypes => new[] { "image/jxl" }; + /// IEnumerable IImageFormat.FileExtensions => new[] { "jxl" }; } diff --git a/src/ImageSharp/Formats/Jxl/JxlImageFormatDetector.cs b/src/ImageSharp/Formats/Jxl/JxlImageFormatDetector.cs new file mode 100644 index 0000000000..9b968bd1e3 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/JxlImageFormatDetector.cs @@ -0,0 +1,49 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; + +namespace SixLabors.ImageSharp.Formats.Jxl; + +/// +/// Checks if the first few bytes of a file represent +/// JPEG XL. +/// +public sealed class JxlImageFormatDetector : IImageFormatDetector +{ + /// + /// Gets file signature bytes which represent a container-based + /// JPEG XL file. + /// + private static ReadOnlySpan ContainerStart => + [ + 0x00, 0x00, 0x00, 0x0C, + 0x4A, 0x58, 0x4C, 0x20, + 0x0D, 0x0A, 0x87, 0x0A, + ]; + + /// + public int HeaderSize => 12; + + /// + public bool TryDetectFormat(ReadOnlySpan header, [NotNullWhen(true)] out IImageFormat? format) + { + if (header[0] == 0xFF && header[1] == 0x0A) + { + // Just codestream. + format = new JxlFormat(); + return true; + } + else if (header.SequenceEqual(ContainerStart)) + { + // Container format. + format = new JxlFormat(); + return true; + } + else + { + format = null; + return false; + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs new file mode 100644 index 0000000000..b17aacd5cb --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs @@ -0,0 +1,109 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; +using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +internal sealed class JxlFrameDecoder +{ + private JxlPassesDecoderState decoderState; + private List toc = []; + private ulong sectionSizesSum; + private JxlFrameHeader frameHeader; + private JxlFrameDimensions frameDimensions; + private JxlImageBundle decoded; + private JxlModularFrameDecoder modularFrameDecoder; + private bool renderSpotcolors = true; + private bool coalescing = true; + private List processedSection = []; + private List decodedPassesPerAcGroup = []; + private List decodedDcGroups = []; + private bool decodedDcGlobal; + private bool decodedAcGlobal; + private bool finalizedDc = true; + private long numSectionsDone; + private bool isFinalized = true; + private bool allocated; + private List groupDecoderCaches = []; + private bool useTaskId; + private bool useSlowRenderingPipeline; + private JxlProgressiveDetail progressiveDetail = JxlProgressiveDetail.Frames; + private List passesToPause = []; + + public static void DecodeGlobalDcInfo(Configuration configuration, JxlBitReader reader, bool isJpeg, JxlPassesDecoderState state) + { + state.SharedStorage.Quantizer.Decode(reader); + + if (!JxlEntropyCoder.DecodeBlockContextMap(configuration, reader, ref state.SharedStorage.BlockContextMap)) + { + throw new InvalidOperationException("Could not decode block context map"); + } + + if (!state.SharedStorage.ColorMap.DecodeDc(reader)) + { + throw new InvalidOperationException("Could not decode DC color correlation map"); + } + + if (isJpeg) + { + state.SharedStorage.Quantizer.ClearDcMultipliers(); + } + + state.SharedStorage.AcStrategy.FillInvalid(); + } + + public static void DecodeFrame(JxlPassesDecoderState decoderState, Stream stream, ref JxlFrameHeader header, JxlImageBundle decoded, JxlCodecMetadata metadata, bool useSlowRenderingPipeline) + { + JxlFrameDecoder frameDecoder = new(decoderState, metadata, useSlowRenderingPipeline); + JxlBitReader reader = new(stream); + + if (!frameDecoder.InitializeFrame(reader, decoded, isPreview: false)) + { + throw new InvalidOperationException("Frame initialization failed"); + } + + if (!frameDecoder.InitializeFrameOutput()) + { + throw new InvalidOperationException("Could not initialize frame output"); + } + + if (header is not null) + { + header = frameDecoder.frameHeader; + } + + bool closeOk = true; + List sectionReaders = []; + List sectionClosers = []; + List sectionInfos = []; + List sectionStatuses = []; + + int index = 0; + + foreach (JxlTocEntry toc in frameDecoder.toc) + { + JxlBitReader br = new(stream); + sectionInfos.Add(new JxlSectionInfo(br, toc.Id, index++)); + sectionClosers.Add(new JxlBitReaderScopedCloser(reader, closeOk)); + sectionReaders.Add(br); + } + + frameDecoder.ProcessSections(sectionInfos, sectionStatuses); + for (int i = 0; i < sectionStatuses.Count; i++) + { + if (sectionStatuses[i] != JxlSectionStatus.Done) + { + throw new InvalidOperationException("Section incomplete"); + } + } + + if (!closeOk) + { + throw new InvalidDataException("Stream cannot be closed"); + } + + frameDecoder.FinalizeFrame(); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlProgressiveDetail.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlProgressiveDetail.cs new file mode 100644 index 0000000000..70c3a4679a --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlProgressiveDetail.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +/// +/// Types of progressive detail. +/// +internal enum JxlProgressiveDetail : byte +{ + /// + /// After completed regular frames + /// + Frames, + + /// + /// After completed DC. + /// + Dc, + + /// + /// After completed AC passes that are the last pass for their + /// resolution target. + /// + LastPasses, + + /// + /// After completed AC passes that are not the last pass for their + /// resolution target. + /// + Passes, + + /// + /// During DC frame when lower resolution are completed. + /// + DcProgressive, + + /// + /// After completed groups. + /// + DcGroups, + + /// + /// After completed groups. + /// + Groups, +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlSectionInfo.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlSectionInfo.cs new file mode 100644 index 0000000000..4637bacbda --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlSectionInfo.cs @@ -0,0 +1,13 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +internal sealed class JxlSectionInfo(JxlBitReader reader, int id, int index) +{ + public JxlBitReader BitReader { get; set; } = reader; + + public int Id { get; set; } = id; + + public int Index { get; set; } = index; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlSectionStatus.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlSectionStatus.cs new file mode 100644 index 0000000000..88c4f1068f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlSectionStatus.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +/// +/// Status of processing a section. +/// +internal enum JxlSectionStatus +{ + /// + /// Processed normally. + /// + Done, + + /// + /// Skipped because other required sections were not yet processed. + /// + Skipped, + + /// + /// Skipped because the section was already processed. + /// + Duplicate, + + /// + /// Only partially decoded. Section will be processed again. + /// + Partial +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlTocEntry.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlTocEntry.cs new file mode 100644 index 0000000000..4e0cfac6bc --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlTocEntry.cs @@ -0,0 +1,11 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +internal struct JxlTocEntry +{ + public int Size; + + public int Id; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlAnsHistogramStrategy.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlAnsHistogramStrategy.cs new file mode 100644 index 0000000000..1928788791 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlAnsHistogramStrategy.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; + +/// +/// ANS histogram strategy during encoding +/// +internal enum JxlAnsHistogramStrategy : byte +{ + /// + /// Only try a few methods, early exit. + /// + Fast, + + /// + /// Only try a few methods. + /// + Approximate, + + /// + /// Try all methods. + /// + Precise +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlClusteringType.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlClusteringType.cs new file mode 100644 index 0000000000..4435c5d199 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlClusteringType.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; + +/// +/// JPEG XL ANS encoder clustering type +/// +internal enum JxlClusteringType : byte +{ + /// + /// Fastest clustering type, with only 4 clusters + /// + Fastest, + + /// + /// A fast clustering type. + /// + Fast, + + /// + /// Slower clustering type. + /// + Best, +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHistogram.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHistogram.cs new file mode 100644 index 0000000000..70a9321071 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHistogram.cs @@ -0,0 +1,111 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; + +/// +/// ANS histogram. +/// +internal sealed class JxlHistogram(int length) +{ + /// + /// Rounding constant + /// + private const int Rounding = 8; + + public List Counts { get; set; } = new(length); + + public int TotalCount { get; set; } + + public float Entropy { get; set; } + + /// + /// Resets all values to their defaults. + /// + public void Clear() + { + this.Counts.Clear(); + this.TotalCount = 0; + this.Entropy = 0f; + } + + /// + /// Adds a new symbol. + /// + /// + /// Index of the symbol to be added or, if it already + /// exists, incremented. + /// + public void Add(int symbol) + { + // Just to be careful here. If the symbol is too large, + // this can allocate a lot of memory. + DebugGuard.MustBeLessThan(symbol, 1_000_000, nameof(symbol)); + + _ = this.Counts.EnsureCapacity(symbol); + this.Counts[symbol]++; + this.TotalCount++; + } + + /// + /// Increments the specified symbol. This is equivalent to + /// but without any checks, like ensuring capacity. + /// + /// Index of the symbol. + public void FastAdd(int symbol) => this.Counts[symbol]++; + + /// + /// Adds the counts of the specified histogram to the current histogram. + /// + /// A specified histogram to add to this histogram. + public void AddHistogram(JxlHistogram other) + { + _ = this.Counts.EnsureCapacity(other.Counts.Count); + + for (int i = 0; i < other.Counts.Count; i++) + { + this.Counts[i] += other.Counts[i]; + } + + this.TotalCount += other.TotalCount; + } + + /// + /// Calculates the alphabet size. + /// + /// The alphabet size. + public int GetAlphabetSize() + { + for (int i = this.Counts.Count - 1; i >= 0; i--) + { + if (this.Counts[i] > 0) + { + return i + 1; + } + } + + return 0; + } + + /// + /// Finds the largest symbol. + /// + /// Largest symbol in the histogram. + public int GetMaxSymbol() + { + if (this.TotalCount == 0) + { + return 0; + } + + for (int i = this.Counts.Count - 1; i > 0; i--) + { + if (this.Counts[i] != 0) + { + return i; + } + } + + return 0; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHistogramParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHistogramParameters.cs new file mode 100644 index 0000000000..4f2047424c --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHistogramParameters.cs @@ -0,0 +1,113 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; + +/// +/// ANS histogram parameters +/// +internal sealed class JxlHistogramParameters +{ + /// + /// Initializes a new instance of the class with default values. + /// + public JxlHistogramParameters() + { + } + + /// + /// Initializes a new instance of the class using the specified + /// speed tier that prioritizes performance over compression and vice versa. + /// + public JxlHistogramParameters(JxlSpeedTier tier) + { + if (tier > JxlSpeedTier.Falcon) + { + // Fast modes + this.Clustering = JxlClusteringType.Fastest; + this.Lz77Method = JxlLz77Method.None; + } + else if (tier > JxlSpeedTier.Tortoise) + { + // Normal modes + this.Clustering = JxlClusteringType.Fast; + } + else + { + // Slow modes + this.Clustering = JxlClusteringType.Best; + } + + if (tier > JxlSpeedTier.Tortoise) + { + this.UIntMethod = JxlHybridUIntMethod.None; + } + + if (tier >= JxlSpeedTier.Squirrel) + { + this.AnsHistogramStrategy = JxlAnsHistogramStrategy.Approximate; + } + } + + /// + /// Gets or sets the clustering type. Default is Best. + /// + public JxlClusteringType Clustering { get; set; } = JxlClusteringType.Best; + + /// + /// Gets or sets the hybrid uint method. Default is Best. + /// + public JxlHybridUIntMethod UIntMethod { get; set; } = JxlHybridUIntMethod.Best; + + /// + /// Gets or sets the LZ77 method. Default is Rle. + /// + public JxlLz77Method Lz77Method { get; set; } = JxlLz77Method.Rle; + + /// + /// Gets or sets the ANS histogram strategy. Default is Precise. + /// + public JxlAnsHistogramStrategy AnsHistogramStrategy { get; set; } = JxlAnsHistogramStrategy.Precise; + + /// + /// Gets or sets image widths. + /// + public List ImageWidths { get; set; } = []; + + /// + /// Gets or sets the max number of histograms. + /// + public uint MaxHistograms { get; set; } = ~0u; + + /// + /// Gets or sets a value indicating whether to prefer Huffman coding. + /// + public bool ForceHuffman { get; set; } + + /// + /// Gets or sets a value indicating whether global state should be initialized. + /// (True by default) + /// + public bool InitializeGlobalState { get; set; } = true; + + /// + /// Gets or sets a value indicating whether streaming mode is enabled. + /// + public bool StreamingMode { get; set; } + + public bool AddMissingSymbols { get; set; } + + public bool AddFixedHistograms { get; set; } + + /// + /// Gets the uint configuration for histogram parameters. + /// + public JxlAnsHybridUIntConfiguration UIntConfig => this.UIntMethod switch + { + JxlHybridUIntMethod.ContextMap => new(2, 0, 1), + JxlHybridUIntMethod.Method000 => new(0, 0, 0), + _ => new(), + }; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHybridUIntMethod.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHybridUIntMethod.cs new file mode 100644 index 0000000000..a5150f7a74 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHybridUIntMethod.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; + +/// +/// Hybrid uint method. +/// +internal enum JxlHybridUIntMethod : byte +{ + /// + /// Simply use HybridUint420Configuration + /// + None, + + /// + /// Force the fastest option. + /// + Method000, + + /// + /// Try a couple of options. + /// + Fast, + + /// + /// Fast choice for context maps. + /// + ContextMap, + + /// + /// Slowest. + /// + Best, +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlLz77Method.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlLz77Method.cs new file mode 100644 index 0000000000..41cb9da764 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlLz77Method.cs @@ -0,0 +1,90 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; + +/// +/// Method for LZ77 compression. +/// +internal enum JxlLz77Method : byte +{ + /// + /// Do not use LZ77. + /// + None, + + /// + /// Use Run Length Encoding + /// + Rle, + + /// + /// LZ77 fast without runtime cost comparison + /// + Lz77b1w3f, + + /// + /// LZ77 + /// + Lz77b3w3f, + + /// + /// LZ77 + /// + Lz77b7w3f, + + /// + /// LZ77 + /// + Lz77b15w3f, + + /// + /// Slow LZ77 + /// + Lz77b31w3f, + + /// + /// Fast LZ77, but with runtime cost comparison. Almost always worse. + /// + Lz77b1w3t, + + /// + /// LZ77 + /// + Lz77b3w3t, + + /// + /// LZ77 + /// + Lz77b7w3t, + + /// + /// LZ77 + /// + Lz77b15w3t, + + /// + /// Slow LZ77 + /// + Lz77b31w3t, + + /// + /// Optimal-matching LZ77 (fast). + /// + Optc1, + + /// + /// Optional-matching LZ77. + /// + Optc3, + + /// + /// Optional-matching LZ77. + /// + Optc8, + + /// + /// Optional-matching LZ77 parsing big chain length. + /// + Optc256, +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs index a33485df5d..b4469baebc 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs @@ -105,7 +105,7 @@ public bool Set(int x, int y, JxlAcStrategyType type) public void FillDct8(in Rectangle rect) => this.FillPlane(((int)JxlAcStrategyType.DCT << 1) | 1, this.layers, in rect); - public void FillDct8() => this.FillDct8(in this.layers.GetRectangle()); + public void FillDct8() => this.FillDct8(this.layers!.GetRectangle()); public void FillInvalid() => this.FillImage(Invalid, this.layers); diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlOverride.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlOverride.cs new file mode 100644 index 0000000000..ebd84b7b68 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlOverride.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Represents a boolean which can be overriden to be a default +/// value. +/// +internal enum JxlOverride : sbyte +{ + /// + /// Specifies a true value. + /// + On = 1, + + /// + /// Specifies a false value. + /// + Off = 0, + + /// + /// Specifies a default value. + /// + Default = -1 +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlOverrideHelpers.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlOverrideHelpers.cs new file mode 100644 index 0000000000..85464a16aa --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlOverrideHelpers.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Override utilities. +/// +internal static class JxlOverrideHelpers +{ + /// + /// Converts a boolean to an override. + /// + /// Input boolean. + /// + /// if true. Otherwise . + /// + public static JxlOverride FromBoolean(bool flag) => flag ? JxlOverride.On : JxlOverride.Off; + + /// + /// Converts an override to a boolean. + /// + /// The override. + /// Default value. + /// + /// If override is returns . + /// Otherwise returns true if , false if . + /// + public static bool ToBoolean(JxlOverride @override, bool defaultValue) + { + if (@override == JxlOverride.On) + { + return true; + } + + if (@override == JxlOverride.Off) + { + return false; + } + + return defaultValue; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs index 806c302f51..880c9e32d2 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +#pragma warning disable SA1401 // Fields should be private + using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; @@ -9,6 +11,9 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing; internal class JxlPassesSharedState { + // Make this a field so we can get a ref to it + public JxlBlockContextMap BlockContextMap; + public JxlCodecMetadata CodecMetadata { get; set; } = new(); public JxlFrameDimensions FrameDimensions { get; set; } @@ -37,8 +42,6 @@ internal class JxlPassesSharedState public JxlImage3F Dc { get; set; } - public JxlBlockContextMap BlockContextMap { get; set; } = new(); - public JxlImage3F[] DcFrames { get; set; } = new JxlImage3F[4]; public JxlReferenceFrame[] ReferenceFrames { get; set; } = new JxlReferenceFrame[4]; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs index 01723479bb..ef9e360bb1 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs @@ -6,6 +6,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; @@ -68,21 +69,6 @@ internal sealed class JxlQuantizer /// private int quantDc; - /// - /// Inverse global scale - /// - private float inverseGlobalScale; - - /// - /// Reciprocal of inverseGlobalScale - /// - private float globalScaleSingle; - - /// - /// Inverse quantizer DC - /// - private float inverseQuantDc; - /// /// The zero bias. /// @@ -116,7 +102,7 @@ public JxlQuantizer(JxlDequantMatrices dequant, int quantDc, int globalScale) this.globalScale = globalScale; this.RecomputeFromGlobalScale(); - this.inverseQuantDc = this.inverseGlobalScale / this.quantDc; + this.InverseQuantDc = this.InverseGlobalScale / this.quantDc; ZeroBiasDefault.CopyTo(this.zeroBias); } @@ -124,17 +110,17 @@ public JxlQuantizer(JxlDequantMatrices dequant, int quantDc, int globalScale) /// /// Gets the scaling factor. /// - public float Scale => this.globalScaleSingle; + public float Scale { get; private set; } /// /// Gets the inverse scaling factor. It is a reciprocal of . /// - public float InverseGlobalScale => this.inverseGlobalScale; + public float InverseGlobalScale { get; private set; } /// /// Gets the inverse DC quantization base value. /// - public float InverseQuantDc => this.inverseQuantDc; + public float InverseQuantDc { get; private set; } public ReadOnlySpan MulDc => this.mulDc; @@ -195,9 +181,9 @@ private float ScaleGlobalScale(float scale) /// public void RecomputeFromGlobalScale() { - this.globalScaleSingle = this.globalScale * (1.0f / GlobalScaleDenominator); - this.inverseGlobalScale = 1.0f * GlobalScaleDenominator / this.globalScale; - this.inverseQuantDc = this.inverseGlobalScale / this.quantDc; + this.Scale = this.globalScale * (1.0f / GlobalScaleDenominator); + this.InverseGlobalScale = 1.0f * GlobalScaleDenominator / this.globalScale; + this.InverseQuantDc = this.InverseGlobalScale / this.quantDc; for (int c = 0; c < 3; c++) { @@ -229,14 +215,14 @@ public ReadOnlySpan InverseDequantMatrix(JxlAcStrategyType strategy, int /// /// The quantization index /// The DC quantization step - public float GetDcStep(int c) => this.inverseQuantDc * this.dequant.DcQuant(c); + public float GetDcStep(int c) => this.InverseQuantDc * this.dequant.DcQuant(c); /// /// Returns the inverse DC quantization step. /// /// The quantization index /// The inverse DC quantization step - public float GetInverseDcStep(int c) => this.dequant.InverseDcQuant(c) * (this.globalScaleSingle * this.quantDc); + public float GetInverseDcStep(int c) => this.dequant.InverseDcQuant(c) * (this.Scale * this.quantDc); /// /// Creates JXL quantizer parameters with values reflecting those in this quantizer instance. @@ -302,7 +288,7 @@ public void ComputeGlobalScaleAndQuant(float quantDc, float quantMedian, float q this.RecomputeFromGlobalScale(); - float valueF = (quantDc * this.inverseGlobalScale) + 0.5f; + float valueF = (quantDc * this.InverseGlobalScale) + 0.5f; float clipValueF = MathF.Min(1 << 16, valueF); int newQuant = (int)clipValueF; this.quantDc = newQuant; @@ -322,7 +308,7 @@ public void SetQuantFieldRect(JxlImageF qf, in Rectangle rect, JxlImageI rawQuan for (int x = 0; x < rect.Width; x++) { - int val = Clamp((rowQf[x] * this.inverseGlobalScale) + 0.5f); + int val = Clamp((rowQf[x] * this.InverseGlobalScale) + 0.5f); rowQi[x] = val; } @@ -387,7 +373,7 @@ public void SetQuant(float quantDc, float quantAc, JxlImageI rawQuantField) { this.ComputeGlobalScaleAndQuant(quantDc, quantAc, 0); - int value = Clamp((quantAc * this.inverseGlobalScale) + 0.5f); + int value = Clamp((quantAc * this.InverseGlobalScale) + 0.5f); rawQuantField.Fill(value); } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlSpeedTier.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlSpeedTier.cs new file mode 100644 index 0000000000..363b4a5e4b --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlSpeedTier.cs @@ -0,0 +1,76 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Defines how quickly or slowly to encode an image. Slower +/// modes may take longer to encode the image but provide +/// better compression, while faster modes skip many algorithms, +/// therefore making compression faster, but at the same time, +/// less efficient. +/// +internal enum JxlSpeedTier : sbyte +{ + /// + /// 🌍 Try multiple combinations of Glacier + /// flags for modular mode. Otherwise like Glacier. + /// + TectonicPlate = -1, + + /// + /// 🧊 Learn a global tree in Modular mode. + /// + Glacier, + + /// + /// 🐢 Turns on FindBestQuantizationHQ loop. + /// + Tortoise, + + /// + /// 🐈 Turns on FindBestQuantization butteraugli loop. + /// + Kitten, + + /// + /// 🐿️ Turns on dots, patches, and spline detection, as well as + /// context clustering. This is the default mode. + /// + Squirrel, + + /// + /// 🐻 Turns on error diffusion and full AC strategy heuristics. This is the + /// equivalent of fast mode. + /// + Wombat, + + /// + /// 🐰 Turns on simple heuristics for AC strategy, quant field, + /// gaborish by default, non-default color map, initial quant field, + /// and non-default Chroma from Luma. + /// + Hare, + + /// + /// 🐆 Turns on clustering and enables coefficient reordering. + /// + Cheetah, + + /// + /// 🦅 Turns off most encoder encoder features. Does context clustering. + /// For modular, uses fixed tree with Weighted predictor. + /// + Falcon, + + /// + /// ⚡ Fastest possible setting for VarDCT. For Modular, uses fixed tree with + /// Gradient predictor. + /// + Thunder, + + /// + /// ⚡ For VarDCT, same as Thunder. For Modular, no tree, Gradient predictor, fast histograms. + /// + Lightning +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlContextPrediction.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlContextPrediction.cs new file mode 100644 index 0000000000..98c1d2dc54 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlContextPrediction.cs @@ -0,0 +1,97 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; + +/// +/// Context prediction helper methods. +/// +internal static class JxlContextPrediction +{ + public static void SetPredictorMode(int i, JxlModularHeader header) + { + ref uint wr = ref header.GetWReference(); + Span w = MemoryMarshal.CreateSpan(ref wr, 4); + + switch (i) + { + case 0: + // ~ lossless16 predictor + w[0] = 0xd; + w[1] = 0xc; + w[2] = 0xc; + w[3] = 0xc; + header.P1C = 16; + header.P2C = 10; + header.P3Ca = 7; + header.P3Cb = 7; + header.P3Cc = 7; + header.P3Cd = 0; + header.P3Ce = 0; + break; + + case 1: + // ~ default lossless8 predictor + w[0] = 0xd; + w[1] = 0xc; + w[2] = 0xc; + w[3] = 0xb; + header.P1C = 8; + header.P2C = 8; + header.P3Ca = 4; + header.P3Cb = 0; + header.P3Cc = 3; + header.P3Cd = 23; + header.P3Ce = 2; + break; + + case 2: + // ~ west lossless8 predictor + w[0] = 0xd; + w[1] = 0xc; + w[2] = 0xd; + w[3] = 0xc; + header.P1C = 10; + header.P2C = 9; + header.P3Ca = 7; + header.P3Cb = 0; + header.P3Cc = 0; + header.P3Cd = 16; + header.P3Ce = 9; + break; + + case 3: + // ~ north lossless8 predictor + w[0] = 0xd; + w[1] = 0xd; + w[2] = 0xc; + w[3] = 0xc; + header.P1C = 16; + header.P2C = 8; + header.P3Ca = 0; + header.P3Cb = 16; + header.P3Cc = 0; + header.P3Cd = 23; + header.P3Ce = 0; + break; + + case 4: + default: + // something else, because why not + w[0] = 0xd; + w[1] = 0xc; + w[2] = 0xc; + w[3] = 0xc; + header.P1C = 10; + header.P2C = 10; + header.P3Ca = 5; + header.P3Cb = 5; + header.P3Cc = 5; + header.P3Cd = 12; + header.P3Ce = 4; + break; + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlFlatDecisionNode.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlFlatDecisionNode.cs new file mode 100644 index 0000000000..122f01befa --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlFlatDecisionNode.cs @@ -0,0 +1,19 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; + +/// +/// Stores a node and its two children at the same time. +/// +internal struct JxlFlatDecisionNode +{ + public int Property0; + public int SplitValue0; + public JxlPredictor Predictor; + public InlineArray2 SplitValues; + public int Multiplier; + public uint ChildID; + public InlineArray2 Properties; + public int PredictorOffset; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlMaTreeLookup.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlMaTreeLookup.cs new file mode 100644 index 0000000000..ef42bf8c76 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlMaTreeLookup.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; + +internal sealed class JxlMaTreeLookup(JxlFlatDecisionNode[] nodes) +{ + public JxlMaTreeLookupResult Lookup(Span properties) + { + uint pos = 0; + while (true) + { + for (int i = 0; i < 2; i++) + { + JxlFlatDecisionNode node = nodes[pos]; + if (node.Property0 < 0) + { + return new(node.ChildID, node.Predictor, node.PredictorOffset, node.Multiplier); + } + + bool p0 = properties[node.Property0] <= node.SplitValue0; + uint off0 = properties[node.Properties[0]] <= node.SplitValues[0] ? 1u : 0u; + uint off1 = 2u | (properties[node.Properties[1]] <= node.SplitValues[1] ? 1u : 0u); + + pos = node.ChildID + (p0 ? off1 : off0); + } + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlMaTreeLookupResult.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlMaTreeLookupResult.cs new file mode 100644 index 0000000000..2609011449 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlMaTreeLookupResult.cs @@ -0,0 +1,9 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; + +/// +/// MA tree lookup result +/// +internal record struct JxlMaTreeLookupResult(uint Context, JxlPredictor Predictor, int Offset, int Multiplier); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularHeader.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularHeader.cs new file mode 100644 index 0000000000..4db1a00dc9 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularHeader.cs @@ -0,0 +1,140 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Jxl.Fields; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; + +/// +/// Context prediction header +/// +internal sealed class JxlModularHeader : IJxlFields +{ + // Backing fields for properties so we can get + // a ref to them. + private bool allDefault; + private int p1C; + private int p2C; + private int p3Ca; + private int p3Cb; + private int p3Cc; + private int p3Cd; + private int p3Ce; + private InlineArray4 w; + + /// + /// Initializes a new instance of the class. + /// + public JxlModularHeader() => JxlBundle.Init(this); + + /// + /// Gets or sets a value indicating whether all values are default. + /// + public bool AllDefault + { + get => this.allDefault; + set => this.allDefault = value; + } + + /// + /// Gets or sets the p1C coefficient. + /// + public int P1C + { + get => this.p1C; + set => this.p1C = value; + } + + /// + /// Gets or sets the p2C coefficient. + /// + public int P2C + { + get => this.p2C; + set => this.p2C = value; + } + + /// + /// Gets or sets the p3Ca coefficient. + /// + public int P3Ca + { + get => this.p3Ca; + set => this.p3Ca = value; + } + + /// + /// Gets or sets the p3Cb coefficient. + /// + public int P3Cb + { + get => this.p3Cb; + set => this.p3Cb = value; + } + + /// + /// Gets or sets the p3Cc coefficient. + /// + public int P3Cc + { + get => this.p3Cc; + set => this.p3Cc = value; + } + + /// + /// Gets or sets the p3Cd coefficient. + /// + public int P3Cd + { + get => this.p3Cd; + set => this.p3Cd = value; + } + + /// + /// Gets or sets the p3Ce coefficient. + /// + public int P3Ce + { + get => this.p3Ce; + set => this.p3Ce = value; + } + + /// + /// Returns a reference to the first w item. + /// + /// Reference to w[0] + public ref uint GetWReference() => ref this.w[0]; + + public bool Visit(JxlVisitor v) + { + if (v.AllDefault(this, ref this.allDefault)) + { + v.SetDefault(this); + return true; + } + + if (!VisitP(16, ref this.p1C) || + !VisitP(10, ref this.p2C) || + !VisitP(7, ref this.p3Ca) || + !VisitP(7, ref this.p3Cb) || + !VisitP(7, ref this.p3Cc) || + !VisitP(0, ref this.p3Cd) || + !VisitP(0, ref this.p3Ce) || + !v.Bits(4, 0xD, ref this.w[0]) || + !v.Bits(4, 0xC, ref this.w[1]) || + !v.Bits(4, 0xC, ref this.w[2]) || + !v.Bits(4, 0xC, ref this.w[3])) + { + return false; + } + + return true; + + bool VisitP(int value, ref int p) + { + ref uint unsignedP = ref Unsafe.As(ref p); + return v.Bits(5, (uint)value, ref unsignedP); + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularMultiplierInfo.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularMultiplierInfo.cs new file mode 100644 index 0000000000..2b0b35511f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularMultiplierInfo.cs @@ -0,0 +1,17 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; + +/// +/// Information about a multiplier for Modular. +/// +/// +/// A static property range, with each item containing channel and group ID. +/// +/// +/// The multiplier. +/// +internal record struct JxlModularMultiplierInfo( + InlineArray2> Range, + uint Multiplier); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs new file mode 100644 index 0000000000..e58897b4fa --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs @@ -0,0 +1,192 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; + +/// +/// State for context prediction +/// +internal sealed class JxlModularState +{ + /// + /// Rounding constant for predictions. + /// + private const long PredictionRound = ((1 << 3) >> 1) - 1; + + private InlineArray4 prediction; + private long pred; + private readonly uint[][] predErrors = new uint[4][]; + private readonly int[] error; + private readonly JxlModularHeader header; + + public JxlModularState(JxlModularHeader header, int width, int height) + { + this.header = header; + + for (int i = 0; i < 4; i++) + { + this.predErrors[i] = new uint[(width + 2) * 2]; + } + + this.error = new int[(width + 2) * 2]; + } + + /// + /// Gets a table for approximating division by a number from + /// 1 to 64. It is defined as follows. + /// + /// for (int i = 0; i < 64; i++) + /// { + /// DivisionLookup[i] = (1u << 24) / (i + 1); + /// } + /// + /// + private static ReadOnlySpan DivisionLookup => + [ + 16777216, 8388608, 5592405, 4194304, 3355443, 2796202, 2396745, 2097152, + 1864135, 1677721, 1525201, 1398101, 1290555, 1198372, 1118481, 1048576, + 986895, 932067, 883011, 838860, 798915, 762600, 729444, 699050, + 671088, 645277, 621378, 599186, 578524, 559240, 541200, 524288, + 508400, 493447, 479349, 466033, 453438, 441505, 430185, 419430, + 409200, 399457, 390167, 381300, 372827, 364722, 356962, 349525, + 342392, 335544, 328965, 322638, 316551, 310689, 305040, 299593, + 294337, 289262, 284359, 279620, 275036, 270600, 266305, 262144 + ]; + + /// + /// Adds extra bits to the prediction. + /// + /// The prediction + /// 3 extra bits added to the prediction + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static long AddBits(long x) => x << 3; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint ErrorWeight(int x, uint maxWeight) + { + int shift = Math.Max(0, JxlMath.FloorLog2Nonzero(x + 1) - 5); // Math.Max call ensures the value isn't negative + return 4 + ((maxWeight * DivisionLookup[x >> shift]) >> shift); + } + + public static long WeightedAverage(Span p, Span w) + { + uint weightSum = w[0] + w[1] + w[2] + w[3]; + + if (weightSum <= 15) + { + throw new InvalidOperationException("Weight sum is too low"); + } + + int logWeight = (int)JxlMath.FloorLog2Nonzero(weightSum); + weightSum = 0; + + for (int i = 0; i < 4; i++) + { + w[i] >>= logWeight - 4; + weightSum += w[i]; + } + + long sum = (weightSum >> 1) - 1; + for (int i = 0; i < 4; i++) + { + // Dot product + sum += p[i] * w[i]; + } + + return (sum * DivisionLookup[(int)weightSum - 1]) >> 24; + } + + public long Predict(bool computeProperties, int x, int y, int width, long n, long w, long ne, long nw, long nn, Span properties, int offset) + { + bool yIsOdd = (y & 1) != 0; + + int cur_row = yIsOdd ? 0 : (width + 2); + int prev_row = yIsOdd ? (width + 2) : 0; + int pos_N = prev_row + x; + int pos_NE = x < width - 1 ? pos_N + 1 : pos_N; + int pos_NW = x > 0 ? pos_N - 1 : pos_N; + + Span weights = stackalloc uint[4]; + ref uint headerW = ref this.header.GetWReference(); + + for (int i = 0; i < 4; i++) + { + Span error = this.predErrors[i].AsSpan(); + weights[i] = error[pos_N] + error[pos_NE] + error[pos_NW]; + weights[i] = ErrorWeight((int)weights[i], Unsafe.Add(ref headerW, i)); + } + + n = AddBits(n); + w = AddBits(w); + ne = AddBits(ne); + nw = AddBits(nw); + nn = AddBits(nn); + + long teW = x == 0 ? 0 : this.error[cur_row + x - 1]; + long teN = this.error[pos_N]; + long teNW = this.error[pos_NW]; + long sumWN = teN + teW; + long teNE = this.error[pos_NE]; + + if (computeProperties) + { + long p = teW; + long absP = Math.Abs(p); + + if (Math.Abs(teN) > absP) + { + p = teN; + } + + if (Math.Abs(teNW) > absP) + { + p = teNW; + } + + if (Math.Abs(teNE) > absP) + { + p = teNE; + } + + properties[offset++] = (int)p; + } + + this.prediction[0] = w + ne - n; + this.prediction[1] = n - (((sumWN + teNE) * this.header.P1C) >> 5); + this.prediction[2] = w - (((sumWN + teNW) * this.header.P2C) >> 5); + this.prediction[3] = + n - (((teNW * this.header.P3Ca) + (teN * this.header.P3Cb) + (teNE * this.header.P3Cc) + + ((nn - n) * this.header.P3Cd) + ((nw - w) * this.header.P3Ce)) >> + 5); + + this.pred = WeightedAverage(this.prediction, weights); + + if (((teN ^ teW) | (teN ^ teNW)) > 0) + { + return (this.pred + PredictionRound) >> 3; + } + + long mx = Math.Max(w, Math.Max(ne, n)); + long mn = Math.Min(w, Math.Min(ne, n)); + this.pred = Math.Max(mn, Math.Min(mx, this.pred)); + return (this.pred + PredictionRound) >> 3; + } + + public void UpdatePredictionErrors(long value, int x, int y, int width) + { + bool yIsOdd = (y & 1) != 0; + + long curRow = yIsOdd ? 0 : (width + 2); + long prevRow = yIsOdd ? (width + 2) : 0; + value = AddBits(value); + this.error[curRow + x] = (int)(this.pred - value); + for (int i = 0; i < 4; i++) + { + long err = (Math.Abs(this.prediction[i] - value) + PredictionRound) >> 3; + this.predErrors[i][curRow + x] = (uint)err; + this.predErrors[i][prevRow + x + 1] += (uint)err; + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlPredictor.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlPredictor.cs new file mode 100644 index 0000000000..6b68b40ea5 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlPredictor.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; + +/// +/// Defines the type of the predictor using neighboring +/// pixels, similar to intra prediction used in codecs like +/// VP8, AV1 and H.264. +/// +internal enum JxlPredictor : uint +{ + Zero = 0, + Left = 1, + Top = 2, + Average0 = 3, + Select = 4, + Gradient = 5, + Weighted = 6, + TopRight = 7, + TopLeft = 8, + LeftLeft = 9, + Average1 = 10, + Average2 = 11, + Average3 = 12, + Average4 = 13, + Best = 14, + Variable = 15, + + /// + /// Undefined predictor. + /// + Undefined = ~0u +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlPredictorFacts.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlPredictorFacts.cs new file mode 100644 index 0000000000..5a360af13c --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlPredictorFacts.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; + +/// +/// Tools and constants for predictor types. +/// +internal static class JxlPredictorFacts +{ + /// + /// Number of modular predictors. + /// + public const int ModularPredictors = (int)JxlPredictor.Average4 + 1; + + /// + /// Number of modular encoder predictors. + /// + public const int ModularEncoderPredictors = (int)JxlPredictor.Variable + 1; + + /// + /// Number of static properties. + /// + public const int StaticProperties = 2; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlTreeKind.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlTreeKind.cs new file mode 100644 index 0000000000..1906d7ec03 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlTreeKind.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; + +/// +/// Kind of tree to use. +/// +// TODO: this enum wasn't documented +// https://github.com/libjxl/libjxl/blob/main/lib/jxl/modular/options.h#L100-L111 +internal enum JxlTreeKind : byte +{ + TrivialTreeNoPredictor, + Learn, + JpegTranscodeAcMeta, + FalconAcMeta, + AcMeta, + WpFixedDc, + GradientFixedDc +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlTreeMode.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlTreeMode.cs new file mode 100644 index 0000000000..93eaa16f8d --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlTreeMode.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; + +/// +/// Decides which kinds of trees should be allowed. +/// +internal enum JxlTreeMode : byte +{ + /// + /// Gradient tree only + /// + GradientOnly, + + /// + /// Weighted predictor only + /// + WpOnly, + + /// + /// Disable weighted predictor + /// + NoWp, + + /// + /// Default trees + /// + Default +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularChannel.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularChannel.cs new file mode 100644 index 0000000000..d5fd89881d --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularChannel.cs @@ -0,0 +1,77 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular; + +/// +/// A wrapper over for modular operations. +/// +internal sealed class JxlModularChannel +{ + /// + /// Underlying plane buffer. + /// + private JxlPlane plane; + + public JxlModularChannel(Configuration configuration, int width, int height, int horizShift, int vertShift) + { + this.HorizontalShift = horizShift; + this.VerticalShift = vertShift; + this.Width = width; + this.Height = height; + this.plane = JxlPlane.Create(configuration, width, height); + } + + /// + /// Gets or sets the image width. + /// + public int Width { get; set; } + + /// + /// Gets or sets the image height. + /// + public int Height { get; set; } + + /// + /// Gets or sets the image horizontal shift. + /// + /// + /// width ~= width >> HorizontalShift + /// + public int HorizontalShift { get; set; } + + /// + /// Gets or sets the image vertical shift. + /// + /// + /// height ~= height >> VerticalShift + /// + public int VerticalShift { get; set; } + + /// + /// Gets or sets the index of the component. + /// + public int Component { get; set; } = -1; + + public void Shrink(Configuration configuration) + { + if (this.plane.XSize == this.Width && this.plane.YSize == this.Height) + { + return; + } + + this.plane.Dispose(); + this.plane = JxlPlane.Create(configuration, this.Width, this.Height); + } + + public void Shrink(Configuration configuration, int newWidth, int newHeight) + { + this.Width = newWidth; + this.Height = newHeight; + this.Shrink(configuration); + } + + public Span GetRow(int y) => this.plane.GetRow(y); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularImage.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularImage.cs new file mode 100644 index 0000000000..488e198c4d --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularImage.cs @@ -0,0 +1,11 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular; + +internal sealed class JxlModularImage +{ + public List Channels { get; set; } = []; + + +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlRct.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlRct.cs new file mode 100644 index 0000000000..35b8656615 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlRct.cs @@ -0,0 +1,115 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; + +/// +/// Reversible Color Transform (RCT) +/// +internal static class JxlRct +{ + public static void InverseRctRow(int transformType, Span in0, Span in1, Span in2, Span out0, Span out1, Span out2, int width) + { + DebugGuard.MustBeBetweenOrEqualTo(transformType, 0, 6, nameof(transformType)); + + int second = transformType >> 1; + int third = transformType & 1; + + int n = Vector.Count; + + if (transformType == 6) + { + // SIMD-aligned loop + int x; + for (x = 0; x + n - 1 < width; x += n) + { + Vector y = new(in0[x..]); + Vector co = new(in1[x..]); + Vector cg = new(in2[x..]); + y -= cg >> 1; + Vector g = cg + y; + y -= co >> 1; + Vector r = y + co; + r.CopyTo(out0[x..]); + g.CopyTo(out1[x..]); + y.CopyTo(out2[x..]); + } + + // Remainder (SIMD-unaligned) + for (; x < width; x++) + { + int y = in0[x]; + int co = in1[x]; + int cg = in2[x]; + int tmp = y + -(cg >> 1); + int g = cg + tmp; + int b = tmp + -(co >> 1); + int r = b + co; + out0[x] = r; + out1[x] = g; + out2[x] = b; + } + } + else + { + // SIMD-aligned loop + int x; + for (x = 0; x + n - 1 < width; x += n) + { + // Add a Vec suffix because the variables + // 'second' and 'third' are already defined. + // Though 'first' isn't, it's still suffixed + // for consistency. + Vector firstVec = new(in0[x..]); + Vector secondVec = new(in1[x..]); + Vector thirdVec = new(in2[x..]); + + if (third > 0) + { + thirdVec += firstVec; + } + + if (second == 1) + { + secondVec += firstVec; + } + else if (second == 2) + { + secondVec += (firstVec + thirdVec) >> 1; + } + + firstVec.CopyTo(out0[x..]); + secondVec.CopyTo(out1[x..]); + thirdVec.CopyTo(out2[x..]); + } + + // Remainder (SIMD-unaligned) + for (; x < width; x++) + { + int firstCoeff = in0[x]; + int secondCoeff = in1[x]; + int thirdCoeff = in2[x]; + + if (third > 0) + { + thirdCoeff += firstCoeff; + } + + if (second == 1) + { + secondCoeff += firstCoeff; + } + else if (second == 2) + { + secondCoeff += (firstCoeff + thirdCoeff) >> 1; + } + + out0[x] = firstCoeff; + out1[x] = secondCoeff; + out2[x] = thirdCoeff; + } + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueezeParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueezeParameters.cs new file mode 100644 index 0000000000..7bb52fe5e5 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueezeParameters.cs @@ -0,0 +1,74 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Fields; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; + +/// +/// Parameters for the squeeze transform. +/// +internal sealed class JxlSqueezeParameters : IJxlFields +{ + private bool horizontal; + private bool inPlace; + private uint beginC; + private uint numC; + + public JxlSqueezeParameters() => JxlBundle.Init(this); + + /// + /// Gets or sets a value indicating whether the transform is horizontal. + /// + public bool Horizontal + { + get => this.horizontal; + set => this.horizontal = value; + } + + /// + /// Gets or sets a value indicating whether the transform is in-place. + /// + public bool InPlace + { + get => this.inPlace; + set => this.inPlace = value; + } + + public uint BeginC + { + get => this.beginC; + set => this.beginC = value; + } + + public uint NumC + { + get => this.numC; + set => this.numC = value; + } + + public bool Visit(JxlVisitor visitor) + { + if (!visitor.Boolean(false, ref this.horizontal) || + !visitor.Boolean(false, ref this.inPlace) || + !visitor.U32( + JxlFieldExpressions.Bits(3), + JxlFieldExpressions.BitsOffset(6, 8), + JxlFieldExpressions.BitsOffset(10, 72), + JxlFieldExpressions.BitsOffset(13, 1096), + 0, + ref this.beginC) || + !visitor.U32( + JxlFieldExpressions.Value(1), + JxlFieldExpressions.Value(2), + JxlFieldExpressions.Value(3), + JxlFieldExpressions.BitsOffset(4, 4), + 2, + ref this.numC)) + { + return false; + } + + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs new file mode 100644 index 0000000000..7803010619 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs @@ -0,0 +1,11 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Fields; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; + +internal sealed class JxlTransform : IJxlFields +{ + public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransformType.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransformType.cs new file mode 100644 index 0000000000..0919425db1 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransformType.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; + +/// +/// Represents the type of a transform. +/// +internal enum JxlTransformType : byte +{ + /// + /// Reversible Color Transform + /// + Rct, + + /// + /// Palette/indexed coding + /// + Palette, + + /// + /// Haar-style squeezing + /// + Squeeze, + + /// + /// Invalid/unknown + /// + Invalid +} From dfd99a0d8a64bdebf22f1e9beb96e6dfeb829b03 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 24 Aug 2026 16:11:54 +0400 Subject: [PATCH 084/328] Reduce errors in AC strategy code --- src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs | 6 +++--- src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs index c6ecc8f16f..6da2926640 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs @@ -81,9 +81,9 @@ public JxlAcStrategy(int rawStrategy) public readonly JxlAcStrategyType Strategy { get; } - public void ComputeNaturalCoefficientOrder(ref int order) => CoefficientOrderAndLookup(this, false, ref order); + public readonly void ComputeNaturalCoefficientOrder(Span order) => CoefficientOrderAndLookup(this, false, order); - public void ComputeNaturalCoefficientOrderLookup(ref int lookup) => CoefficientOrderAndLookup(this, true, ref lookup); + public readonly void ComputeNaturalCoefficientOrderLookup(Span lookup) => CoefficientOrderAndLookup(this, true, lookup); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int GetTypeBit(JxlAcStrategyType type) => 1 << (int)type; @@ -97,7 +97,7 @@ private static void CoefficientOrderAndLookup(JxlAcStrategy strategy, bool isLoo int cx = strategy.CoveredBlocksX; int cy = strategy.CoveredBlocksY; - CoefficientLayout(ref cx, ref cy); + JxlForwardCoefficientOrder.CoefficientLayout(ref cx, ref cy); int xs = cx / cy; int xsm = xs - 1; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs index b4469baebc..236d518140 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs @@ -103,11 +103,11 @@ public bool Set(int x, int y, JxlAcStrategyType type) return this.SetNoBoundsChecks(x, y, type, check: false); } - public void FillDct8(in Rectangle rect) => this.FillPlane(((int)JxlAcStrategyType.DCT << 1) | 1, this.layers, in rect); + public void FillDct8(in Rectangle rect) => JxlImageOperations.FillPlane((byte)(((int)JxlAcStrategyType.DCT << 1) | 1), this.layers ?? throw new InvalidOperationException("Image is missing"), rect); public void FillDct8() => this.FillDct8(this.layers!.GetRectangle()); - public void FillInvalid() => this.FillImage(Invalid, this.layers); + public void FillInvalid() => JxlImageOperations.FillImage(Invalid, this.layers ?? throw new InvalidOperationException("Image is missing")); public void Dispose() { From a2f8fe6f2ca5b11ba1a0f0c7aed00946ff39eb9b Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 24 Aug 2026 16:33:14 +0400 Subject: [PATCH 085/328] Reduce errors --- .../Formats/Jxl/Cms/JxlOpsinConstants.cs | 24 +++++++++++++++++++ .../Decoder/JxlContextMapDecoder.cs | 8 +++++++ .../Jxl/Processing/Decoder/JxlXybDecoder.cs | 10 ++++---- .../Processing/Encoder/Ans/JxlHistogram.cs | 5 ---- .../Jxl/Processing/JxlColorCorrelation.cs | 3 ++- .../Jxl/Processing/JxlDctAcImage{T}.cs | 4 ++-- .../Formats/Jxl/Processing/JxlLoopFilter.cs | 9 +++---- .../Jxl/Processing/JxlWeightsSeparable5.cs | 1 - .../ContextPrediction/JxlModularState.cs | 16 ++++++------- .../Processing/Splines/JxlQuantizedSpline.cs | 15 ++++++------ 10 files changed, 62 insertions(+), 33 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlOpsinConstants.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlContextMapDecoder.cs diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlOpsinConstants.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlOpsinConstants.cs new file mode 100644 index 0000000000..a0029c0c27 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlOpsinConstants.cs @@ -0,0 +1,24 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms; + +/// +/// Opsin constants used by the color management system +/// +internal static class JxlOpsinConstants +{ + public const float BScale = 1f; + + // The following constants are used for XYB. + // They can be adjusted to change how Y<->B ratio + // works. For example, YToBRatio works better + // with 0.50017729543783418. + public const float YToBRatio = 1f; + public const float BToYRatio = 1f / YToBRatio; + + // Adjusting these constants influences the opsin absorbance. + public const float OpsinAbsorbanceBias0 = 0.0037930732552754493f; + public const float OpsinAbsorbanceBias1 = OpsinAbsorbanceBias0; + public const float OpsinAbsorbanceBias2 = OpsinAbsorbanceBias0; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlContextMapDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlContextMapDecoder.cs new file mode 100644 index 0000000000..78b989910a --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlContextMapDecoder.cs @@ -0,0 +1,8 @@ +using System; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +public class JxlContextMapDecoder +{ + +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs index 940b01170a..7fd7d54c6e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs @@ -33,9 +33,9 @@ public static void ConvertXybToRgb( ref Vector linearG, ref Vector linearB) { - Vector negBiasR = new(opsinParameters.OpsinBiaases[0]); - Vector negBiasG = new(opsinParameters.OpsinBiaases[1]); - Vector negBiasB = new(opsinParameters.OpsinBiaases[2]); + Vector negBiasR = new(opsinParameters.OpsinBiases[0]); + Vector negBiasG = new(opsinParameters.OpsinBiases[1]); + Vector negBiasB = new(opsinParameters.OpsinBiases[2]); Vector gammaR = opsinX + opsinY; Vector gammaG = opsinY - opsinX; @@ -49,7 +49,7 @@ public static void ConvertXybToRgb( Vector mixedG = (gammaG2 * gammaG) + negBiasG; Vector mixedB = (gammaB2 * gammaB) + negBiasB; - Span inverseMatrix = opsinParameters.GetInverseOpsinMatrixSpan(); + Span inverseMatrix = opsinParameters.InverseOpsinMatrix; linearR = LoadDuplicate128(ref inverseMatrix[0 * 4]) * mixedR; linearG = LoadDuplicate128(ref inverseMatrix[3 * 4]) * mixedR; @@ -66,7 +66,7 @@ public static void ConvertXybToRgb( public static bool OpsinToLinear(JxlImage3F opsin, Rectangle rect, JxlImage3F linear, JxlOpsinParameters opsinParameters) { - if (!SameSize(rect, linear)) + if (!JxlImageOperations.SameSize(rect, linear)) { return false; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHistogram.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHistogram.cs index 70a9321071..52637c9898 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHistogram.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHistogram.cs @@ -8,11 +8,6 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; /// internal sealed class JxlHistogram(int length) { - /// - /// Rounding constant - /// - private const int Rounding = 8; - public List Counts { get; set; } = new(length); public int TotalCount { get; set; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelation.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelation.cs index 69e617ea90..7047c2c7cb 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelation.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelation.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.Cms; using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; @@ -9,7 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing; internal sealed class JxlColorCorrelation { private float baseCorrelationX; - private float baseCorrelationB = DefaultYToBRatio; + private float baseCorrelationB = JxlOpsinConstants.YToBRatio; private readonly float[] dcFactors = new float[4]; private uint colorFactor = JxlChromaFromLuma.DefaultColorFactor; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcImage{T}.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcImage{T}.cs index 2052daee6e..8e74c26b72 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcImage{T}.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcImage{T}.cs @@ -23,9 +23,9 @@ public unsafe JxlDctAcImage(Configuration configuration, int width, int height) public bool IsEmpty => this.image.XSize == 0 || this.image.YSize == 0; - public void Clear() => JxlImageOperations.ClearImage(this.image); + public void Clear() => JxlImageOperations.ZeroFillImage(this.image); - public void Clear(int plane = 0) => JxlImageOperations.ClearImage(this.image); + public void Clear(int plane = 0) => JxlImageOperations.ZeroFillImage(this.image); public unsafe JxlDctAcPointer GetPlaneRow(int channel, int y, int xBase = 0) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs index 312138db43..2df002fba6 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs @@ -4,6 +4,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; @@ -181,13 +182,13 @@ public bool ComputeSigma(Rectangle blockRect, JxlPassesDecoderState state) JxlAcStrategyImage acStrategy = state.Shared.AcStrategy; float quantScale = state.Shared.Quantizer.Scale; - int sigmaStride = state.Sigma.PixelsPerRow; + int sigmaStride = state.Sigma!.PixelsPerRow; int sharpnessStride = state.Shared.EpfSharpness.PixelsPerRow; for (int by = 0; by < blockRect.Height; by++) { - Span sigmaRow = state.Sigma.GetRowSpan(by); - Span sharpnessRow = state.Shared.EpfSharpness.GetRowSpan(by); + Span sigmaRow = state.Sigma.GetRow(by); + Span sharpnessRow = state.Shared.EpfSharpness.GetRow(by); JxlAcStrategyRow acsRow = acStrategy.GetRow(in blockRect, by); Span rowQuant = state.Shared.RawQuantField.GetRow(by); @@ -245,7 +246,7 @@ public bool ComputeSigma(Rectangle blockRect, JxlPassesDecoderState state) } } - if (by + blockRect.Y + acs.CoveredBlocksX == state.Shared.FrameDimensions.YSizeBloks) + if (by + blockRect.Y + acs.CoveredBlocksX == state.Shared.FrameDimensions.YSizeBlocks) { for (int iy = 0; iy < SigmaBorder; iy++) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs index 7fdec43b03..b2a0c0c55f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs @@ -6,7 +6,6 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing; internal struct JxlWeightsSeparable5 { // Don't make these a property so we can ref into them. - public InlineArray12 Horizontal; public InlineArray12 Vertical; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs index e58897b4fa..f731dc9677 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs @@ -21,7 +21,7 @@ internal sealed class JxlModularState private readonly int[] error; private readonly JxlModularHeader header; - public JxlModularState(JxlModularHeader header, int width, int height) + public JxlModularState(JxlModularHeader header, int width) { this.header = header; @@ -104,9 +104,9 @@ public long Predict(bool computeProperties, int x, int y, int width, long n, lon int cur_row = yIsOdd ? 0 : (width + 2); int prev_row = yIsOdd ? (width + 2) : 0; - int pos_N = prev_row + x; - int pos_NE = x < width - 1 ? pos_N + 1 : pos_N; - int pos_NW = x > 0 ? pos_N - 1 : pos_N; + int posN = prev_row + x; + int posNE = x < width - 1 ? posN + 1 : posN; + int posNW = x > 0 ? posN - 1 : posN; Span weights = stackalloc uint[4]; ref uint headerW = ref this.header.GetWReference(); @@ -114,7 +114,7 @@ public long Predict(bool computeProperties, int x, int y, int width, long n, lon for (int i = 0; i < 4; i++) { Span error = this.predErrors[i].AsSpan(); - weights[i] = error[pos_N] + error[pos_NE] + error[pos_NW]; + weights[i] = error[posN] + error[posNE] + error[posNW]; weights[i] = ErrorWeight((int)weights[i], Unsafe.Add(ref headerW, i)); } @@ -125,10 +125,10 @@ public long Predict(bool computeProperties, int x, int y, int width, long n, lon nn = AddBits(nn); long teW = x == 0 ? 0 : this.error[cur_row + x - 1]; - long teN = this.error[pos_N]; - long teNW = this.error[pos_NW]; + long teN = this.error[posN]; + long teNW = this.error[posNW]; long sumWN = teN + teW; - long teNE = this.error[pos_NE]; + long teNE = this.error[posNE]; if (computeProperties) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs index 7e46f49b05..731c22835b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs @@ -50,10 +50,11 @@ public void ReserveControlPoints(Configuration configuration, int n) public static JxlQuantizedSpline Create(Configuration configuration, JxlSpline original, int quantizationAdjustment, float yToX, float yToB) { JxlQuantizedSpline spline = new(); + Span controlPoints = original.ControlPoints.Span; - spline.ReserveControlPoints(configuration, original.ControlPoints.Count - 1); + spline.ReserveControlPoints(configuration, controlPoints.Length - 1); - PointF startingPoint = original.ControlPoints.First(); + PointF startingPoint = controlPoints[0]; int previousX = (int)MathF.Round(startingPoint.X); int previousY = (int)MathF.Round(startingPoint.Y); int previousDx = 0; // D stands for delta @@ -65,7 +66,7 @@ public static JxlQuantizedSpline Create(Configuration configuration, JxlSpline o for (int i = 0; i < length; i++) { - PointF controlPoint = original.ControlPoints[i]; + PointF controlPoint = controlPoints[i]; int newX = (int)MathF.Round(controlPoint.X); int newY = (int)MathF.Round(controlPoint.Y); @@ -93,8 +94,8 @@ public static JxlQuantizedSpline Create(Configuration configuration, JxlSpline o // for i=0 and adding a separate loop for i=1..31 for (int i = 0; i < 32; i++) { - float dctFactor = (i == 0) ? Sqrt2 : 1.0f; - float inverseDctFactor = (i == 0) ? Sqrt05 : 1.0f; + float dctFactor = (i == 0) ? JxlDctScales.Sqrt2 : 1.0f; + float inverseDctFactor = (i == 0) ? JxlDctScales.Sqrt05 : 1.0f; float restoredY = spline.ColorDct[1][i] * inverseDctFactor * ChannelWeight[1] * inverseQuant; float decorrelated = spline.ColorDct[c][i] - (factor * restoredY); spline.ColorDct[c][i] = ConvertToInteger(decorrelated * dctFactor * quant / ChannelWeight[c]); @@ -103,7 +104,7 @@ public static JxlQuantizedSpline Create(Configuration configuration, JxlSpline o for (int i = 0; i < 32; i++) { - float dctFactor = (i == 0) ? Sqrt2 : 1.0f; + float dctFactor = (i == 0) ? JxlDctScales.Sqrt2 : 1.0f; spline.SigmaDct[i] = ConvertToInteger(original.SigmaDct[i] * dctFactor * quant / ChannelWeight[1]); } @@ -190,7 +191,7 @@ public bool Dequantize( { for (int i = 0; i < 32; i++) { - float inverseDctFactor = (i == 0) ? Sqrt05 : 1.0f; + float inverseDctFactor = (i == 0) ? JxlDctScales.Sqrt05 : 1.0f; result.ColorDct[c][i] = this.ColorDct[c][i] * inverseDctFactor * ChannelWeight[c] * inverseQuant; } } From 352b14df9a2d97ec5e077787de572a2f45e0c2fa Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 24 Aug 2026 21:17:12 +0400 Subject: [PATCH 086/328] Reduce errors, remove unused constant --- .../Formats/Jxl/Processing/JxlLoopFilter.cs | 5 ----- .../Formats/Jxl/Processing/JxlQuantizer.cs | 18 +++++++++--------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs index 2df002fba6..825c81cbbc 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs @@ -21,11 +21,6 @@ internal sealed class JxlLoopFilter : IJxlFields /// private const float InverseSigmaNum = -1.1715728752538099024f; - /// - /// kInvSigmaNum / 0.3 - /// - private const float MinSigma = -3.90524291751269967465540850526868f; - /// /// Gets the number of EPF (Edge-preserving filter) sharp entries. /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs index ef9e360bb1..9551b97e09 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs @@ -52,12 +52,12 @@ internal sealed class JxlQuantizer /// /// Represents the multipliers for the DC coefficients. /// - private readonly InlineArray4 mulDc; + private InlineArray4 mulDc; /// /// Represents the inverse multipliers for the DC coefficients. /// - private readonly InlineArray4 inverseMulDc; + private InlineArray4 inverseMulDc; /// /// Global scale @@ -148,8 +148,8 @@ public JxlQuantizer(JxlDequantMatrices dequant, int quantDc, int globalScale) /// public void ClearDcMultipliers() { - Array.Fill(this.mulDc, 1f); - Array.Fill(this.inverseMulDc, 1f); + ((Span)this.mulDc).Fill(1f); + ((Span)this.inverseMulDc).Fill(1f); } /// @@ -303,8 +303,8 @@ public void SetQuantFieldRect(JxlImageF qf, in Rectangle rect, JxlImageI rawQuan { for (int y = 0; y < rect.Height; y++) { - ReadOnlySpan rowQf = qf.GetRow(in rect, y); - Span rowQi = rawQuantField.GetRow(in rect, y); + ReadOnlySpan rowQf = qf.GetRow(rect, y); + Span rowQi = rawQuantField.GetRow(rect, y); for (int x = 0; x < rect.Width; x++) { @@ -352,7 +352,7 @@ public bool SetQuantField(Configuration configuration, float quantDc, JxlImageF if (rawQuantField != null) { - if (rawQuantField.GetSize() != qf.GetSize()) + if (rawQuantField.GetRectangle() != qf.GetRectangle()) { data.Dispose(); deviations.Dispose(); @@ -360,7 +360,7 @@ public bool SetQuantField(Configuration configuration, float quantDc, JxlImageF return false; } - this.SetQuantField(qf, qf.GetRectangle(), rawQuantField); + this.SetQuantFieldRect(qf, qf.GetRectangle(), rawQuantField); } data.Dispose(); @@ -374,6 +374,6 @@ public void SetQuant(float quantDc, float quantAc, JxlImageI rawQuantField) this.ComputeGlobalScaleAndQuant(quantDc, quantAc, 0); int value = Clamp((quantAc * this.InverseGlobalScale) + 0.5f); - rawQuantField.Fill(value); + JxlImageOperations.FillImage(value, rawQuantField); } } From 093d84612c2901eb95a1043b700341281d8bed51 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 24 Aug 2026 21:40:59 +0400 Subject: [PATCH 087/328] Finish inverse RCT --- .../Processing/Modular/Transforms/JxlRct.cs | 83 ++++++++++++++++++- .../Modular/Transforms/JxlTransform.cs | 28 +++++++ 2 files changed, 110 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlRct.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlRct.cs index 35b8656615..ab55b12974 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlRct.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlRct.cs @@ -10,10 +10,22 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; /// internal static class JxlRct { - public static void InverseRctRow(int transformType, Span in0, Span in1, Span in2, Span out0, Span out1, Span out2, int width) + /// + /// Performs Inverse Reversible Color Transform (RCT) on one row. + /// + /// The kind of RCT. + /// Input Y + /// Input Co + /// Input Cg + /// Output R + /// Output G + /// Output B + private static void InverseRctRow(int transformType, Span in0, Span in1, Span in2, Span out0, Span out1, Span out2) { DebugGuard.MustBeBetweenOrEqualTo(transformType, 0, 6, nameof(transformType)); + int width = in0.Length; // All input & output channels have equal widths + int second = transformType >> 1; int third = transformType & 1; @@ -112,4 +124,73 @@ public static void InverseRctRow(int transformType, Span in0, Span in1 } } } + + /// + /// Performs Inverse Reversible Color Transform (RCT) on an entire + /// image. + /// + /// + /// The configuration is used to access maximum degree of parallelism. + /// + /// + /// Image to compute inverse RCT. + /// + /// + /// Offset of the color channel for Y, Co, Cg. + /// + /// + /// Type of Reversible Color Transform + /// + /// + /// Invoked when RCT/permutation is invalid. + /// + public static void InverseRct(Configuration configuration, JxlModularImage img, int beginC, int rctType) + { + JxlTransform.CheckEqualChannels(img, beginC, beginC + 2); + + int m = beginC; + JxlModularChannel c0 = img.Channels[m + 0]; + int w = c0.Width; + int h = c0.Height; + + if (rctType == 0) + { + // No-op + return; + } + + int permutation = rctType / 7; + + if (permutation >= 7) + { + throw new InvalidOperationException("Permutation must be <= 6"); + } + + int custom = rctType % 7; + + if (custom == 0) + { + // Permute-only. + JxlModularChannel ch0 = img.Channels[m]; + JxlModularChannel ch1 = img.Channels[m + 1]; + JxlModularChannel ch2 = img.Channels[m + 2]; + img.Channels[m + (permutation % 3)] = ch0; + img.Channels[m + ((permutation + 1 + (permutation / 3)) % 3)] = ch1; + img.Channels[m + ((permutation + 2 - (permutation / 3)) % 3)] = ch2; + return; + } + + _ = Parallel.For(0, configuration.MaxDegreeOfParallelism, y => + { + Span in0 = img.Channels[m].GetRow(y); + Span in1 = img.Channels[m + 1].GetRow(y); + Span in2 = img.Channels[m + 2].GetRow(y); + + Span out0 = img.Channels[m + (permutation % 3)].GetRow(y); + Span out1 = img.Channels[m + ((permutation + 1 + (permutation / 3)) % 3)].GetRow(y); + Span out2 = img.Channels[m + ((permutation + 2 - (permutation / 3)) % 3)].GetRow(y); + + InverseRctRow(custom, in0, in1, in2, out0, out1, out2); + }); + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs index 7803010619..860adb4692 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs @@ -8,4 +8,32 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; internal sealed class JxlTransform : IJxlFields { public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); + + public static void CheckEqualChannels(JxlModularImage image, int c1, int c2) + { + int channelsCount = image.Channels.Count; + + if (c1 > channelsCount || c2 >= channelsCount || c2 < c1) + { + throw new InvalidOperationException($"Invalid channel range: {c1}..{c2} (there are only {channelsCount} channels)"); + } + + if (c1 < image.NbMetaChannels && c2 >= image.NbMetaChannels) + { + throw new InvalidOperationException("Invalid: transforming mix of meta and nonmeta"); + } + + JxlModularChannel ch1 = image.Channels[c1]; + for (int c = c1 + 1; c <= c2; c++) + { + JxlModularChannel ch2 = image.Channels[c]; + if (ch1.Width != ch2.Width || + ch1.Height != ch2.Height || + ch1.HorizontalShift != ch2.HorizontalShift || + ch1.VerticalShift != ch2.VerticalShift) + { + throw new InvalidOperationException($"Channel {c} is not equal"); + } + } + } } From c5a7bb876256c5971387a15a80d0bae077cc3b6b Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 24 Aug 2026 22:37:05 +0400 Subject: [PATCH 088/328] Complete RCT --- .../Processing/Modular/Transforms/JxlRct.cs | 210 +++++++++++++++++- 1 file changed, 208 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlRct.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlRct.cs index ab55b12974..0f32e47aee 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlRct.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlRct.cs @@ -13,7 +13,9 @@ internal static class JxlRct /// /// Performs Inverse Reversible Color Transform (RCT) on one row. /// - /// The kind of RCT. + /// + /// The kind of RCT. + /// /// Input Y /// Input Co /// Input Cg @@ -180,7 +182,7 @@ public static void InverseRct(Configuration configuration, JxlModularImage img, return; } - _ = Parallel.For(0, configuration.MaxDegreeOfParallelism, y => + _ = Parallel.For(0, h, configuration.GetParallelOptions(), y => { Span in0 = img.Channels[m].GetRow(y); Span in1 = img.Channels[m + 1].GetRow(y); @@ -193,4 +195,208 @@ public static void InverseRct(Configuration configuration, JxlModularImage img, InverseRctRow(custom, in0, in1, in2, out0, out1, out2); }); } + + /// + /// Performs Reversible Color Transform (RCT) on one row. + /// + /// + /// The type of RCT transform method. + /// + /// R + /// G + /// B + /// Y + /// Co + /// Cg + private static void ForwardRctRow(int transform, Span in0, Span in1, Span in2, Span out0, Span out1, Span out2) + { + DebugGuard.MustBeLessThanOrEqualTo(transform, 6, nameof(transform)); + + int second = transform >> 1; + int third = transform & 1; + + int width = in0.Length; // All input & output channels have equal widths + + if (!Vector.IsHardwareAccelerated || Vector.Count == 1) + { + // No SIMD support. Use scalar. + if (transform == 6) + { + for (int x = 0; x < width; x++) + { + int r = in0[x]; + int g = in1[x]; + int b = in2[x]; + int o1 = r - b; + int tmp = b + (o1 >> 1); + int o2 = g - tmp; + out0[x] = tmp + (o2 >> 1); + out1[x] = o1; + out2[x] = o2; + } + } + else + { + for (int x = 0; x < width; x++) + { + int firstCoeff = in0[x]; + int secondCoeff = in1[x]; + int thirdCoeff = in2[x]; + + if (second == 1) + { + secondCoeff -= firstCoeff; + } + else if (second == 2) + { + secondCoeff -= (firstCoeff + thirdCoeff) >> 1; + } + + if (third != 0) + { + thirdCoeff -= firstCoeff; + } + + out0[x] = firstCoeff; + out1[x] = secondCoeff; + out2[x] = thirdCoeff; + } + } + } + else + { + // Have SIMD support + int lanes = Vector.Count; + + if (transform == 6) + { + for (int x = 0; x < width; x += lanes) + { + Vector r = new(in0[x..]); + Vector g = new(in1[x..]); + Vector b = new(in2[x..]); + Vector o1 = r - b; + Vector tmp = b + (o1 >> 1); + Vector o2 = g - tmp; + Vector o0 = tmp + (o2 >> 1); + o0.CopyTo(out0[x..]); + o1.CopyTo(out1[x..]); + o2.CopyTo(out2[x..]); + } + } + else + { + for (int x = 0; x < width; x += lanes) + { + Vector i0 = new(in0[x..]); + Vector i1 = new(in1[x..]); + Vector i2 = new(in2[x..]); + Vector o1 = i1; + + // TODO: duplicate loops for second == 1, second == 2 + // and otherwise? We should reduce the number of branches in + // loops. + if (second == 1) + { + o1 -= i0; + } + else if (second == 2) + { + o1 -= (i0 + i2) >> 1; + } + + Vector o2 = i2; + + if (third != 0) + { + o2 -= i0; + } + + i0.CopyTo(out0[x..]); + o1.CopyTo(out1[x..]); + o2.CopyTo(out2[x..]); + } + } + } + } + + private static void RctPermute(InlineArray3 input, int permutation, ref InlineArray3 output) + { + output[0] = input[permutation % 3]; + output[1] = input[(permutation + 1 + (permutation / 3)) % 3]; + output[2] = input[(permutation + 2 - (permutation / 3)) % 3]; + } + + /// + /// Performs Forward Reversible Color Transform. (Internal method) + /// + /// + /// Configuration for parallelism. + /// + /// + /// Input channels. + /// + /// + /// Output channels. + /// + /// + /// Kind of RCT. + /// + private static void ForwardRctCore(Configuration configuration, InlineArray3 input, InlineArray3 output, int rctType) + { + int permutation = rctType / 7; + int transform = rctType % 7; + + InlineArray3 inp = default; + RctPermute(input, permutation, ref inp); + + int width = output[0].Width; + int height = output[0].Height; + + _ = Parallel.For(0, height, configuration.GetParallelOptions(), y => + { + Span i0 = inp[0].GetRow(y); + Span i1 = inp[1].GetRow(y); + Span i2 = inp[2].GetRow(y); + + Span o0 = output[0].GetRow(y); + Span o1 = output[1].GetRow(y); + Span o2 = output[2].GetRow(y); + + ForwardRctRow(transform, i0, i1, i2, o0, o1, o2); + }); + } + + /// + /// Performs forward Reversible Color Transform (RCT). + /// + /// + /// Configuration for parallelism. + /// + /// + /// Image to perform Reversible Color Transform. + /// + /// + /// Offset of the color channel. + /// + /// + /// Kind of RCT. + /// + public static void ForwardRct(Configuration configuration, JxlModularImage image, int beginC, int rctType) + { + JxlTransform.CheckEqualChannels(image, beginC, beginC + 2); + + if (rctType == 0) + { + // No-op + return; + } + + InlineArray3 channels = default; + channels[0] = image.Channels[beginC]; + channels[1] = image.Channels[beginC + 1]; + channels[2] = image.Channels[beginC + 2]; + + return ForwardRct(configuration, new JxlModularImage(channels[0], channels[1], channels[2]), beginC, rctType); + } } From c8b0a5d9cbf00ae20c8c2ba704150453599134c8 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 24 Aug 2026 23:31:30 +0400 Subject: [PATCH 089/328] Simplify decoder core & add palette shared methods --- .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 92 +++-------- .../Modular/Transforms/JxlPalette.cs | 152 ++++++++++++++++++ 2 files changed, 173 insertions(+), 71 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index d1edd36b9d..30dc630ff2 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -1525,10 +1525,9 @@ private bool ReadBundle(Span data, JxlBitReader br, T bundle) /// /// Reads all basic metadata and headers. /// - /// Status of the parsing. /// Thrown if the data is incorrect. /// Thrown if the data is malformed. - public bool ReadBasicInfo(Stream stream) + public void ReadBasicInfo(Stream stream) { if (!this.gotCodestreamSignature) { @@ -1568,16 +1567,13 @@ public bool ReadBasicInfo(Stream stream) { throw new InvalidOperationException("The image is too large"); } - - return true; } /// /// Parses all necessary headers. /// - /// Status of the parsing. /// Thrown if data is incorrect. - public bool ReadAllHeaders() + public void ReadAllHeaders() { if (!this.gotTransformData) { @@ -1635,8 +1631,6 @@ public bool ReadAllHeaders() } this.imageMetadata = this.metadata.ImageMetadata; - - return true; } /// @@ -1651,8 +1645,8 @@ public void ProcessSections() var toc = this.frameDecoder!.Toc; long pos = 0; - List sectionInfo = []; - List sectionStatus = []; + List sectionInfo = []; + List sectionStatus = []; for (long i = this.nextSection; i < toc.Size; i++) { @@ -1671,7 +1665,7 @@ public void ProcessSections() } JxlBitReader br = new(span.Slice((int)pos, (int)size)); - sectionInfo.Add(new(br, id, i)); + sectionInfo.Add(new(br, (int)id, (int)i)); sectionStatus.Add(default); pos += size; } @@ -1680,7 +1674,7 @@ public void ProcessSections() bool outOfBounds = false; - foreach (JxlFrameDecoder.SectionInfo info in sectionInfo) + foreach (JxlSectionInfo info in sectionInfo) { if (!info.BitReader.AllReadsWithinBounds) { @@ -1696,13 +1690,13 @@ public void ProcessSections() for (int i = 0; i < sectionStatus.Count; i++) { - JxlFrameDecoder.SectionStatus ss = sectionStatus[i]; + JxlSectionStatus ss = sectionStatus[i]; - if (ss == JxlFrameDecoder.Done) + if (ss == JxlSectionStatus.Done) { this.sectionProcessed[sectionInfo[i].Index] = 1; } - else if (ss != JxlFrameDecoder.Skipped) + else if (ss != JxlSectionStatus.Skipped) { throw new InvalidOperationException("Unexpected section status"); } @@ -1729,12 +1723,7 @@ public int ProcessCodestream() { if (!this.gotBasicInfo) { - bool status = this.ReadBasicInfo(); - - if (!status) - { - throw new InvalidOperationException("Could not parse basic info"); - } + this.ReadBasicInfo(); } if ((this.eventsWanted & BasicInfo) != 0) @@ -1751,12 +1740,7 @@ public int ProcessCodestream() if (!this.gotAllHeaders) { - bool status = this.ReadAllHeaders(); - - if (!status) - { - throw new InvalidOperationException("Could not parse headers"); - } + this.ReadAllHeaders(); } if ((this.eventsWanted & ColorEncoding) != 0) @@ -1781,12 +1765,7 @@ public int ProcessCodestream() while (true) { bool parseFrames = (this.eventsWanted & (PreviewImage | DecodedFrame | FullImage)) != 0; - if (!parseFrames) - { - break; - } - - if (this.frameStage == JxlFrameStage.Header && this.isLastTotal) + if (!parseFrames || (this.frameStage == JxlFrameStage.Header && this.isLastTotal)) { break; } @@ -1934,8 +1913,7 @@ public int ProcessCodestream() this.frameDecoder.SetRenderSpotcolors(this.renderSpotcolors); this.frameDecoder.SetCoalescing(this.coalescing); - if (!this.previewFrame && - (this.eventsWanted & FrameProgression) != 0) + if (!this.previewFrame && (this.eventsWanted & FrameProgression) != 0) { this.frameProgressiveDetail = this.frameDecoder.SetPauseAtProgressive(this.progressiveDetail); } @@ -2312,20 +2290,12 @@ public int ProcessBoxes(Stream stream) if (this.reconstructionExifSize > 0) { - int status = JxlToJpegDecoder.SetExif(this.exifMetadata!.Memory, jpegData); - if (status != Success) - { - return status; - } + JxlToJpegDecoder.SetExif(this.exifMetadata!.Memory, jpegData); } if (this.reconstructionXmpSize > 0) { - int status = JxlToJpegDecoder.SetXmp(this.xmpMetadata!.Memory, jpegData); - if (status != Success) - { - return status; - } + JxlToJpegDecoder.SetXmp(this.xmpMetadata!.Memory, jpegData); } this.reconstructionOutputJpeg = JpegReconstructionStage.Output; @@ -2333,11 +2303,7 @@ public int ProcessBoxes(Stream stream) if (this.reconstructionOutputJpeg == JpegReconstructionStage.Output && !this.JbrdNeedsMoreBoxes()) { - int status = this.jpegDecoder!.WriteOutput(this.imageBundle!.JpegData); - if (status != Success) - { - return status; - } + this.jpegDecoder!.WriteOutput(this.imageBundle!.JpegData); this.reconstructionOutputJpeg = JpegReconstructionStage.None; this.imageBundle.Reset(); @@ -2365,22 +2331,12 @@ public int ProcessBoxes(Stream stream) if (this.availableInput == 0) { - if (this.decoderStage != JxlDecoderStage.CodeStreamFinished) - { - return NeedMoreInput; - } - - if (this.JbrdNeedsMoreBoxes()) + if (this.decoderStage != JxlDecoderStage.CodeStreamFinished || this.JbrdNeedsMoreBoxes()) { return NeedMoreInput; } - if (this.inputClosed) - { - return Success; - } - - if ((this.eventsWanted & Box) != 0) + if (this.inputClosed || (this.eventsWanted & Box) != 0) { return Success; } @@ -2610,12 +2566,11 @@ public int ProcessBoxes(Stream stream) { bool hasMoreData = this.TryInjectNextBufferedJxlpBox(); - if (hasMoreData) + if (!hasMoreData) { - continue; + this.boxStage = JxlBoxStage.Header; } - this.boxStage = JxlBoxStage.Header; continue; } } @@ -2732,12 +2687,7 @@ public int ProcessBoxes(Stream stream) { if (this.boxContentsUnbounded) { - if (this.inputClosed) - { - return Success; - } - - if (!this.boxOutBufferSet) + if (this.inputClosed || !this.boxOutBufferSet) { return Success; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs new file mode 100644 index 0000000000..4babb79f32 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs @@ -0,0 +1,152 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; + +/// +/// Palette/indexed coding +/// +internal static class JxlPalette +{ + /// + /// Represents maximum number of colors in a palette. + /// + private const int MaxPaletteLookupTableSize = 1 << 16; + + /// + /// Represents number of channels for RGB. This is 3 because RGB + /// has three channels: R (Red), G (Green), B (Blue). + /// + private const int RgbChannels = 3; + + /// + /// 5x5x5 color cube for the larger cube. + /// + private const int LargeCube = 5; + + /// + /// Smaller interleaved color cube to fill the holes of the larger cube. + /// + private const int SmallCube = 4; + + /// + /// Number of bits required to represent a small cube. + /// + private const int SmallCubeBits = 2; // 2 bits gives us 0..3 inclusive, so it's perfect to represent a small cube + + /// + /// Cube of SmallCube + /// + private const int LargeCubeOffset = SmallCube * SmallCube * SmallCube; + + private const int ImplicitPaletteSize = LargeCubeOffset + (LargeCube * LargeCube * LargeCube); + + /// + /// Static delta palette used by GetPaletteValue. + /// + private static readonly int[][] DeltaPalette = + [ + [0, 0, 0], [4, 4, 4], [11, 0, 0], + [0, 0, -13], [0, -12, 0], [-10, -10, -10], + [-18, -18, -18], [-27, -27, -27], [-18, -18, 0], + [0, 0, -32], [-32, 0, 0], [-37, -37, -37], + [0, -32, -32], [24, 24, 45], [50, 50, 50], + [-45, -24, -24], [-24, -45, -45], [0, -24, -24], + [-34, -34, 0], [-24, 0, -24], [-45, -45, -24], + [64, 64, 64], [-32, 0, -32], [0, -32, 0], + [-32, 0, 32], [-24, -45, -24], [45, 24, 45], + [24, -24, -45], [-45, -24, 24], [80, 80, 80], + [64, 0, 0], [0, 0, -64], [0, -64, -64], + [-24, -24, 45], [96, 96, 96], [64, 64, 0], + [45, -24, -24], [34, -34, 0], [112, 112, 112], + [24, -45, -45], [45, 45, -24], [0, -32, 32], + [24, -24, 45], [0, 96, 96], [45, -24, 24], + [24, -45, -24], [-24, -45, 24], [0, -64, 0], + [96, 0, 0], [128, 128, 128], [64, 0, 64], + [144, 144, 144], [96, 96, 0], [-36, -36, 36], + [45, -24, -45], [45, -45, -24], [0, 0, -96], + [0, 128, 128], [0, 96, 0], [45, 24, -45], + [-128, 0, 0], [24, -45, 24], [-45, 24, -45], + [64, 0, -64], [64, -64, -64], [96, 0, 96], + [45, -45, 24], [24, 45, -45], [64, 64, -64], + [128, 128, 0], [0, 0, -128], [-24, 45, -45] + ]; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Scale(int denominator, int value, int bitDepth) + { + DebugGuard.IsTrue(denominator == 4, "Denominator is defined as 4"); + + return (value * ((1 << bitDepth) - 1)) >> 2; + } + + public static int GetPaletteValue(Span palette, int index, int c, int oneRow, int bitDepth) + { + if (index < 0) + { + if (c >= RgbChannels) + { + return 0; + } + + index = -(index + 1); + index %= 1 + (2 * (DeltaPalette.Length - 1)); + + // JPEG XL reference uses: + // static constexpr int kMultiplier[] = {-1, 1}; + // kMultiplier[index & 1] + // + // we use: + // ((index & 1) != 0 ? 1 : -1) + // + // The latter avoids multiplication and can make use + // of CPU registers instead of memory access (if the JIT allows it). + int result = DeltaPalette[(index + 1) >> 1][c] * ((index & 1) != 0 ? 1 : -1); + + if (bitDepth > 8) + { + result *= 1 << (bitDepth - 8); + } + + return result; + } + else if (palette.Length <= index && index < palette.Length + LargeCubeOffset) + { + if (c >= RgbChannels) + { + return 0; + } + + index -= palette.Length; + index >>= c * SmallCubeBits; + return Scale(SmallCube, index % SmallCube, bitDepth) + (1 << Math.Max(0, bitDepth - 3)); + } + else if (palette.Length + LargeCubeOffset <= index) + { + if (c >= RgbChannels) + { + return 0; + } + + switch (c) + { + case 1: + index /= LargeCube; + break; + + case 2: + index /= LargeCube * LargeCube; + break; + + default: + break; + } + + return Scale(LargeCube - 1, index % LargeCube, bitDepth); + } + + return palette[(c * oneRow) + index]; + } +} From c079cf9165a30b24764ea6a6e64218236b6074a7 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 24 Aug 2026 23:49:27 +0400 Subject: [PATCH 090/328] Add missing types, rename JxlBitDepth metadata to JxlBitDepthMetadata, reduce errors in JxlDecoderCore --- ...{JxlBitDepth.cs => JxlBitDepthMetadata.cs} | 6 ++-- .../Jxl/IO/Metadata/JxlExtraChannelInfo.cs | 2 +- .../Jxl/IO/Metadata/JxlImageMetadata.cs | 2 +- .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 36 +++---------------- .../Formats/Jxl/Processing/JxlBitDepth.cs | 27 ++++++++++++++ .../Formats/Jxl/Processing/JxlBitDepthType.cs | 35 ++++++++++++++++++ .../Formats/Jxl/Processing/JxlDataType.cs | 31 ++++++++++++++++ .../Formats/Jxl/Processing/JxlEndianness.cs | 25 +++++++++++++ .../Formats/Jxl/Processing/JxlPixelFormat.cs | 34 ++++++++++++++++++ 9 files changed, 162 insertions(+), 36 deletions(-) rename src/ImageSharp/Formats/Jxl/IO/Metadata/{JxlBitDepth.cs => JxlBitDepthMetadata.cs} (96%) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlBitDepth.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlBitDepthType.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlDataType.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlEndianness.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlPixelFormat.cs diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlBitDepth.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlBitDepthMetadata.cs similarity index 96% rename from src/ImageSharp/Formats/Jxl/IO/Metadata/JxlBitDepth.cs rename to src/ImageSharp/Formats/Jxl/IO/Metadata/JxlBitDepthMetadata.cs index 0a9db0f575..a6c9f72482 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlBitDepth.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlBitDepthMetadata.cs @@ -8,15 +8,15 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; /// /// Represents the JPEG XL Bit Depth image metadata. /// -internal sealed class JxlBitDepth : IJxlFields +internal sealed class JxlBitDepthMetadata : IJxlFields { private uint bitsPerSample; private uint exponentBitsPerSample; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public JxlBitDepth() => JxlBundle.Init(this); + public JxlBitDepthMetadata() => JxlBundle.Init(this); /// /// Gets or sets a value indicating whether diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs index 5b492b1026..16e149d1d4 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs @@ -11,7 +11,7 @@ internal sealed class JxlExtraChannelInfo : IJxlFields public JxlExtraChannel Type { get; set; } - public JxlBitDepth? BitDepth { get; set; } + public JxlBitDepthMetadata? BitDepth { get; set; } public int DimensionShift { get; set; } diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs index 1fcf00b6fe..e05bb09512 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs @@ -10,7 +10,7 @@ internal sealed class JxlImageMetadata : IJxlFields { public bool AllDefault { get; set; } - public JxlBitDepth? BitDepth { get; set; } + public JxlBitDepthMetadata? BitDepth { get; set; } public bool Modular16BitBufferSufficient { get; set; } // Otherwise, 32 is diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index 30dc630ff2..35c52cc460 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -442,7 +442,7 @@ internal sealed class JxlDecoderCore : ImageDecoderCore, IDisposable /// /// Bit depth for image output. /// - private JxlBitDepth imageOutputBitDepth = new(); + private JxlBitDepthMetadata imageOutputBitDepth = new(); public JxlDecoderCore(DecoderOptions options) : base(options) @@ -595,32 +595,6 @@ private enum JxlSignature : byte Container } - /// - /// Represents a data type. - /// - private enum JxlDataType : byte - { - /// - /// - /// - UInt8, - - /// - /// - /// - UInt16, - - /// - /// - /// - Float, - - /// - /// - /// - Float16 - } - /// /// Frame stage for this decoder. /// @@ -858,12 +832,12 @@ private static JxlSignature DetectSignature(Stream buffer) return JxlSignature.Invalid; } - private static int BitsPerChannel(JxlDataType dataType) + private static uint BitsPerChannel(JxlDataType dataType) => dataType switch { - JxlDataType.UInt8 => 8, - JxlDataType.UInt16 or JxlDataType.Float16 => 16, - JxlDataType.Float => 32, + JxlDataType.Byte => 8, + JxlDataType.UInt16 or JxlDataType.Half => 16, + JxlDataType.Single => 32, _ => 0 }; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlBitDepth.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlBitDepth.cs new file mode 100644 index 0000000000..52b384bb94 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlBitDepth.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Describes the interpretation of the input and output +/// buffers. +/// +internal struct JxlBitDepth +{ + /// + /// Gets or sets the kind of bit depth. + /// + public JxlBitDepthType Type { get; set; } + + /// + /// Gets or sets the number of bits per sample when the + /// bit depth type is custom. + /// + public uint BitsPerSample { get; set; } + + /// + /// Gets or sets the custom exponent bits per sample. + /// + public uint ExponentBitsPerSample { get; set; } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlBitDepthType.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlBitDepthType.cs new file mode 100644 index 0000000000..e942d57a47 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlBitDepthType.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Specifies the kind of bit depth. +/// +internal enum JxlBitDepthType : byte +{ + /// + /// Default setting, where the encoder expects the + /// input pixels to use the full range of the pixel format + /// data type (e.g. for ushort, the input range is 0..65535 + /// and the value 65535 is mapped to 1.0 when converting + /// to float), and the decoder uses the full range to output + /// pixels. + /// + FromPixelFormat, + + /// + /// When selected, the encoder expects the input pixels + /// to be in the range defined by the bits per sample value of the + /// basic info (e.g., for 12-bit images using ushort data types + /// the range is 0..4095 and the 4095 value is mapped to 1.0 when + /// converting to float), and the decoder outputs pixels in + /// this range. + /// + FromCodeStream, + + /// + /// Specifies custom ranges for pixel outputs. + /// + Custom = 2, +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDataType.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlDataType.cs new file mode 100644 index 0000000000..a9c8057e6f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlDataType.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Specifies which data type to use for sample values +/// per channel per pixel. +/// +internal enum JxlDataType : byte +{ + /// + /// Use float + /// + Single = 0, + + /// + /// Use byte. May clip wide color gamut data. + /// + Byte = 2, + + /// + /// Use ushort. May clip wide color gamut data. + /// + UInt16 = 3, + + /// + /// Use 16-bit IEEE 754 half-precision floating-point values. + /// + Half = 5 +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlEndianness.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlEndianness.cs new file mode 100644 index 0000000000..a7c9dbc1a2 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlEndianness.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Specifies the ordering of multi-byte data. +/// +internal enum JxlEndianness : byte +{ + /// + /// Use endianness of the CPU/system. + /// + Native, + + /// + /// Force little endian. + /// + Little, + + /// + /// Force big endian. + /// + Big +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlPixelFormat.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlPixelFormat.cs new file mode 100644 index 0000000000..15aa587159 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlPixelFormat.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Data type for the sample values per channel per pixel +/// for the output buffer for pixels. +/// +internal struct JxlPixelFormat +{ + /// + /// Gets or sets the amount of channels available in a pixel buffer. + /// + public int Channels { get; set; } + + /// + /// Gets or sets the data type of each channel. + /// + public JxlDataType DataType { get; set; } + + /// + /// Gets or sets a value that denotes whether multi-byte data types are represented in + /// big-endian or little-endian format. Applies to ushort + /// and float data types. + /// + public JxlEndianness Endianness { get; set; } + + /// + /// Gets or sets the alignment of scanlines to a multiple of + /// align bytes. + /// + public int Align { get; set; } +} From 7e236700ad16fc30b89582a23d6e7cac1eabbf32 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 24 Aug 2026 23:57:39 +0400 Subject: [PATCH 091/328] Add CodestreamMarker, don't manually check for out-of-bounds Bit Reader should automatically throw if it reads out of bounds anyway --- .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 36 ++++++------------- .../Formats/Jxl/Processing/JxlShared.cs | 7 ++++ 2 files changed, 17 insertions(+), 26 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index 35c52cc460..0287760e43 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -223,7 +223,7 @@ internal sealed class JxlDecoderCore : ImageDecoderCore, IDisposable /// /// Output data for extra channels. /// - private List extraChannelOutputs = []; + private readonly List extraChannelOutputs = []; /// /// Codec metadata if present. @@ -255,7 +255,7 @@ internal sealed class JxlDecoderCore : ImageDecoderCore, IDisposable /// private long nextSection; - private List sectionProcessed = []; + private readonly List sectionProcessed = []; /// /// The frame header, if present. @@ -301,11 +301,11 @@ internal sealed class JxlDecoderCore : ImageDecoderCore, IDisposable /// /// All frame reference.s /// - private List frameReferences = []; + private readonly List frameReferences = []; - private List frameExternalToInternal = []; + private readonly List frameExternalToInternal = []; - private List frameRequired = []; + private readonly List frameRequired = []; /// /// Codestream input data is temporarily copied here. @@ -802,7 +802,7 @@ private static JxlSignature DetectSignature(Stream buffer) throw new EndOfStreamException(); } - if (secondByte == CodestreamMarker) + if (secondByte == JxlShared.CodestreamMarker) { return JxlSignature.CodeStream; } @@ -961,7 +961,7 @@ public bool CanAddBuffer(long length) { const long bufferLimit = 1 << 48; return length < bufferLimit && - (length + this.jxlpOooBufferTotal + (this.codestreamCopy?.Memory.Length ?? 0)) < bufferLimit; + (length + this.jxlpOooBufferTotal + (this.codestreamCopy?.AsMemory().Length ?? 0)) < bufferLimit; } public bool TryInjectNextBufferedJxlpBox() @@ -1471,14 +1471,14 @@ private void BeforeUpdateState(string propertyName) /// Reads a single bundle into . /// /// Type of the bundle to read. - /// Bundle binary data. + /// Stream to read data from. /// Bit reader to continue from. /// The bundle to parse. /// Status of parsing the bundle. - private bool ReadBundle(Span data, JxlBitReader br, T bundle) + private bool ReadBundle(Stream stream, JxlBitReader br, T bundle) where T : IJxlFields { - JxlBitReader reader = new(data); + JxlBitReader reader = new(stream); reader.SkipBits64((ulong)br.TotalBitsConsumed); bool canRead = JxlBundle.CanRead(reader, bundle); @@ -1646,22 +1646,6 @@ public void ProcessSections() this.frameDecoder.ProcessSections(sectionInfo, sectionStatus); - bool outOfBounds = false; - - foreach (JxlSectionInfo info in sectionInfo) - { - if (!info.BitReader.AllReadsWithinBounds) - { - outOfBounds = true; - break; - } - } - - if (outOfBounds) - { - throw new InvalidOperationException("Frame out of bounds"); - } - for (int i = 0; i < sectionStatus.Count; i++) { JxlSectionStatus ss = sectionStatus[i]; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlShared.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlShared.cs index 711dbc27ea..e8126bc4dc 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlShared.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlShared.cs @@ -18,6 +18,13 @@ internal static class JxlShared /// public const int MaximumNumberOfReferenceFrames = 4; + /// + /// Reserved by ISO/IEC 10918-1. LF causes files opened in text mode + /// to be rejected because the marker changes to 0x0D instead. The + /// 0xFF prefix also ensures there were no 7-bit transmission limitations. + /// + public const byte CodestreamMarker = 0x0A; + /// /// Gets the 12-byte signature (a.k.a. magic) for JPEG XL files. /// From b8f4094a77ff077b8bcf616e8fadd25693abe689 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 25 Aug 2026 00:12:11 +0400 Subject: [PATCH 092/328] Reduce errors --- .../Formats/Jxl/Fields/JxlReadVisitor.cs | 12 ---- .../Jxl/Processing/Decoder/JxlBitReader.cs | 65 +++++++++---------- .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 15 ++--- 3 files changed, 36 insertions(+), 56 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs index c60e5985ee..d54dba6706 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs @@ -116,18 +116,6 @@ public override bool EndExtensions() reader.SkipBits64((uint)remainingBits); } - return this.ThrowIfEndOfStreamOrReturnTrue(); - } - - private bool ThrowIfEndOfStreamOrReturnTrue() - { - if (reader.IsEndOfStream) - { - DebugGuard.IsTrue(false, "Got an invalid end-of-stream"); - this.notEnoughBytes = true; - return true; - } - return true; } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs index 9dbc090aec..33b7307d30 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs @@ -8,56 +8,61 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; /// /// Represents a bitstream reader. /// -internal ref struct JxlBitReader(ReadOnlySpan bytes) +internal sealed class JxlBitReader(Stream stream) { - private readonly ReadOnlySpan data = bytes; - private ulong buffer; private uint bufferRemainingBits; private int pointer; - /// - /// Gets a value indicating whether this marks an end of stream. - /// - public bool IsEndOfStream { get; private set; } - /// /// Gets the total number of bits consumed. /// - public readonly long TotalBitsConsumed => ((long)this.pointer * 8) + (64 - this.bufferRemainingBits); + public long TotalBitsConsumed => ((long)this.pointer * 8) + (64 - this.bufferRemainingBits); /// /// Fetches a new buffer. /// private void RefillCore() { - int remaining = this.data.Length - this.pointer; - if (remaining <= 0) - { - // we don't have any more data... mark an end of stream - this.buffer = 0; - this.bufferRemainingBits = 0; - this.IsEndOfStream = true; - return; - } - - if (remaining >= 8) + Span temp = stackalloc byte[8]; + int bytesRead = stream.Read(temp); + if (bytesRead == 8) { - this.buffer = BinaryPrimitives.ReadUInt64LittleEndian(this.data[this.pointer..]); + this.buffer = BinaryPrimitives.ReadUInt64LittleEndian(temp); this.bufferRemainingBits = 64u; this.pointer += 8; } else { + if (bytesRead == 0) + { + throw new EndOfStreamException(); + } + ulong value = 0; - for (int i = 0; i < remaining; i++) + for (int i = 0; i < bytesRead; i++) { - value |= (ulong)this.data[this.pointer + i] << (8 * i); + value |= (ulong)temp[i] << (8 * i); } this.buffer = value; - this.bufferRemainingBits = (uint)(remaining * 8); - this.pointer += remaining; + this.bufferRemainingBits = (uint)(bytesRead * 8); + this.pointer += bytesRead; + } + } + + public void JumpToByteBoundary() + { + uint remainder = (uint)(this.TotalBitsConsumed % 8); + + if (remainder == 0) + { + return; + } + + if (this.ReadBits32(8u - remainder) != 0) + { + throw new InvalidDataException("Non-zero padding bits"); } } @@ -74,11 +79,6 @@ private ulong ReadBits64Core(uint n, bool peek = false) DebugGuard.MustBeLessThanOrEqualTo(n, 64u, nameof(n)); this.MaybeRefill(); - if (this.IsEndOfStream) - { - JxlThrowHelper.ThrowEndOfStream(); - } - if (n <= this.bufferRemainingBits) { ulong result = this.buffer & ((1UL << (int)n) - 1); @@ -119,11 +119,6 @@ private uint ReadBits32Core(uint n, bool peek = false) DebugGuard.MustBeLessThanOrEqualTo(n, 32u, nameof(n)); this.MaybeRefill(); - if (this.IsEndOfStream) - { - JxlThrowHelper.ThrowEndOfStream(); - } - if (n <= this.bufferRemainingBits) { uint result = (uint)(this.buffer & ((1UL << (int)n) - 1)); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index 0287760e43..0fe20686a5 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -1753,11 +1753,6 @@ public int ProcessCodestream() this.frameDecoder.InitializeFrame(reader, this.imageBundle!, this.previewFrame); - if (!reader.AllReadsWithinBounds) - { - return this.TryRequestMoreInput() ? 1 : 0; - } - this.AdvanceCodeStream(reader.TotalBitsConsumed / JxlMath.BitsPerByte); this.frameHeader = this.frameDecoder.GetFrameHeader(); @@ -2701,13 +2696,13 @@ public void DecodeInput() if (!this.gotSignature) { - JxlSignatureCheck status = CheckSignature(this.nextInput, this.availableInput); - if (status == JxlSignatureCheck.InvalidSignature) + JxlSignature signature = DetectSignature(this.nextInput, this.availableInput); + if (signature == JxlSignature.Invalid) { throw new InvalidOperationException("The signature is invalid."); } - if (status == JxlSignatureCheck.NotEnoughBytes) + if (signature == JxlSignature.NotEnoughBytes) { if (this.inputClosed) { @@ -2719,7 +2714,7 @@ public void DecodeInput() this.gotSignature = true; - if (status == JxlSignatureCheck.Container) + if (signature == JxlSignature.Container) { this.haveContainer = true; } @@ -2750,6 +2745,8 @@ public void DecodeInput() } } + private static void ThrowNotEnoughData() => throw new InvalidOperationException("Not enough data"); + protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) => throw new NotImplementedException(); protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) => throw new NotImplementedException(); From 2b43fd6d14e5e280005b2fe64d23f642448fdfab Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 25 Aug 2026 00:17:28 +0400 Subject: [PATCH 093/328] Simplify --- .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index 0fe20686a5..016269e4da 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -2314,11 +2314,6 @@ public int ProcessBoxes(Stream stream) if (this.boxType == JxlBoxTypes.Brob) { - if (this.availableInput < headerSize + 4) - { - return NeedMoreInput; - } - this.boxDecodedType = BitConverter.ToInt32(this.nextInput!.Memory.Span[(int)headerSize..]); } else @@ -2423,11 +2418,6 @@ public int ProcessBoxes(Stream stream) throw new InvalidOperationException("The file type box is too small"); } - if (this.availableInput < 8) - { - return NeedMoreInput; - } - if (BinaryUtils.ReadInt32BigEndian(stream) != 0x6A786C20) // Bytes "jxl " in Big Endian { throw new InvalidOperationException("File type box major brand must be \"jxl \""); @@ -2449,11 +2439,6 @@ public int ProcessBoxes(Stream stream) throw new InvalidOperationException("Cannot have jxlp box after last jxlp box"); } - if (this.availableInput < 4) - { - return NeedMoreInput; - } - if (!this.boxContentsUnbounded && this.boxContentsSize < 4) { throw new InvalidOperationException("jxlp box is too small to contain an index"); From 9f8eb4a434c7386bdb6bebdc2d1bcc49e84ac30e Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 25 Aug 2026 19:34:40 +0400 Subject: [PATCH 094/328] Add box content decoder with Brotli compression & reduce errors --- .../Decoder/JxlBoxContentDecoder.cs | 108 ++++++++++++++++++ .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 10 +- 2 files changed, 113 insertions(+), 5 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs new file mode 100644 index 0000000000..7621102ef9 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs @@ -0,0 +1,108 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.IO.Compression; +using SixLabors.ImageSharp.Formats.Jxl.IO; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +/// +/// Allows decoding and decompressing box data. +/// +internal sealed class JxlBoxContentDecoder +{ + /// + /// Specifies how many bytes to read to fetch box data. This is ignored + /// if the box extends till EOF. + /// + private ulong boxSize; + + /// + /// When true the box size is ignored and is unbounded - that is, keeps going + /// till the end of the file or stream. + /// + private bool boxExtendsTillEnd; + + /// + /// This contains flags that determine whether the box is Brotli-compressed or + /// not. + /// + private JxlBoxCodingMode codingMode; + + /// + /// Prepares parsing the box. + /// + /// Specifies box compression. + /// Specifies whether or not the box size keeps going till the end of stream. + /// Specifies the fixed size of the box when it is not unbounded. + public void Initialize(JxlBoxCodingMode codingMode, bool isUnbounded, ulong size) + { + this.boxSize = size; + this.codingMode = codingMode; + this.boxExtendsTillEnd = isUnbounded; + } + + /// + /// Prepares parsing the box. + /// + /// True if the box is compressed with Brotli. If uncompressed - false. + /// Specifies whether or not the box size keeps going till the end of stream. + /// Specifies the fixed size of the box when it is not unbounded. + public void Initialize(bool isBrotliCompressed, bool isUnbounded, ulong size) + => this.Initialize( + isBrotliCompressed ? JxlBoxCodingMode.Brotli : JxlBoxCodingMode.Uncompressed, + isUnbounded, + size); + + public void Process(Stream stream, JxlMemoryWriter writer) + { + byte[] cache = ArrayPool.Shared.Rent(16384); + + try + { + if (this.codingMode == JxlBoxCodingMode.Brotli) + { + using BrotliStream brotli = new(stream, CompressionMode.Decompress, leaveOpen: true); + + int bytesRead; + while ((bytesRead = brotli.Read(cache, 0, cache.Length)) > 0) + { + writer.Write(cache.AsSpan(0, bytesRead)); + } + } + else + { + if (this.boxExtendsTillEnd) + { + int bytesRead; + while ((bytesRead = stream.Read(cache, 0, cache.Length)) > 0) + { + writer.Write(cache.AsSpan(0, bytesRead)); + } + } + else + { + ulong bytesLeft = this.boxSize; + while (bytesLeft > 0) + { + int toRead = (int)Math.Min((ulong)cache.Length, bytesLeft); + int bytesRead = stream.Read(cache, 0, toRead); + + if (bytesRead == 0) + { + throw new EndOfStreamException("Unexpected EOF while reading box content"); + } + + writer.Write(cache.AsSpan(0, bytesRead)); + bytesLeft -= (ulong)bytesRead; + } + } + } + } + finally + { + ArrayPool.Shared.Return(cache); + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index 016269e4da..7ca3ace0e9 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -1508,7 +1508,7 @@ public void ReadBasicInfo(Stream stream) Span fileSignature = stackalloc byte[2]; stream.ReadExactly(fileSignature); - if (fileSignature[0] != 0xFF || fileSignature[1] != CodestreamMarker) + if (fileSignature[0] != 0xFF || fileSignature[1] != JxlShared.CodestreamMarker) { throw new InvalidOperationException("The file signature is invalid"); } @@ -1553,7 +1553,7 @@ public void ReadAllHeaders() { Span span = this.GetCodeStreamSpan(); - JxlBitReader reader = new(span); + JxlBitReader reader = new(this.stream); reader.SkipBits64((ulong)this.codestreamBitsAhead); this.metadata!.CustomTransformData!.NonserializedXybEncoded = this.metadata.ImageMetadata!.XybEncoded; @@ -2286,7 +2286,7 @@ public int ProcessBoxes(Stream stream) { if (this.decoderStage != JxlDecoderStage.CodeStreamFinished || this.JbrdNeedsMoreBoxes()) { - return NeedMoreInput; + ThrowNotEnoughData(); } if (this.inputClosed || (this.eventsWanted & Box) != 0) @@ -2294,7 +2294,7 @@ public int ProcessBoxes(Stream stream) return Success; } - return NeedMoreInput; + ThrowNotEnoughData(); } bool boxedCodestreamDone = ((this.eventsWanted & Box) != 0) @@ -2556,7 +2556,7 @@ public int ProcessBoxes(Stream stream) if (!boxDone) { - return NeedMoreInput; + ThrowNotEnoughData(); } this.boxStage = JxlBoxStage.Header; From 315ea0fb9e4fe00b7ba059e15b8b520782db00a1 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 25 Aug 2026 19:45:52 +0400 Subject: [PATCH 095/328] Reduce errors & refine documentation --- .../Jxl/Processing/Decoder/JxlBoxContentDecoder.cs | 3 ++- .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 14 +++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs index 7621102ef9..5d1beddf3c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs @@ -8,7 +8,8 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; /// -/// Allows decoding and decompressing box data. +/// Allows decoding and decompressing box data in JPEG XL +/// container format. /// internal sealed class JxlBoxContentDecoder { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index 7ca3ace0e9..92c0962c8b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -347,7 +347,7 @@ internal sealed class JxlDecoderCore : ImageDecoderCore, IDisposable /// OOO jxlp payloads keyed by counter. Keys are: codestream bytes without /// 4byte header, and is_last. /// - private Dictionary jxlpOooBuffer = []; + private readonly Dictionary jxlpOooBuffer = []; private long jxlpOooBufferTotal; @@ -358,14 +358,14 @@ internal sealed class JxlDecoderCore : ImageDecoderCore, IDisposable /// /// Decompresses box contents. /// - private JxlBoxContentDecoder? boxContentDecoder; + private readonly JxlBoxContentDecoder? boxContentDecoder; /// /// Decodes JPEG XL to JPEG. /// private JxlToJpegDecoder? jpegDecoder; - private JxlBoxContentDecoder? metadataDecoder; + private readonly JxlBoxContentDecoder? metadataDecoder; /// /// Raw bytes for EXIF metadata. @@ -2363,13 +2363,13 @@ public int ProcessBoxes(Stream stream) if ((this.eventsWanted & Box) != 0) { bool decompress = this.decompressBoxes && this.boxType == JxlBoxTypes.Brob; - this.boxContentDecoder.StartBox(decompress, this.boxContentsUnbounded, this.boxContentsSize); + this.boxContentDecoder!.Initialize(decompress, this.boxContentsUnbounded, (ulong)this.boxContentsSize); } if (this.storeExif == 1 || this.storeXmp == 1) { bool brob = this.boxType == JxlBoxTypes.Brob; - this.metadataDecoder.StartBox(brob, this.boxContentsUnbounded, this.boxContentsSize); + this.metadataDecoder!.Initialize(brob, this.boxContentsUnbounded, (ulong)this.boxContentsSize); } if (this.boxType == JxlBoxTypes.FileType) @@ -2631,7 +2631,7 @@ public int ProcessBoxes(Stream stream) } this.AdvanceInput(this.availableInput); - return NeedMoreInput; + ThrowNotEnoughData(); } long remaining = this.boxContentsEnd - this.filePosition; @@ -2639,7 +2639,7 @@ public int ProcessBoxes(Stream stream) { this.basicInfoSizeHint = InitialBasicInfoSizeHint() + this.boxContentsEnd - this.filePosition; this.AdvanceInput(this.availableInput); - return NeedMoreInput; + ThrowNotEnoughData(); } else { From 57958f3b1e82fc1d8493f4e850f08d8ee3dc3fe4 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 25 Aug 2026 19:54:15 +0400 Subject: [PATCH 096/328] Reduce errors & remove unnecessary if statements --- .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 26 +++++-------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index 92c0962c8b..51c05d1f3a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -2111,6 +2111,7 @@ private static void ParseBoxHeader(Stream input, out JxlBoxType type, out long b /// /// Processes all boxes and their contents if this is a container format. /// + /// Stream to decode from. /// Status of processing. /// Thrown when data is invalid. public int ProcessBoxes(Stream stream) @@ -2664,24 +2665,16 @@ public int ProcessBoxes(Stream stream) /// /// Main core decoding routine. /// + /// Stream to decode from. /// Thrown when data or input parameters are invalid. - public void DecodeInput() + public void DecodeInput(Stream stream) { - if (this.decoderStage == JxlDecoderStage.Initialized) - { - this.decoderStage = JxlDecoderStage.Started; - } - - if (this.decoderStage == JxlDecoderStage.Error) - { - // Should NEVER occur! If it does make sure to always reset the decoder - // in the Decode method. - throw new InvalidOperationException("The core decoder cannot be used because it contains an error. A reset must be made."); - } + this.Reset(); + this.decoderStage = JxlDecoderStage.Started; if (!this.gotSignature) { - JxlSignature signature = DetectSignature(this.nextInput, this.availableInput); + JxlSignature signature = DetectSignature(stream); if (signature == JxlSignature.Invalid) { throw new InvalidOperationException("The signature is invalid."); @@ -2709,12 +2702,7 @@ public void DecodeInput() } } - int status = this.ProcessBoxes(); - - if (status == NeedMoreInput && this.inputClosed) - { - ThrowNotEnoughData(); - } + int status = this.ProcessBoxes(stream); if (status == Success) { From 0678f35b83baf349cdea9502881ac60cd8366480 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 25 Aug 2026 20:31:58 +0400 Subject: [PATCH 097/328] Add missing types, reduce errors --- .../Decoder/JxlBoxTypes.Generated.cs | 47 +++++++ .../Jxl/Processing/Decoder/JxlBoxTypes.tt | 47 +++++++ .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 124 +++++++++--------- .../Processing/Decoder/JxlDecoderStatus.cs | 87 ++++++++++++ 4 files changed, 243 insertions(+), 62 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxTypes.Generated.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxTypes.tt create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderStatus.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxTypes.Generated.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxTypes.Generated.cs new file mode 100644 index 0000000000..66bb65a4df --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxTypes.Generated.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +/// +/// Contains constants which represent JPEG XL container format +/// box types as an integer. +/// +internal static class JxlBoxTypes +{ + /// + /// ftyp + /// + public const int FileType = 0x66747970; + + /// + /// jxlc + /// + public const int JxlCodeStream = 0x6A786C63; + + /// + /// jxlp + /// + public const int JxlPartialCodeStream = 0x6A786C70; + + /// + /// brob + /// + public const int Brob = 0x62726F62; + + /// + /// xml + /// + public const int Xml = 0x786D6C20; + + /// + /// Exif + /// + public const int Exif = 0x45786966; + + /// + /// jxl + /// + public const int Jxl = 0x6A786C20; + +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxTypes.tt b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxTypes.tt new file mode 100644 index 0000000000..92be8c00c1 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxTypes.tt @@ -0,0 +1,47 @@ +<#@ template language="C#" #> +<#@ import namespace="System" #> +<#@ import namespace="System.IO" #> +<#@ import namespace="System.Collections.Generic" #> +<#@ output extension=".Generated.cs" #> +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +<# + string[] boxTypes = new[] + { + "FileType=ftyp", + "JxlCodeStream=jxlc", + "JxlPartialCodeStream=jxlp", + "Brob=brob", + "Xml=xml ", + "Exif=Exif", + "Jxl=jxl " + }; +#> +/// +/// Contains constants which represent JPEG XL container format +/// box types as an integer. +/// +internal static class JxlBoxTypes +{ +<# + foreach (string boxType in boxTypes) + { + string[] split = boxType.Split('='); + string name = split[0]; + string fourCC = split[1]; + uint boxNum = ((uint)fourCC[0] << 24) | + ((uint)fourCC[1] << 16) | + ((uint)fourCC[2] << 8) | + fourCC[3]; + string hex = $"0x{boxNum:X8}"; +#> + /// + /// <#= fourCC #> + /// + public const int <#= name #> = <#= hex #>; + +<# } #> +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index 51c05d1f3a..818f1ec76f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -1684,16 +1684,16 @@ public int ProcessCodestream() this.ReadBasicInfo(); } - if ((this.eventsWanted & BasicInfo) != 0) + if ((this.eventsWanted & JxlDecoderStatus.BasicInfo) != 0) { - this.eventsWanted &= ~BasicInfo; - return JxlCodestreamType.BasicInfo; + this.eventsWanted &= ~JxlDecoderStatus.BasicInfo; + return JxlDecoderStatus.BasicInfo; } if (this.eventsWanted == 0) { this.decoderStage = JxlDecoderStage.CodeStreamFinished; - return JxlCodestreamType.Success; + return JxlDecoderStatus.Success; } if (!this.gotAllHeaders) @@ -1701,16 +1701,16 @@ public int ProcessCodestream() this.ReadAllHeaders(); } - if ((this.eventsWanted & ColorEncoding) != 0) + if ((this.eventsWanted & JxlDecoderStatus.ColorEncoding) != 0) { - this.eventsWanted &= ~ColorEncoding; - return JxlCodestreamType.ColorEncoding; + this.eventsWanted &= ~JxlDecoderStatus.ColorEncoding; + return JxlDecoderStatus.ColorEncoding; } if (this.eventsWanted == 0) { this.decoderStage = JxlDecoderStage.CodeStreamFinished; - return JxlCodestreamType.Success; + return JxlDecoderStatus.Success; } this.postHeaders = true; @@ -1722,7 +1722,7 @@ public int ProcessCodestream() while (true) { - bool parseFrames = (this.eventsWanted & (PreviewImage | DecodedFrame | FullImage)) != 0; + bool parseFrames = (this.eventsWanted & (JxlDecoderStatus.PreviewImage | JxlDecoderStatus.Frame | JxlDecoderStatus.FullImage)) != 0; if (!parseFrames || (this.frameStage == JxlFrameStage.Header && this.isLastTotal)) { break; @@ -1763,7 +1763,7 @@ public int ProcessCodestream() throw new InvalidOperationException("Frame is too large"); } - int outputType = this.previewFrame ? PreviewImage : FullImage; + int outputType = this.previewFrame ? JxlDecoderStatus.PreviewImage : JxlDecoderStatus.FullImage; bool outputNeeded = (this.eventsWanted & outputType) != 0; if (outputNeeded) @@ -1776,7 +1776,7 @@ public int ProcessCodestream() this.frameStage = JxlFrameStage.Toc; if (this.previewFrame) { - if ((this.eventsWanted & PreviewImage) == 0) + if ((this.eventsWanted & JxlDecoderStatus.PreviewImage) == 0) { this.frameStage = JxlFrameStage.Header; this.AdvanceCodeStream(this.remainingFrameSize); @@ -1853,11 +1853,11 @@ public int ProcessCodestream() } } - if ((this.eventsWanted & Frame) != 0 && this.isLastOfStill) + if ((this.eventsWanted & JxlDecoderStatus.Frame) != 0 && this.isLastOfStill) { if (!this.skippingFrame) { - return Frame; + return JxlDecoderStatus.Frame; } } @@ -1866,7 +1866,7 @@ public int ProcessCodestream() this.frameDecoder.SetRenderSpotcolors(this.renderSpotcolors); this.frameDecoder.SetCoalescing(this.coalescing); - if (!this.previewFrame && (this.eventsWanted & FrameProgression) != 0) + if (!this.previewFrame && (this.eventsWanted & JxlDecoderStatus.Progression) != 0) { this.frameProgressiveDetail = this.frameDecoder.SetPauseAtProgressive(this.progressiveDetail); } @@ -1880,7 +1880,7 @@ public int ProcessCodestream() this.sectionProcessed.Clear(); ResizeSectionProcessed(this.frameDecoder.Toc.Size); - if (this.previewFrame || (this.eventsWanted & FullImage) != 0) + if (this.previewFrame || (this.eventsWanted & JxlDecoderStatus.FullImage) != 0) { this.frameStage = JxlFrameStage.Full; } @@ -1902,14 +1902,14 @@ public int ProcessCodestream() { if (this.previewFrame) { - return NeedPreviewOutBuffer; + return JxlDecoderStatus.NeedPreviewOutBuffer; } if ((!this.jpegDecoder.IsOutputSet || this.imageBundle!.JpegData is null) && this.isLastOfStill && !this.skippingFrame) { - return NeedImageOutputBuffer; + throw new InvalidOperationException("Image output buffer is too small"); } } @@ -1960,7 +1960,7 @@ public int ProcessCodestream() { this.dcFrameProgressionDone = true; this.downsamplingTarget = 8; - return Progression; + return JxlDecoderStatus.Progression; } bool newProgressionStepDone = this.frameDecoder.NumCompletePasses >= nextNumPassesToPause; @@ -1970,7 +1970,7 @@ public int ProcessCodestream() newProgressionStepDone) { this.downsamplingTarget = this.frameHeader.Passes.GetDownsamplingTargetForCompletedPasses(this.frameDecoder.NumCompletePasses); - return Progression; + return JxlDecoderStatus.Progression; } if (!allSectionsDone) @@ -1996,7 +1996,7 @@ public int ProcessCodestream() this.frameStage = JxlFrameStage.Header; this.reconstructionOutputJpeg = JpegReconstructionStage.SetMetadata; - return FullImage; + return JxlDecoderStatus.FullImage; } if (this.previewFrame || this.isLastOfStill) @@ -2013,12 +2013,12 @@ public int ProcessCodestream() { this.gotPreviewImage = true; this.previewFrame = false; - this.eventsWanted &= ~PreviewImage; - return PreviewImage; + this.eventsWanted &= ~JxlDecoderStatus.PreviewImage; + return JxlDecoderStatus.PreviewImage; } - else if (this.isLastOfStill && (this.eventsWanted & FullImage) != 0 && !this.skippingFrame) + else if (this.isLastOfStill && (this.eventsWanted & JxlDecoderStatus.FullImage) != 0 && !this.skippingFrame) { - return FullImage; + return JxlDecoderStatus.FullImage; } } } @@ -2100,12 +2100,12 @@ public void SetJpegBuffer(Memory data) /// /// Thrown when data is invalid. /// - private static void ParseBoxHeader(Stream input, out JxlBoxType type, out long boxSize, out long headerSize) + private static void ParseBoxHeader(Stream input, out int type, out long boxSize, out long headerSize) { JxlBoxHeader header = JxlBoxHeader.ReadHeader(input); boxSize = (long)header.Size; headerSize = (header.ContainsLargeSize ? 12 : 4) + 4; - type = (JxlBoxType)header.Type; + type = (int)header.Type; } /// @@ -2124,12 +2124,12 @@ public int ProcessBoxes(Stream stream) // this.AdvanceInput(this.headerSize); this.headerSize = 0; - if ((this.eventsWanted & Box) != 0 && this.boxEvent && !this.boxOutBufferSetCurrentBox) + if ((this.eventsWanted & JxlDecoderStatus.Box) != 0 && this.boxEvent && !this.boxOutBufferSetCurrentBox) { this.boxEvent = false; } - if ((this.eventsWanted & Box) != 0 && this.boxOutBufferSetCurrentBox) + if ((this.eventsWanted & JxlDecoderStatus.Box) != 0 && this.boxOutBufferSetCurrentBox) { Memory nextOut = this.boxOutputBuffer!.Memory[(int)this.boxOutBufferPos..]; long availOut = this.boxOutBufferSize - this.boxOutBufferPos; @@ -2147,12 +2147,12 @@ public int ProcessBoxes(Stream stream) long produced = startSlice.Length - availOut; this.boxOutBufferPos += produced; - if (status == Complete && (this.eventsWanted & Complete) == 0) + if (status == JxlDecoderStatus.Complete && (this.eventsWanted & JxlDecoderStatus.Complete) == 0) { - status = Success; + status = JxlDecoderStatus.Success; } - if (status is not (Success or NeedMoreInput)) + if (status is not (JxlDecoderStatus.Success or JxlDecoderStatus.NeedMoreInput)) { return status; } @@ -2191,7 +2191,7 @@ public int ProcessBoxes(Stream stream) long produced = originalNextOutput.Length - nextOutput.Length; this.reconstructionOutputBufferPos += produced; - if (boxResult == NeedMoreOutput) + if (boxResult == JxlDecoderStatus.NeedMoreOutput) { if (md.Length >= blockSizeLimit) { @@ -2200,11 +2200,11 @@ public int ProcessBoxes(Stream stream) Array.Resize(ref md, md.Length * 2); } - else if (boxResult == NeedMoreInput) + else if (boxResult == JxlDecoderStatus.NeedMoreInput) { break; } - else if (boxResult == Complete) + else if (boxResult == JxlDecoderStatus.Complete) { long neededSize = this.storeExif == 1 ? this.reconstructionExifSize : this.reconstructionXmpSize; @@ -2262,9 +2262,9 @@ public int ProcessBoxes(Stream stream) this.reconstructionOutputJpeg = JpegReconstructionStage.None; this.imageBundle.Reset(); - if ((this.eventsWanted & FullImage) != 0) + if ((this.eventsWanted & JxlDecoderStatus.FullImage) != 0) { - return FullImage; + return JxlDecoderStatus.FullImage; } } @@ -2274,7 +2274,7 @@ public int ProcessBoxes(Stream stream) { if (this.decoderStage == JxlDecoderStage.CodeStreamFinished) { - return Success; + return JxlDecoderStatus.Success; } this.boxStage = JxlBoxStage.CodeStream; @@ -2290,15 +2290,15 @@ public int ProcessBoxes(Stream stream) ThrowNotEnoughData(); } - if (this.inputClosed || (this.eventsWanted & Box) != 0) + if (this.inputClosed || (this.eventsWanted & JxlDecoderStatus.) != 0) { - return Success; + return JxlDecoderStatus.Success; } ThrowNotEnoughData(); } - bool boxedCodestreamDone = ((this.eventsWanted & Box) != 0) + bool boxedCodestreamDone = ((this.eventsWanted & JxlDecoderStatus.Box) != 0) && this.decoderStage == JxlDecoderStage.CodeStreamFinished && !this.JbrdNeedsMoreBoxes() && this.lastCodestreamSeen; @@ -2306,9 +2306,9 @@ public int ProcessBoxes(Stream stream) if (boxedCodestreamDone && this.availableInput >= 2 && this.nextInput!.Memory.Span[0] == 0xFF && - this.nextInput.Memory.Span[1] == CodestreamMarker) + this.nextInput.Memory.Span[1] == JxlShared.CodestreamMarker) { - return Success; + return JxlDecoderStatus.Success; } int status = ParseBoxHeader(this.nextInput, this.availableInput, 0, this.filePosition, this.boxType, out long boxSize, out long headerSize); @@ -2326,10 +2326,10 @@ public int ProcessBoxes(Stream stream) if (boxedCodestreamDone && this.boxType == JxlBoxTypes.Jxl) { - return Success; + return JxlDecoderStatus.Success; } - if (this.boxCount == 2 && this.boxType != JxlBoxType.FileType) + if (this.boxCount == 2 && this.boxType != JxlBoxTypes.FileType) { throw new InvalidOperationException("The second box must be a ftyp (File Type) box"); } @@ -2346,7 +2346,7 @@ public int ProcessBoxes(Stream stream) this.boxSize = boxSize; this.headerSize = headerSize; - if ((this.originalEventsWanted & JpegReconstruction) != 0) + if ((this.originalEventsWanted & JxlDecoderStatus.JpegReconstruction) != 0) { if (this.storeExif == 0 && this.boxDecodedType == JxlBoxTypes.Exif) { @@ -2361,7 +2361,7 @@ public int ProcessBoxes(Stream stream) } } - if ((this.eventsWanted & Box) != 0) + if ((this.eventsWanted & JxlDecoderStatus.Box) != 0) { bool decompress = this.decompressBoxes && this.boxType == JxlBoxTypes.Brob; this.boxContentDecoder!.Initialize(decompress, this.boxContentsUnbounded, (ulong)this.boxContentsSize); @@ -2391,9 +2391,9 @@ public int ProcessBoxes(Stream stream) { this.boxStage = JxlBoxStage.PartialCodeStream; } - else if ((this.originalEventsWanted & JpegReconstruction) != 0 && this.boxType == JxlBoxTypes.JpegReconstructionData) + else if ((this.originalEventsWanted & JxlDecoderStatus.JpegReconstruction) != 0 && this.boxType == JxlBoxTypes.JpegReconstructionData) { - if ((this.eventsWanted & JpegReconstruction) == 0) + if ((this.eventsWanted & JxlDecoderStatus.JpegReconstruction) == 0) { throw new InvalidOperationException("Multiple JPEG reconstruction boxes detected"); } @@ -2405,11 +2405,11 @@ public int ProcessBoxes(Stream stream) this.boxStage = JxlBoxStage.Skip; } - if ((this.eventsWanted & Box) != 0) + if ((this.eventsWanted & JxlDecoderStatus.Box) != 0) { this.boxEvent = true; this.boxOutBufferSetCurrentBox = false; - return Box; + return JxlDecoderStatus.Box; } } else if (this.boxStage == JxlBoxStage.Ftyp) @@ -2469,7 +2469,7 @@ public int ProcessBoxes(Stream stream) { if (this.jxlpOooBuffer.Count >= NumBuffersLimit) { - return Error; + return JxlDecoderStatus.Error; } // When creating a new OOO (Out-of-order) entry, @@ -2491,7 +2491,7 @@ public int ProcessBoxes(Stream stream) { int status = this.ProcessCodestream(); - if (status == FullImage) + if (status == JxlDecoderStatus.FullImage) { if (this.reconstructionOutputJpeg != JpegReconstructionStage.None) { @@ -2499,7 +2499,7 @@ public int ProcessBoxes(Stream stream) } } - if (status == NeedMoreInput) + if (status == JxlDecoderStatus.NeedMoreInput) { if (this.filePosition == this.boxContentsEnd && !this.boxContentsUnbounded) { @@ -2514,7 +2514,7 @@ public int ProcessBoxes(Stream stream) } } - if (status == Success) + if (status == JxlDecoderStatus.Success) { if (this.JbrdNeedsMoreBoxes()) { @@ -2527,7 +2527,7 @@ public int ProcessBoxes(Stream stream) break; } - if ((this.eventsWanted & Box) != 0) + if ((this.eventsWanted & JxlDecoderStatus.Box) != 0) { this.boxStage = JxlBoxStage.Skip; continue; @@ -2544,7 +2544,7 @@ public int ProcessBoxes(Stream stream) if (!this.CanAddBuffer(remaining) || !this.jxlpOooBuffer.TryGetValue(this.bufferingJxlpIndex, out JxlOooEntry? entry)) { - return Error; + return JxlDecoderStatus.Error; } // Now we want to write the 'remaining' number of bytes @@ -2577,7 +2577,7 @@ public int ProcessBoxes(Stream stream) long consumed = this.nextInput.Memory.Length - nextInput.Length; this.AdvanceInput(consumed); - if (reconstructionResult == JpegReconstruction) + if (reconstructionResult == JxlDecoderStatus.JpegReconstruction) { JxlJpegData jpegData = this.jpegDecoder!.GetJpegData(); long numExif = JxlToJpegDecoder.NumExifMarkers(jpegData); @@ -2611,10 +2611,10 @@ public int ProcessBoxes(Stream stream) this.boxStage = JxlBoxStage.Header; - if ((this.eventsWanted & JpegReconstruction) != 0) + if ((this.eventsWanted & JxlDecoderStatus.JpegReconstruction) != 0) { - this.eventsWanted &= ~JpegReconstruction; - return JpegReconstruction; + this.eventsWanted &= ~JxlDecoderStatus.JpegReconstruction; + return JxlDecoderStatus.JpegReconstruction; } } else @@ -2628,7 +2628,7 @@ public int ProcessBoxes(Stream stream) { if (this.inputClosed || !this.boxOutBufferSet) { - return Success; + return JxlDecoderStatus.Success; } this.AdvanceInput(this.availableInput); @@ -2654,7 +2654,7 @@ public int ProcessBoxes(Stream stream) } } - return Success; + return JxlDecoderStatus.Success; } /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderStatus.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderStatus.cs new file mode 100644 index 0000000000..93a4e0b365 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderStatus.cs @@ -0,0 +1,87 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +/// +/// Pending event masks and procedure statuses used by the JPEG XL decoder. +/// +internal static class JxlDecoderStatus +{ + /// + /// Everything went smoothly. + /// + public const int Success = 0; + + /// + /// An error occurred. + /// + public const int Error = 1; + + /// + /// Not enough bytes left to continue. + /// + public const int NeedMoreInput = 2; + + /// + /// The decoder can decode a preview image and requests setting + /// a preview output buffer. + /// + public const int NeedPreviewOutBuffer = 3; + + /// + /// Not enough memory allocated for the output image buffer. + /// + public const int NeedMoreOutput = 6; + + /// + /// Specifies an event mask for parsing the JXL basic info. + /// + public const int BasicInfo = 0x40; + + /// + /// Specifies an event mask for parsing and decoding the ICC color profile. + /// + public const int ColorEncoding = 0x100; + + /// + /// Specifies decoding a preview image or a small frame. + /// + public const int PreviewImage = 0x200; + + /// + /// Specifies an event mask for decoding a single frame. This + /// event is called once for a still image and multiple times for + /// animated JPEG XLs. + /// + public const int Frame = 0x400; + + /// + /// Specifies an event mask for decoding a full frame (or layer in case coalescing + /// is disabled). + /// + public const int FullImage = 0x1000; + + /// + /// Specifies pending JXL->JPEG decoding. + /// + public const int JpegReconstruction = 0x2000; + + /// + /// Specifies pending decompression of box data. + /// See . + /// + public const int Box = 0x4000; + + /// + /// Specifies an event mask for a progressive step in decoding + /// the frame. + /// + public const int Progression = 0x8000; + + /// + /// Specifies an event mask that specifies a box being decoded is + /// now complete. + /// + public const int Complete = 0x10000; +} From 4118c4a191e013ac4e6248c6f27bea1042e2953a Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 26 Aug 2026 00:33:41 +0400 Subject: [PATCH 098/328] Quantizer improvements, reduce errors - Use Stream for Box Content Decoder - Reduce errors in JxlDecoderCore - Add GetStride method to JxlFrameDecoder - Add JxlDctQuantWeightParameters and JxlQuantizerEncoding to implement more quant_weights.h components, and add proper documentation to each member of JxlQuantMode. - Remove InlineArray2 (there's already one built into System.Runtime.CompilerServices, so prefer to use that) --- src/ImageSharp/Formats/Jxl/InlineArrays.cs | 9 - .../Decoder/JxlBoxContentDecoder.cs | 2 +- .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 11 +- .../Jxl/Processing/Decoder/JxlFrameDecoder.cs | 25 +++ .../Processing/JxlDctQuantWeightParameters.cs | 15 +- .../Formats/Jxl/Processing/JxlQuantMode.cs | 34 +++- .../Jxl/Processing/JxlQuantizerEncoding.cs | 180 ++++++++++++++++++ 7 files changed, 251 insertions(+), 25 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerEncoding.cs diff --git a/src/ImageSharp/Formats/Jxl/InlineArrays.cs b/src/ImageSharp/Formats/Jxl/InlineArrays.cs index 6276051700..f006d2fd09 100644 --- a/src/ImageSharp/Formats/Jxl/InlineArrays.cs +++ b/src/ImageSharp/Formats/Jxl/InlineArrays.cs @@ -7,15 +7,6 @@ namespace SixLabors.ImageSharp.Formats.Jxl; -/// -/// Used by Butteraugli -/// -[InlineArray(2)] -internal struct InlineArray2 -{ - private T first; -} - [InlineArray(3)] internal struct InlineArray3 { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs index 5d1beddf3c..d339e69087 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs @@ -56,7 +56,7 @@ public void Initialize(bool isBrotliCompressed, bool isUnbounded, ulong size) isUnbounded, size); - public void Process(Stream stream, JxlMemoryWriter writer) + public void Process(Stream stream, Stream writer) { byte[] cache = ArrayPool.Shared.Rent(16384); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index 818f1ec76f..5578f47534 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -2138,11 +2138,8 @@ public int ProcessBoxes(Stream stream) Span startSlice = bufferSpan[(int)this.boxOutBufferPos..]; int status = this.boxContentDecoder!.Process( - this.nextInput, - this.availableInput, - this.filePosition - this.boxContentsBegin, - nextOut, - ref availOut); + stream, + this.boxOutputBuffer); long produced = startSlice.Length - availOut; this.boxOutBufferPos += produced; @@ -2290,7 +2287,7 @@ public int ProcessBoxes(Stream stream) ThrowNotEnoughData(); } - if (this.inputClosed || (this.eventsWanted & JxlDecoderStatus.) != 0) + if (this.inputClosed || (this.eventsWanted & JxlDecoderStatus.Box) != 0) { return JxlDecoderStatus.Success; } @@ -2704,7 +2701,7 @@ public void DecodeInput(Stream stream) int status = this.ProcessBoxes(stream); - if (status == Success) + if (status == JxlDecoderStatus.Success) { if (this.CanUseMoreCodestreamInput()) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs index b17aacd5cb..e127354a7b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs @@ -32,6 +32,31 @@ internal sealed class JxlFrameDecoder private JxlProgressiveDetail progressiveDetail = JxlProgressiveDetail.Frames; private List passesToPause = []; + /// + /// Gets a value indicating whether there are any DC groups left to decode. + /// + private bool ContainsDcGroupToDecode => this.decodedDcGroups.Any(x => x == 0); + + private static int GetStride(int width, JxlPixelFormat format) + { + if (!JxlMath.SafeMultiply(BytesPerChannel(format.DataType), format.Channels, out int xStride)) + { + throw new InvalidOperationException("Image too large"); + } + + if (!JxlMath.SafeMultiply(xStride, width, out int yStride)) + { + throw new InvalidOperationException("Image too large"); + } + + if (!JxlMath.SafeRoundUpTo(yStride, format.Align, yStride)) + { + throw new InvalidOperationException("Image too large"); + } + + return yStride; + } + public static void DecodeGlobalDcInfo(Configuration configuration, JxlBitReader reader, bool isJpeg, JxlPassesDecoderState state) { state.SharedStorage.Quantizer.Decode(reader); diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctQuantWeightParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlDctQuantWeightParameters.cs index 288d129956..8cd8a16abd 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlDctQuantWeightParameters.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlDctQuantWeightParameters.cs @@ -8,21 +8,22 @@ internal sealed class JxlDctQuantWeightParameters private const int Log2MaxDistanceBands = 4; private const int MaxDistanceBands = 1 + (1 << Log2MaxDistanceBands); - private int numDistanceBands; - private readonly float[][] distanceBands; - public JxlDctQuantWeightParameters() { - this.distanceBands = new float[3][]; + this.DistanceBands = new float[3][]; for (int i = 0; i < 3; i++) { - this.distanceBands[i] = new float[MaxDistanceBands]; + this.DistanceBands[i] = new float[MaxDistanceBands]; } } public JxlDctQuantWeightParameters(float[][] distanceBands, int numDistanceBands) { - this.numDistanceBands = numDistanceBands; - this.distanceBands = distanceBands; + this.NumDistanceBands = numDistanceBands; + this.DistanceBands = distanceBands; } + + public int NumDistanceBands { get; set; } + + public float[][] DistanceBands { get; } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantMode.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantMode.cs index fa4dd20518..1b974343a4 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantMode.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantMode.cs @@ -4,16 +4,48 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing; /// -/// Quantization mode. +/// Specifies which algorithm should be used to quantize coefficients. /// internal enum JxlQuantMode : byte { + /// + /// The quantizer relies on predefined tables for quantization. + /// The idea is similar to Huffman coding. + /// Library, + + /// + /// The quantizer uses an Identity transform. + /// Id, + + /// + /// The quantizer uses a 2x2 Discrete Cosine Transform. + /// Dct2, + + /// + /// The quantizer uses a 4x4 Discrete Cosine Transform. + /// Dct4, + + /// + /// The quantizer uses a 4x8 Discrete Cosine Transform. + /// Dct4x8, + + /// + /// The quantizer uses the AFV transform. + /// Afv, + + /// + /// The quantizer uses a Discrete Cosine Transform with custom block size. + /// Dct, + + /// + /// No quantization is performed. Input data becomes the output as-is. + /// Raw } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerEncoding.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerEncoding.cs new file mode 100644 index 0000000000..a50b7ad25b --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerEncoding.cs @@ -0,0 +1,180 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Specifies weights and quantizer modes. +/// +internal sealed class JxlQuantizerEncoding +{ + /// + /// Gets or sets the kind of transform used for this quantizer encoding. + /// + public JxlQuantMode Mode { get; set; } + + /// + /// Gets or sets the weights for DCT4+ tables. + /// + public JxlDctQuantWeightParameters? DctParameters { get; set; } + + /// + /// Gets or sets the weights for the 4x4 sub-block in AFV. + /// + public JxlDctQuantWeightParameters? DctParametersAfv4x4 { get; set; } + + /// + /// Gets or sets the weights for the identity transform. + /// + public InlineArray3> IdWeights { get; set; } + + /// + /// Gets or sets the weights for the DCT2 transform. + /// + public InlineArray3> Dct2Weights { get; set; } + + /// + /// Gets or sets the multipliers for the DCT4 transform. + /// + public InlineArray3> Dct4Multipliers { get; set; } + + /// + /// Gets or sets the weights for the AFV transform. + /// + public InlineArray3> AfvWeights { get; set; } + + /// + /// Gets or sets the multipliers for the 4x8 DCT block-based transform. + /// + public InlineArray3 Dct4x8Multipliers { get; set; } + + /// + /// Gets or sets the explicit quantization table (like in JPEG). + /// + /// + /// Only used when == . + /// + public int[]? QuantizationTable { get; set; } + + /// + /// Gets or sets the denominator for each item in the explicit quantization table. + /// + /// + /// Only used when == . + /// + public float QuantizationTableDenominator { get; set; } = 1f / (8 * 255); + + /// + /// Gets or sets a value indicating which predefined table to use. The value is + /// only used when == . + /// + public byte Predefined { get; set; } + + /// + /// Creates a new quantizer encoding with the Library quantizer mode + /// and the specified library index. + /// + /// The library index (aka predefined table). + /// A new Library quantizer encoding. + public static JxlQuantizerEncoding Library(int libraryIndex) + { + DebugGuard.MustBeLessThan(libraryIndex, JxlQuantWeights.NumPredefinedTables, nameof(libraryIndex)); + + return new() + { + Mode = JxlQuantMode.Library, + Predefined = (byte)libraryIndex + }; + } + + /// + /// Creates a new quantizer encoding with the Identity quantizer mode + /// and the specified XYB/identity weights. + /// + /// Weights for the identity transform. + /// A new Identity quantizer encoding. + public static JxlQuantizerEncoding Identity(in InlineArray3> xybWeights) + => new() + { + Mode = JxlQuantMode.Id, + IdWeights = xybWeights + }; + + /// + /// Creates a new quantizer encoding with the DCT2x2 quantizer mode + /// and the specified XYB/DCT2x2 weights. + /// + /// Weights for the DCT2x2 transform. + /// A new DCT2x2 quantizer encoding. + public static JxlQuantizerEncoding Dct2(in InlineArray3> xybWeights) + => new() + { + Mode = JxlQuantMode.Dct2, + Dct2Weights = xybWeights + }; + + /// + /// Creates a new quantizer encoding with the DCT4x4 quantizer mode, + /// the specified XYB/DCT4x4 multipliers, and quantizer weight parameters. + /// + /// Quantizer weights for the DCT4x4 transform. + /// XYB multipliers for the DCT4x4 transform. + /// A new DCT4x4 quantizer encoding. + public static JxlQuantizerEncoding Dct4(JxlDctQuantWeightParameters parameters, in InlineArray3> xybMul) + => new() + { + Mode = JxlQuantMode.Dct4, + DctParameters = parameters, + Dct4Multipliers = xybMul + }; + + /// + /// Creates a new quantizer encoding with the DCT4x8 quantizer mode, + /// the specified XYB/DCT4x8 multipliers, and quantizer weight parameters. + /// + /// Quantizer weights for the DCT4x8 transform. + /// XYB multipliers for the DCT4x8 transform. + /// A new DCT4x8 quantizer encoding. + public static JxlQuantizerEncoding Dct4x8(JxlDctQuantWeightParameters parameters, in InlineArray3 xybMul) + => new() + { + Mode = JxlQuantMode.Dct4x8, + DctParameters = parameters, + Dct4x8Multipliers = xybMul + }; + + /// + /// Creates a new quantizer encoding with the DCT quantizer mode + /// and quantizer weight parameters. + /// + /// Quantizer weights for the DCT transform. + /// A new DCT quantizer encoding. + public static JxlQuantizerEncoding Dct(JxlDctQuantWeightParameters parameters) + => new() + { + Mode = JxlQuantMode.Dct, + DctParameters = parameters, + }; + + /// + /// Creates a new quantizer encoding with the AFV quantizer mode, + /// quantizer weight parameters for 4x8/4x4 blocks, and weights. + /// + /// Quantizer weights for the 4x8 sub-block for the AFV transform. + /// Quantizer weights for the 4x4 sub-block for the AFV transform. + /// Quantizer weights. + /// A new DCT quantizer encoding. + public static JxlQuantizerEncoding Afv( + JxlDctQuantWeightParameters params4x8, + JxlDctQuantWeightParameters params4x4, + in InlineArray3> weights) + => new() + { + Mode = JxlQuantMode.Afv, + DctParameters = params4x8, + AfvWeights = weights, + DctParametersAfv4x4 = params4x4 + }; +} From 874ec5bcfb67d5a214b839481f5f03704e6c01b5 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 26 Aug 2026 00:46:03 +0400 Subject: [PATCH 099/328] Complete quantizer encoding --- .../Jxl/Processing/JxlQuantizerEncoding.cs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerEncoding.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerEncoding.cs index a50b7ad25b..fead5354cd 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerEncoding.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerEncoding.cs @@ -10,6 +10,34 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing; /// internal sealed class JxlQuantizerEncoding { + public JxlQuantizerEncoding() + { + } + + public JxlQuantizerEncoding(JxlQuantizerEncoding other) + { + // Simple shallow copy + this.AfvWeights = other.AfvWeights; + this.Dct2Weights = other.Dct2Weights; + this.Dct4Multipliers = other.Dct4Multipliers; + this.Dct4x8Multipliers = other.Dct4x8Multipliers; + this.DctParameters = other.DctParameters; + this.DctParametersAfv4x4 = other.DctParametersAfv4x4; + this.IdWeights = other.IdWeights; + this.Mode = other.Mode; + this.Predefined = other.Predefined; + this.QuantizationTable = other.QuantizationTable; + this.QuantizationTableDenominator = other.QuantizationTableDenominator; + + if (other.QuantizationTable is not null) + { + // Do a deep clone for the quantization table. + // Using AsSpan() should be way faster than a normal array copy... + this.QuantizationTable = GC.AllocateUninitializedArray(other.QuantizationTable.Length); + other.QuantizationTable.AsSpan().CopyTo(this.QuantizationTable); + } + } + /// /// Gets or sets the kind of transform used for this quantizer encoding. /// @@ -177,4 +205,23 @@ public static JxlQuantizerEncoding Afv( AfvWeights = weights, DctParametersAfv4x4 = params4x4 }; + + /// + /// Creates a new raw quantizer encoding. + /// + /// The quantization table for raw quantization. + /// The shift value for the denominator. + /// A raw quantizer encoding. + public static JxlQuantizerEncoding Raw(Span quantizationTable, int shift = 0) + { + JxlQuantizerEncoding encoding = new() + { + Mode = JxlQuantMode.Raw, + QuantizationTableDenominator = (1 << shift) * (1f / (8 * 255)), + QuantizationTable = GC.AllocateUninitializedArray(quantizationTable.Length) + }; + + quantizationTable.CopyTo(encoding.QuantizationTable); + return encoding; + } } From acc474ea354527c2a186c272c2394e342940b389 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 31 Aug 2026 20:25:32 +0400 Subject: [PATCH 100/328] Complete modular transforms, context prediction Common/Helpers - Add InterleaveLower and InterleaveUpper to Vector128_ and Vector256_ - Add unit test for InterleaveLower and InterleaveUpper (specifically for Vector256_) - Add Average to Numerics.cs Common - Add 32 and 33 to the InlineArray.tt text template Formats/Jxl/IO/Metadata - Remove unnecessary System.Runtime.CompilerServices using directive from JxlCustomTransformData and JxlOpsinInvreseMatrix Formats/Jxl/Processing/Decoder - Remove unncessary using SixLabors.ImageSharp.Formats.Jxl.IO Formats/Jxl/Processing/Encoder - Add partial Fast Lossless Encoder work (+enc_fast_lossless.cc; largest file in libjxl source) - Add linear algebra (+enc_linalg.cc, +enc_linalg.h) Formats/Jxl/Processing/Jpeg - Work that would later become JXL<->JPEG lossless coding mode Formats/Jxl/Processing/Modular/Encoding/ContextPrediction - Finish context prediction (+context_predict.h) Formats/Jxl/Processing/Modular/Transforms - Finish Reversible Color Transform (+rct.cc, +rct.h, +enc_rct.cc, +enc_rct.h) - Finish Palette/Indexed coding (+palette.cc, +palette.h, +enc_palette.cc, enc_palette.h) - Finish Squeeze transform (+squeeze.cc, +squeeze.h, +enc_squeeze.cc, +enc_squeeze.h) Formats/Jxl/Processing/RenderPipeline - Incomplete render pipeline abstractions with EPF (Edge Preserving Filter) 0 stage (+render_pipeline_stage.cc, +render_pipeline_stage.h, +stage_epf.cc, +stage_epf.h) Formats/Jxl/Processing/Splines - Remove unnecessary System.Runtime.CompilerServices using directive Formats/Jxl/Processing - Add dequantizer matrices - Remove JxlEndianness (prefer ByteOrder from ImageSharp/Common) - Add missing constant to JxlLoopFilter - Remove unnecessary using SixLabors.ImageSharp.Common.Helpers from JxlMath - Replace JxlPixelFormat to use ByteOrder - Update quantizers to use dequantizer matrices and quantizer weights - Add quantizer encoding and constants - Add SIMD utilities - Remove System.Runtime.CompilerServices using from JxlWeightsSeparable5 - Remove InlineArray3, InlineArray36 and InlineArray15 from InlineArrays (3 and 15 already exist in System.Runtime.CompilerServices; 36 already exists in InlineArray.tt from ImageSharp/Common) NEXT STEPS The current focus would be applying refactors and optimizations from reviews, followed by completing the JPEG XL modular. --- src/ImageSharp/Common/Helpers/Numerics.cs | 9 + .../Common/Helpers/Vector128Utilities.cs | 40 + .../Common/Helpers/Vector256Utilities.cs | 166 ++ src/ImageSharp/Common/InlineArray.cs | 22 +- src/ImageSharp/Common/InlineArray.tt | 2 +- .../Jxl/IO/Metadata/JxlCustomTransformData.cs | 1 + .../Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs | 1 + src/ImageSharp/Formats/Jxl/InlineArrays.cs | 33 - .../Decoder/JxlBoxContentDecoder.cs | 1 - .../Encoder/JxlFastLosslessEncoder.cs | 1365 +++++++++++++++++ .../Processing/Encoder/JxlLinearAlgebra.cs | 52 + .../Jxl/Processing/Jpeg/JpegAppMarkerType.cs | 30 + .../Jxl/Processing/JxlDequantMatrices.cs | 265 ++++ .../Formats/Jxl/Processing/JxlEndianness.cs | 25 - .../Formats/Jxl/Processing/JxlLoopFilter.cs | 5 + .../Formats/Jxl/Processing/JxlMath.cs | 53 + .../Formats/Jxl/Processing/JxlPixelFormat.cs | 2 +- .../Formats/Jxl/Processing/JxlQuantWeights.cs | 512 +++++++ .../Formats/Jxl/Processing/JxlQuantizer.cs | 12 +- .../Jxl/Processing/JxlQuantizerConstants.cs | 46 + .../Jxl/Processing/JxlQuantizerEncoding.cs | 22 +- ...JxlSimdUtils.StoreInterleaved.Generated.cs | 133 ++ .../JxlSimdUtils.StoreInterleaved.tt | 44 + .../Formats/Jxl/Processing/JxlSimdUtils.cs | 104 ++ .../Jxl/Processing/JxlWeightsSeparable5.cs | 2 + .../ContextPrediction/JxlContextPrediction.cs | 390 +++++ .../ContextPrediction/JxlPredictionResult.cs | 13 + .../ContextPrediction/JxlPredictorMode.cs | 35 + .../Processing/Modular/JxlModularChannel.cs | 24 +- .../Jxl/Processing/Modular/JxlModularImage.cs | 10 +- .../Modular/Transforms/JxlPalette.cs | 1227 ++++++++++++++- .../Processing/Modular/Transforms/JxlRct.cs | 1 + .../Modular/Transforms/JxlSqueeze.cs | 793 ++++++++++ .../Transforms/JxlSqueezeParameters.cs | 18 +- .../Modular/Transforms/JxlTransform.cs | 25 + .../Processing/RenderPipeline/Epf0Stage.cs | 109 ++ .../Processing/RenderPipeline/EpfStageType.cs | 14 + .../Jxl/Processing/RenderPipeline/EpfUtils.cs | 22 + .../RenderPipelineChannelMode.cs | 30 + .../RenderPipeline/RenderPipelineStageBase.cs | 92 ++ .../RenderPipelineStageConfiguration.cs | 21 + .../Processing/Splines/JxlSplineSegment.cs | 2 + src/ImageSharp/ImageSharp.csproj | 14 + .../Common/Vector256UtilitiesTests.cs | 48 + 44 files changed, 5731 insertions(+), 104 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlLinearAlgebra.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Jpeg/JpegAppMarkerType.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlDequantMatrices.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlEndianness.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerConstants.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.Generated.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.tt create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlPredictionResult.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlPredictorMode.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf0Stage.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/EpfStageType.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/EpfUtils.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineChannelMode.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineStageBase.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineStageConfiguration.cs create mode 100644 tests/ImageSharp.Tests/Common/Vector256UtilitiesTests.cs diff --git a/src/ImageSharp/Common/Helpers/Numerics.cs b/src/ImageSharp/Common/Helpers/Numerics.cs index e5a6b45493..a5a5571795 100644 --- a/src/ImageSharp/Common/Helpers/Numerics.cs +++ b/src/ImageSharp/Common/Helpers/Numerics.cs @@ -1033,4 +1033,13 @@ public static nuint Vector512Count(this ReadOnlySpan span) public static nuint Vector512Count(int length) where TVector : struct => (uint)length / (uint)Vector512.Count; + + /// + /// Computes the average of two integers. + /// + /// First integer + /// Second integer + /// The average of x, y. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Average(int x, int y) => (x + y + ((x > y) ? 1 : 0)) >> 1; } diff --git a/src/ImageSharp/Common/Helpers/Vector128Utilities.cs b/src/ImageSharp/Common/Helpers/Vector128Utilities.cs index 6bb1f59ef8..6b4c6ad63c 100644 --- a/src/ImageSharp/Common/Helpers/Vector128Utilities.cs +++ b/src/ImageSharp/Common/Helpers/Vector128Utilities.cs @@ -859,4 +859,44 @@ public static Vector128 UnpackLow(Vector128 left, Vector128 Vector128 unpacked = Vector128.Create(left.GetLower(), right.GetLower()); return Vector128.ShuffleNative(unpacked, Vector128.Create(0, 8, 1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15)); } + + /// + /// Interleaves the lower half of the vector. + /// + /// First vector + /// Second vector + /// + /// { a[0], b[0], a[1], b[1] } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 InterleaveLower(Vector128 a, Vector128 b) + { + Vector128 shuffledA = Vector128.Shuffle(a, Vector128.Create(0, 0, 1, 1)); + Vector128 shuffledB = Vector128.Shuffle(b, Vector128.Create(0, 0, 1, 1)); + + Vector128 maskA = Vector128.Create(-1, 0, -1, 0); + Vector128 maskB = Vector128.Create(0, -1, 0, -1); + + return (shuffledA & maskA) | (shuffledB & maskB); + } + + /// + /// Interleaves the upper half of the vector. + /// + /// First vector + /// Second vector + /// + /// { a[2], b[2], a[3], b[3] } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 InterleaveUpper(Vector128 a, Vector128 b) + { + Vector128 shuffledA = Vector128.Shuffle(a, Vector128.Create(2, 2, 3, 3)); + Vector128 shuffledB = Vector128.Shuffle(b, Vector128.Create(2, 2, 3, 3)); + + Vector128 maskA = Vector128.Create(-1, 0, -1, 0); + Vector128 maskB = Vector128.Create(0, -1, 0, -1); + + return (shuffledA & maskA) | (shuffledB & maskB); + } } diff --git a/src/ImageSharp/Common/Helpers/Vector256Utilities.cs b/src/ImageSharp/Common/Helpers/Vector256Utilities.cs index 1dd7122713..4bd78b88fd 100644 --- a/src/ImageSharp/Common/Helpers/Vector256Utilities.cs +++ b/src/ImageSharp/Common/Helpers/Vector256Utilities.cs @@ -397,4 +397,170 @@ public static Vector256 UnpackLow(Vector256 left, Vector256 ri return Vector256.Create(lo, hi); } + + /// + /// Multiplies only the even indices of the two 256-bit vectors, + /// producing half as many elements of twice the element width. + /// + /// Left vector to multiply. + /// Right vector to multiply + /// + /// + /// { + /// A[0] * B[0], + /// A[2] * B[2], + /// A[4] * B[4], + /// A[6] * B[6] + /// } + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 MultiplyEven(Vector256 left, Vector256 right) + => Vector256.Create( + left[0] * right[0], + left[2] * right[2], + left[4] * right[4], + left[6] * right[6]); + + /// + /// Multiplies only the odd indices of the two 256-bit vectors, + /// producing half as many elements of twice the element width. + /// + /// Left vector to multiply. + /// Right vector to multiply + /// + /// + /// { + /// A[1] * B[1], + /// A[3] * B[3], + /// A[5] * B[5], + /// A[7] * B[7] + /// } + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 MultiplyOdd(Vector256 left, Vector256 right) + => Vector256.Create( + left[1] * right[1], + left[3] * right[3], + left[5] * right[5], + left[7] * right[7]); + + /// + /// Produces a vector by interleaving the even-indexed elements + /// of the left and right vectors. + /// + /// Left vector to interleave. + /// Right vector to interleave. + /// + /// + /// { + /// A[0], B[0], + /// A[2], B[2], + /// A[4], B[4], + /// A[6], B[6] + /// } + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 InterleaveEven( + Vector256 left, + Vector256 right) + => Vector256.Create( + left[0], + right[0], + left[2], + right[2], + left[4], + right[4], + left[6], + right[6]); + + /// + /// Produces a vector by interleaving the odd-indexed elements + /// of the left and right vectors. + /// + /// Left vector to interleave. + /// Right vector to interleave. + /// + /// + /// { + /// A[1], B[1], + /// A[3], B[3], + /// A[5], B[5], + /// A[7], B[7] + /// } + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 InterleaveOdd( + Vector256 left, + Vector256 right) + => Vector256.Create( + left[1], + right[1], + left[3], + right[3], + left[5], + right[5], + left[7], + right[7]); + + /// + /// Produces a vector with masks where 0xFFFFFFFF specifies + /// that the left value does not equal to the right value and + /// 0x00000000 specifies that the value equals to the + /// right value. + /// + /// Left vector to compare for inequality. + /// Right vector to compare for inequality. + /// + /// 0xFFFFFFFF for values that aren't equal, 0x00000000 for + /// values that are equal. This is essentially the inverse of + /// . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 NotEqual( + Vector256 left, + Vector256 right) => ~Vector256.Equals(left, right); + + /// + /// Interleaves the lower half of the vector. + /// + /// First vector + /// Second vector + /// + /// { a[0], b[0], a[1], b[1], a[2], b[2], a[3], b[3] } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 InterleaveLower(Vector256 a, Vector256 b) + { + Vector256 shuffledA = Vector256.Shuffle(a, Vector256.Create(0, 0, 1, 1, 2, 2, 3, 3)); + Vector256 shuffledB = Vector256.Shuffle(b, Vector256.Create(0, 0, 1, 1, 2, 2, 3, 3)); + + Vector256 maskA = Vector256.Create(-1, 0, -1, 0, -1, 0, -1, 0); + Vector256 maskB = Vector256.Create(0, -1, 0, -1, 0, -1, 0, -1); + + return (shuffledA & maskA) | (shuffledB & maskB); + } + + /// + /// Interleaves the upper half of the vector. + /// + /// First vector + /// Second vector + /// + /// { a[4], b[4], a[5], b[5], a[6], b[6], a[7], b[7] } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 InterleaveUpper(Vector256 a, Vector256 b) + { + Vector256 shuffledA = Vector256.Shuffle(a, Vector256.Create(4, 4, 5, 5, 6, 6, 7, 7)); + Vector256 shuffledB = Vector256.Shuffle(b, Vector256.Create(4, 4, 5, 5, 6, 6, 7, 7)); + + Vector256 maskA = Vector256.Create(-1, 0, -1, 0, -1, 0, -1, 0); + Vector256 maskB = Vector256.Create(0, -1, 0, -1, 0, -1, 0, -1); + + return (shuffledA & maskA) | (shuffledB & maskB); + } } diff --git a/src/ImageSharp/Common/InlineArray.cs b/src/ImageSharp/Common/InlineArray.cs index 700551a8f3..d4cde5f256 100644 --- a/src/ImageSharp/Common/InlineArray.cs +++ b/src/ImageSharp/Common/InlineArray.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. // @@ -71,6 +71,24 @@ internal struct InlineArray26 private T t; } +/// +/// Represents a safe, fixed sized buffer of 32 elements. +/// +[InlineArray(32)] +internal struct InlineArray32 +{ + private T t; +} + +/// +/// Represents a safe, fixed sized buffer of 33 elements. +/// +[InlineArray(33)] +internal struct InlineArray33 +{ + private T t; +} + /// /// Represents a safe, fixed sized buffer of 36 elements. /// @@ -88,3 +106,5 @@ internal struct InlineArray256 { private T t; } + + diff --git a/src/ImageSharp/Common/InlineArray.tt b/src/ImageSharp/Common/InlineArray.tt index d689b0469a..998f8ae105 100644 --- a/src/ImageSharp/Common/InlineArray.tt +++ b/src/ImageSharp/Common/InlineArray.tt @@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp; <#GenerateInlineArrays();#> <#+ -private static int[] Lengths = [4, 8, 14, 16, 18, 19, 26, 36, 256]; +private static int[] Lengths = [4, 8, 14, 16, 18, 19, 26, 32, 33, 36, 256]; void GenerateInlineArrays() { diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCustomTransformData.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCustomTransformData.cs index 3b3c6bc200..7cc5b9fe56 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCustomTransformData.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCustomTransformData.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Fields; namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs index ef74d80122..a6df675acf 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs @@ -3,6 +3,7 @@ #pragma warning disable SA1401 // Fields should be private +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.Processing; diff --git a/src/ImageSharp/Formats/Jxl/InlineArrays.cs b/src/ImageSharp/Formats/Jxl/InlineArrays.cs index f006d2fd09..acb9c3d413 100644 --- a/src/ImageSharp/Formats/Jxl/InlineArrays.cs +++ b/src/ImageSharp/Formats/Jxl/InlineArrays.cs @@ -7,30 +7,6 @@ namespace SixLabors.ImageSharp.Formats.Jxl; -[InlineArray(3)] -internal struct InlineArray3 -{ - private T first; -} - -/// -/// Used by JxlOpsinParameters -/// -[InlineArray(36)] -internal struct InlineArray36 -{ - private T first; -} - -/// -/// Used by JxlCustomTransformData -/// -[InlineArray(15)] -internal struct InlineArray15 -{ - private T first; -} - /// /// Used by JxlCustomTransformData /// @@ -48,12 +24,3 @@ internal struct InlineArray210 { private T first; } - -/// -/// Used by JxlWeightsSeparable5 -/// -[InlineArray(12)] -internal struct InlineArray12 -{ - private T first; -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs index d339e69087..8a37b7dbaa 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs @@ -3,7 +3,6 @@ using System.Buffers; using System.IO.Compression; -using SixLabors.ImageSharp.Formats.Jxl.IO; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs new file mode 100644 index 0000000000..316f1ed12f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs @@ -0,0 +1,1365 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// Suppress IDE0057. This is so we can stack-allocate +// a powers of 2 and then slice it to the appropriate +// length (which produces better code). +// +// Without this suppression, the analyzer produces a warning, +// recommending changing this: +// stackalloc ulong[32].Slice(0, 18) +// to: +// (stackalloc ulong[32])[..18] +// +// But then the analyzer produces a new warning, recommending +// to remove the paranthesis, changing this: +// (stackalloc ulong[32])[..18] +// to: +// stackalloc ulong[32][..18] +// +// which is invalid C# syntax. +#pragma warning disable IDE0057 // Use range operator + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; + +/// +/// Extreme performance JPEG XL encoder which provides minimal lossless compression. +/// It also uses minimal dependencies. +/// +internal sealed class JxlFastLosslessEncoder +{ + /// + /// Specifies maximum number of bytes a frame header may use. + /// + private const int MaxFrameHeaderSize = 5; + + private const int NumRawSymbols = 19; + + private const int NumLz77 = 33; + + /// + /// Cache/dictionary size for LZ77 + /// + private const int Lz77CacheSize = 32; + + private const int Lz77Offset = 224; + + private const int Lz77MinLength = 7; + + /// + /// Input frame data is stored here. + /// + private readonly IFjxlFrameInputSource input; + + /// + /// Image width of the input image. + /// + private readonly int width; + + /// + /// Image height of the input image. + /// + private readonly int height; + + /// + /// Image width in groups. + /// + private readonly int numGroupsX; + + /// + /// Image height in groups. + /// + private readonly int numGroupsY; + + /// + /// Image width in groups (DC). + /// + private readonly int numDcGroupsX; + + /// + /// Image height in groups (DC). + /// + private readonly int numDcGroupsY; + + /// + /// Number of channels. (f.e. RGBA is 4, YUV is 3) + /// + private readonly int channels; + + /// + /// Number of bits represented per pixel. (f.e. 8 means pixels + /// have a 0-255 range) + /// + /// + /// Higher bit depths can represent more colors. + /// + private readonly int bitDepth; + + /// + /// Should the output image be stored in big-endian order? + /// + private readonly bool isBigEndian; + + private readonly int effort; + + private readonly bool collided; + + /// + /// Prefix codes for LZ77. + /// + private InlineArray4 hcode; + + private readonly List lookup = []; + + /// + /// Bit writer to write the JPEG XL headers. + /// + private readonly BitWriter header; + + /// + /// Bit writers for writing JPEG XL groups. + /// + private readonly List> groupData = []; + + /// + /// Sizes for each group. + /// + private readonly List groupSizes = []; + + private int acGroupDataOffset; + + private int minDcGlobalSize; + + private int currentBitWriter; + + private int bitWriterBytePos; + + private int bitsInBuffer; + + private long bitBuffer; + + private bool processDone; + + /// + /// Abstracts access to a raster frame data required for encoding. + /// + internal interface IFjxlFrameInputSource : IDisposable + { + /// + /// Returns a span that wraps over channel color data at the + /// specified rectangular position. + /// + /// Target type of the color data. + /// Left offset + /// Right offset + /// Selection width + /// Selection height + /// The actual offset of the row in row-major order is stored here. + /// + /// A wrapper over the color data of the channel at the specified + /// position. + /// + public Span GetColorChannelData(int x, int y, int width, int height, out long rowOffset) + where T : unmanaged; + } + + /// + /// Gets minimum raw lengths for prefix coding. + /// + private static ReadOnlySpan MinimumRawLength => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + + /// + /// Gets maximum raw lengths for prefix coding. + /// + private static ReadOnlySpan MaximumRawLength => [7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 10]; + + /// + /// Gets a lookup used by the method + /// to translate a bucket into a base group size. + /// + private static ReadOnlySpan GroupSizeOffset => + [ + 0, + 1024, + 17408, + 4211712 + ]; + + /// + /// Gets a lookup to determine how many bits a TOC bucket uses. + /// + private static ReadOnlySpan TocBits => [12, 16, 24, 32]; + + /// + /// Approximates Floor(Log2(v)) using integers. + /// + /// Value to retrieve Floor(Log2(v)) of. + /// Floor of second logarithm of v, or 31 if v is equal to 0. + /// This method may use CPU intrinsics provided by the .NET Runtime (e.g. BMI1 on x86). + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint FloorLog2(uint v) => v == 0 ? 0 : 31u - (uint)BitOperations.LeadingZeroCount(v); + + /// + /// Approximates count trailing zeros of v using integers. + /// + /// Value to retrieve number of 0 bits after last 1 bit of. + /// After the least significant 1 bit, returns the number of 0 bits. E.g. 1000 1000 00 -> 5. + /// This method may use CPU intrinsics provided by the .NET Runtime (e.g. BMI1 on x86). + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint CtzNonZero(ulong v) => (uint)BitOperations.TrailingZeroCount(v); + + /// + /// Returns a TOC bucket based on the group size. + /// + /// Specified group size. + /// TOC bucket matching the appropriate group size. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int TocBucket(int groupSize) + { + int bucket = 0; + + while (bucket < 3 && groupSize >= GroupSizeOffset[bucket + 1]) + { + bucket++; + } + + return bucket; + } + + /// + /// Returns the total number of bits required to represent + /// all given group sizes in the TOC. + /// + /// Group sizes to calculate bit sizes of. + /// Accumulated number of bits required to represent each group size. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int TocSize(Span groupSizes) + { + int tocBits = 0; + + ref int unsafeRef = ref MemoryMarshal.GetReference(groupSizes); + + for (int i = 0; i < groupSizes.Length; i++) + { + // TODO: we can try using AVX2 gather intrinsics, + // especially because TocBits can absolutely fit + // in the L1 cache + int groupSize = Unsafe.Add(ref unsafeRef, i); + int bucketForGroupSize = TocBucket(groupSize); + int bitsUsedByBucket = TocBits[bucketForGroupSize]; + + tocBits += bitsUsedByBucket; + } + + return tocBits; + } + + /// + /// Returns the number of bytes for the frame header. + /// + /// Indicates presence of the alpha channel. + /// Indicates whether this is the final frame. + /// Frame header size in bytes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int FrameHeaderSize(bool containsAlpha, bool isLast) + { + // Original code (from libjxl): + // + // size_t nbits = 28 + (have_alpha ? 4 : 0) + (is_last ? 0 : 2); + // return (nbits + 7) / 8; + // + // In this implementation we just use constants to shave a few CPU cycles. + // The total amount of branches is reduced by one (for the !containsAlpha case), + // but we remove the arithmetic/shifting instructions. + unchecked + { + if (containsAlpha) + { + if (isLast) + { + return 5; // (34 + 7) / 8 + } + else + { + return 4; // (32 + 7) / 8 + } + } + else + { + return 4; // (30 + 7) / 8 AND (28 + 7) / 8 yield the same result + } + } + } + + private static long GetSectionSize(InlineArray4 groupData) + { + long size = 0; + + for (int j = 0; j < 4; j++) + { + BitWriter writer = groupData[j]; + + size += (writer.BytesWritten * 8) + writer.BitsInBuffer; + } + + return (size + 7) / 8; + } + + /// + /// Approximates number of bytes needed for the output image buffer. + /// + /// Bytes for the frame buffer. + private long GetOutputSize() + { + long totalSizeGroups = 0; + + Span> groups = CollectionsMarshal.AsSpan(this.groupData); + + for (int i = 0; i < groups.Length; i++) + { + InlineArray4 section = groups[i]; + + totalSizeGroups += GetSectionSize(section); + } + + return this.header.BytesWritten + totalSizeGroups; + } + + /// + /// Returns the maximum amount of bytes potentially required for the image buffer. + /// + /// Upper bound of bytes for frame buffer. + private long GetMaxRequiredOutput() => this.GetOutputSize() + 32; + + private void WriteHeader(bool addImageHeader, bool isLast) + { + BitWriter output = this.header; + bool haveAlpha = this.channels is 2 or 4; + + if (addImageHeader) + { + // File signature. This signature specifies + // a raw codestream. No container format here. + output.Write(16, 0x0AFF); + + // Handcrafted size header. + output.Write(1, 0); // Not small + + WriteSize(this.height); + output.Write(3, 0b000); // No special ratio + WriteSize(this.width); + + // Handcrafted image metadata + output.Write(1, 0); // all_default = 0 (don't assume values to be set to their defaults) + output.Write(1, 0); // extra_fields = 0 (extra fields are disabled and therefore not present) + output.Write(1, 0); // bit_depth.floating_point_sample = 0 (samples are integers) + + if (this.bitDepth == 8) + { + output.Write(2, 0b00); // bit_depth.bits_per_sample = 8 (predefined bit depth of 8 bits) + } + else if (this.bitDepth == 10) + { + output.Write(2, 0b01); // bit_depth.bits_per_sample = 10 (predefined bit depth of 10 bits) + } + else if (this.bitDepth == 12) + { + output.Write(2, 0b10); // bit_depth.bits_per_sample = 12 (predefined bit depth of 12 bits) + } + else + { + output.Write(2, 0b11); // Custom bit depth + output.Write(6, (ulong)this.bitDepth - 1); // bit depth minus 1 (so 0 becomes 1, 9 becomes 10, etc) + } + + if (this.bitDepth <= 14) + { + output.Write(1, 1); // 16-bit-buffer is sufficient + } + else + { + output.Write(1, 0); // 16-bit-buffer is NOT sufficient + } + + if (haveAlpha) + { + output.Write(2, 0b01); // Emit one extra channel (the alpha channel) + + if (this.bitDepth == 8) + { + output.Write(1, 1); // all_default = 1 (8-bit alpha is the default) + } + else + { + output.Write(1, 0); // all_default = 0 + output.Write(2, 0); // type = alpha + output.Write(1, 0); // samples are not floating point + + if (this.bitDepth == 10) + { + output.Write(2, 0b01); // bit_depth.bits_per_sample = 10 (predefined bit depth of 10 bits) + } + else if (this.bitDepth == 12) + { + output.Write(2, 0b10); // bit_depth.bits_per_sample = 12 (predefined bit depth of 12 bits) + } + else + { + output.Write(2, 0b11); // Custom bit depth + output.Write(6, (ulong)this.bitDepth - 1); // bit depth minus 1 (so 0 becomes 1, 9 becomes 10, etc) + } + + output.Write(2, 0); // dim_shift = 0 + output.Write(2, 0); // name_len = 0 + output.Write(1, 0); // alpha_associated = 0 + } + } + else + { + output.Write(2, 0b00); // 0 extra channels + } + + output.Write(1, 0); // not XYB + + if (this.channels > 2) + { + output.Write(1, 1); // color_encoding.all_default = 1 (sRGB) + } + else + { + output.Write(1, 0); // color_encoding.all_default = 0 + output.Write(1, 0); // color_encoding.want_icc = 0 + output.Write(2, 0b01); // Grayscale + output.Write(2, 0b01); // D65 + output.Write(1, 0); // No gamma transfer function + output.Write(2, 0b10); // transfer function: 2 + u(4) + output.Write(4, 11); // transfer function (specifies sRGB) + output.Write(2, 1); // relative rendering intent + } + + output.Write(2, 0b00); // No extensions + output.Write(1, 1); // all_default transform data + output.ZeroPadToByte(); // No ICC and no preview. Frame should start at byte boundary. + } + + // Handcrafted frame header + output.Write(1, 0); // all_default = 0 (non-default values) + output.Write(2, 0b00); // regular frame + output.Write(1, 1); // modular + output.Write(2, 0b00); // default flags + output.Write(1, 0); // not Y'Cb'Cr + output.Write(2, 0b00); // no upsampling + + if (haveAlpha) + { + output.Write(2, 0b00); // no alpha upsampling + } + + output.Write(2, 0b01); // default group size + output.Write(2, 0b00); // exactly one pass + output.Write(1, 0); // no custom size or origin + output.Write(2, 0b00); // Replace blending mode + + if (haveAlpha) + { + output.Write(2, 0b00); // Replace blending mode for alpha channel + } + + output.Write(2, 0b00); // a frame has no name + output.Write(1, 0); // loop filter is not all_default + output.Write(1, 0); // no Gaborish transform + output.Write(2, 0b00); // 0 EPF filters + output.Write(2, 0b00); // no LF extensions + output.Write(2, 0b00); // no FH extensions + + output.Write(1, 0); // no TOC permutation + output.ZeroPadToByte(); // TOC is byte aligned + + Span groupSizes = CollectionsMarshal.AsSpan(this.groupSizes); + + for (int i = 0; i < groupSizes.Length; i++) + { + int groupSize = groupSizes[i]; + + int bucket = TocBucket(groupSize); + output.Write(2, (ulong)bucket); + output.Write(TocBits[bucket] - 2, (ulong)(groupSize - GroupSizeOffset[bucket])); + } + + output.ZeroPadToByte(); // Groups are byte-aligned + + // Sizes are coded using a special variable-length + // kind of coding. This method does that here. + // + // It has a prefix of 2 bits, followed by the suffix of N + // bits which depend on the prefix: + // + // prefix 0b00: 9 consecutive bits + // prefix 0b01: 13 consecutive bits + // prefix 0b10: 18 consecutive bits + // prefix 0b11: 30 consecutive bits + void WriteSize(int size) + { + ulong sizeMinus1 = (ulong)size - 1uL; + + if (sizeMinus1 < (1 << 9)) + { + output.Write(2, 0b00); // 9 bits + output.Write(9, sizeMinus1); + } + else if (sizeMinus1 < (1 << 13)) + { + output.Write(2, 0b01); // 13 bits + output.Write(13, sizeMinus1); + } + else if (sizeMinus1 < (1 << 18)) + { + output.Write(2, 0b10); // 18 bits + output.Write(18, sizeMinus1); + } + else + { + output.Write(2, 0b11); // 30 bits + output.Write(30, sizeMinus1); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int ComputeDcGlobalPadding(Span groupSizes, int acGroupDataOffset, int minDcGlobalSize, bool containsAlpha, bool isLast) + { + // Libjxl reference implements this method like this: + /* + size_t ComputeDcGlobalPadding(const std::vector& group_sizes, + size_t ac_group_data_offset, + size_t min_dc_global_size, bool have_alpha, + bool is_last) { + std::vector new_group_sizes = group_sizes; + new_group_sizes[0] = min_dc_global_size; + size_t toc_size = TOCSize(new_group_sizes); + size_t actual_offset = + FrameHeaderSize(have_alpha, is_last) + toc_size + group_sizes[0]; + return ac_group_data_offset - actual_offset; + } + */ + // The reference implementation copies the entire vector so that + // element 0 can be modified without affecting the original. + // Since TocSize() does not throw, temporarily modify element 0 + // instead, avoiding the allocation and copy. + int firstItem = groupSizes[0]; + groupSizes[0] = minDcGlobalSize; + int tocSize = TocSize(groupSizes); + int actualOffset = FrameHeaderSize(containsAlpha, isLast) + tocSize + firstItem; + groupSizes[0] = firstItem; + return acGroupDataOffset - actualOffset; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void EncodeHybridUintLz77(int value, out int token, out int nBits, out int bits) + { + unchecked + { + int n = (int)FloorLog2((uint)value); + + if (value < 16) + { + token = value; + nBits = 0; + bits = 0; + } + else + { + token = 16 + n - 4; + nBits = n; + bits = value - (1 << n); + } + } + } + + /// + /// SIMD Mask32 + /// + private struct Mask32 + { + /// + /// Actual mask. + /// + public ushort Mask; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly uint CountPrefix() => CtzNonZero(~(uint)this.Mask); + } + + /// + /// Wrapper over a 32-bit integer vector. + /// + /// Underlying vector. + private struct SimdVec32(Vector vector) + { + /// + /// The actual vector for this simd vector. + /// + public Vector Vec = vector; + + /// + /// Adds both vectors. + /// + /// First vector + /// Second vector + /// a + b + public static SimdVec32 operator +(SimdVec32 a, SimdVec32 b) => new(a.Vec + b.Vec); + + /// + /// Subtracts both vectors. + /// + /// First vector + /// Second vector + /// a - b + public static SimdVec32 operator -(SimdVec32 a, SimdVec32 b) => new(a.Vec - b.Vec); + + /// + /// XORs both vectors. + /// + /// First vector + /// Second vector + /// a ^ b + public static SimdVec32 operator ^(SimdVec32 a, SimdVec32 b) => new(a.Vec ^ b.Vec); + + /// + /// Sets bits to all 1 if vector items are equal, otherwise to all 0. + /// For example, if vector a is {5, 2, 4, 1} and vector b is {7, 3, 4, 5} + /// the result is {0, 0, 0xFFFFFFFF, 0}. + /// + /// First vector + /// Second vector + /// a == b + public static SimdVec32 operator ==(SimdVec32 a, SimdVec32 b) => new(Vector.Equals(a.Vec, b.Vec)); + + // We don't use this. It's to remove an error where == requires !=. + public static SimdVec32 operator !=(SimdVec32 a, SimdVec32 b) => new(Vector.Equals(a.Vec, b.Vec)); + + /// + /// Sets bits to all 1 if vector items are larger, otherwise to all 0. + /// For example, if vector a is {5, 2, 4, 1} and vector b is {3, 3, 4, 5} + /// the result is {0xFFFFFFFF, 0, 0, 0}. + /// + /// First vector + /// Second vector + /// a > b + public static SimdVec32 operator >(SimdVec32 a, SimdVec32 b) => new(Vector.GreaterThan(a.Vec, b.Vec)); + + /// + /// Sets bits to all 1 if vector items are lower, otherwise to all 0. + /// For example, if vector a is {5, 2, 4, 1} and vector b is {3, 3, 4, 5} + /// the result is {0, 0xFFFFFFFF, 0, 0xFFFFFFFF}. + /// + /// First vector + /// Second vector + /// a < b + public static SimdVec32 operator <(SimdVec32 a, SimdVec32 b) => new(Vector.LessThan(a.Vec, b.Vec)); + + /// + /// Converts this vector to a mask. + /// + /// + /// Mask where bits are 1 if the item + /// at the index is set to all 1, otherwise 0. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Mask32 ToMask() + { + int mask = 0; + + for (int i = 0; i < 16 && i < Vector.Count; i++) + { + if (this.Vec[i] == uint.MaxValue) + { + mask |= 1 << i; + } + } + + return new() { Mask = (ushort)mask }; + } + + public static SimdVec32 Load(Span data) => new(new Vector(data)); + + public static SimdVec32 Value(uint value) => new(new Vector(value)); + + public readonly SimdVec32 ValueToToken() => new(new Vector(32u) - GetLzcnt(this.Vec)); + + public readonly SimdVec32 SaturateSubtract(SimdVec32 toSubtract) => new(Vector.Max(this.Vec, toSubtract.Vec) - toSubtract.Vec); + + public readonly SimdVec32 Pow2() => new(Vector.ShiftLeft(Vector.One, unchecked((int)this.Vec[0]))); + + public readonly void Store(Span data) => this.Vec.CopyTo(data); + + // We don't use this. + public override readonly bool Equals(object? obj) => false; + + // We don't use this. + public override readonly int GetHashCode() => this.Vec.GetHashCode(); + } + + /// + /// SIMD Mask16 + /// + private struct Mask16 + { + /// + /// Actual mask. + /// + public uint Mask; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly uint CountPrefix() => CtzNonZero(~this.Mask); + } + + /// + /// Wrapper over a 16-bit integer vector. + /// + /// Underlying vector. + private struct SimdVec16(Vector vector) + { + /// + /// The actual vector for this simd vector. + /// + public Vector Vec = vector; + + /// + /// Adds both vectors. + /// + /// First vector + /// Second vector + /// a + b + public static SimdVec16 operator +(SimdVec16 a, SimdVec16 b) => new(a.Vec + b.Vec); + + /// + /// Subtracts both vectors. + /// + /// First vector + /// Second vector + /// a - b + public static SimdVec16 operator -(SimdVec16 a, SimdVec16 b) => new(a.Vec - b.Vec); + + /// + /// XORs both vectors. + /// + /// First vector + /// Second vector + /// a ^ b + public static SimdVec16 operator ^(SimdVec16 a, SimdVec16 b) => new(a.Vec ^ b.Vec); + + /// + /// Sets bits to all 1 if vector items are equal, otherwise to all 0. + /// For example, if vector a is {5, 2, 4, 1} and vector b is {7, 3, 4, 5} + /// the result is {0, 0, 0xFFFF, 0}. + /// + /// First vector + /// Second vector + /// a == b + public static SimdVec16 operator ==(SimdVec16 a, SimdVec16 b) => new(Vector.Equals(a.Vec, b.Vec)); + + // We don't use this. It's to remove an error where == requires !=. + public static SimdVec16 operator !=(SimdVec16 a, SimdVec16 b) => new(Vector.Equals(a.Vec, b.Vec)); + + /// + /// Sets bits to all 1 if vector items are larger, otherwise to all 0. + /// For example, if vector a is {5, 2, 4, 1} and vector b is {3, 3, 4, 5} + /// the result is {0xFFFF, 0, 0, 0}. + /// + /// First vector + /// Second vector + /// a > b + public static SimdVec16 operator >(SimdVec16 a, SimdVec16 b) => new(Vector.GreaterThan(a.Vec, b.Vec)); + + /// + /// Sets bits to all 1 if vector items are lower, otherwise to all 0. + /// For example, if vector a is {5, 2, 4, 1} and vector b is {3, 3, 4, 5} + /// the result is {0, 0xFFFF, 0, 0xFFFF}. + /// + /// First vector + /// Second vector + /// a < b + public static SimdVec16 operator <(SimdVec16 a, SimdVec16 b) => new(Vector.LessThan(a.Vec, b.Vec)); + + /// + /// Converts this vector to a mask. + /// + /// + /// Mask where bits are 1 if the item + /// at the index is set to all 1, otherwise 0. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Mask16 ToMask() + { + uint mask = 0; + + for (int i = 0; i < 32 && i < Vector.Count; i++) + { + if (this.Vec[i] == ushort.MaxValue) + { + mask |= 1u << i; + } + } + + return new() { Mask = mask }; + } + + public static SimdVec16 FromTwo32(SimdVec32 lo, SimdVec32 hi) + { + Vector narrow = Vector.Narrow(lo.Vec, hi.Vec); + return new(narrow); + } + + public static SimdVec16 Load(Span data) => new(new Vector(data)); + + public static SimdVec16 Value(ushort value) => new(new Vector(value)); + + public readonly SimdVec16 ValueToToken() => new(new Vector(16) - GetLzcnt(this.Vec)); + + public readonly SimdVec16 SaturateSubtract(SimdVec16 toSubtract) => new(Vector.Max(this.Vec, toSubtract.Vec) - toSubtract.Vec); + + public readonly SimdVec16 Pow2() => new(Vector.ShiftLeft(Vector.One, unchecked(this.Vec[0]))); + + public readonly void Store(Span data) => this.Vec.CopyTo(data); + + // We don't use this. + public override readonly bool Equals(object? obj) => false; + + // We don't use this. + public override readonly int GetHashCode() => this.Vec.GetHashCode(); + } + + /// + /// Pair of two vectors. + /// + /// Type of the vector. + /// Low vector + /// High vector + private struct VectorPair(T lo, T hi) + where T : unmanaged + { + public T Low = lo; + public T High = hi; + } + + /// + /// The prefix code is used for encoding LZ77-compressed coefficients. + /// + private sealed class PrefixCode + { +#pragma warning disable SA1401 // Fields should be private + + /// + /// Maximum number of raw symbols for prefix coding. + /// + private const int MaxNumSymbols = NumRawSymbols + 1 < NumLz77 ? NumLz77 : NumRawSymbols + 1; + + /// + /// Gets or sets the Huffman raw bit lengths. + /// + public InlineArray19 RawLengths; + + /// + /// Gets or sets the Huffman raw code values. + /// + public InlineArray19 RawCodes; + + /// + /// Gets or sets the Huffman LZ77 bit lengths. + /// + public InlineArray33 Lz77Lengths; + + /// + /// Gets or sets the Huffman LZ77 code values. + /// + public InlineArray33 Lz77Codes; + + /// + /// Gets or sets the Huffman LZ77 cache code values. + /// + public InlineArray32 Lz77CacheBits; + + /// + /// Gets or sets the Huffman LZ77 cache bit lengths. + /// + public InlineArray32 Lz77CacheLengths; + + public PrefixCode(Span rawCounts, Span lz77Counts) + { + Span level1Counts = stackalloc ulong[NumRawSymbols + 1]; + rawCounts[..NumRawSymbols].CopyTo(level1Counts); + + this.RawCount = NumRawSymbols; + + while (this.RawCount > 0 && level1Counts[this.RawCount - 1] == 0) + { + this.RawCount--; + } + + level1Counts[this.RawCount] = 0; + + for (int i = 0; i < NumLz77; i++) + { + level1Counts[this.RawCount] += lz77Counts[i]; + } + + Span level1Lengths = stackalloc byte[NumRawSymbols + 1]; + level1Lengths.Clear(); + + ComputeCodeLengths(level1Counts, this.RawCount + 1, MinimumRawLength, MaximumRawLength, level1Lengths); + + Span level2Lengths = stackalloc byte[NumLz77]; + Span minLengths = stackalloc byte[NumLz77]; + + level2Lengths.Clear(); + minLengths.Clear(); + + int l = 15 - level1Lengths[this.RawCount]; + Span maxLengths = stackalloc byte[NumLz77]; + maxLengths.Fill((byte)l); + + int numLz77 = NumLz77; + while (numLz77 > 0 && lz77Counts[numLz77 - 1] == 0) + { + numLz77--; + } + + ComputeCodeLengths(lz77Counts, numLz77, minLengths, maxLengths, level2Lengths); + + level1Lengths[..this.RawCount].CopyTo(this.RawLengths); + + for (int i = 0; i < numLz77; i++) + { + this.Lz77Lengths[i] = (byte)(level2Lengths[i] != 0 ? level1Lengths[this.RawCount] + level2Lengths[i] : 0); + } + + ComputeCanonicalCode(this.RawLengths, this.RawCodes, this.Lz77Lengths, this.Lz77Codes); + + // Prepare the LZ77 cache + for (int count = 0; count < Lz77CacheSize; count++) + { + EncodeHybridUintLz77(count, out int token, out int nbits, out int bits); + this.Lz77CacheLengths[count] = (byte)(this.Lz77Lengths[token] + nbits + this.RawLengths[0]); + this.Lz77CacheBits[count] = + (ulong)((((bits << this.Lz77Lengths[token]) | this.Lz77Codes[token]) << this.RawLengths[0]) | + this.RawLengths[0]); + } + } + + /// + /// Gets a lookup used to reverse integers bit-wise. + /// + private static ReadOnlySpan ReverseNibbleLookup => + [ + 0b0000, 0b1000, 0b0100, 0b1100, 0b0010, 0b1010, 0b0110, 0b1110, + 0b0001, 0b1001, 0b0101, 0b1101, 0b0011, 0b1011, 0b0111, 0b1111, + ]; + +#pragma warning restore SA1401 // Fields should be private + + /// + /// Gets or sets the number of raw codes. + /// + public int RawCount { get; set; } + + /// + /// Reverses the integer bit-wise. + /// + /// Number of bits for the integer. + /// Actual bits to reverse. + /// + /// Input integer but reversed. F.e. 10010 becomes 01001. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ushort BitReverse(int nbits, ushort bits) + { + unchecked + { + ushort rev16 = (ushort)((ReverseNibbleLookup[bits & 0xF] << 12) | + (ReverseNibbleLookup[(bits >> 4) & 0xF] << 8) | + (ReverseNibbleLookup[(bits >> 8) & 0xF] << 4) | + ReverseNibbleLookup[bits >> 12]); + return (ushort)(rev16 >> (16 - nbits)); + } + } + + private static void ComputeCanonicalCode(Span firstChunkLengths, Span firstChunkCodes, Span secondChunkLengths, Span secondChunkCodes) + { + const int maxCodeLength = 15; + + Span codeLengthCounts = stackalloc byte[maxCodeLength + 1]; + codeLengthCounts.Clear(); + + for (int i = 0; i < firstChunkCodes.Length; i++) + { + codeLengthCounts[firstChunkLengths[i]]++; + + if (firstChunkLengths[i] > 8) + { + throw new InvalidOperationException("First chunk length is too large"); + } + + if (firstChunkLengths[i] <= 0) + { + throw new InvalidOperationException("First chunk length cannot be <= 0"); + } + } + + for (int i = 0; i < secondChunkCodes.Length; i++) + { + codeLengthCounts[secondChunkLengths[i]]++; + + if (secondChunkLengths[i] > maxCodeLength) + { + throw new InvalidOperationException("Second chunk length is too large"); + } + } + + Span nextCode = stackalloc ushort[maxCodeLength + 1]; + nextCode.Clear(); + + ushort code = 0; + + for (int i = 1; i < maxCodeLength + 1; i++) + { + code = unchecked((ushort)((code + codeLengthCounts[i - 1]) << 1)); + nextCode[i] = code; + } + + unchecked + { + for (int i = 0; i < firstChunkCodes.Length; i++) + { + firstChunkCodes[i] = (byte)BitReverse(firstChunkLengths[i], nextCode[firstChunkLengths[i]]++); + } + + for (int i = 0; i < secondChunkCodes.Length; i++) + { + secondChunkCodes[i] = (byte)BitReverse(secondChunkLengths[i], nextCode[secondChunkLengths[i]]++); + } + } + } + + private static void ComputeCodeLengthsNonZeroImpl( + Span freqs, + int n, + int precision, + T infty, + Span minLimit, + Span maxLimit, + Span nbits) + where T : unmanaged, INumber + { + DebugGuard.MustBeLessThan(precision, 15, nameof(precision)); + DebugGuard.MustBeLessThanOrEqualTo(n, MaxNumSymbols, nameof(n)); + + int scale = 1 << precision; + int width = scale + 1; + + Span dynp = stackalloc T[width * (n + 1)]; + dynp.Fill(infty); + dynp[0] = T.Zero; + + for (int sym = 0; sym < n; sym++) + { + for (int bits = minLimit[sym]; bits <= maxLimit[sym]; bits++) + { + int offsetDelta = 1 << (precision - bits); + T cost = T.CreateChecked(freqs[sym]) * T.CreateChecked(bits); + + for (int off = 0; off + offsetDelta <= scale; off++) + { + int current = (sym * width) + off; + int next = ((sym + 1) * width) + off + offsetDelta; + + dynp[next] = T.Min(dynp[current] + cost, dynp[next]); + } + } + } + + int offFinal = scale; + + for (int sym = n - 1; sym >= 0; sym--) + { + if (offFinal <= 0) + { + throw new InvalidOperationException("Offset should be greater than zero"); + } + + for (int bits = minLimit[sym]; bits <= maxLimit[sym]; bits++) + { + int offsetDelta = 1 << (precision - bits); + + if (offsetDelta <= offFinal) + { + int current = (sym * width) + offFinal; + int previous = (sym * width) + offFinal - offsetDelta; + + T cost = T.CreateChecked(freqs[sym]) * T.CreateChecked(bits); + + if (dynp[current] == dynp[previous] + cost) + { + offFinal -= offsetDelta; + nbits[sym] = (byte)bits; + break; + } + } + } + } + } + + private static void ComputeCodeLengthsNonZero(Span freqs, int n, Span minLimit, Span maxLimit, Span nbits) + { + int precision = 0; + int shortestLength = 255; + ulong frequencySum = 0; + + for (int i = 0; i < n; i++) + { + frequencySum += freqs[i]; + + if (minLimit[i] < 1) + { + minLimit[i] = 1; + } + + precision = Math.Max(maxLimit[i], precision); + shortestLength = Math.Min(minLimit[i], shortestLength); + } + + precision -= shortestLength - 1; + ulong infinity = frequencySum * (ulong)precision; + + if (infinity < uint.MaxValue / 2) + { + ComputeCodeLengthsNonZeroImpl(freqs, n, precision, (uint)infinity, minLimit, maxLimit, nbits); + } + else + { + ComputeCodeLengthsNonZeroImpl(freqs, n, precision, infinity, minLimit, maxLimit, nbits); + } + } + + private static void ComputeCodeLengths(Span freqs, int n, ReadOnlySpan minLimitIn, ReadOnlySpan maxLimitIn, Span nbits) + { + DebugGuard.MustBeLessThanOrEqualTo(n, MaxNumSymbols, nameof(n)); + + Span compactFreqs = stackalloc ulong[MaxNumSymbols]; + Span minLimit = stackalloc byte[MaxNumSymbols]; + Span maxLimit = stackalloc byte[MaxNumSymbols]; + + int ni = 0; + for (int i = 0; i < n; i++) + { + if (freqs[i] != 0) + { + compactFreqs[ni] = freqs[i]; + minLimit[ni] = minLimitIn[i]; + maxLimit[ni] = maxLimitIn[i]; + ni++; + } + } + + compactFreqs[ni..].Clear(); + minLimit[ni..].Clear(); + maxLimit[ni..].Clear(); + + Span numBits = stackalloc byte[MaxNumSymbols]; + numBits.Clear(); + + ComputeCodeLengthsNonZero(compactFreqs, ni, minLimit, maxLimit, numBits); + + ni = 0; + + for (int i = 0; i < n; i++) + { + nbits[i] = 0; + if (freqs[i] != 0) + { + nbits[i] = numBits[ni++]; + } + } + } + + /// + /// Writes this LZ77 prefix code into the bit-stream. + /// + /// The bit-stream to write the prefix code into. + public void Write(BitWriter writer) + { + Span codeLengthCounts = stackalloc ulong[32].Slice(0, 18); + codeLengthCounts.Clear(); + codeLengthCounts[17] = 3 + (2 * (NumLz77 - 1)); + + for (int i = 0; i < 19; i++) + { + byte rawLength = this.RawLengths[i]; + + codeLengthCounts[rawLength]++; + } + + for (int i = 0; i < 33; i++) + { + byte lz77Length = this.Lz77Lengths[i]; + + codeLengthCounts[lz77Length]++; + } + + // Lengths for representing the code length + Span codeLengthLengths = stackalloc byte[32].Slice(0, 18); + Span codeLengthLengthsMinimum = stackalloc byte[32].Slice(0, 18); + Span codeLengthLengthsMaximum = stackalloc byte[32].Slice(0, 18); + + codeLengthLengths.Clear(); + codeLengthLengthsMinimum.Clear(); + codeLengthLengthsMaximum.Fill(5); + + ComputeCodeLengths(codeLengthCounts, 18, codeLengthLengthsMinimum, codeLengthLengthsMaximum, codeLengthLengths); + + writer.Write(2, 0b00); // HSKIP = 0 (Don't skip code lengths) + + // As per Brotli RFC + Span codeLengthOrder = [1, 2, 3, 4, 0, 5, 17, 6, 16, + 7, 8, 9, 10, 11, 12, 13, 14, 15]; + + // Lengths & codes for representing lengths of code lengths + Span codeLengthLengthLengths = [2, 4, 3, 2, 2, 4]; + Span codeLengthLengthCodes = [0, 7, 3, 2, 1, 15]; + + // Maximum number of code lengths + int numCodeLengths = 18; + while (codeLengthLengths[codeLengthOrder[numCodeLengths - 1]] == 0) + { + numCodeLengths--; + } + + // Max bits written in this loop: 18 * 4 = 72 + for (int i = 0; i < numCodeLengths; i++) + { + int symbol = codeLengthLengths[codeLengthOrder[i]]; + writer.Write(codeLengthLengthLengths[symbol], codeLengthLengthCodes[symbol]); + } + + Span codeLengthBits = stackalloc ushort[32].Slice(0, 18); + codeLengthBits.Clear(); + ComputeCanonicalCode([], [], codeLengthLengths, codeLengthBits); + + for (int i = 0; i < 19; i++) + { + byte rawLength = this.RawLengths[i]; + + writer.Write(codeLengthLengths[rawLength], codeLengthBits[rawLength]); + } + + int numLz77 = NumLz77; + while (this.Lz77Lengths[numLz77 - 1] == 0) + { + numLz77--; + } + + // Max bits in this block: 24 + writer.Write(codeLengthLengths[17], codeLengthBits[17]); + writer.Write(3, 0b010); // 5 + writer.Write(codeLengthLengths[17], codeLengthBits[17]); + writer.Write(3, 0b000); // (5 - 2) * 8 + 3 = 27 + writer.Write(codeLengthLengths[17], codeLengthBits[17]); + writer.Write(3, 0b010); // (27 - 2) * 8 + 5 = 205 + + // Encode LZ77 symbols with values 224 + i. + // Max. bits in this loop: 33 * 5 = 165 + for (int i = 0; i < numLz77; i++) + { + writer.Write(codeLengthLengths[this.Lz77Lengths[i]], codeLengthBits[this.Lz77Lengths[i]]); + } + } + } + + /// + /// Simple MSB-first bit-stream writer implementation built on top + /// of a stream. + /// + /// Output bytes are written here. + private sealed class BitWriter(Stream stream) : IDisposable + { + /// + /// Temporary cache used to store pending written bits + /// before they're written to the output stream. + /// + private ulong buffer; + + /// + /// Gets the total number of bytes written to the output buffer so far. + /// + public long BytesWritten { get; private set; } + + /// + /// Gets the number of bits actively in the bit cache. + /// This is used to track how many bits were written into + /// the cache prior to sending the cache to the stream. + /// + public int BitsInBuffer { get; private set; } + + /// + /// Writes the specified bits in the Most Significant Byte (MSB) + /// order. + /// + /// Represents the number of bits to write to the bit-stream. + /// Represents the value to write to the bit-stream. + public void Write(int count, ulong bits) + { + DebugGuard.MustBeBetweenOrEqualTo(count, 0, 56, nameof(count)); + + if (count < 64) + { + bits &= (1UL << count) - 1; + } + + this.buffer |= bits << this.BitsInBuffer; + this.BitsInBuffer += count; + + this.FlushBytes(); + } + + /// + /// Internal method used to flush bytes from the cache + /// () into the output stream. + /// + private void FlushBytes() + { + int bytes = this.BitsInBuffer / 8; + + for (int i = 0; i < bytes; i++) + { + stream.WriteByte((byte)this.buffer); + this.BytesWritten++; + this.buffer >>= 8; + } + + this.BitsInBuffer -= bytes * 8; + } + + /// + /// Used by the dispose method to flush the remaining bits + /// that are not byte-aligned. F.e. if we dispose this reader + /// and we have 5 bits left, those final 5 bits are set to all 0 + /// and the byte is written to the stream. + /// + public void ZeroPadToByte() + { + if (this.BitsInBuffer != 0) + { + this.Write(8 - this.BitsInBuffer, 0); + } + } + + /// + /// Flushes out the final bytes. + /// + public void Dispose() => this.ZeroPadToByte(); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlLinearAlgebra.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlLinearAlgebra.cs new file mode 100644 index 0000000000..9b8e2f9fa6 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlLinearAlgebra.cs @@ -0,0 +1,52 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using Matrix2x2 = System.Runtime.CompilerServices.InlineArray2>; +using Vector2 = System.Runtime.CompilerServices.InlineArray2; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; + +/// +/// Handles linear algebra for encoding. +/// +internal static class JxlLinearAlgebra +{ + public static void ConvertToDiagonal(Matrix2x2 a, Vector2 diag, Matrix2x2 u) + { + DebugGuard.MustBeLessThan(Math.Abs(a[0][1] - a[1][0]), 1e-15, nameof(a)); + + double b = -(a[0][0] + a[1][1]); + double c = (a[0][0] * a[1][1]) - (a[0][1] * a[0][1]); + double d = (b * b) - (4.0 * c); + + if (Math.Abs(a[0][1]) < 1e-10 || d < 0) + { + // Already diagonal. + diag[0] = a[0][0]; + diag[1] = a[1][1]; + u[0][0] = u[1][1] = 1.0; + u[0][1] = u[1][0] = 0.0; + return; + } + + double sqd = Math.Sqrt(d); + double l1 = (-b - sqd) * 0.5; + double l2 = (-b + sqd) * 0.5; + + Vector2 v1 = default; + v1[0] = a[0][0] - l1; + v1[1] = a[1][0]; + + double v1n = 1.0 / JxlMath.Hypot(v1[0], v1[1]); + v1[0] = v1[0] * v1n; + v1[1] = v1[1] * v1n; + + diag[0] = l1; + diag[1] = l2; + + u[0][0] = v1[1]; + u[0][1] = -v1[0]; + u[1][0] = v1[0]; + u[1][1] = v1[1]; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Jpeg/JpegAppMarkerType.cs b/src/ImageSharp/Formats/Jxl/Processing/Jpeg/JpegAppMarkerType.cs new file mode 100644 index 0000000000..2b05317ab4 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Jpeg/JpegAppMarkerType.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Jpeg; + +/// +/// Identifies the kind of APP marker in a JPEG file. +/// +internal enum JpegAppMarkerType : byte +{ + /// + /// Unknown APP marker + /// + Unknown, + + /// + /// Contains ICC profile metadata + /// + Icc, + + /// + /// Contains EXIF profile metadata + /// + Exif, + + /// + /// Contains XMP profile metadata + /// + Xmp +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDequantMatrices.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlDequantMatrices.cs new file mode 100644 index 0000000000..15b0935415 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlDequantMatrices.cs @@ -0,0 +1,265 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Contains matrices used to inverse quantize coefficients. +/// +internal sealed class JxlDequantMatrices +{ + /// + /// Sum(DotProduct(RequiredSizeX, RequiredSizeY)). + /// + private const int SumRequiredXY = 2056; + + private const int TotalTableSize = SumRequiredXY * JxlFrameDimensions.DctBlockSize * 3; + + /// + /// Contains weights & multipliers for transforms used by the codec (e.g. DCT, identity, AFV). + /// + public static readonly JxlQuantizerEncoding[] Library = GetLibrary(); + + private uint computedMask; + + /// + /// Storage for quantization. + /// + private readonly Memory tableStorage; + + /// + /// Contains matrices for forward quantization. + /// + private readonly Memory table; + + /// + /// Contains matrices for inverse quantization. + /// + private readonly Memory inverseTable; + + /// + /// Quantization table for DC + /// + private InlineArray3 dcQuant; + + /// + /// Inverse quantization table for DC + /// + private InlineArray3 inverseDcQuant; + + /// + /// Table offsets. + /// + private readonly int[] tableOffsets = new int[JxlAcStrategy.NumberOfValidStrategies * 3]; + + /// + /// Quantizer encodings. Multiple may be used depending on the kind of transform. + /// + private JxlQuantizerEncoding[] encodings = []; + + /// + /// Initializes a new instance of the class. + /// + public JxlDequantMatrices() + { + // float dc_quant_[3] = {kDCQuant[0], kDCQuant[1], kDCQuant[2]}; + // float inv_dc_quant_[3] = {kInvDCQuant[0], kInvDCQuant[1], kInvDCQuant[2]}; + this.dcQuant[0] = JxlQuantizerConstants.DcQuant[0]; + this.dcQuant[1] = JxlQuantizerConstants.DcQuant[1]; + this.dcQuant[2] = JxlQuantizerConstants.DcQuant[2]; + + this.inverseDcQuant[0] = JxlQuantizerConstants.InverseDcQuant[0]; + this.inverseDcQuant[1] = JxlQuantizerConstants.InverseDcQuant[1]; + this.inverseDcQuant[2] = JxlQuantizerConstants.InverseDcQuant[2]; + + this.encodings = new JxlQuantizerEncoding[JxlQuantizerConstants.NumberOfQuantizerTables]; + for (int i = 0; i < this.encodings.Length; i++) + { + this.encodings[i] = JxlQuantizerEncoding.Library(0); + } + + int pos = 0; + Span offsets = stackalloc int[JxlQuantizerConstants.NumberOfQuantizerTables * 3]; + + for (int i = 0; i < JxlQuantizerConstants.NumberOfQuantizerTables; i++) + { + int numBlocks = RequiredSizeX[i] * RequiredSizeY[i]; + int num = numBlocks * JxlFrameDimensions.DctBlockSize; + int i3 = 3 * i; + + for (int c = 0; c < 3; c++) + { + offsets[i3 + c] = pos + (c * num); + } + + pos += 3 * num; + } + + for (int i = 0; i < JxlAcStrategy.NumberOfValidStrategies; i++) + { + for (int c = 0; c < 3; c++) + { + this.tableOffsets[(i * 3) + c] = offsets[((int)JxlQuantizerConstants.AcStrategyToQuantTableMap[i] * 3) + c]; + } + } + } + + /// + /// Gets a lookup which represents required widths for each quantizer. + /// + private static ReadOnlySpan RequiredSizeX => [1, 1, 1, 1, 2, 4, 1, 1, 2, 1, 1, 8, 4, 16, 8, 32, 16]; + + /// + /// Gets a lookup which represents required heights for each quantizer. + /// + private static ReadOnlySpan RequiredSizeY => [1, 1, 1, 1, 2, 4, 2, 4, 4, 1, 1, 8, 8, 16, 16, 32, 32]; + + /// + /// Returns the default library with quantizer encodings for all transforms + /// used by the JPEG XL codec. + /// + /// Encodings for all kinds of transforms. + /// Used when quantization constants were partially updated. + public static JxlQuantizerEncoding[] GetLibrary() + { + if (JxlQuantizerConstants.NumberOfQuantizerTables != 17) + { + throw new InvalidOperationException("This function should be updated when adding new quantization types"); + } + + if (JxlQuantWeights.NumPredefinedTables != 1) + { + throw new InvalidOperationException("This function should be updated when adding new quantization matrices to the library"); + } + + Verify(0, JxlQuantTable.DCT); + Verify(1, JxlQuantTable.IDENTITY); + Verify(2, JxlQuantTable.DCT2X2); + Verify(3, JxlQuantTable.DCT4X4); + Verify(4, JxlQuantTable.DCT16X16); + Verify(5, JxlQuantTable.DCT32X32); + Verify(6, JxlQuantTable.DCT8X16); + Verify(7, JxlQuantTable.DCT8X32); + Verify(8, JxlQuantTable.DCT16X32); + Verify(9, JxlQuantTable.DCT4X8); + Verify(10, JxlQuantTable.AFV0); + Verify(11, JxlQuantTable.DCT64X64); + Verify(12, JxlQuantTable.DCT32X64); + Verify(13, JxlQuantTable.DCT128X128); + Verify(14, JxlQuantTable.DCT64X128); + Verify(15, JxlQuantTable.DCT256X256); + Verify(16, JxlQuantTable.DCT128X256); + + return + [ + JxlQuantWeights.Dct, + JxlQuantWeights.Identity, + JxlQuantWeights.Dct2x2, + JxlQuantWeights.Dct4x4, + JxlQuantWeights.Dct16x16, + JxlQuantWeights.Dct32x32, + JxlQuantWeights.Dct8x16, + JxlQuantWeights.Dct8x32, + JxlQuantWeights.Dct16x32, + JxlQuantWeights.Dct4x8, + JxlQuantWeights.Afv, + JxlQuantWeights.Dct64x64, + JxlQuantWeights.Dct32x32, + JxlQuantWeights.Dct128x128, + JxlQuantWeights.Dct64x128, + JxlQuantWeights.Dct256x256, + JxlQuantWeights.Dct128x256 + ]; + + [Conditional("DEBUG")] + static void Verify(int expected, JxlQuantTable actual) + { + if (expected != (byte)actual) + { + throw new InvalidOperationException("Quantizer modes were partially updated; this method needs to be updated too"); + } + } + } + + /// + /// Returns a matrix for the specified kind of quantizer and index. + /// + /// Quantizer kind + /// Index + /// Matrix + public Span GetMatrix(JxlAcStrategyType quantKind, int c) + { + DebugGuard.MustBeGreaterThan((1 << (int)quantKind) & this.computedMask, 0, nameof(quantKind)); + return this.table.Span[this.tableOffsets[((int)quantKind * 3) + c]..]; + } + + /// + /// Returns an inverse matrix for the specified kind of quantizer and index. + /// + /// Quantizer kind + /// Index + /// Inverse matrix + public Span GetInverseMatrix(JxlAcStrategyType quantKind, int c) + { + DebugGuard.MustBeGreaterThan((1 << (int)quantKind) & this.computedMask, 0, nameof(quantKind)); + return this.inverseTable.Span[this.tableOffsets[((int)quantKind * 3) + c]..]; + } + + /// + /// Returns a DC quant for index c. + /// + /// The DC quantizer index. + /// DC quant for index . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float GetDcQuant(int c) => this.dcQuant[c]; + + /// + /// Returns all DC quantizers. See also . + /// + /// Span that covers all DC quantizers. + public Span GetDcQuants() => this.dcQuant; + + /// + /// Returns an inverse DC quant for index c. + /// + /// The inverse DC quantizer index. + /// Inverse DC quant for index . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float GetInverseDcQuant(int c) => this.inverseDcQuant[c]; + + /// + /// Applies the specified DC quantizer. + /// + /// DC quantizer to apply to the dequantization matrices. + public void SetDcQuant(InlineArray3 dc) + { + for (int c = 0; c < 3; c++) + { + this.dcQuant[c] = 1f / dc[c]; + this.inverseDcQuant[c] = dc[c]; + } + } + + /// + /// Sets custom quantizer encodings for transform functions. + /// + /// The encodings to identify required transform functions. + public void SetEncodings(JxlQuantizerEncoding[] encodings) + { + this.encodings = encodings; + this.computedMask = 0; + } + + /// + /// Returns quantizer encodings for this dequant matrices instance. + /// + /// + /// Encodings set by the method. + /// By default (when the aforementioned method wasn't invoked), the result + /// is simply an empty span. + /// + public Span GetEncodings() => this.encodings; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlEndianness.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlEndianness.cs deleted file mode 100644 index a7c9dbc1a2..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlEndianness.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; - -/// -/// Specifies the ordering of multi-byte data. -/// -internal enum JxlEndianness : byte -{ - /// - /// Use endianness of the CPU/system. - /// - Native, - - /// - /// Force little endian. - /// - Little, - - /// - /// Force big endian. - /// - Big -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs index 825c81cbbc..2edfeca0c1 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs @@ -21,6 +21,11 @@ internal sealed class JxlLoopFilter : IJxlFields /// private const float InverseSigmaNum = -1.1715728752538099024f; + /// + /// / 3 + /// + public const float MinimumSigma = -3.90524291751269967465540850526868f; + /// /// Gets the number of EPF (Edge-preserving filter) sharp entries. /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs index 79eee30226..0157dba4e1 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs @@ -3,6 +3,7 @@ using System.Numerics; using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Common.Helpers; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; @@ -679,4 +680,56 @@ public static ulong CeilLog2Nonzero(ulong x) return floorLog2 + 1; } + + /// + /// Computes the hypotenuse of x and y. + /// + /// X + /// Y + /// Hypotenuse of x and y + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double Hypot(double x, double y) + { + x = Math.Abs(x); + y = Math.Abs(y); + + if (x < y) + { + RuntimeUtility.Swap(ref x, ref y); + } + + if (x == 0.0) + { + return 0.0; + } + + double ratio = y / x; + return x * Math.Sqrt(1 + (ratio * ratio)); + } + + /// + /// Computes the hypotenuse of x and y. + /// + /// X + /// Y + /// Hypotenuse of x and y + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Hypot(float x, float y) + { + x = MathF.Abs(x); + y = MathF.Abs(y); + + if (x < y) + { + RuntimeUtility.Swap(ref x, ref y); + } + + if (x == 0.0f) + { + return 0.0f; + } + + float ratio = y / x; + return x * MathF.Sqrt(1 + (ratio * ratio)); + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlPixelFormat.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlPixelFormat.cs index 15aa587159..fadb8709fb 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlPixelFormat.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlPixelFormat.cs @@ -24,7 +24,7 @@ internal struct JxlPixelFormat /// big-endian or little-endian format. Applies to ushort /// and float data types. /// - public JxlEndianness Endianness { get; set; } + public ByteOrder Endianness { get; set; } /// /// Gets or sets the alignment of scanlines to a multiple of diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantWeights.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantWeights.cs index 2c50d92afd..dfe12b8f30 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantWeights.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantWeights.cs @@ -12,4 +12,516 @@ internal static class JxlQuantWeights public const int CeilLog2NumPredefinedTables = 0; public const int Log2NumQuantModes = 3; + + /// + /// DCT quantizer encoding. (6 distance bands) + /// + public static readonly JxlQuantizerEncoding Dct = JxlQuantizerEncoding.Dct( + new JxlDctQuantWeightParameters( + [ + [3150f, 0f, -0.4f, -0.4f, -0.4f, -2f], + [560f, 0f, -0.3f, -0.3f, -0.3f, -0.3f], + [512f, -2f, -1f, 0f, -1f, -2f] + ], + 6)); + + /// + /// Identity quantizer encoding. + /// + public static readonly JxlQuantizerEncoding Identity = JxlQuantizerEncoding.Identity( + [ + [280f, 3160f, 3160f], + [60f, 864f, 864f], + [18f, 200f, 200f], + ]); + + /// + /// DCT2X2 quantizer encoding. + /// + public static readonly JxlQuantizerEncoding Dct2x2 = JxlQuantizerEncoding.Dct2( + [ + [3840f, 2560f, 1280f, 640f, 480f, 300f], + [960f, 640f, 320f, 180f, 140f, 120f], + [640f, 320f, 128f, 64f, 32f, 16f], + ]); + + /// + /// DCT4X4 quantizer encoding. + /// + public static readonly JxlQuantizerEncoding Dct4x4 = JxlQuantizerEncoding.Dct4( + new JxlDctQuantWeightParameters( + [ + [2200, 0, 0, 0], + [392, 0, 0, 0], + [112, -0.25f, -0.25f, -0.5f] + ], + 4), + [ + [1, 1], + [1, 1], + [1, 1] + ]); + + /// + /// DCT16x16 quantizer encoding. + /// + public static readonly JxlQuantizerEncoding Dct16x16 = JxlQuantizerEncoding.Dct( + new JxlDctQuantWeightParameters( + [ + [ + 8996.8725711814115328f, + -1.3000777393353804f, + -0.49424529824571225f, + -0.439093774457103443f, + -0.6350101832695744f, + -0.90177264050827612f, + -1.6162099239887414f, + ], + [ + 3191.48366296844234752f, + -0.67424582104194355f, + -0.80745813428471001f, + -0.44925837484843441f, + -0.35865440981033403f, + -0.31322389111877305f, + -0.37615025315725483f, + ], + [ + 1157.50408145487200256f, + -2.0531423165804414f, + -1.4f, + -0.50687130033378396f, + -0.42708730624733904f, + -1.4856834539296244f, + -4.9209142884401604f, + ] + ], + 7)); + + /// + /// DCT32x32 quantizer encoding. + /// + public static readonly JxlQuantizerEncoding Dct32x32 = JxlQuantizerEncoding.Dct( + new JxlDctQuantWeightParameters( + [ + [ + 15718.40830982518931456f, + -1.025f, + -0.98f, + -0.9012f, + -0.4f, + -0.48819395464f, + -0.421064f, + -0.27f, + ], + [ + 7305.7636810695983104f, + -0.8041958212306401f, + -0.7633036457487539f, + -0.55660379990111464f, + -0.49785304658857626f, + -0.43699592683512467f, + -0.40180866526242109f, + -0.27321683125358037f, + ], + [ + 3803.53173721215041536f, + -3.060733579805728f, + -2.0413270132490346f, + -2.0235650159727417f, + -0.5495389509954993f, + -0.4f, + -0.4f, + -0.3f, + ] + ], + 7)); + + /// + /// DCT8x16 quantizer encoding. + /// + public static readonly JxlQuantizerEncoding Dct8x16 = JxlQuantizerEncoding.Dct( + new JxlDctQuantWeightParameters( + [ + [ + 7240.7734393502f, + -0.7f, + -0.7f, + -0.2f, + -0.2f, + -0.2f, + -0.5f, + ], + [ + 1448.15468787004f, + -0.5f, + -0.5f, + -0.5f, + -0.2f, + -0.2f, + -0.2f, + ], + [ + 506.854140754517f, + -1.4f, + -0.2f, + -0.5f, + -0.5f, + -1.5f, + -3.6f, + ] + ], + 7)); + + /// + /// DCT8x32 quantizer encoding. + /// + public static readonly JxlQuantizerEncoding Dct8x32 = JxlQuantizerEncoding.Dct( + new JxlDctQuantWeightParameters( + [ + [ + 16283.2494710648897f, + -1.7812845336559429f, + -1.6309059012653515f, + -1.0382179034313539f, + -0.85f, + -0.7f, + -0.9f, + -1.2360638576849587f, + ], + [ + 5089.15750884921511936f, + -0.320049391452786891f, + -0.35362849922161446f, + -0.30340000000000003f, + -0.61f, + -0.5f, + -0.5f, + -0.6f, + ], + [ + 3397.77603275308720128f, + -0.321327362693153371f, + -0.34507619223117997f, + -0.70340000000000003f, + -0.9f, + -1.0f, + -1.0f, + -1.1754605576265209f, + ] + ], + 8)); + + /// + /// DCT16x32 quantizer encoding. + /// + public static readonly JxlQuantizerEncoding Dct16x32 = JxlQuantizerEncoding.Dct( + new JxlDctQuantWeightParameters( + [ + [ + 13844.97076442300573f, + -0.97113799999999995f, + -0.658f, + -0.42026f, + -0.22712f, + -0.2206f, + -0.226f, + -0.6f, + ], + [ + 4798.964084220744293f, + -0.61125308982767057f, + -0.83770786552491361f, + -0.79014862079498627f, + -0.2692727459704829f, + -0.38272769465388551f, + -0.22924222653091453f, + -0.20719098826199578f, + ], + [ + 1807.236946760964614f, + -1.2f, + -1.2f, + -0.7f, + -0.7f, + -0.7f, + -0.4f, + -0.5f, + ] + ], + 8)); + + /// + /// DCT4x8 quantizer encoding. + /// + public static readonly JxlQuantizerEncoding Dct4x8 = JxlQuantizerEncoding.Dct4x8( + new JxlDctQuantWeightParameters( + [ + [ + 2198.050556016380522f, + -0.96269623020744692f, + -0.76194253026666783f, + -0.6551140670773547f + ], + [ + 764.3655248643528689f, + -0.92630200888366945f, + -0.9675229603596517f, + -0.27845290869168118f + ], + [ + 527.107573587542228f, + -1.4594385811273854f, + -1.450082094097871593f, + -1.5843722511996204f + ] + ], + 4), + [1, 1, 1]); + + /// + /// AFV quantizer encoding. + /// + public static readonly JxlQuantizerEncoding Afv = JxlQuantizerEncoding.Afv( + Dct4x8.DctParameters!, + Dct4x4.DctParameters!, + [ + [3072, 3072, 256, 256, 256, 414, 0, 0, 0], + [1024, 1024, 50, 50, 50, 58, 0, 0, 0], + [384, 384, 12, 12, 12, 22, -0.25f, -0.25f, -0.25f] + ]); + + /// + /// DCT64x64 quantizer encoding. + /// + public static readonly JxlQuantizerEncoding Dct64x64 = JxlQuantizerEncoding.Dct( + new JxlDctQuantWeightParameters( + [ + [ + 0.9f * 26629.073922049845f, + -1.025f, + -0.78f, + -0.65012f, + -0.19041574084286472f, + -0.20819395464f, + -0.421064f, + -0.32733845535848671f, + ], + [ + 0.9f * 9311.3238710010046f, + -0.3041958212306401f, + -0.3633036457487539f, + -0.35660379990111464f, + -0.3443074455424403f, + -0.33699592683512467f, + -0.30180866526242109f, + -0.27321683125358037f, + ], + [ + 0.9f * 4992.2486445538634f, + -1.2f, + -1.2f, + -0.8f, + -0.7f, + -0.7f, + -0.4f, + -0.5f, + ] + ], + 8)); + + /// + /// DCT32x64 quantizer encoding. + /// + public static readonly JxlQuantizerEncoding Dct32x64 = JxlQuantizerEncoding.Dct( + new JxlDctQuantWeightParameters( + [ + [ + 0.65f * 23629.073922049845f, + -1.025f, + -0.78f, + -0.65012f, + -0.19041574084286472f, + -0.20819395464f, + -0.421064f, + -0.32733845535848671f, + ], + [ + 0.65f * 8611.3238710010046f, + -0.3041958212306401f, + -0.3633036457487539f, + -0.35660379990111464f, + -0.3443074455424403f, + -0.33699592683512467f, + -0.30180866526242109f, + -0.27321683125358037f, + ], + [ + 0.65f * 4492.2486445538634f, + -1.2f, + -1.2f, + -0.8f, + -0.7f, + -0.7f, + -0.4f, + -0.5f, + ] + ], + 8)); + + /// + /// DCT128x128 quantizer encoding. + /// + public static readonly JxlQuantizerEncoding Dct128x128 = JxlQuantizerEncoding.Dct( + new JxlDctQuantWeightParameters( + [ + [ + 1.8f * 26629.073922049845f, + -1.025f, + -0.78f, + -0.65012f, + -0.19041574084286472f, + -0.20819395464f, + -0.421064f, + -0.32733845535848671f, + ], + [ + 1.8f * 9311.3238710010046f, + -0.3041958212306401f, + -0.3633036457487539f, + -0.35660379990111464f, + -0.3443074455424403f, + -0.33699592683512467f, + -0.30180866526242109f, + -0.27321683125358037f, + ], + [ + 1.8f * 4992.2486445538634f, + -1.2f, + -1.2f, + -0.8f, + -0.7f, + -0.7f, + -0.4f, + -0.5f, + ] + ], + 8)); + + /// + /// DCT64x128 quantizer encoding. + /// + public static readonly JxlQuantizerEncoding Dct64x128 = JxlQuantizerEncoding.Dct( + new JxlDctQuantWeightParameters( + [ + [ + 1.3f * 23629.073922049845f, + -1.025f, + -0.78f, + -0.65012f, + -0.19041574084286472f, + -0.20819395464f, + -0.421064f, + -0.32733845535848671f, + ], + [ + 1.3f * 8611.3238710010046f, + -0.3041958212306401f, + -0.3633036457487539f, + -0.35660379990111464f, + -0.3443074455424403f, + -0.33699592683512467f, + -0.30180866526242109f, + -0.27321683125358037f, + ], + [ + 1.3f * 4492.2486445538634f, + -1.2f, + -1.2f, + -0.8f, + -0.7f, + -0.7f, + -0.4f, + -0.5f, + ] + ], + 8)); + + /// + /// DCT256x256 quantizer encoding. + /// + public static readonly JxlQuantizerEncoding Dct256x256 = JxlQuantizerEncoding.Dct( + new JxlDctQuantWeightParameters( + [ + [ + 3.6f * 26629.073922049845f, + -1.025f, + -0.78f, + -0.65012f, + -0.19041574084286472f, + -0.20819395464f, + -0.421064f, + -0.32733845535848671f, + ], + [ + 3.6f * 9311.3238710010046f, + -0.3041958212306401f, + -0.3633036457487539f, + -0.35660379990111464f, + -0.3443074455424403f, + -0.33699592683512467f, + -0.30180866526242109f, + -0.27321683125358037f, + ], + [ + 3.6f * 4992.2486445538634f, + -1.2f, + -1.2f, + -0.8f, + -0.7f, + -0.7f, + -0.4f, + -0.5f, + ] + ], + 8)); + + /// + /// DCT128x256 quantizer encoding. + /// + public static readonly JxlQuantizerEncoding Dct128x256 = JxlQuantizerEncoding.Dct( + new JxlDctQuantWeightParameters( + [ + [ + 2.6f * 23629.073922049845f, + -1.025f, + -0.78f, + -0.65012f, + -0.19041574084286472f, + -0.20819395464f, + -0.421064f, + -0.32733845535848671f, + ], + [ + 2.6f * 8611.3238710010046f, + -0.3041958212306401f, + -0.3633036457487539f, + -0.35660379990111464f, + -0.3443074455424403f, + -0.33699592683512467f, + -0.30180866526242109f, + -0.27321683125358037f, + ], + [ + 2.6f * 4492.2486445538634f, + -1.2f, + -1.2f, + -0.8f, + -0.7f, + -0.7f, + -0.4f, + -0.5f, + ] + ], + 8)); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs index 9551b97e09..54a41f99ab 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs @@ -134,7 +134,7 @@ public JxlQuantizer(JxlDequantMatrices dequant, int quantDc, int globalScale) /// /// Gets the default bias for quant. /// - private static ReadOnlySpan DefaultQuantBias => + public static ReadOnlySpan DefaultQuantBias => [ 1.0f - 0.05465007330715401f, 1.0f - 0.07005449891748593f, @@ -165,7 +165,7 @@ public void ClearDcMultipliers() /// /// The new scale /// The scale value, scaled by the global scale. - private float ScaleGlobalScale(float scale) + public float ScaleGlobalScale(float scale) { int newGlobalScale = (int)MathF.Round(this.globalScale * scale, MidpointRounding.AwayFromZero); float scaleOut = newGlobalScale * 1.0f / this.globalScale; @@ -199,7 +199,7 @@ public void RecomputeFromGlobalScale() /// The quantization index /// The dequant matrix. public ReadOnlySpan DequantMatrix(JxlAcStrategyType strategy, int c) - => this.dequant.Matrix(strategy, c); + => this.dequant!.GetMatrix(strategy, c); /// /// Returns the inverse dequant matrix. @@ -208,21 +208,21 @@ public ReadOnlySpan DequantMatrix(JxlAcStrategyType strategy, int c) /// The quantization index /// The inverse dequant matrix. public ReadOnlySpan InverseDequantMatrix(JxlAcStrategyType strategy, int c) - => this.dequant.InverseMatrix(strategy, c); + => this.dequant!.GetInverseMatrix(strategy, c); /// /// Returns the DC quantization step. /// /// The quantization index /// The DC quantization step - public float GetDcStep(int c) => this.InverseQuantDc * this.dequant.DcQuant(c); + public float GetDcStep(int c) => this.InverseQuantDc * this.dequant!.GetDcQuant(c); /// /// Returns the inverse DC quantization step. /// /// The quantization index /// The inverse DC quantization step - public float GetInverseDcStep(int c) => this.dequant.InverseDcQuant(c) * (this.Scale * this.quantDc); + public float GetInverseDcStep(int c) => this.dequant!.GetInverseDcQuant(c) * (this.Scale * this.quantDc); /// /// Creates JXL quantizer parameters with values reflecting those in this quantizer instance. diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerConstants.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerConstants.cs new file mode 100644 index 0000000000..daf28db696 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerConstants.cs @@ -0,0 +1,46 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Shared constants used by the quantizer. +/// +internal static class JxlQuantizerConstants +{ + /// + /// Total number of quantization tables. + /// + public const byte NumberOfQuantizerTables = (byte)(JxlQuantTable.DCT128X256 + 1); + + /// + /// Gets the inverse DC quantization table. + /// + public static ReadOnlySpan InverseDcQuant => [4096f, 512f, 256f]; + + /// + /// Gets the forward DC quantization table. + /// + public static ReadOnlySpan DcQuant => [ + 1f / 4096f, + 1f / 512f, + 1f / 256f]; + + /// + /// Gets a translation table for converting AC strategies to quant tables. + /// Simply pass the index of the AC strategy enum and you'll get back the + /// matching quant table. + /// + public static ReadOnlySpan AcStrategyToQuantTableMap => + [ + JxlQuantTable.DCT, JxlQuantTable.IDENTITY, JxlQuantTable.DCT2X2, + JxlQuantTable.DCT4X4, JxlQuantTable.DCT16X16, JxlQuantTable.DCT32X32, + JxlQuantTable.DCT8X16, JxlQuantTable.DCT8X16, JxlQuantTable.DCT8X32, + JxlQuantTable.DCT8X32, JxlQuantTable.DCT16X32, JxlQuantTable.DCT16X32, + JxlQuantTable.DCT4X8, JxlQuantTable.DCT4X8, JxlQuantTable.AFV0, + JxlQuantTable.AFV0, JxlQuantTable.AFV0, JxlQuantTable.AFV0, + JxlQuantTable.DCT64X64, JxlQuantTable.DCT32X64, JxlQuantTable.DCT32X64, + JxlQuantTable.DCT128X128, JxlQuantTable.DCT64X128, JxlQuantTable.DCT64X128, + JxlQuantTable.DCT256X256, JxlQuantTable.DCT128X256, JxlQuantTable.DCT128X256 + ]; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerEncoding.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerEncoding.cs index fead5354cd..8a12e6dbf5 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerEncoding.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerEncoding.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Runtime.CompilerServices; - namespace SixLabors.ImageSharp.Formats.Jxl.Processing; /// @@ -56,27 +54,27 @@ public JxlQuantizerEncoding(JxlQuantizerEncoding other) /// /// Gets or sets the weights for the identity transform. /// - public InlineArray3> IdWeights { get; set; } + public float[][]? IdWeights { get; set; } /// /// Gets or sets the weights for the DCT2 transform. /// - public InlineArray3> Dct2Weights { get; set; } + public float[][]? Dct2Weights { get; set; } /// /// Gets or sets the multipliers for the DCT4 transform. /// - public InlineArray3> Dct4Multipliers { get; set; } + public float[][]? Dct4Multipliers { get; set; } /// /// Gets or sets the weights for the AFV transform. /// - public InlineArray3> AfvWeights { get; set; } + public float[][]? AfvWeights { get; set; } /// /// Gets or sets the multipliers for the 4x8 DCT block-based transform. /// - public InlineArray3 Dct4x8Multipliers { get; set; } + public float[]? Dct4x8Multipliers { get; set; } /// /// Gets or sets the explicit quantization table (like in JPEG). @@ -123,7 +121,7 @@ public static JxlQuantizerEncoding Library(int libraryIndex) /// /// Weights for the identity transform. /// A new Identity quantizer encoding. - public static JxlQuantizerEncoding Identity(in InlineArray3> xybWeights) + public static JxlQuantizerEncoding Identity(float[][] xybWeights) => new() { Mode = JxlQuantMode.Id, @@ -136,7 +134,7 @@ public static JxlQuantizerEncoding Identity(in InlineArray3> /// /// Weights for the DCT2x2 transform. /// A new DCT2x2 quantizer encoding. - public static JxlQuantizerEncoding Dct2(in InlineArray3> xybWeights) + public static JxlQuantizerEncoding Dct2(float[][] xybWeights) => new() { Mode = JxlQuantMode.Dct2, @@ -150,7 +148,7 @@ public static JxlQuantizerEncoding Dct2(in InlineArray3> xyb /// Quantizer weights for the DCT4x4 transform. /// XYB multipliers for the DCT4x4 transform. /// A new DCT4x4 quantizer encoding. - public static JxlQuantizerEncoding Dct4(JxlDctQuantWeightParameters parameters, in InlineArray3> xybMul) + public static JxlQuantizerEncoding Dct4(JxlDctQuantWeightParameters parameters, float[][] xybMul) => new() { Mode = JxlQuantMode.Dct4, @@ -165,7 +163,7 @@ public static JxlQuantizerEncoding Dct4(JxlDctQuantWeightParameters parameters, /// Quantizer weights for the DCT4x8 transform. /// XYB multipliers for the DCT4x8 transform. /// A new DCT4x8 quantizer encoding. - public static JxlQuantizerEncoding Dct4x8(JxlDctQuantWeightParameters parameters, in InlineArray3 xybMul) + public static JxlQuantizerEncoding Dct4x8(JxlDctQuantWeightParameters parameters, float[] xybMul) => new() { Mode = JxlQuantMode.Dct4x8, @@ -197,7 +195,7 @@ public static JxlQuantizerEncoding Dct(JxlDctQuantWeightParameters parameters) public static JxlQuantizerEncoding Afv( JxlDctQuantWeightParameters params4x8, JxlDctQuantWeightParameters params4x4, - in InlineArray3> weights) + float[][] weights) => new() { Mode = JxlQuantMode.Afv, diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.Generated.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.Generated.cs new file mode 100644 index 0000000000..b03c77eced --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.Generated.cs @@ -0,0 +1,133 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal static partial class JxlSimdUtils +{ + public static void StoreInterleaved(Vector v1, Vector v2, ref T memory) + { + v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); + } + + public static void StoreInterleaved(Vector v1, Vector v2, Vector v3, ref T memory) + { + v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); + } + + public static void StoreInterleaved(Vector v1, Vector v2, Vector v3, Vector v4, ref T memory) + { + v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, 3)); + } + + public static void StoreInterleaved(Vector v1, Vector v2, Vector v3, Vector v4, Vector v5, ref T memory) + { + v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, 3)); + v5.StoreUnsafe(ref Unsafe.Add(ref memory, 4)); + } + + public static void StoreInterleaved(Vector v1, Vector v2, Vector v3, Vector v4, Vector v5, Vector v6, ref T memory) + { + v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, 3)); + v5.StoreUnsafe(ref Unsafe.Add(ref memory, 4)); + v6.StoreUnsafe(ref Unsafe.Add(ref memory, 5)); + } + + public static void StoreInterleaved(Vector128 v1, Vector128 v2, ref T memory) + { + v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); + } + + public static void StoreInterleaved(Vector128 v1, Vector128 v2, Vector128 v3, ref T memory) + { + v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); + } + + public static void StoreInterleaved(Vector128 v1, Vector128 v2, Vector128 v3, Vector128 v4, ref T memory) + { + v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, 3)); + } + + public static void StoreInterleaved(Vector128 v1, Vector128 v2, Vector128 v3, Vector128 v4, Vector128 v5, ref T memory) + { + v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, 3)); + v5.StoreUnsafe(ref Unsafe.Add(ref memory, 4)); + } + + public static void StoreInterleaved(Vector128 v1, Vector128 v2, Vector128 v3, Vector128 v4, Vector128 v5, Vector128 v6, ref T memory) + { + v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, 3)); + v5.StoreUnsafe(ref Unsafe.Add(ref memory, 4)); + v6.StoreUnsafe(ref Unsafe.Add(ref memory, 5)); + } + + public static void StoreInterleaved(Vector256 v1, Vector256 v2, ref T memory) + { + v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); + } + + public static void StoreInterleaved(Vector256 v1, Vector256 v2, Vector256 v3, ref T memory) + { + v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); + } + + public static void StoreInterleaved(Vector256 v1, Vector256 v2, Vector256 v3, Vector256 v4, ref T memory) + { + v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, 3)); + } + + public static void StoreInterleaved(Vector256 v1, Vector256 v2, Vector256 v3, Vector256 v4, Vector256 v5, ref T memory) + { + v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, 3)); + v5.StoreUnsafe(ref Unsafe.Add(ref memory, 4)); + } + + public static void StoreInterleaved(Vector256 v1, Vector256 v2, Vector256 v3, Vector256 v4, Vector256 v5, Vector256 v6, ref T memory) + { + v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, 3)); + v5.StoreUnsafe(ref Unsafe.Add(ref memory, 4)); + v6.StoreUnsafe(ref Unsafe.Add(ref memory, 5)); + } + +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.tt b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.tt new file mode 100644 index 0000000000..41a5b9c90d --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.tt @@ -0,0 +1,44 @@ +<#@ template debug="false" hostspecific="false" language="C#" #> +<#@ assembly name="System.Core" #> +<#@ import namespace="System.Linq" #> +<#@ import namespace="System.Text" #> +<#@ import namespace="System.Collections.Generic" #> +<#@ output extension=".Generated.cs" #> +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal static partial class JxlSimdUtils +{ +<# + string[] vectorTypes = [ + "Vector", + "Vector128", + "Vector256" + ]; + + const int maxVectorSize = 6; + + foreach (string vect in vectorTypes) { + for (int i = 2; i <= maxVectorSize; i++) { + List vectorParameters = []; + for (int j = 0; j < i; j++) { + vectorParameters.Add($"{vect} v{j + 1}"); + } + string inlineParameters = string.Join(", ", vectorParameters) + ", "; +#> + public static void StoreInterleaved(<#= inlineParameters #>ref T memory) + { +<# for (int j = 0; j < i; j++) { #> + v<#= j + 1 #>.StoreUnsafe(ref Unsafe.Add(ref memory, <#= j #>)); +<# } #> + } + +<# } } #> +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs new file mode 100644 index 0000000000..dd8e7314ee --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs @@ -0,0 +1,104 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Shared SIMD-accelerated utilities. +/// +internal static partial class JxlSimdUtils +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 ConcatLowerLower(Vector256 a, Vector256 b) => Vector256.Create(a.GetLower(), b.GetLower()); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 ConcatUpperUpper(Vector256 a, Vector256 b) => Vector256.Create(a.GetUpper(), b.GetUpper()); + + public static void Transpose8x8Block(Span fromSpan, Span toSpan, int stride) + { + ref int from = ref MemoryMarshal.GetReference(fromSpan); + ref int to = ref MemoryMarshal.GetReference(toSpan); + + if (Vector256.IsHardwareAccelerated) + { + Vector256 i0 = Vector256.LoadUnsafe(ref from); + Vector256 i1 = Vector256.LoadUnsafe(ref Unsafe.Add(ref from, stride)); + Vector256 i2 = Vector256.LoadUnsafe(ref Unsafe.Add(ref from, 2 * stride)); + Vector256 i3 = Vector256.LoadUnsafe(ref Unsafe.Add(ref from, 3 * stride)); + Vector256 i4 = Vector256.LoadUnsafe(ref Unsafe.Add(ref from, 4 * stride)); + Vector256 i5 = Vector256.LoadUnsafe(ref Unsafe.Add(ref from, 5 * stride)); + Vector256 i6 = Vector256.LoadUnsafe(ref Unsafe.Add(ref from, 6 * stride)); + Vector256 i7 = Vector256.LoadUnsafe(ref Unsafe.Add(ref from, 7 * stride)); + + Vector256 q0 = Vector256_.InterleaveLower(i0, i2); + Vector256 q1 = Vector256_.InterleaveLower(i1, i3); + Vector256 q2 = Vector256_.InterleaveUpper(i0, i2); + Vector256 q3 = Vector256_.InterleaveUpper(i1, i3); + Vector256 q4 = Vector256_.InterleaveLower(i4, i6); + Vector256 q5 = Vector256_.InterleaveLower(i5, i7); + Vector256 q6 = Vector256_.InterleaveUpper(i4, i6); + Vector256 q7 = Vector256_.InterleaveUpper(i5, i7); + + Vector256 r0 = Vector256_.InterleaveLower(q0, q1); + Vector256 r1 = Vector256_.InterleaveUpper(q0, q1); + Vector256 r2 = Vector256_.InterleaveLower(q2, q3); + Vector256 r3 = Vector256_.InterleaveUpper(q2, q3); + Vector256 r4 = Vector256_.InterleaveLower(q4, q5); + Vector256 r5 = Vector256_.InterleaveUpper(q4, q5); + Vector256 r6 = Vector256_.InterleaveLower(q6, q7); + Vector256 r7 = Vector256_.InterleaveUpper(q6, q7); + + i0 = ConcatLowerLower(r4, r0); + i1 = ConcatLowerLower(r5, r1); + i2 = ConcatLowerLower(r6, r2); + i3 = ConcatLowerLower(r7, r3); + i4 = ConcatUpperUpper(r4, r0); + i5 = ConcatUpperUpper(r5, r1); + i6 = ConcatUpperUpper(r6, r2); + i7 = ConcatUpperUpper(r7, r3); + + i0.StoreUnsafe(ref to); + i1.StoreUnsafe(ref Unsafe.Add(ref to, 8)); + i2.StoreUnsafe(ref Unsafe.Add(ref to, 16)); + i3.StoreUnsafe(ref Unsafe.Add(ref to, 24)); + i4.StoreUnsafe(ref Unsafe.Add(ref to, 32)); + i5.StoreUnsafe(ref Unsafe.Add(ref to, 40)); + i6.StoreUnsafe(ref Unsafe.Add(ref to, 48)); + i7.StoreUnsafe(ref Unsafe.Add(ref to, 56)); + } + else + { + // Vector128 fallback + for (int n = 0; n < 8; n += 4) + { + for (int m = 0; m < 8; m += 4) + { + Vector128 p0 = Vector128.LoadUnsafe(ref Unsafe.Add(ref from, (n * stride) + m)); + Vector128 p1 = Vector128.LoadUnsafe(ref Unsafe.Add(ref from, ((n + 1) * stride) + m)); + Vector128 p2 = Vector128.LoadUnsafe(ref Unsafe.Add(ref from, ((n + 2) * stride) + m)); + Vector128 p3 = Vector128.LoadUnsafe(ref Unsafe.Add(ref from, ((n + 3) * stride) + m)); + + Vector128 q0 = Vector128_.InterleaveLower(p0, p2); + Vector128 q1 = Vector128_.InterleaveLower(p1, p3); + Vector128 q2 = Vector128_.InterleaveUpper(p0, p2); + Vector128 q3 = Vector128_.InterleaveUpper(p1, p3); + + Vector128 r0 = Vector128_.InterleaveLower(q0, q1); + Vector128 r1 = Vector128_.InterleaveUpper(q0, q1); + Vector128 r2 = Vector128_.InterleaveLower(q2, q3); + Vector128 r3 = Vector128_.InterleaveUpper(q2, q3); + + r0.StoreUnsafe(ref Unsafe.Add(ref to, (m * 8) + n)); + r1.StoreUnsafe(ref Unsafe.Add(ref to, ((m + 1) * 8) + n)); + r2.StoreUnsafe(ref Unsafe.Add(ref to, ((m + 2) * 8) + n)); + r3.StoreUnsafe(ref Unsafe.Add(ref to, ((m + 3) * 8) + n)); + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs index b2a0c0c55f..91258b8529 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Runtime.CompilerServices; + namespace SixLabors.ImageSharp.Formats.Jxl.Processing; internal struct JxlWeightsSeparable5 diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlContextPrediction.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlContextPrediction.cs index 98c1d2dc54..fcbef7afd7 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlContextPrediction.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlContextPrediction.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; @@ -10,6 +11,10 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPr /// internal static class JxlContextPrediction { + private const int ExtraPropertiesPerChannel = 4; + + private const int NumberOfProperties = 1; + public static void SetPredictorMode(int i, JxlModularHeader header) { ref uint wr = ref header.GetWReference(); @@ -94,4 +99,389 @@ public static void SetPredictorMode(int i, JxlModularHeader header) break; } } + + /// + /// Returns true if the (meta)predictor makes use of the weighted predictor. + /// + /// The input predictor. + /// Value indicating whether the predictor uses weighted prediction. + public static bool IsWeightedPredictor(JxlPredictor predictor) => predictor switch + { + JxlPredictor.Zero or + JxlPredictor.Left or + JxlPredictor.Top or + JxlPredictor.Average0 or + JxlPredictor.Select or + JxlPredictor.Gradient => false, + + JxlPredictor.Weighted => true, + + JxlPredictor.TopRight or + JxlPredictor.TopLeft or + JxlPredictor.LeftLeft or + JxlPredictor.Average1 or + JxlPredictor.Average2 or + JxlPredictor.Average3 or + JxlPredictor.Average4 => false, + + JxlPredictor.Best or + JxlPredictor.Variable => true, + + _ => false, + }; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int ClampedGradient(int n, int w, int l) + { + int min = Math.Min(n, w); + int max = Math.Max(n, w); + + int gradient = n + w - l; + + int clamp = l < min ? max : gradient; + return l > max ? min : clamp; + } + + // This is actually a simple Paeth predictor, we'd often see + // this in PNG files + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Select(int a, int b, int c) + { + int p = a + b - c; + int pa = Numerics.Abs(p - a); + int pb = Numerics.Abs(p - b); + return pa < pb ? a : b; + } + + public static void PrecomputeReferences(JxlModularChannel channel, int y, JxlModularImage image, int i, JxlModularChannel references) + { + references.Plane.Clear(); + int offset = 0; + int numExtraProps = references.Width; + int oneRow = references.Plane.PixelsPerRow; + JxlModularChannel channelI = image.Channels[i]; + + for (int j = i - 1; i >= 0 && offset < numExtraProps; j--) + { + JxlModularChannel channelJ = image.Channels[j]; + + if (channelJ.Width != channelI.Width || channelJ.Height != channelI.Height) + { + continue; + } + + if (channelJ.HorizontalShift != channelI.HorizontalShift || + channelJ.VerticalShift != channelI.VerticalShift) + { + continue; + } + + Span rp = references.GetRow(0)[offset..]; + Span rpp = channelJ.GetRow(y); + Span rpprev = channelJ.GetRow(y > 0 ? y - 1 : 0); + + for (int x = 0; x < channel.Width; x++, rp = rp[oneRow..]) + { + int v = rpp[x]; + rp[0] = Numerics.Abs(v); + rp[1] = v; + + // Neighboring variables + int vleft = x > 0 ? rpp[x - 1] : 0; + int vtop = y > 0 ? rpprev[x] : vleft; + int vtopleft = x > 0 && y > 0 ? rpprev[x - 1] : vleft; + + // Prediction + int vpredicted = ClampedGradient(vleft, vtop, vtopleft); + rp[2] = Numerics.Abs(v - vpredicted); + rp[3] = v - vpredicted; + } + + offset += ExtraPropertiesPerChannel; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void InitializePropertiesForRow(Span p, InlineArray2 staticProperties, int y) + { + p[0] = staticProperties[0]; + p[1] = staticProperties[1]; + p[2] = y; + p[9] = 0; // Local gradient + } + + // Prediction for one pixel using neighbors + [MethodImpl(InliningOptions.HotPath)] // This method is called frequently + public static int PredictOne( + JxlPredictor p, + int left, + int top, + int toptop, + int topleft, + int topright, + int leftleft, + int toprightright, + int wpPred) => p switch + { + JxlPredictor.Zero => 0, + JxlPredictor.Left => left, + JxlPredictor.Top => top, + JxlPredictor.Select => Select(left, top, topleft), + JxlPredictor.Weighted => wpPred, + JxlPredictor.Gradient => ClampedGradient(left, top, topleft), + JxlPredictor.TopLeft => topleft, + JxlPredictor.TopRight => topright, + JxlPredictor.LeftLeft => leftleft, + JxlPredictor.Average0 => (left + top) / 2, + JxlPredictor.Average1 => (left + topleft) / 2, + JxlPredictor.Average2 => (topleft + top) / 2, + JxlPredictor.Average3 => (top + topright) / 2, + JxlPredictor.Average4 => ((6 * top) - (2 * toptop) + (7 * left) + (1 * leftleft) + + (1 * toprightright) + (3 * topright) + 8) / + 16, + _ => 0, + }; + + public static JxlPredictionResult Predict( + JxlPredictorMode mode, + Span p, // contains properties + int w, // block width + ref int pp, // This is a reference to the output pixel stored in row-major order. Negative offsets are accessed to reference other pixels in the image, specifically neighboring pixles. + int oneRow, // Number of pixels on one row + int x, + int y, + JxlPredictor predictor, + JxlMaTreeLookup? lookup, + JxlModularChannel? references, + JxlModularState? wpState, + Span predictions) + { + int offset = 3; // Start at position 3 because of 2 static properties + y + + // Status flags + // computeProperties = should the p (properties) variable be updated? + // nec = are there no edge cases? + bool computeProperties = (mode & JxlPredictorMode.UseTree) != 0 || (mode & JxlPredictorMode.ForceComputeProperties) != 0; + bool nec = (mode & JxlPredictorMode.NoEdgeCases) != 0; + + // The following variables are neighboring pixels relative to the pixel to predict. + // Pixels may be unavailable and therefore replaced with default values. For example, + // at Y=0, the top pixel may not be available because we're already at the very top + // of the image, there's no "above" of that. + int left = nec || x > 0 ? Unsafe.Subtract(ref pp, 1) : (y > 0 ? Unsafe.Subtract(ref pp, oneRow) : 0); // ⬅️ (or 0 if unavailable) + int top = nec || y > 0 ? Unsafe.Subtract(ref pp, oneRow) : left; // ⬆️ (or ⬅️ if unavailable) + int topleft = nec || (x > 0 && y > 0) ? Unsafe.Add(ref pp, -1 - oneRow) : left; // ↗️ (or ⬅️ if unavailable) + int topright = nec || (x + 1 < w && y > 0) ? Unsafe.Add(ref pp, 1 - oneRow) : top; // ↖️ (or ⬆️ if unavailable) + int leftleft = nec || x > 1 ? Unsafe.Subtract(ref pp, 2) : left; // ⬅️⬅️ (or ⬅️ if unavailable) + int toptop = nec || y > 1 ? Unsafe.Add(ref pp, -oneRow - oneRow) : top; // ⬆️⬆️ (or ⬆️ if unavailable) + int toprightright = nec || (x + 2 < w && y > 0) ? Unsafe.Add(ref pp, 2 - oneRow) : topright; // ↗️➡️ (or ↗️ if unavailable) + + if (computeProperties) + { + p[offset++] = x; + p[offset++] = top > 0 ? top : -top; + p[offset++] = left > 0 ? left : -left; + p[offset++] = top; + p[offset++] = left; + + // Local gradient + p[offset] = left - p[offset + 1]; + offset++; + + // Local gradient + p[offset++] = left + top - topleft; + + // FFV1 context properties + p[offset++] = left - topleft; + p[offset++] = topleft - top; + p[offset++] = top - topright; + p[offset++] = top - toptop; + p[offset++] = left - leftleft; + } + + // Predicted weighted prediction value + int wpPred = 0; + + if ((mode & JxlPredictorMode.UseWeightedPrediction) != 0) + { + if (wpState is null) + { + throw new InvalidOperationException("Weighted prediction state is missing"); + } + + wpPred = unchecked((int)wpState.Predict(computeProperties, x, y, w, top, left, topright, topleft, toptop, p, offset)); + } + + if (!nec && computeProperties) + { + if (references is null) + { + throw new InvalidOperationException("References are missing"); + } + + offset += NumberOfProperties; + + // Extra properties + Span rp = references.GetRow(x); + for (int i = 0; i < references.Width; i++) + { + p[offset++] = rp[i]; + } + } + + JxlPredictionResult predResult = default; + + if ((mode & JxlPredictorMode.UseTree) != 0) + { + if (lookup is null) + { + throw new InvalidOperationException("Lookup is missing"); + } + + JxlMaTreeLookupResult result = lookup.Lookup(p); + predictor = result.Predictor; + predResult = new((int)result.Context, result.Offset, default, result.Multiplier); + } + + if ((mode & JxlPredictorMode.AllPredictions) != 0) + { + for (int i = 0; i < JxlPredictorFacts.ModularPredictors; i++) + { + predictions[i] = PredictOne((JxlPredictor)i, left, top, toptop, topleft, topright, leftleft, toprightright, wpPred); + } + } + + predResult = new( + predResult.Context, + predResult.Guess + PredictOne(predictor, left, top, toptop, topleft, topright, leftleft, toprightright, wpPred), + predictor, + predResult.Multiplier); + + return predResult; + } + + // The following methods are just wrappers over the Predict + // method. + // See https://github.com/libjxl/libjxl/blob/main/lib/jxl/modular/encoding/context_predict.h#L593-L709 + public static JxlPredictionResult PredictNoTreeNoWeightedPrediction( + int w, + ref int pp, + int oneRow, + int x, + int y, + JxlPredictor predictor) + => Predict(0, [], w, ref pp, oneRow, x, y, predictor, null, null, null, []); + + public static JxlPredictionResult PredictNoTreeWeightedPrediction( + int w, + ref int pp, + int oneRow, + int x, + int y, + JxlPredictor predictor, + JxlModularState wpState) + => Predict(JxlPredictorMode.UseTree, [], w, ref pp, oneRow, x, y, predictor, null, null, wpState, []); + + public static JxlPredictionResult PredictTreeNoWeightedPrediction( + Span p, + int w, + ref int pp, + int oneRow, + int x, + int y, + JxlMaTreeLookup treeLookup, + JxlModularChannel references) + => Predict(JxlPredictorMode.UseTree, p, w, ref pp, oneRow, x, y, JxlPredictor.Zero, treeLookup, references, null, []); + + public static JxlPredictionResult PredictTreeNoWeightedPredictionNoEdgeCases( + Span p, + int w, + ref int pp, + int oneRow, + int x, + int y, + JxlMaTreeLookup treeLookup, + JxlModularChannel references) + => Predict(JxlPredictorMode.UseTree | JxlPredictorMode.NoEdgeCases, p, w, ref pp, oneRow, x, y, JxlPredictor.Zero, treeLookup, references, null, []); + + public static JxlPredictionResult PredictTreeWeightedPrediction( + Span p, + int w, + ref int pp, + int oneRow, + int x, + int y, + JxlMaTreeLookup treeLookup, + JxlModularChannel references, + JxlModularState wpState) + => Predict(JxlPredictorMode.UseTree | JxlPredictorMode.UseWeightedPrediction, p, w, ref pp, oneRow, x, y, JxlPredictor.Zero, treeLookup, references, wpState, []); + + public static JxlPredictionResult PredictTreeWeightedPredictionNoEdgeCases( + Span p, + int w, + ref int pp, + int oneRow, + int x, + int y, + JxlMaTreeLookup treeLookup, + JxlModularChannel references, + JxlModularState wpState) + => Predict(JxlPredictorMode.UseTree | JxlPredictorMode.UseWeightedPrediction | JxlPredictorMode.NoEdgeCases, p, w, ref pp, oneRow, x, y, JxlPredictor.Zero, treeLookup, references, wpState, []); + + public static JxlPredictionResult PredictLearn( + Span p, + int w, + ref int pp, + int oneRow, + int x, + int y, + JxlPredictor predictor, + JxlModularChannel references, + JxlModularState wpState) + => Predict(JxlPredictorMode.ForceComputeProperties | JxlPredictorMode.UseWeightedPrediction, p, w, ref pp, oneRow, x, y, predictor, null, references, wpState, []); + + public static JxlPredictionResult PredictLearnAll( + Span p, + int w, + ref int pp, + int oneRow, + int x, + int y, + JxlModularChannel references, + JxlModularState wpState, + Span predictions) + => Predict(JxlPredictorMode.ForceComputeProperties | JxlPredictorMode.UseWeightedPrediction | JxlPredictorMode.AllPredictions, p, w, ref pp, oneRow, x, y, JxlPredictor.Zero, null, references, wpState, predictions); + + public static JxlPredictionResult PredictLearnNoEdgeCases( + Span p, + int w, + ref int pp, + int oneRow, + int x, + int y, + JxlPredictor predictor, + JxlModularChannel references, + JxlModularState wpState) + => Predict(JxlPredictorMode.ForceComputeProperties | JxlPredictorMode.UseWeightedPrediction | JxlPredictorMode.NoEdgeCases, p, w, ref pp, oneRow, x, y, predictor, null, references, wpState, []); + + public static JxlPredictionResult PredictLearnAllNoEdgeCases( + Span p, + int w, + ref int pp, + int oneRow, + int x, + int y, + JxlModularChannel references, + JxlModularState wpState, + Span predictions) + => Predict(JxlPredictorMode.ForceComputeProperties | JxlPredictorMode.UseWeightedPrediction | JxlPredictorMode.AllPredictions | JxlPredictorMode.NoEdgeCases, p, w, ref pp, oneRow, x, y, JxlPredictor.Zero, null, references, wpState, predictions); + + public static JxlPredictionResult PredictAllNoWeightedPrediction( + int w, + ref int pp, + int oneRow, + int x, + int y, + Span predictions) + => Predict(JxlPredictorMode.AllPredictions, [], w, ref pp, oneRow, x, y, JxlPredictor.Zero, null, null, null, predictions); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlPredictionResult.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlPredictionResult.cs new file mode 100644 index 0000000000..f1822a2a07 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlPredictionResult.cs @@ -0,0 +1,13 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; + +/// +/// The result of context prediction. +/// +/// Context used in MA lookup. +/// Predicted coefficient. +/// Kind of predictor mode used. +/// Multiplier used in MA lookup. +internal record struct JxlPredictionResult(int Context, int Guess, JxlPredictor Predictor, int Multiplier); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlPredictorMode.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlPredictorMode.cs new file mode 100644 index 0000000000..5144f14cb6 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlPredictorMode.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; + +/// +/// Flags for context prediction. +/// +[Flags] +internal enum JxlPredictorMode : byte +{ + /// + /// Should tree-based prediction be used? + /// + UseTree = 1, + + /// + /// Should the weighted predictor be used? + /// + UseWeightedPrediction = 2, + + /// + /// Should properties be computed? (When this bit is 0, + /// the properties are not set and therefore have their + /// default values) + /// + ForceComputeProperties = 4, + + /// + /// Try all predictors? + /// + AllPredictions = 8, + + NoEdgeCases = 16 +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularChannel.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularChannel.cs index d5fd89881d..acf3b9532c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularChannel.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularChannel.cs @@ -1,27 +1,22 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.Memory; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular; /// -/// A wrapper over for modular operations. +/// A wrapper over for modular operations. /// internal sealed class JxlModularChannel { - /// - /// Underlying plane buffer. - /// - private JxlPlane plane; - public JxlModularChannel(Configuration configuration, int width, int height, int horizShift, int vertShift) { this.HorizontalShift = horizShift; this.VerticalShift = vertShift; this.Width = width; this.Height = height; - this.plane = JxlPlane.Create(configuration, width, height); + this.Plane = new JxlImageI(configuration, width, height); } /// @@ -55,15 +50,20 @@ public JxlModularChannel(Configuration configuration, int width, int height, int /// public int Component { get; set; } = -1; + /// + /// Gets or sets the backing plane buffer. + /// + public JxlImageI Plane { get; set; } + public void Shrink(Configuration configuration) { - if (this.plane.XSize == this.Width && this.plane.YSize == this.Height) + if (this.Plane.XSize == this.Width && this.Plane.YSize == this.Height) { return; } - this.plane.Dispose(); - this.plane = JxlPlane.Create(configuration, this.Width, this.Height); + this.Plane.Dispose(); + this.Plane = new JxlImageI(configuration, this.Width, this.Height); } public void Shrink(Configuration configuration, int newWidth, int newHeight) @@ -73,5 +73,5 @@ public void Shrink(Configuration configuration, int newWidth, int newHeight) this.Shrink(configuration); } - public Span GetRow(int y) => this.plane.GetRow(y); + public Span GetRow(int y) => this.Plane.GetRow(y); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularImage.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularImage.cs index 488e198c4d..ebcdcdcdc8 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularImage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularImage.cs @@ -7,5 +7,13 @@ internal sealed class JxlModularImage { public List Channels { get; set; } = []; - + /// + /// Gets or sets the total number of metachannels in this image. + /// + public int MetaChannels { get; set; } + + /// + /// Gets or sets the bit depth used in this image. + /// + public int BitDepth { get; set; } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs index 4babb79f32..bfcd683c5b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs @@ -2,11 +2,14 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; /// -/// Palette/indexed coding +/// Palette/indexed decoder and encoder. /// internal static class JxlPalette { @@ -43,8 +46,39 @@ internal static class JxlPalette private const int ImplicitPaletteSize = LargeCubeOffset + (LargeCube * LargeCube * LargeCube); + private const bool EncodeToHighQualityImplicitPalette = true; + + /// + /// Minimum index required to use the implicit palette. + /// + private const int MinimumImplicitPaletteIndex = -((2 * 72) - 1); + + /// + /// Backing array used to construct data for the matrix. + /// + private static readonly int[,] DefaultOffsetsData = + { + { 1, 2 }, + { 0, 3 }, + { 0, 4 }, + { 1, 1 }, + { 1, 3 }, + { 2, 2 }, + { 1, 0 }, + { 1, 4 }, + { 2, 1 }, + { 2, 3 }, + { 2, 0 }, + { 2, 4 } + }; + + /// + /// Used by the palette encoder. + /// + private static readonly DenseMatrix DefaultOffsets = new(DefaultOffsetsData); + /// - /// Static delta palette used by GetPaletteValue. + /// Static delta palette used by . /// private static readonly int[][] DeltaPalette = [ @@ -149,4 +183,1193 @@ public static int GetPaletteValue(Span palette, int index, int c, int oneRo return palette[(c * oneRow) + index]; } + + public static void MetaPalette(Configuration configuration, JxlModularImage input, int beginC, int endC, int numberOfColors, int numberOfDeltas) + { + JxlTransform.CheckEqualChannels(input, beginC, endC); + int nb = endC - beginC + 1; + if (beginC >= input.MetaChannels) + { + // Palette was done on normal channels + input.MetaChannels++; + } + else + { + // Palette was done on metachannels + if (endC >= input.MetaChannels) + { + throw new InvalidOperationException("End channel offset is out of bounds"); + } + + input.MetaChannels += 2 - nb; + } + + input.Channels.RemoveRange(beginC + 1, endC - beginC); + JxlModularChannel ch = new(configuration, numberOfColors + numberOfDeltas, nb, -1, -1); + input.Channels.Insert(0, ch); + } + + /// + /// Decodes palette/indexed images. + /// + /// Configuration for parallelism. + /// Input & out images. + /// Offset of output channel. + /// Number of colors. + /// Number of deltas. + /// Kind of predictor mode to use. + /// For weighted prediction. + /// Thrown when the input for prediction is invalid. + /// Thrown when there are too many channels. + public static void InversePalette(Configuration configuration, JxlModularImage input, int beginC, int nbColors, int nbDeltas, JxlPredictor predictor, JxlModularHeader weightedHeader) + { + if (input.MetaChannels < 1) + { + throw new InvalidOperationException("A palette transform was invoked without a palette"); + } + + int nb = input.Channels[0].Height; + int c0 = beginC + 1; + + if (c0 >= input.Channels.Count) + { + throw new InvalidOperationException("Channel is out of range"); + } + + JxlModularChannel channel = input.Channels[c0]; + int w = channel.Width; + int h = channel.Height; + + if (nb < 1) + { + throw new InvalidOperationException("Transforms are corrupted"); + } + + for (int i = 1; i < nb; i++) + { + JxlModularChannel newChannel = new(configuration, w, h, channel.HorizontalShift, channel.VerticalShift); + input.Channels.Insert(c0 + 1, newChannel); + } + + JxlModularChannel palette = input.Channels[0]; + + int oneRow = palette.Plane.PixelsPerRow; + int oneRowImage = channel.Plane.PixelsPerRow; + int bitDepth = Math.Min(input.BitDepth, 24); + + if (w == 0) + { + // Channel is empty. Don't do anything. + } + else if (nbDeltas == 0 && predictor == JxlPredictor.Zero) + { + if (nb == 1) + { + _ = Parallel.For(0, h, configuration.GetParallelOptions(), y => + { + Span p = channel.GetRow(y); + Span paletteData = palette.GetRow(0); + + for (int x = 0; x < w; x++) + { + int index = Math.Clamp(p[x], 0, palette.Width - 1); + + p[x] = GetPaletteValue(paletteData, index, 0, oneRow, bitDepth); + } + }); + } + else if (nb == 2) + { + _ = Parallel.For(0, h, configuration.GetParallelOptions(), y => + { + Span paletteData = palette.GetRow(0); + Span p0 = channel.GetRow(y); + Span p1 = input.Channels[c0 + 1].GetRow(y); + + for (int x = 0; x < w; x++) + { + int index0 = Math.Clamp(p0[x], 0, palette.Width - 1); + int index1 = Math.Clamp(p1[x], 0, palette.Width - 1); + + p0[x] = GetPaletteValue(paletteData, index0, 0, oneRow, bitDepth); + p1[x] = GetPaletteValue(paletteData, index1, 0, oneRow, bitDepth); + } + }); + } + else if (nb == 3) + { + _ = Parallel.For(0, h, configuration.GetParallelOptions(), y => + { + Span paletteData = palette.GetRow(0); + Span p0 = channel.GetRow(y); + Span p1 = input.Channels[c0 + 1].GetRow(y); + Span p2 = input.Channels[c0 + 2].GetRow(y); + + for (int x = 0; x < w; x++) + { + int index0 = Math.Clamp(p0[x], 0, palette.Width - 1); + int index1 = Math.Clamp(p1[x], 0, palette.Width - 1); + int index2 = Math.Clamp(p2[x], 0, palette.Width - 1); + + p0[x] = GetPaletteValue(paletteData, index0, 0, oneRow, bitDepth); + p1[x] = GetPaletteValue(paletteData, index1, 0, oneRow, bitDepth); + p2[x] = GetPaletteValue(paletteData, index2, 0, oneRow, bitDepth); + } + }); + } + else if (nb == 4) + { + _ = Parallel.For(0, h, configuration.GetParallelOptions(), y => + { + Span paletteData = palette.GetRow(0); + Span p0 = channel.GetRow(y); + Span p1 = input.Channels[c0 + 1].GetRow(y); + Span p2 = input.Channels[c0 + 2].GetRow(y); + Span p3 = input.Channels[c0 + 3].GetRow(y); + + for (int x = 0; x < w; x++) + { + int index0 = Math.Clamp(p0[x], 0, palette.Width - 1); + int index1 = Math.Clamp(p1[x], 0, palette.Width - 1); + int index2 = Math.Clamp(p2[x], 0, palette.Width - 1); + int index3 = Math.Clamp(p3[x], 0, palette.Width - 1); + + p0[x] = GetPaletteValue(paletteData, index0, 0, oneRow, bitDepth); + p1[x] = GetPaletteValue(paletteData, index1, 0, oneRow, bitDepth); + p2[x] = GetPaletteValue(paletteData, index2, 0, oneRow, bitDepth); + p3[x] = GetPaletteValue(paletteData, index3, 0, oneRow, bitDepth); + } + }); + } + else if (nb == 5) + { + _ = Parallel.For(0, h, configuration.GetParallelOptions(), y => + { + Span paletteData = palette.GetRow(0); + Span p0 = channel.GetRow(y); + Span p1 = input.Channels[c0 + 1].GetRow(y); + Span p2 = input.Channels[c0 + 2].GetRow(y); + Span p3 = input.Channels[c0 + 3].GetRow(y); + Span p4 = input.Channels[c0 + 4].GetRow(y); + + for (int x = 0; x < w; x++) + { + int index0 = Math.Clamp(p0[x], 0, palette.Width - 1); + int index1 = Math.Clamp(p1[x], 0, palette.Width - 1); + int index2 = Math.Clamp(p2[x], 0, palette.Width - 1); + int index3 = Math.Clamp(p3[x], 0, palette.Width - 1); + int index4 = Math.Clamp(p4[x], 0, palette.Width - 1); + + p0[x] = GetPaletteValue(paletteData, index0, 0, oneRow, bitDepth); + p1[x] = GetPaletteValue(paletteData, index1, 0, oneRow, bitDepth); + p2[x] = GetPaletteValue(paletteData, index2, 0, oneRow, bitDepth); + p3[x] = GetPaletteValue(paletteData, index3, 0, oneRow, bitDepth); + p4[x] = GetPaletteValue(paletteData, index4, 0, oneRow, bitDepth); + } + }); + } + else + { + throw new NotImplementedException($"Too many channels for palette compressed images: {nb}"); + } + } + else + { + JxlImageI plane = input.Channels[c0].Plane; + JxlImageI indices = new(configuration, plane.XSize, plane.YSize); + input.Channels[c0].Plane = indices; + + if (predictor == JxlPredictor.Weighted) + { + _ = Parallel.For(0, nb, configuration.GetParallelOptions(), c => + { + JxlModularChannel channel = input.Channels[c0 + c]; + JxlModularState wpState = new(weightedHeader, channel.Width); + Span paletteData = palette.GetRow(0); + + for (int y = 0; y < channel.Height; y++) + { + Span p = channel.GetRow(y); + Span idx = indices.GetRow(y); + + for (int x = 0; x < channel.Width; x++) + { + int index = idx[x]; + int value = 0; + int paletteEntry = GetPaletteValue(paletteData, index, c, oneRow, bitDepth); + JxlPredictionResult pred = JxlContextPrediction.PredictTreeNoWeightedPrediction(channel.Width, p[x..], oneRowImage, x, y, predictor, wpState); + + if (index < nbDeltas) + { + value = pred.Guess + paletteEntry; + } + else + { + value = paletteEntry; + } + + p[x] = value; + wpState.UpdatePredictionErrors(p[x], x, y, channel.Width); + } + } + }); + } + else + { + _ = Parallel.For(0, nb, configuration.GetParallelOptions(), c => + { + JxlModularChannel channel = input.Channels[c0 + c]; + Span paletteData = palette.GetRow(0); + + for (int y = 0; y < channel.Height; y++) + { + Span p = channel.GetRow(y); + Span idx = indices.GetRow(y); + + for (int x = 0; x < channel.Width; x++) + { + int index = idx[x]; + int value = 0; + int paletteEntry = GetPaletteValue(paletteData, index, c, oneRow, bitDepth); + + if (index < nbDeltas) + { + JxlPredictionResult pred = JxlContextPrediction.PredictNoTreeNoWeightedPrediction(channel.Width, p[x..], oneRowImage, x, y, predictor); + value = pred.Guess + paletteEntry; + } + else + { + value = paletteEntry; + } + + p[x] = value; + } + } + }); + } + } + + if (c0 >= input.MetaChannels) + { + input.MetaChannels--; + } + else + { + if (input.MetaChannels >= 2 - nb) + { + throw new InvalidOperationException("Too many meta channels"); + } + + input.MetaChannels -= 2 - nb; + + if (beginC + nb - 1 < input.MetaChannels) + { + throw new InvalidOperationException("Too many meta channels"); + } + + input.Channels.RemoveAt(0); + } + } + + private static float ColorDistance(Span a, Span b) + { + InlineArray3 array = default; + array[0] = b[0]; + array[1] = b[1]; + array[2] = b[2]; + return ColorDistance(a, array); + } + + private static float ColorDistance(Span a, InlineArray3 b) + { + if (a.Length != 3) + { + throw new InvalidOperationException("Length mismatch"); + } + + float distance = 0; + float ave3 = 0; + + if (a.Length >= 3) + { + ave3 = (a[0] + b[0] + a[1] + b[1] + a[2] + b[2]) * (1.21f / 3.0f); + } + + float sumA = 0; + float sumB = 0; + + for (int c = 0; c < a.Length; c++) + { + float diff = a[c] - b[c]; + float weight = c == 0 ? 3f : c == 1 ? 5f : 2f; + + if (c < 3 && (a[c] + b[c] >= ave3)) + { + weight += c == 2 ? 1.12f : 1.15f; + + if (c == 2 && ((a[2] + b[2]) < 1.22f * ave3)) + { + weight -= 0.5f; + } + } + + distance += diff * diff * weight * weight; + int sumWeight = c == 0 ? 3 : c == 1 ? 5 : 1; + + sumA += a[c] * sumWeight; + sumB += b[c] * sumWeight; + } + + distance *= 4; + float sumDiff = sumA - sumB; + distance += sumDiff * sumDiff; + return distance; + } + + private static int QuantizeColorToImplicitPaletteIndex(Span color, int paletteSize, int bitDepth, bool highQuality) + { + int index = 1; + int quant = (1 << bitDepth) - 1; + int half = bitDepth > 1 ? (1 << (bitDepth - 1)) : 0; + + if (highQuality) + { + int multiplier = 1; + + for (int i = 0; i < color.Length; i++) + { + int value = color[i]; + int quantized = (((LargeCube - 1) * value) + half) / quant; + index += quantized * multiplier; + multiplier *= LargeCube; + } + + return index + (paletteSize * LargeCubeOffset); + } + else + { + int multiplier = 1; + int bdMinus3 = 1 << Math.Max(0, bitDepth - 3); + + for (int i = 0; i < color.Length; i++) + { + int value = color[i]; + value -= bdMinus3; + value = Math.Max(0, value); + + int quantized = (((LargeCube - 1) * value) + half) / quant; + quantized = Math.Min(quantized, SmallCube - 1); // cannot be > SmallCube - 1 + + index += quantized * multiplier; + multiplier *= SmallCube; + } + + return index + paletteSize; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int RoundInteger(int value, int div) + { + if (value < 0) + { + return (-value + (div / 2)) / div; + } + else + { + return (value + (div / 2)) / div; + } + } + + /// + /// Encodes an image into palette/indexed coding mode. + /// + /// Configuration for memory allocation/management. + /// Input image. + /// Offset of starting channel. + /// Offset of final channel. + /// Number of colors. + /// Number of deltas. + /// Should the output palette produced by this method be sorted? + /// Should the palette be quantized in lossy mode? (May discard subtle pixel values) + /// The kind of predictor that was used. + /// Header for weighted prediction. + public static void ForwardPalette( + Configuration configuration, + JxlModularImage input, + int beginC, + int endC, + ref int numberOfColors, + ref int numberOfDeltas, + bool ordered, + bool lossy, + ref JxlPredictor predictor, + JxlModularHeader wpHeader) + { + PaletteIterationData paletteIterationData = new(); + int originalNumberOfColors = numberOfColors; + int originalNumberOfDeltas = numberOfDeltas; + + if (lossy && input.BitDepth >= 8) + { + ForwardPaletteIteration( + configuration, + input, + beginC, + endC, + ref originalNumberOfColors, + ref originalNumberOfDeltas, + ordered, + lossy, + ref predictor, + wpHeader, + paletteIterationData); + } + + paletteIterationData.IsFinalRun = false; + ForwardPaletteIteration( + configuration, + input, + beginC, + endC, + ref numberOfColors, + ref numberOfDeltas, + ordered, + lossy, + ref predictor, + wpHeader, + paletteIterationData); + } + + private static void ForwardPaletteIteration( + Configuration configuration, + JxlModularImage input, + int beginC, + int endC, + ref int numberOfColors, + ref int numberOfDeltas, + bool ordered, + bool lossy, + ref JxlPredictor predictor, + JxlModularHeader wpHeader, + PaletteIterationData paletteIterationData) + { + JxlTransform.CheckEqualChannels(input, beginC, endC); + DebugGuard.MustBeGreaterThanOrEqualTo(beginC, input.MetaChannels, nameof(beginC)); + int nb = endC - beginC + 1; // inclusive number of channels + + JxlModularChannel beginCChannel = input.Channels[beginC]; + int w = beginCChannel.Width; + int h = beginCChannel.Height; + + if (input.BitDepth >= 32) + { + throw new InvalidOperationException("Bit depth is too large"); + } + + if (!lossy && numberOfColors < 2) + { + throw new InvalidOperationException("Lossless palette transform needs at least 3 channels"); + } + + int idx = 0; + + if (!lossy && nb == 1) + { + if (numberOfColors == 0) + { + throw new InvalidOperationException("No colors"); + } + + JxlTransform.ComputeMinMax(beginCChannel, out int minValue, out int maxValue); + int lookupTableSize = maxValue - minValue + 1; + + if (lookupTableSize < MaxPaletteLookupTableSize) + { + HashSet chPalette = []; + + for (int y = 0; y < h; y++) + { + Span p = beginCChannel.GetRow(y); + + for (int x = 0; x < w; x++) + { + bool newColor = chPalette.Add(p[x]); + + if (newColor) + { + idx++; + + if (idx > numberOfColors) + { + throw new InvalidOperationException("Index out of bounds"); + } + } + } + } + + // Don't dispose. The channel is stored into the input. + JxlModularChannel modularChannel = new(configuration, idx, 1, -1, -1); + + numberOfColors = idx; + idx = 0; + + Span ppalette = modularChannel.GetRow(0); + + foreach (int p in chPalette) + { + ppalette[idx++] = p; + } + + for (int y = 0; y < h; y++) + { + Span p = beginCChannel.GetRow(y); + + for (int x = 0; x < w; x++) + { + for (idx = 0; p[x] != ppalette[idx] && idx < numberOfColors; idx++) + { + // nop; this is to find the value of idx + } + + p[x] = idx; + } + } + + predictor = JxlPredictor.Zero; + input.MetaChannels++; + input.Channels.Insert(0, modularChannel); + + return; + } + + Span lookup = stackalloc int[lookupTableSize]; + idx = 0; + + for (int y = 0; y < h; y++) + { + Span p = beginCChannel.GetRow(y); + for (int x = 0; x < w; x++) + { + if (lookup[p[x] - minValue] == 0) + { + lookup[p[x] - minValue] = 1; + idx++; + + if (idx > numberOfColors) + { + throw new InvalidOperationException("Index out of bounds"); + } + } + } + } + + // Don't dispose. The channel is stored into the input. + JxlModularChannel channel = new(configuration, idx, 1, -1, -1); + numberOfColors = idx; + idx = 0; + Span pPalette = channel.GetRow(0); + + for (int i = 0; i < lookupTableSize; i++) + { + if (lookup[i] != 0) + { + pPalette[idx] = i + minValue; + lookup[i] = idx; + idx++; + } + } + + for (int y = 0; y < h; y++) + { + Span p = beginCChannel.GetRow(y); + + for (int x = 0; x < w; x++) + { + p[x] = lookup[p[x] - minValue]; + } + } + + predictor = JxlPredictor.Zero; + input.MetaChannels++; + input.Channels.Insert(0, channel); + + return; + } + + JxlModularImage quantizedInput = new(configuration, 1, 1, -1, -1); + + if (lossy) + { + quantizedInput.Dispose(); + quantizedInput = new(configuration, w, h, input.BitDepth, nb); + + for (int c = 0; c < nb; c++) + { + if (!JxlImageOperations.CopyImage(input.Channels[beginC + c].Plane, quantizedInput.Channels[c].Plane)) + { + throw new InvalidOperationException("Copying failed"); + } + } + } + + numberOfDeltas = 0; + bool deltaUsed = false; + List candidatePalette = []; + List candidatePaletteImageOrder = []; + Dictionary inversePalette = []; + + // Don't use stackalloc for color so we can store it as a + // dictionary member in colorFrequencyMap (see below) + int[] color = new int[nb]; + Span colorSpan = color.AsSpan(); + Span colorWithError = stackalloc float[nb]; + + if (lossy) + { + paletteIterationData.FindFrequentColorDeltas(w * h, input.BitDepth); + numberOfDeltas = paletteIterationData.FrequentDeltas[0].Count; + Dictionary colorFrequencyMap = []; + + DenseMatrix offsets = new(4, 2); + + for (int y = 1; y + 1 < h; y++) + { + for (int x = 1; x + 1 < w; x++) + { + for (int c = 0; c < nb; c++) + { + colorSpan[c] = input.Channels[beginC + c].GetRow(y)[x]; + } + + // Defaults + offsets[0, 0] = 1; + offsets[0, 0] = 0; + offsets[1, 0] = -1; + offsets[1, 1] = 0; + offsets[2, 0] = 0; + offsets[2, 1] = 1; + offsets[3, 0] = 0; + offsets[3, 1] = -1; + + bool makesCross = true; + + for (int i = 0; i < 4 && makesCross; ++i) + { + int dx = offsets[i, 0]; + int dy = offsets[i, 1]; + + for (int c = 0; c < nb && makesCross; c++) + { + if (input.Channels[beginC + c].GetRow(y + dy)[x + dx] != colorSpan[c]) + { + makesCross = false; + } + } + } + + if (makesCross) + { + colorFrequencyMap[color]++; + } + } + } + + const float imageFraction = 0.01f; + int colorFrequencyLowerBound = 5 + (int)(input.Height * input.Width * imageFraction); + + foreach (KeyValuePair colorFreq in colorFrequencyMap) + { + if (colorFreq.Value > colorFrequencyLowerBound) + { + candidatePalette.Insert(0, colorFreq.Key); + candidatePaletteImageOrder.Add(colorFreq.Key); + } + } + } + + Dictionary implicitColor = []; + int[][] implicitColors = new int[ImplicitPaletteSize][]; + + for (int k = 0; k < ImplicitPaletteSize; k++) + { + for (int i = 0; i < nb; i++) + { + color[i] = GetPaletteValue([], k, i, 0, input.BitDepth); + } + + implicitColor[color] = true; + implicitColors[k] = color; + } + + int implicitColorsUsed = 0; + Dictionary colorFreqMap = []; + for (int y = 0; y < h; y++) + { + for (int x = 0; x < w; x++) + { + if (lossy && candidatePalette.Count >= numberOfColors) + { + break; + } + + for (int c = 0; c < nb; c++) + { + colorSpan[c] = input.Channels[beginC + c].GetRow(y)[x]; + } + + const bool new_color = candidatePalette.Add(color).second; + if (new_color) + { + if (implicitColor[color]) + { + implicitColorsUsed++; + } + else + { + candidatePaletteImageOrder.Add(color); + if (candidatePaletteImageOrder.Count > numberOfColors) + { + throw new InvalidOperationException("Too many colors for palette/indexed"); + } + } + } + + colorFreqMap[color]++; + } + } + + numberOfColors = numberOfDeltas + candidatePaletteImageOrder.Count; + if (!lossy && numberOfColors + implicitColorsUsed == 1) + { + // It's not useful to have a single-color palette. + throw new InvalidOperationException("Palette only has one color"); + } + + for (int k = 0; k < ImplicitPaletteSize; k++) + { + color = implicitColors[k]; + if (colorFreqMap[color] > 10) + { + numberOfColors++; + candidatePaletteImageOrder.Add(color); + } + } + + for (int k = 0; k < ImplicitPaletteSize; k++) + { + color = implicitColors[k]; + inversePalette[color] = numberOfColors + k; + } + + // Don't dispose this. + JxlModularChannel newChannel = new(configuration, numberOfColors, nb, -1, -1); + Span palette = newChannel.GetRow(0); + int oneRow = newChannel.Plane.PixelsPerRow; + int oneRowImage = beginCChannel.Plane.PixelsPerRow; + int bitDepth = Math.Min(input.BitDepth, 24); // max. 24 bits, cannot be greater + + if (lossy) + { + for (int i = 0; i < numberOfDeltas; i++) + { + for (int c = 0; c < 3; c++) + { + palette[(c * oneRow) + i] = paletteIterationData.FrequentDeltas[c][i]; + } + } + } + + float frequencyThreshold = 4f; + int clr = 0; + + if (ordered && nb >= 3) + { + candidatePaletteImageOrder.Sort((ap, bp) => + { + float ay = (0.299f * ap[0]) + (0.587f * ap[1]) + (0.114f * ap[2]) + 0.1f; + + if (ap.Length > 3) + { + ay *= 1f + ap[3]; + } + + float by = (0.299f * bp[0]) + (0.587f * bp[1]) + (0.114f * bp[2]) + 0.1f; + + if (bp.Length > 3) + { + by *= 1f + bp[3]; + } + + ay = colorFreqMap[ap] > frequencyThreshold ? -ay : ay; + by = colorFreqMap[bp] > frequencyThreshold ? -by : by; + + return ay.CompareTo(by); + }); + } + + foreach (int[] pcol in candidatePaletteImageOrder) + { + Span pcolSpan = pcol.AsSpan(); + + for (int i = 0; i < nb; i++) + { + palette[numberOfDeltas + (i * oneRow) + clr] = pcolSpan[i]; + } + + inversePalette[pcol] = clr++; + } + + List wpStates = []; + + for (int c = 0; c < nb; c++) + { + wpStates.Add(new JxlModularState(wpHeader, w)); + } + + InlineArray3> errorRow = default; + + if (lossy) + { + errorRow[0] = new(nb, w + 4); + errorRow[1] = new(nb, w + 4); + errorRow[2] = new(nb, w + 4); + } + + Span bestValue = stackalloc int[nb]; + Span idealResidual = stackalloc int[nb]; + Span quantizedValue = stackalloc int[nb]; + Span predictions = stackalloc int[nb]; + + // This is a temporary buffer, values are copied here. + // It is so we can swap spans. Since spans are just a view + // of memory, using CopyTo as a swap means we need a + // separate buffer like this for the swapping value. + Span tempBuffer = stackalloc float[w + 4]; + + for (int y = 0; y < h; y++) + { + for (int c = 0; c < nb; c++) + { + p_in[c] = input.channel[begin_c + c].Row(y); + if (lossy) + p_quant[c] = quantized_input.channel[c].Row(y); + } + + Span p = beginCChannel.GetRow(y); + + for (int x = 0; x < w; x++) + { + int index; + if (!lossy) + { + for (int c = 0; c < nb; c++) + { + color[c] = p_in[c][x]; + } + + index = inversePalette[color]; + } + else + { + int best_index = 0; + bool best_is_delta = false; + float best_distance = float.PositiveInfinity; + + bestValue.Clear(); + idealResidual.Clear(); + quantizedValue.Clear(); + predictions.Clear(); + + foreach (double diffusion_multiplier in (Span)[0.55, 0.75]) + { + for (int c = 0; c < nb; c++) + { + colorWithError[c] = + p_in[c][x] + ((paletteIterationData.IsFinalRun ? 1 : 0) * + diffusion_multiplier * errorRow[0][c, x + 2]); + color[c] = (int)Math.Clamp(MathF.Round(colorWithError[c]), 0, (1 << input.BitDepth) - 1); + } + + for (int c = 0; c < nb; c++) + { + predictions[c] = PredictTreeNoWeightedPrediction(w, p_quant[c] + x, oneRowImage, x, y, predictor, wpStates[c]).Guess; + } + + void TryIndex(int index, Span predictions, Span idealResidual, Span colorWithError, ref Span bestValue, ref Span quantizedValue, Span palette, ref int numberOfColors, ref int numberOfDeltas) + { + for (int c = 0; c < nb; c++) + { + quantizedValue[c] = GetPaletteValue(palette, index, c, oneRow, bitDepth); + if (index < numberOfDeltas) + { + quantizedValue[c] += predictions[c]; + } + } + + float color_distance = 32.0f / (1 << Math.Max(0, 2 * (bitDepth - 8))) * ColorDistance(colorWithError, quantizedValue); + + float indexPenalty = 0; + if (index == -1) + { + indexPenalty = -124; + } + else if (index < 0) + { + indexPenalty = -2 * index; + } + else if (index < numberOfDeltas) + { + indexPenalty = 250; + } + else if (index < numberOfColors) + { + indexPenalty = 150; + } + else if (index < numberOfColors + LargeCubeOffset) + { + indexPenalty = 70; + } + else + { + indexPenalty = 256; + } + + float distance = color_distance + indexPenalty; + if (distance < best_distance) + { + best_distance = distance; + best_index = index; + best_is_delta = index < numberOfDeltas; + + RuntimeUtility.Swap(ref bestValue, ref quantizedValue); + + for (int c = 0; c < nb; c++) + { + idealResidual[c] = (int)(colorWithError[c] - predictions[c]); + } + } + } + + for (index = MinimumImplicitPaletteIndex; index < numberOfColors; index++) + { + TryIndex(index); + } + + TryIndex(QuantizeColorToImplicitPaletteIndex(color, numberOfColors, bitDepth, false)); + + if (EncodeToHighQualityImplicitPalette) + { + TryIndex(QuantizeColorToImplicitPaletteIndex(color, numberOfColors, bitDepth, true)); + } + } + + index = best_index; + deltaUsed |= best_is_delta; + + if (!paletteIterationData.IsFinalRun) + { + for (int c = 0; c < 3; c++) + { + paletteIterationData.Deltas[c].Add(idealResidual[c]); + } + + paletteIterationData.DeltaDistances.Add(best_distance); + } + + for (int c = 0; c < nb; c++) + { + wpStates[c].UpdatePredictionErrors(bestValue[c], x, y, w); + p_quant[c][x] = bestValue[c]; + } + + float len_error = 0; + for (int c = 0; c < nb; c++) + { + float local_error = colorWithError[c] - bestValue[c]; + len_error += local_error * local_error; + } + + len_error = MathF.Sqrt(len_error); + float modulate = 1f; + long len_limit = 38 << Math.Max(0, bitDepth - 8); + if (len_error > len_limit) + { + modulate *= len_limit / len_error; + } + + DenseMatrix offsets = new(12, 2); + + for (int c = 0; c < nb; c++) + { + float total_error = colorWithError[c] - bestValue[c]; + + DefaultOffsets.Data.AsSpan().CopyTo(offsets.Data); + + float total_available = 0; + for (int i = 0; i < 11; i++) + { + int row = offsets[i, 0]; + int col = offsets[i, 1]; + + if (Math.Sign(errorRow[row][c, x + col]) != Math.Sign(total_error)) + { + total_available += errorRow[row][c, x + col]; + } + } + + float weight = MathF.Abs(total_error) / (MathF.Abs(total_available) + 1e-3f); + weight = MathF.Min(weight, 1.0f); + + for (int i = 0; i < 11; ++i) + { + int row = offsets[i, 0]; + int col = offsets[i, 1]; + + if (Math.Sign(errorRow[row][c, x + col]) != Math.Sign(total_error)) + { + total_error += weight * errorRow[row][c, x + col]; + errorRow[row][c, x + col] *= 1 - weight; + } + } + + total_error *= modulate; + float remaining_error = (1.0f / 14f) * total_error; + errorRow[0][c, x + 3] += 2 * remaining_error; + errorRow[0][c, x + 4] += remaining_error; + errorRow[1][c, x + 0] += remaining_error; + + for (int i = 0; i < 5; ++i) + { + errorRow[1][c, x + i] += remaining_error; + errorRow[2][c, x + i] += remaining_error; + } + } + } + + if (paletteIterationData.IsFinalRun) + { + p[x] = index; + } + } + + if (lossy) + { + for (int c = 0; c < nb; c++) + { + // Variables for swapping + Span pos0 = errorRow[0].Data.AsSpan(c, w + 4); + Span pos1 = errorRow[1].Data.AsSpan(c, w + 4); + Span pos2 = errorRow[2].Data.AsSpan(c, w + 4); + + // we need to swap: + // error_row[0][c].swap(error_row[1][c]); + pos0.CopyTo(tempBuffer); + pos1.CopyTo(pos0); + tempBuffer.CopyTo(pos1); + + // swap old1, old2 + pos1.CopyTo(tempBuffer); + pos2.CopyTo(pos1); + + pos2.Clear(); + } + } + } + + if (!deltaUsed) + { + predictor = JxlPredictor.Zero; + } + + if (paletteIterationData.IsFinalRun) + { + input.MetaChannels++; + input.Channels.RemoveRange(beginC + 1, endC - beginC); + input.Channels.Insert(0, newChannel); + } + + numberOfColors -= numberOfDeltas; + } + + /// + /// For palette encoding. + /// + internal sealed class PaletteIterationData + { + /// + /// Maximum number of deltas. + /// + private const int MaxDeltas = 128; + + private InlineArray3> deltas; + + public bool IsFinalRun { get; set; } + + public InlineArray3> Deltas + { + get => this.deltas; + set => this.deltas = value; + } + + public List DeltaDistances { get; set; } = []; + + public List[] FrequentDeltas { get; set; } = new List[3]; + + public void FindFrequentColorDeltas(int numPixels, int bitDepth) + { + Dictionary, double> deltaFrequencyMap = []; + int bucketSize = 3 << Math.Max(0, bitDepth - 3); + for (int i = 0; i < this.Deltas[0].Count; i++) + { + InlineArray3 delta = default; + delta[0] = RoundInteger(this.Deltas[0][i], bucketSize); + delta[1] = RoundInteger(this.Deltas[1][i], bucketSize); + delta[2] = RoundInteger(this.Deltas[2][i], bucketSize); + + // Condition equivalent to delta[0] == 0 && delta[1] == 0 && delta[2] == 0 + if ((delta[0] | delta[1] | delta[2]) == 0) + { + continue; + } + + deltaFrequencyMap[delta] += Math.Sqrt(Math.Sqrt(this.DeltaDistances[i])); + } + + float deltaDistanceMultiplier = 1f / numPixels; + Span allZero = [0, 0, 0]; + + foreach (KeyValuePair, double> deltaFrequency in deltaFrequencyMap) + { + float deltaDistance = MathF.Sqrt(ColorDistance(allZero, deltaFrequency.Key)) + 1f; + double second = deltaFrequency.Value * deltaDistance * deltaDistanceMultiplier; + deltaFrequencyMap[deltaFrequency.Key] = second; + } + + Dictionary, double> sorted = deltaFrequencyMap.ToDictionary( + entry => entry.Key, + entry => entry.Value); + + IOrderedEnumerable, double>> sortedEnumerator = sorted.OrderBy( + x => x.Value); + + foreach (KeyValuePair, double> deltaFrequency in sortedEnumerator) + { + if (this.FrequentDeltas[0].Count >= MaxDeltas) + { + break; + } + + if (deltaFrequency.Value < 17) + { + break; + } + + for (int c = 0; c < 3; c++) + { + this.FrequentDeltas[c].Add(deltaFrequency.Key[c] * bucketSize); + } + } + } + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlRct.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlRct.cs index 0f32e47aee..315fcf2b4e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlRct.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlRct.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs new file mode 100644 index 0000000000..d21d0438c7 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs @@ -0,0 +1,793 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; + +#pragma warning disable IDE0057 // Use range operator + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; + +/// +/// Implements the squeeze transform. +/// +/// +/// The squeeze transform in JXL is a reversible +/// wavelet-like decomposition used in the modular mode +/// to reduce redundancy and improve compression, +/// especially for structured or synthetic images. +/// It works by hierarchically splitting channesl +/// into lower-resolution representations plus +/// residuals, giving us multi-resolution coding while +/// remaining lossless. +/// +internal static class JxlSqueeze +{ + private const int MaxFirstPreviewSize = 8; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int SmoothTendency(int b, int a, int n) + { + int diff = 0; + if (b >= a && a >= n) + { + diff = ((4 * b) - (3 * n) - a + 6) / 12; + + if (diff - (diff & 1) > 2 * (b - a)) + { + diff = (2 * (b - a)) + 1; + } + + if (diff + (diff & 1) > 2 * (a - n)) + { + diff = 2 * (a - n); + } + } + else if (b <= a && a <= n) + { + diff = ((4 * b) - (3 * n) - a - 6) / 12; + + if (diff + (diff & 1) < 2 * (b - a)) + { + diff = (2 * (b - a)) - 1; + } + + if (diff - (diff & 1) < 2 * (a - n)) + { + diff = 2 * (a - n); + } + } + + return diff; + } + + // The function operates on 256-bit fixed size vectors, + // 8 elements at a time. It should still work even on CPUs + // without 256-bit vector support (the JIT will translate + // these into 128-bit halves, or scalar without SIMD support). + // + // The FastUnsqueeze method CAN operate on vectors below + // 256-bit, but not above. It's better to simply use Vector256 + // rather than duplicate everything. Vector may be a problem + // as its number of elements can be greater than 8 which is too + // much for this method. + [MethodImpl(InliningOptions.HotPath)] // Called on an entire image + private static void FastUnsqueeze(Span pResidual, Span pAvg, Span pNAvg, Span pPout, Span pOut, Span pNOut) + { + Vector256 oneThird = Vector256.Create(0x55555556); + + ref int pAvgRef = ref MemoryMarshal.GetReference(pAvg); + ref int pNAvgRef = ref MemoryMarshal.GetReference(pNAvg); + ref int pPoutReference = ref MemoryMarshal.GetReference(pPout); + ref int pResidualRef = ref MemoryMarshal.GetReference(pResidual); + ref int pOutRef = ref MemoryMarshal.GetReference(pOut); + ref int pNOutRef = ref MemoryMarshal.GetReference(pNOut); + + Vector256 avg = Vector256.LoadUnsafe(ref pAvgRef); + Vector256 nextAvg = Vector256.LoadUnsafe(ref pNAvgRef); + Vector256 top = Vector256.LoadUnsafe(ref pPoutReference); + + Vector256 ba = top - avg; + Vector256 an = avg - nextAvg; + Vector256 nonmono = ba ^ an; + Vector256 absba = Vector256.Abs(ba); + Vector256 absan = Vector256.Abs(an); + Vector256 absbn = Vector256.Abs(top - nextAvg); + + Vector256 a3eh = Vector256_.MultiplyEven(absba, oneThird); + Vector256 a3oh = Vector256_.MultiplyOdd(absba, oneThird); + + Vector256 a3 = BitConverter.IsLittleEndian + ? Vector256_.InterleaveOdd(a3eh.AsInt32(), a3oh.AsInt32()) + : Vector256_.InterleaveEven(a3eh.AsInt32(), a3oh.AsInt32()); + + a3 += absbn + Vector256.Create(2); + + Vector256 absdiff = a3 >> 2; + + Vector256 skipdiff = Vector256_.NotEqual(ba, Vector256.Zero); + skipdiff &= Vector256_.NotEqual(an, Vector256.Zero); + skipdiff &= Vector256.LessThan(nonmono, Vector256.Zero); + + Vector256 absBa2 = (absba << 1) + (absdiff & Vector256.One); + + absdiff = Vector256.ConditionalSelect( + Vector256.GreaterThan(absdiff, absBa2), + (absba << 1) + Vector256.One, + absdiff); + + Vector256 absan2 = absan << 1; + absdiff = Vector256.ConditionalSelect( + Vector256.GreaterThan(absdiff + (absdiff & Vector256.One), absan2), + absan2, + absdiff); + + Vector256 diff1 = Vector256.ConditionalSelect( + Vector256.LessThan(top, nextAvg), + -absdiff, + absdiff); + + Vector256 tendency = diff1 & ~skipdiff; + Vector256 diffMinusTendency = Vector256.LoadUnsafe(ref pResidualRef); + Vector256 diff = diffMinusTendency + tendency; + Vector256 output = avg + (diff + (diff << 31)); + + output.StoreUnsafe(ref pOutRef); + (output - diff).StoreUnsafe(ref pNOutRef); + } + + public static void InverseHorizontalSqueeze(Configuration configuration, JxlModularImage input, int c, int rc) + { + // Channel offsets should not overflow. + DebugGuard.MustBeLessThan(c, input.Channels.Count, nameof(c)); + DebugGuard.MustBeLessThan(rc, input.Channels.Count, nameof(c)); + + JxlModularChannel inputChannel = input.Channels[c]; + JxlModularChannel inputResidualChannel = input.Channels[rc]; + + if (inputChannel.Width != JxlMath.DivCeil(inputChannel.Width + inputResidualChannel.Width, 2)) + { + throw new InvalidOperationException("Invalid width"); + } + + if (inputChannel.Height != inputResidualChannel.Height) + { + throw new InvalidOperationException("Height of the input channel must be equal to the height of the residual channel"); + } + + if (inputResidualChannel.Width == 0) + { + input.Channels[c].HorizontalShift--; + return; + } + + // Do not dispose. + JxlModularChannel outputChannel = new( + configuration, + inputChannel.Width + inputResidualChannel.Width, + inputChannel.Height, + inputChannel.HorizontalShift - 1, + inputChannel.VerticalShift); + + if (inputResidualChannel.Height == 0) + { + input.Channels[c] = outputChannel; + return; + } + + // The number of rows a single parallel iteration computes + // is stored here. + const int rowsPerThread = 8; + + // rowsPerThread * 9, aligned to the power of 2. + const int rowsPerThreadMul9Alignment = 128; + + // rowsPerThread * 8, aligned to the power of 2. + const int rowsPerThreadMul8Alignment = 64; + + _ = Parallel.For(0, JxlMath.DivCeil(inputChannel.Height, rowsPerThread), configuration.GetParallelOptions(), idx => + { + int y0 = idx * rowsPerThread; + int rows = Math.Min(rowsPerThread, inputChannel.Height - y0); + int x = 0; + + int onerow_in = inputChannel.Plane.PixelsPerRow; + int onerow_inr = inputResidualChannel.Plane.PixelsPerRow; + int onerow_out = outputChannel.Plane.PixelsPerRow; + Span pResidual = inputResidualChannel.GetRow(y0); + Span pAverage = inputChannel.GetRow(y0); + Span pOut = outputChannel.GetRow(y0); + ref int pOutRef = ref MemoryMarshal.GetReference(pOut); + + Span bpAvg = stackalloc int[rowsPerThreadMul9Alignment].Slice(0, rowsPerThread * 9); + Span bpResidual = stackalloc int[rowsPerThreadMul8Alignment].Slice(0, rowsPerThread * 8); + Span bpOutEven = stackalloc int[rowsPerThreadMul8Alignment].Slice(0, rowsPerThread * 8); + Span bpOutOdd = stackalloc int[rowsPerThreadMul8Alignment].Slice(0, rowsPerThread * 8); + Span bpOutEvenT = stackalloc int[rowsPerThreadMul8Alignment].Slice(0, rowsPerThread * 8); + Span bpOutOddT = stackalloc int[rowsPerThreadMul8Alignment].Slice(0, rowsPerThread * 8); + + ref int bpOutEvenTRef = ref MemoryMarshal.GetReference(bpOutEvenT); + ref int bpOutOddTRef = ref MemoryMarshal.GetReference(bpOutOddT); + + int n = Vector256.Count; + + if (inputResidualChannel.Width > 16 && rows == rowsPerThread) + { + for (; x < inputResidualChannel.Width - 9; x += 8) + { + JxlSimdUtils.Transpose8x8Block(pResidual[x..], bpResidual, onerow_inr); + JxlSimdUtils.Transpose8x8Block(pAverage[x..], bpAvg, onerow_in); + + for (int y = 0; y < rowsPerThread; y++) + { + bpAvg[64 + y] = pAverage[x + 8 + (onerow_in * y)]; + } + + for (int i = 0; i < 8; i++) + { + // i * 8 + int i8 = i << 3; + + FastUnsqueeze( + bpResidual[i8..], + bpAvg[i8..], + bpAvg[(8 * (i + 1))..], + (x + i > 0) ? bpOutOdd[(8 * ((x + i - 1) & 7))..] : bpAvg[i8..], + bpOutEven[i8..], + bpOutOdd[i8..]); + } + + JxlSimdUtils.Transpose8x8Block(bpOutEven, bpOutEvenT, 8); + JxlSimdUtils.Transpose8x8Block(bpOutOdd, bpOutOddT, 8); + + for (int y = 0; y < rowsPerThread; y++) + { + // y * 8 + int y8 = y << 3; + + for (int i = 0; i < rowsPerThread; i += n) + { + int offset = y8 + i; + + Vector256 even = Vector256.LoadUnsafe(ref Unsafe.Add(ref bpOutEvenTRef, offset)); + Vector256 odd = Vector256.LoadUnsafe(ref Unsafe.Add(ref bpOutOddTRef, offset)); + + JxlSimdUtils.StoreInterleaved( + even, + odd, + ref Unsafe.Add(ref pOutRef, ((x + i) << 1) + (onerow_out * y))); + } + } + } + } + + for (int y = 0; y < rows; y++) + { + UnsqueezeRow(y0 + y, x); + } + }); + + input.Channels[c] = outputChannel; + + void UnsqueezeRow(int y, int x0) + { + Span residual = inputResidualChannel.GetRow(y); + Span average = inputChannel.GetRow(y); + Span output = outputChannel.GetRow(y); + int inputChannelWidth = inputChannel.Width; + int outputChannelWidth = outputChannel.Width; + + for (int x = x0; x < inputResidualChannel.Width; x++) + { + int xLsh1 = x << 1; // Prevents left shifting three times. Saves on CPU cycles. + + int diffMinusTendency = residual[x]; + int avg = average[x]; + int nextAverage = x + 1 < inputChannelWidth ? average[x + 1] : avg; + + int left = x > 0 ? output[xLsh1 - 1] : avg; + int tendency = SmoothTendency(left, avg, nextAverage); + int diff = diffMinusTendency + tendency; + + int a = avg + (diff / 2); + output[xLsh1] = a; + + int b = a - diff; + output[xLsh1 + 1] = b; + } + + if ((outputChannelWidth & 1) > 0) + { + output[outputChannelWidth - 1] = average[inputChannelWidth - 1]; + } + } + } + + public static void InverseVerticalSqueeze(Configuration configuration, JxlModularImage input, int c, int rc) + { + // Channel offsets should not overflow. + DebugGuard.MustBeLessThan(c, input.Channels.Count, nameof(c)); + DebugGuard.MustBeLessThan(rc, input.Channels.Count, nameof(c)); + + JxlModularChannel inputChannel = input.Channels[c]; + JxlModularChannel inputResidualChannel = input.Channels[rc]; + + if (inputChannel.Height != JxlMath.DivCeil(inputChannel.Height + inputResidualChannel.Height, 2)) + { + throw new InvalidOperationException("Invalid height"); + } + + if (inputChannel.Width != inputResidualChannel.Width) + { + throw new InvalidOperationException("Width of the input channel must be equal to the width of the residual channel"); + } + + if (inputResidualChannel.Height == 0) + { + input.Channels[c].VerticalShift--; + return; + } + + // Do not dispose. + JxlModularChannel outputChannel = new( + configuration, + inputChannel.Width, + inputChannel.Height + inputResidualChannel.Height, + inputChannel.HorizontalShift, + inputChannel.VerticalShift - 1); + + if (inputResidualChannel.Width == 0) + { + input.Channels[c] = outputChannel; + return; + } + + // The number of columns a single parallel iteration computes + // is stored here. + const int colsPerThread = 8; + + _ = Parallel.For(0, JxlMath.DivCeil(inputChannel.Width, colsPerThread), configuration.GetParallelOptions(), idx => + { + int x0 = idx * colsPerThread; + int x1 = Math.Min((idx + 1) * colsPerThread, inputChannel.Width); + int w = x1 - x0; + + for (int y = 0; y < inputResidualChannel.Height; y++) + { + int yLsh1 = y << 1; + + Span pResidual = inputResidualChannel.GetRow(y)[x0..]; + Span pAverage = inputChannel.GetRow(y)[x0..]; + Span pNAvg = inputChannel.GetRow(y + 1 < inputChannel.Height ? y + 1 : y)[x0..]; + Span pOut = outputChannel.GetRow(yLsh1)[x0..]; + Span pNOut = outputChannel.GetRow(yLsh1 + 1)[x0..]; + Span pPOut = y > 0 ? outputChannel.GetRow(yLsh1 - 1)[x0..] : pNAvg; + int x = 0; + + for (; x + 7 < w; x += 8) + { + FastUnsqueeze( + pResidual[x..], + pAverage[x..], + pNAvg[x..], + pPOut[x..], + pOut[x..], + pNOut[x..]); + } + + // Remainder + for (; x < w; x++) + { + int avg = pNAvg[x]; + int nextAvg = pNAvg[x]; + int top = pPOut[x]; + int tendency = SmoothTendency(top, avg, nextAvg); + int diffMinusTendency = pResidual[x]; + int diff = diffMinusTendency + tendency; + int output = avg + (diff >> 1); + pOut[x] = output; + pNOut[x] = output - diff; + } + } + }); + + if ((outputChannel.Height & 1) > 0) + { + int y = inputChannel.Height - 1; + + Span pAverage = inputChannel.GetRow(y); + Span pOutput = outputChannel.GetRow(y << 1); + + for (int x = 0; x < inputChannel.Width; x++) + { + pOutput[x] = pAverage[x]; + } + } + + input.Channels[c] = outputChannel; + } + + public static void InverseSqueeze(Configuration configuration, JxlModularImage input, Span parameters) + { + int totalNumberOfChannels = input.Channels.Count; + + for (int i = parameters.Length - 1; i >= 0; i--) + { + ref JxlSqueezeParameters parameter = ref parameters[i]; + + CheckMetaSqueezeParameters(parameter, totalNumberOfChannels); + + bool horizontal = parameter.Horizontal; + bool inPlace = parameter.InPlace; + int beginC = parameter.BeginC; + int endC = parameter.BeginC + parameter.NumC - 1; + + int offset = inPlace + ? endC + 1 + : totalNumberOfChannels + beginC + endC - 1; + + if (beginC < input.MetaChannels) + { + if (input.MetaChannels <= parameter.NumC) + { + throw new InvalidOperationException("Not enough meta channels"); + } + + input.MetaChannels -= parameter.NumC; + } + + for (int c = beginC; c <= endC; c++) + { + int rc = offset + c - beginC; + + if (rc >= totalNumberOfChannels) + { + throw new InvalidOperationException("Residual channel offset out of bounds"); + } + + JxlModularChannel channelC = input.Channels[c]; // Input channel + JxlModularChannel channelRC = input.Channels[rc]; // Residual channel + + if (channelC.Width < channelRC.Width || channelC.Height < channelRC.Height) + { + throw new InvalidOperationException("Input channel width or height does not match residual channel width/height"); + } + + if (horizontal) + { + InverseHorizontalSqueeze(configuration, input, c, rc); + } + else + { + InverseVerticalSqueeze(configuration, input, c, rc); + } + } + } + } + + public static void DefaultSqueezeParameters(List squeezeParameters, JxlModularImage image) + { + int numberOfChannels = image.Channels.Count - image.MetaChannels; + squeezeParameters.Clear(); + + JxlModularChannel numMetaChannelsChannel = image.Channels[image.MetaChannels]; + int w = numMetaChannelsChannel.Width; + int h = numMetaChannelsChannel.Height; + bool wide = w > h; + + JxlModularChannel nextNumMetaChannelsChannel = image.Channels[image.MetaChannels + 1]; + + if (numberOfChannels > 2 && nextNumMetaChannelsChannel.Width == w && nextNumMetaChannelsChannel.Height == h) + { + JxlSqueezeParameters parameters = new() + { + Horizontal = true, + InPlace = false, + BeginC = image.MetaChannels + 1, + NumC = 2 + }; + + squeezeParameters.Add(parameters); + parameters.Horizontal = false; + squeezeParameters.Add(parameters); + } + + JxlSqueezeParameters newParameters = new() + { + BeginC = image.MetaChannels, + NumC = numberOfChannels, + InPlace = true + }; + + if (!wide) + { + if (h > MaxFirstPreviewSize) + { + newParameters.Horizontal = false; + squeezeParameters.Add(newParameters); + h = (h + 1) >> 1; + } + } + + while (w > MaxFirstPreviewSize || h > MaxFirstPreviewSize) + { + if (w > MaxFirstPreviewSize) + { + newParameters.Horizontal = true; + squeezeParameters.Add(newParameters); + w = (w + 1) >> 1; + } + + if (w > MaxFirstPreviewSize) + { + newParameters.Horizontal = false; + squeezeParameters.Add(newParameters); + h = (h + 1) >> 1; + } + } + } + + private static void CheckMetaSqueezeParameters(in JxlSqueezeParameters parameter, int numChannels) + { + int c1 = parameter.BeginC; + int c2 = parameter.BeginC + parameter.NumC - 1; + + if (c1 < 0 || + c1 >= numChannels || + c2 < 0 || + c2 >= numChannels || + c2 < c1) + { + throw new InvalidOperationException("Invalid channel range"); + } + } + + public static void MetaSqueeze(Configuration configuration, JxlModularImage image, List parameters) + { + if (parameters.Count == 0) + { + DefaultSqueezeParameters(parameters, image); + } + + foreach (JxlSqueezeParameters parameter in parameters) + { + CheckMetaSqueezeParameters(parameter, image.Channels.Count); + + bool horizontal = parameter.Horizontal; + bool inPlace = parameter.InPlace; + int beginC = parameter.BeginC; + int endC = parameter.BeginC + parameter.NumC - 1; + + if (beginC < image.MetaChannels) + { + if (endC >= image.MetaChannels) + { + throw new InvalidOperationException("Invalid squeeze: mix of meta and nonmeta channels"); + } + + if (!inPlace) + { + throw new InvalidOperationException("Invalid squeeze: meta channels require in-place residuals"); + } + + image.MetaChannels += parameter.NumC; + } + + int offset = inPlace + ? endC + 1 + : image.Channels.Count; + + for (int c = beginC; c <= endC; c++) + { + JxlModularChannel channel = image.Channels[c]; + + if (channel.Height > 30 || channel.VerticalShift > 30) + { + throw new InvalidOperationException("Too many squeezes: shift > 30"); + } + + int w = channel.Width; + int h = channel.Height; + + if ((w & h) == 0) // either w, or h, is 0 + { + throw new InvalidOperationException("Squeezing empty channel"); + } + + if (horizontal) + { + channel.Width = (w + 1) >> 1; + + if (channel.HorizontalShift >= 0) + { + channel.HorizontalShift++; + } + + w -= (w + 1) >> 1; + } + else + { + channel.HorizontalShift = (h + 1) >> 1; + + if (channel.VerticalShift >= 0) + { + channel.VerticalShift++; + } + + h -= (h + 1) >> 1; + } + + channel.Shrink(configuration); + + JxlModularChannel placeholder = new(configuration, w, h, channel.HorizontalShift, channel.VerticalShift) + { + Component = channel.Component + }; + + image.Channels.Insert(offset + (c - beginC), placeholder); + } + } + } + + public static void ForwardHorizontalSqueeze(Configuration configuration, JxlModularImage input, int c, int rc) + { + JxlModularChannel inputChannel = input.Channels[c]; + + // Do not dispose these. + JxlModularChannel outputChannel = new(configuration, (inputChannel.Width + 1) >> 1, inputChannel.Height, inputChannel.HorizontalShift + 1, inputChannel.VerticalShift); + JxlModularChannel outputChannelResidual = new(configuration, inputChannel.Width - outputChannel.Width, outputChannel.Height, inputChannel.HorizontalShift + 1, inputChannel.VerticalShift); + + outputChannel.Component = inputChannel.Component; + outputChannelResidual.Component = inputChannel.Component; + + for (int y = 0; y < outputChannel.Height; y++) + { + Span pIn = inputChannel.GetRow(y); + Span pOut = outputChannel.GetRow(y); + Span pRes = outputChannelResidual.GetRow(y); + + for (int x = 0; x < outputChannelResidual.Width; x++) + { + int x2 = x << 1; // x * 2 + + int a = pIn[x2]; + int b = pIn[x2 + 1]; + int avg = Numerics.Average(a, b); + pOut[x] = avg; + int diff = a - b; + int nextAvg = avg; + + if (x + 1 < outputChannelResidual.Width) + { + int c2 = pIn[x2 + 2]; // actually C, but 1. variable 'c' already defined 2. names should be camelCase + int d = pIn[x2 + 3]; + + nextAvg = Numerics.Average(c2, d); + } + else if ((inputChannel.Width & 1) != 0) + { + nextAvg = pIn[x2 + 2]; + } + + int left = x > 0 ? pIn[x2 - 1] : avg; + int tendency = SmoothTendency(left, avg, nextAvg); + + pRes[x] = diff - tendency; + } + + if ((inputChannel.Width & 1) != 0) + { + int x = outputChannel.Width - 1; + pOut[x] = pIn[x * 2]; + } + } + + input.Channels[c] = outputChannel; + input.Channels.Insert(rc, outputChannelResidual); + } + + public static void ForwardVerticalSqueeze(Configuration configuration, JxlModularImage input, int c, int rc) + { + JxlModularChannel inputChannel = input.Channels[c]; + + // Do not dispose these. + JxlModularChannel outputChannel = new(configuration, inputChannel.Width, (inputChannel.Height + 1) >> 1, inputChannel.HorizontalShift, inputChannel.VerticalShift + 1); + JxlModularChannel outputResidualChannel = new(configuration, inputChannel.Width, inputChannel.Height - outputChannel.Height, inputChannel.HorizontalShift, inputChannel.VerticalShift + 1); + + outputChannel.Component = inputChannel.Component; + outputResidualChannel.Component = inputChannel.Component; + + int oneRowInput = inputChannel.Plane.PixelsPerRow; + + for (int y = 0; y < outputChannel.Height; y++) + { + Span pIn = inputChannel.GetRow(y * 2); + Span pOut = outputChannel.GetRow(y); + Span pResidual = outputResidualChannel.GetRow(y); + + for (int x = 0; x < outputChannel.Width; x++) + { + int a = pIn[x]; + int b = pIn[x + oneRowInput]; + int avg = Numerics.Average(a, b); + pOut[x] = avg; + int diff = a - b; + int nextAvg = avg; + + if (y + 1 < outputResidualChannel.Height) + { + int c2 = pIn[x + (2 * oneRowInput)]; // actually C, but 1. variable 'c' already defined 2. names should be camelCase + int d = pIn[x + (3 * oneRowInput)]; + nextAvg = Numerics.Average(c2, d); + } + else if ((inputChannel.Height & 1) != 0) + { + nextAvg = pIn[x + (2 * oneRowInput)]; + } + + int top = y > 0 ? pIn[x - oneRowInput] : avg; + int tendency = SmoothTendency(top, avg, nextAvg); + + pResidual[x] = diff - tendency; + } + } + + if ((inputChannel.Height & 1) != 0) + { + int y = outputChannel.Height - 1; + + Span pIn = inputChannel.GetRow(y * 2); + Span pOut = outputChannel.GetRow(y); + + for (int x = 0; x < outputChannel.Width; x++) + { + pOut[x] = pIn[x]; + } + } + + input.Channels[c] = outputChannel; + input.Channels.Insert(rc, outputResidualChannel); + } + + public static void ForwardSqueeze(Configuration configuration, JxlModularImage input, List parameters) + { + if (parameters.Count == 0) + { + DefaultSqueezeParameters(parameters, input); + + if (parameters.Count == 0) + { + // If there's nothing to do, don't squeeze. + return; + } + } + + foreach (JxlSqueezeParameters parameter in parameters) + { + CheckMetaSqueezeParameters(parameter, input.Channels.Count); + + bool horizontal = parameter.Horizontal; + bool inPlace = parameter.InPlace; + int beginC = parameter.BeginC; + int endC = parameter.BeginC + parameter.NumC - 1; + + int offset = inPlace + ? endC + 1 + : input.Channels.Count; + + for (int c = beginC; c <= endC; c++) + { + if (horizontal) + { + ForwardHorizontalSqueeze(configuration, input, c, offset + c - beginC); + } + else + { + ForwardVerticalSqueeze(configuration, input, c, offset + c - beginC); + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueezeParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueezeParameters.cs index 7bb52fe5e5..1d4c332770 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueezeParameters.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueezeParameters.cs @@ -8,12 +8,12 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; /// /// Parameters for the squeeze transform. /// -internal sealed class JxlSqueezeParameters : IJxlFields +internal struct JxlSqueezeParameters : IJxlFields { private bool horizontal; private bool inPlace; - private uint beginC; - private uint numC; + private int beginC; + private int numC; public JxlSqueezeParameters() => JxlBundle.Init(this); @@ -22,7 +22,7 @@ internal sealed class JxlSqueezeParameters : IJxlFields /// public bool Horizontal { - get => this.horizontal; + readonly get => this.horizontal; set => this.horizontal = value; } @@ -31,19 +31,19 @@ public bool Horizontal /// public bool InPlace { - get => this.inPlace; + readonly get => this.inPlace; set => this.inPlace = value; } - public uint BeginC + public int BeginC { - get => this.beginC; + readonly get => this.beginC; set => this.beginC = value; } - public uint NumC + public int NumC { - get => this.numC; + readonly get => this.numC; set => this.numC = value; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs index 860adb4692..75ba986086 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs @@ -36,4 +36,29 @@ public static void CheckEqualChannels(JxlModularImage image, int c1, int c2) } } } + + public static void ComputeMinMax(JxlModularChannel channel, out int min, out int max) + { + // Start with opposite bounds so the first iteration + // guarantees to set these values + min = int.MaxValue; + max = int.MinValue; + + for (int y = 0; y < channel.Height; y++) + { + Span p = channel.GetRow(y); + for (int x = 0; x < channel.Width; x++) + { + if (p[x] < min) + { + min = p[x]; + } + + if (p[x] > max) + { + max = p[x]; + } + } + } + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf0Stage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf0Stage.cs new file mode 100644 index 0000000000..56f52fab8a --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf0Stage.cs @@ -0,0 +1,109 @@ +// 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 0) stage +/// +internal sealed class Epf0Stage : RenderPipelineStageBase +{ + private static readonly int[][] SadOffsets = + [ + [-2, 0], [-1, -1], [-1, 0], [-1, 1], [0, -2], [0, -1], + [0, 1], [0, 2], [1, -1], [1, 0], [1, 1], [2, 0] + ]; + + private readonly JxlLoopFilter loopFilter; + private readonly JxlImageF sigma; + + public Epf0Stage(JxlLoopFilter loopFilter, JxlImageF sigma, Configuration configuration) : base(configuration) + { + this.loopFilter = loopFilter; + this.sigma = sigma; + this.Settings = RenderPipelineStageConfiguration.CreateSymmetricBorderOnly(3); + } + + public override string Name => "EPF0"; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void AddPixel( + int row, + InlineArray7>> rows, + int x, + Vector256 sad, + Vector256 inverseSigma, + ref Vector256 xOut, + ref Vector256 yOut, + ref Vector256 bOut, + ref Vector256 wOut) + { + int rowPlus3 = row + 3; + Vector256 cx = Vector256.Create(rows[0][rowPlus3].Span[x..]); + Vector256 cy = Vector256.Create(rows[1][rowPlus3].Span[x..]); + Vector256 cb = Vector256.Create(rows[2][rowPlus3].Span[x..]); + Vector256 weight = EpfUtils.Weight(sad, inverseSigma); + wOut += weight; + xOut += (weight * cx) + xOut; + yOut += (weight * cy) + yOut; + bOut += (weight * cb) + bOut; + } + + public override void ProcessRow(Buffer2D> inputRows, Buffer2D> outputRows, int xExtraLeft, int xExtraRight, int width, int xPos, int yPos) + { + Span> sads = stackalloc Vector256[16].Slice(0, 12); + sads.Clear(); + + int xStart = -JxlMath.RoundUpTo(xExtraLeft, Vector256.Count); + int xEnd = width + xExtraRight; + Span rowSigma = this.sigma.GetRow((yPos / JxlFrameDimensions.BlockDimensions) + JxlDecoderCache.SigmaPadding); + + float sm = this.loopFilter.EpfPass0SigmaScale * 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]; + + int yPosModBlockDim = yPos % JxlFrameDimensions.BlockDimensions; + Span sadMul = yPosModBlockDim is 0 or JxlFrameDimensions.BlockDimensions - 1 + ? sadMulBorder + : sadMulCenter; + + InlineArray3>> rows = default; + for (int c = 0; c < 3; c++) + { + for (int i = 0; i < 7; i++) + { + rows[c][i] = this.GetInputRowMemory(inputRows, c, i - 3); + } + } + + for (int x = xStart; x < xEnd; x += Vector256.Count) + { + int xPlusXpos = x + xPos; + + int bx = (xPlusXpos + (JxlDecoderCache.SigmaPadding * JxlFrameDimensions.BlockDimensions)) / JxlFrameDimensions.BlockDimensions; + int ix = xPlusXpos % JxlFrameDimensions.BlockDimensions; + + if (rowSigma[bx] < JxlLoopFilter.MinimumSigma) + { + for (int c = 0; c < 3; c++) + { + Vector256 px = Vector256.Create(rows[c][3].Span[x..]); + px.CopyTo(GetOutputRow(outputRows, c, 0)[x..]); + } + + continue; + } + + Vector256 vsm = Vector256.Create(sadMul[ix..]); + Vector256 inverseSigma = Vector256.Create(rowSigma[bx]) * vsm; + + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/EpfStageType.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/EpfStageType.cs new file mode 100644 index 0000000000..8a85367158 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/EpfStageType.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +/// +/// Used by the EPF render pipeline stage. +/// +internal enum EpfStageType : byte +{ + Zero, + One, + Two +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/EpfUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/EpfUtils.cs new file mode 100644 index 0000000000..52150ae27f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/EpfUtils.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +/// +/// Utilities for EPF stages. +/// +internal static class EpfUtils +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Weight(Vector256 sad, Vector256 inverseSigma) + { + Vector256 v = (sad * inverseSigma) + Vector256.One; + Vector256 whereNegative = Vector256.LessThan(v, Vector256.Zero); + Vector256 zeroIfNegative = Vector256.ConditionalSelect(whereNegative, Vector256.Zero, whereNegative); + return zeroIfNegative; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineChannelMode.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineChannelMode.cs new file mode 100644 index 0000000000..4d936af8f2 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineChannelMode.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +/// +/// Specifies how does a render pipeline stage apply to channels. +/// +internal enum RenderPipelineChannelMode : byte +{ + /// + /// Channel is not modified. + /// + Ignored, + + /// + /// Channel is in-place. + /// + InPlace, + + /// + /// Channel is modified and written to a new buffer. + /// + InOut, + + /// + /// Read-only channel. + /// + Input +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineStageBase.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineStageBase.cs new file mode 100644 index 0000000000..b02b62db7a --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineStageBase.cs @@ -0,0 +1,92 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +/// +/// Base class for a render pipeline stage. +/// +[DebuggerDisplay($"{{{nameof(Name)}}}")] +internal abstract class RenderPipelineStageBase(Configuration configuration) : IDisposable +{ + private const int RenderPipelineXOffset = 32; + + /// + /// Gets or sets the configuration for this render pipeline stage. + /// + public RenderPipelineStageConfiguration Settings { get; set; } + + /// + /// Gets a value indicating whether this stage is initialized and is therefore + /// ready to use. + /// + public virtual bool IsInitialized => true; + + /// + /// Gets a value indicating whether, from this stage on, the pipeline will operate + /// on an image rather than the frame-sized buffer. Only one stage in the pipeline + /// should return true, and it should implement . + /// + public virtual bool SwitchToImageDimensions => false; + + /// + /// Gets a friendly name representing this stage. + /// + public virtual string Name => "(invalid pipeline stage)"; + + /// + /// If any unmanaged or pooled memory is present by the derived stage, releases + /// memory used by that. + /// + public virtual void Dispose() + { + } + + public virtual void ProcessRow( + Buffer2D> inputRows, + Buffer2D> outputRows, + int xExtraLeft, + int xExtraRight, + int width, + int xPos, + int yPos) + { + } + + /// + /// Represents how each channel will be processed. + /// + /// Desired channel. + /// Mode specifying how the specified channel will be processed. + public virtual RenderPipelineChannelMode GetChannelMode(int channel) + => RenderPipelineChannelMode.Ignored; + + public virtual void SetInputSizes(Span inputSizes) + { + } + + public Span GetInputRow(Buffer2D> inputRows, int c, int offset) + => inputRows[c, this.Settings.BorderY + offset].Span[RenderPipelineXOffset..]; + + public Memory GetInputRowMemory(Buffer2D> inputRows, int c, int offset) + => inputRows[c, this.Settings.BorderY + offset][RenderPipelineXOffset..]; + + public static Span GetOutputRow(Buffer2D> outputRows, int c, int offset) + => outputRows[c, offset].Span[RenderPipelineXOffset..]; + + public virtual void GetImageDimensions(out int width, out int height, out Point frameOrigin) + { + width = 0; + height = 0; + frameOrigin = default; + } + + public virtual void ProcessPaddingRow(Buffer2D> outputRows, int width, int xPos, int yPos) + { + } + + protected Configuration GetConfiguration() => configuration; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineStageConfiguration.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineStageConfiguration.cs new file mode 100644 index 0000000000..acd7b8b765 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineStageConfiguration.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +internal record struct RenderPipelineStageConfiguration(int BorderX, int BorderY, int ShiftX, int ShiftY) +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RenderPipelineStageConfiguration CreateShiftX(int shift, int border) => new(border, 0, shift, 0); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RenderPipelineStageConfiguration CreateShiftY(int shift, int border) => new(0, border, 0, shift); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RenderPipelineStageConfiguration CreateSymmetric(int shift, int border) => new(border, border, shift, shift); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RenderPipelineStageConfiguration CreateSymmetricBorderOnly(int border) => CreateSymmetric(shift: 0, border); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineSegment.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineSegment.cs index 65f2177927..fd9a068edd 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineSegment.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineSegment.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Runtime.CompilerServices; + namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; internal struct JxlSplineSegment diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index 971d73b849..b3925b7a7c 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -44,6 +44,11 @@ + + True + True + JxlSimdUtils.StoreInterleaved.tt + @@ -57,6 +62,11 @@ True InlineArray.tt + + True + True + JxlSimdUtils.StoreInterleaved.tt + True True @@ -164,6 +174,10 @@ TextTemplatingFileGenerator InlineArray.cs + + TextTemplatingFileGenerator + JxlSimdUtils.StoreInterleaved.Generated.cs + ImageMetadataExtensions.cs TextTemplatingFileGenerator diff --git a/tests/ImageSharp.Tests/Common/Vector256UtilitiesTests.cs b/tests/ImageSharp.Tests/Common/Vector256UtilitiesTests.cs new file mode 100644 index 0000000000..1c294afa44 --- /dev/null +++ b/tests/ImageSharp.Tests/Common/Vector256UtilitiesTests.cs @@ -0,0 +1,48 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Tests.Common; + +public class Vector256UtilitiesTests +{ + [Theory] + [InlineData(new int[] { 4, 5, 6, 7, 8, 9, 10, 11 }, new int[] { 1, 2, 3, 4, 0, -1, -2, -3 }, new int[] { 4, 1, 5, 2, 6, 3, 7, 4 })] + public void TestVector256InterleaveLower(int[] a, int[] b, int[] expected) + { + Vector256 v256a = Vector256.Create(a); + Vector256 v256b = Vector256.Create(b); + + Vector256 v256 = Vector256_.InterleaveLower(v256a, v256b); + + int[] result = new int[Vector256.Count]; + v256.CopyTo(result); + + bool isEqual = expected.SequenceEqual(result); + if (!isEqual) + { + Assert.Fail($"Lower shuffle failed.\n\nExpected: [{string.Join(", ", expected)}]\nActual: [{string.Join(", ", result)}]"); + } + } + + [Theory] + [InlineData(new int[] { 4, 5, 6, 7, 8, 9, 10, 11 }, new int[] { 1, 2, 3, 4, 0, -1, -2, -3 }, new int[] { 8, 0, 9, -1, 10, -2, 11, -3 })] + public void TestVector256InterleaveUpper(int[] a, int[] b, int[] expected) + { + Vector256 v256a = Vector256.Create(a); + Vector256 v256b = Vector256.Create(b); + + Vector256 v256 = Vector256_.InterleaveUpper(v256a, v256b); + + int[] result = new int[Vector256.Count]; + v256.CopyTo(result); + + bool isEqual = expected.SequenceEqual(result); + if (!isEqual) + { + Assert.Fail($"Lower shuffle failed.\n\nExpected: [{string.Join(", ", expected)}]\nActual: [{string.Join(", ", result)}]"); + } + } +} From 7b8749d8dce01f002da88671cc04208ef229774e Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 31 Aug 2026 20:31:47 +0400 Subject: [PATCH 101/328] Don't use stackalloc for two bytes This avoids the stack cookie --- src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index 5578f47534..a057a270ff 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -1505,7 +1505,7 @@ public void ReadBasicInfo(Stream stream) { if (!this.gotCodestreamSignature) { - Span fileSignature = stackalloc byte[2]; + Span fileSignature = [0, 0]; stream.ReadExactly(fileSignature); if (fileSignature[0] != 0xFF || fileSignature[1] != JxlShared.CodestreamMarker) From 15c32fe1a25cbad160a62bf2df8b9194d30ab8e5 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 31 Aug 2026 20:33:11 +0400 Subject: [PATCH 102/328] Prefer Math.DivRem --- .../Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index a057a270ff..7fb1775e08 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -1530,9 +1530,10 @@ public void ReadBasicInfo(Stream stream) long totalBits = bitReader.TotalBitsConsumed; - this.AdvanceCodeStream(totalBits / JxlMath.BitsPerByte); + (long div, long rem) = Math.DivRem(totalBits, JxlMath.BitsPerByte); + this.AdvanceCodeStream(div); - this.codestreamBitsAhead = totalBits % JxlMath.BitsPerByte; + this.codestreamBitsAhead = rem; this.gotBasicInfo = true; this.basicInfoSizeHint = 0; this.imageMetadata = this.metadata.ImageMetadata; From de8c6443354540adbc98c73588a2a97447a89cfc Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 31 Aug 2026 20:35:19 +0400 Subject: [PATCH 103/328] Use Math.DivRem --- .../Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index 7fb1775e08..8dcd47711b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -1565,8 +1565,10 @@ public void ReadAllHeaders() } long totalBits = reader.TotalBitsConsumed; - this.AdvanceCodeStream(totalBits / JxlMath.BitsPerByte); - this.codestreamBitsAhead = totalBits % JxlMath.BitsPerByte; + (long div, long rem) = Math.DivRem(totalBits, JxlMath.BitsPerByte); + + this.AdvanceCodeStream(div); + this.codestreamBitsAhead = rem; this.gotTransformData = true; } From a5d5ab6c8ba493ebd9b6bf2144f681f40999490d Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 31 Aug 2026 20:35:51 +0400 Subject: [PATCH 104/328] Prefer byte --- .../Formats/Jxl/Processing/Decoder/JxlSectionStatus.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlSectionStatus.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlSectionStatus.cs index 88c4f1068f..eefcf9ad90 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlSectionStatus.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlSectionStatus.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; /// /// Status of processing a section. /// -internal enum JxlSectionStatus +internal enum JxlSectionStatus : byte { /// /// Processed normally. From 1b6e824f6d8e77b0bfad5aef1450aeed0d0a9864 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 31 Aug 2026 20:39:47 +0400 Subject: [PATCH 105/328] Don't use GetWReference (it should return span) --- .../Encoding/ContextPrediction/JxlContextPrediction.cs | 4 +--- .../Modular/Encoding/ContextPrediction/JxlModularHeader.cs | 6 +++--- .../Modular/Encoding/ContextPrediction/JxlModularState.cs | 4 ++-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlContextPrediction.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlContextPrediction.cs index fcbef7afd7..f856ef0e39 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlContextPrediction.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlContextPrediction.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; @@ -17,8 +16,7 @@ internal static class JxlContextPrediction public static void SetPredictorMode(int i, JxlModularHeader header) { - ref uint wr = ref header.GetWReference(); - Span w = MemoryMarshal.CreateSpan(ref wr, 4); + Span w = header.GetW(); switch (i) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularHeader.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularHeader.cs index 4db1a00dc9..04a50fe270 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularHeader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularHeader.cs @@ -101,10 +101,10 @@ public int P3Ce } /// - /// Returns a reference to the first w item. + /// Returns a span to the w array. /// - /// Reference to w[0] - public ref uint GetWReference() => ref this.w[0]; + /// Reference to w + public Span GetW() => this.w; public bool Visit(JxlVisitor v) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs index f731dc9677..7e52a62e57 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs @@ -109,13 +109,13 @@ public long Predict(bool computeProperties, int x, int y, int width, long n, lon int posNW = x > 0 ? posN - 1 : posN; Span weights = stackalloc uint[4]; - ref uint headerW = ref this.header.GetWReference(); + Span headerW = this.header.GetW(); for (int i = 0; i < 4; i++) { Span error = this.predErrors[i].AsSpan(); weights[i] = error[posN] + error[posNE] + error[posNW]; - weights[i] = ErrorWeight((int)weights[i], Unsafe.Add(ref headerW, i)); + weights[i] = ErrorWeight((int)weights[i], headerW[i]); } n = AddBits(n); From c6c69428222540d90ff0b521bd581d7489565c7b Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 31 Aug 2026 20:41:13 +0400 Subject: [PATCH 106/328] Parenthesize --- .../Modular/Encoding/ContextPrediction/JxlModularState.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs index 7e52a62e57..76aceefbcd 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs @@ -72,7 +72,7 @@ public static uint ErrorWeight(int x, uint maxWeight) public static long WeightedAverage(Span p, Span w) { - uint weightSum = w[0] + w[1] + w[2] + w[3]; + uint weightSum = (w[0] + w[1]) + (w[2] + w[3]); if (weightSum <= 15) { From 79b0113c280f053f09e5a7dd5e4d9a0d1ad004b7 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 31 Aug 2026 20:42:09 +0400 Subject: [PATCH 107/328] Don't use stackalloc --- .../Modular/Encoding/ContextPrediction/JxlModularState.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs index 76aceefbcd..2b112ff3cc 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs @@ -108,7 +108,7 @@ public long Predict(bool computeProperties, int x, int y, int width, long n, lon int posNE = x < width - 1 ? posN + 1 : posN; int posNW = x > 0 ? posN - 1 : posN; - Span weights = stackalloc uint[4]; + Span weights = [0, 0, 0, 0]; Span headerW = this.header.GetW(); for (int i = 0; i < 4; i++) From e7d3608b3dbe1cb65fc6ae4f7af53e2c7548472e Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 31 Aug 2026 20:45:33 +0400 Subject: [PATCH 108/328] Remove SuppressFinalize from sealed class --- src/ImageSharp/Formats/Jxl/Processing/JxlDctAcImage{T}.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcImage{T}.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcImage{T}.cs index 8e74c26b72..4f311f8982 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcImage{T}.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcImage{T}.cs @@ -55,9 +55,5 @@ public unsafe JxlDctReadOnlyAcPointer GetReadOnlyPlaneRow(int channel, int y, in } } - public void Dispose() - { - this.image.Dispose(); - GC.SuppressFinalize(this); - } + public void Dispose() => this.image.Dispose(); } From e6ed4baefd76de2e4ea479914dde49c09992a709 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 31 Aug 2026 20:46:24 +0400 Subject: [PATCH 109/328] Use Math.Clamp --- src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs index 54a41f99ab..3b7500fd05 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs @@ -261,15 +261,7 @@ public void ComputeGlobalScaleAndQuant(float quantDc, float quantMedian, float q const int quantFieldTarget = 5; float scale = GlobalScaleDenominator * (quantMedian - quantMedianAbsd) / quantFieldTarget; - if (scale < 1) - { - scale = 1; - } - - if (scale > (1 << 15)) - { - scale = 1 << 15; - } + scale = Math.Clamp(scale, 1, 1 << 15); int newGlobalScale = (int)scale; int scaledQuantDc = (int)(quantDc * GlobalScaleNumerator * 1.6f); From 4858ef3783f943b718aafcef59d2794e3e4cafab Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 31 Aug 2026 20:47:10 +0400 Subject: [PATCH 110/328] Use StartsWith --- src/ImageSharp/Formats/Jxl/JxlImageFormatDetector.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jxl/JxlImageFormatDetector.cs b/src/ImageSharp/Formats/Jxl/JxlImageFormatDetector.cs index 9b968bd1e3..17484114ce 100644 --- a/src/ImageSharp/Formats/Jxl/JxlImageFormatDetector.cs +++ b/src/ImageSharp/Formats/Jxl/JxlImageFormatDetector.cs @@ -28,7 +28,7 @@ public sealed class JxlImageFormatDetector : IImageFormatDetector /// public bool TryDetectFormat(ReadOnlySpan header, [NotNullWhen(true)] out IImageFormat? format) { - if (header[0] == 0xFF && header[1] == 0x0A) + if (header.StartsWith([(byte)0xFF, (byte)0x0A])) { // Just codestream. format = new JxlFormat(); From 2835528a72d6d40a2da9b9c16f291501a4573bfa Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 31 Aug 2026 20:48:41 +0400 Subject: [PATCH 111/328] Remove try/finally --- .../Decoder/JxlBoxContentDecoder.cs | 53 +++++++++---------- 1 file changed, 24 insertions(+), 29 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs index 8a37b7dbaa..93fe598304 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs @@ -59,50 +59,45 @@ public void Process(Stream stream, Stream writer) { byte[] cache = ArrayPool.Shared.Rent(16384); - try + if (this.codingMode == JxlBoxCodingMode.Brotli) { - if (this.codingMode == JxlBoxCodingMode.Brotli) - { - using BrotliStream brotli = new(stream, CompressionMode.Decompress, leaveOpen: true); + using BrotliStream brotli = new(stream, CompressionMode.Decompress, leaveOpen: true); + int bytesRead; + while ((bytesRead = brotli.Read(cache, 0, cache.Length)) > 0) + { + writer.Write(cache.AsSpan(0, bytesRead)); + } + } + else + { + if (this.boxExtendsTillEnd) + { int bytesRead; - while ((bytesRead = brotli.Read(cache, 0, cache.Length)) > 0) + while ((bytesRead = stream.Read(cache, 0, cache.Length)) > 0) { writer.Write(cache.AsSpan(0, bytesRead)); } } else { - if (this.boxExtendsTillEnd) + ulong bytesLeft = this.boxSize; + while (bytesLeft > 0) { - int bytesRead; - while ((bytesRead = stream.Read(cache, 0, cache.Length)) > 0) + int toRead = (int)Math.Min((ulong)cache.Length, bytesLeft); + int bytesRead = stream.Read(cache, 0, toRead); + + if (bytesRead == 0) { - writer.Write(cache.AsSpan(0, bytesRead)); + throw new EndOfStreamException("Unexpected EOF while reading box content"); } - } - else - { - ulong bytesLeft = this.boxSize; - while (bytesLeft > 0) - { - int toRead = (int)Math.Min((ulong)cache.Length, bytesLeft); - int bytesRead = stream.Read(cache, 0, toRead); - if (bytesRead == 0) - { - throw new EndOfStreamException("Unexpected EOF while reading box content"); - } - - writer.Write(cache.AsSpan(0, bytesRead)); - bytesLeft -= (ulong)bytesRead; - } + writer.Write(cache.AsSpan(0, bytesRead)); + bytesLeft -= (ulong)bytesRead; } } } - finally - { - ArrayPool.Shared.Return(cache); - } + + ArrayPool.Shared.Return(cache); } } From 90ddb9de23f897eb66e0ab7ef27235d19497216e Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 31 Aug 2026 21:02:05 +0400 Subject: [PATCH 112/328] Add JPEG parsed data --- .../Jpeg/{ => Data}/JpegAppMarkerType.cs | 2 +- .../Processing/Jpeg/Data/JpegDataConstants.cs | 20 +++++++++++++++++++ .../Formats/Jxl/Processing/Jpeg/README.md | 9 +++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) rename src/ImageSharp/Formats/Jxl/Processing/Jpeg/{ => Data}/JpegAppMarkerType.cs (89%) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Jpeg/Data/JpegDataConstants.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Jpeg/README.md diff --git a/src/ImageSharp/Formats/Jxl/Processing/Jpeg/JpegAppMarkerType.cs b/src/ImageSharp/Formats/Jxl/Processing/Jpeg/Data/JpegAppMarkerType.cs similarity index 89% rename from src/ImageSharp/Formats/Jxl/Processing/Jpeg/JpegAppMarkerType.cs rename to src/ImageSharp/Formats/Jxl/Processing/Jpeg/Data/JpegAppMarkerType.cs index 2b05317ab4..ba121c80f2 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Jpeg/JpegAppMarkerType.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Jpeg/Data/JpegAppMarkerType.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Jpeg; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Jpeg.Data; /// /// Identifies the kind of APP marker in a JPEG file. diff --git a/src/ImageSharp/Formats/Jxl/Processing/Jpeg/Data/JpegDataConstants.cs b/src/ImageSharp/Formats/Jxl/Processing/Jpeg/Data/JpegDataConstants.cs new file mode 100644 index 0000000000..525d3f4c0a --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Jpeg/Data/JpegDataConstants.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Jpeg.Data; + +/// +/// Constants used in parsed JPEG data. +/// +internal static class JpegDataConstants +{ + /// + /// Maximum number of components. + /// + public const int MaxComponents = 4; + + /// + /// Maximum number of quantizer tables. + /// + public const int MaximumQuantizationTables = 4; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Jpeg/README.md b/src/ImageSharp/Formats/Jxl/Processing/Jpeg/README.md new file mode 100644 index 0000000000..4b98e0aff0 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Jpeg/README.md @@ -0,0 +1,9 @@ +# Jxl/Processing/Jpeg +This folder contains logic to: + +- Parse and represent JPEG data +- Write JPEG markers +- JPEG to JXL +- JXL to JPEG + +This does not contain a JPEG codec. From 30599106625324ded636cf0fad48e4fc3b3d8020 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 31 Aug 2026 21:03:23 +0400 Subject: [PATCH 113/328] Use InlineArray for color correlation --- src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelation.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelation.cs index 7047c2c7cb..4ec5258801 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelation.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelation.cs @@ -12,7 +12,7 @@ internal sealed class JxlColorCorrelation private float baseCorrelationX; private float baseCorrelationB = JxlOpsinConstants.YToBRatio; - private readonly float[] dcFactors = new float[4]; + private InlineArray4 dcFactors; private uint colorFactor = JxlChromaFromLuma.DefaultColorFactor; private float colorScale = 1.0f / JxlChromaFromLuma.DefaultColorFactor; From a95594607d120d211822464b91be0c4864936fbf Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 31 Aug 2026 21:11:16 +0400 Subject: [PATCH 114/328] Add JPEG data constants --- .../Processing/Jpeg/Data/JpegDataConstants.cs | 85 +++++++++++++++++++ .../Formats/Jxl/Processing/Jpeg/README.md | 2 + 2 files changed, 87 insertions(+) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Jpeg/Data/JpegDataConstants.cs b/src/ImageSharp/Formats/Jxl/Processing/Jpeg/Data/JpegDataConstants.cs index 525d3f4c0a..7dd73a50c1 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Jpeg/Data/JpegDataConstants.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Jpeg/Data/JpegDataConstants.cs @@ -17,4 +17,89 @@ internal static class JpegDataConstants /// Maximum number of quantizer tables. /// public const int MaximumQuantizationTables = 4; + + /// + /// Maximum number of Huffman code tables. + /// + public const int MaxHuffmanTables = 4; + + /// + /// Maximum number of bits for a Huffman code. + /// + public const int JpegHuffmanMaxBitLength = 16; + + /// + /// Alphabet size for Huffman tables used in the JPEG format. + /// + public const int JpegHuffmanAlphabetSize = 256; + + /// + /// Alphabet size for Huffman tables used in the JPEG format. + /// + /// + /// This is specific to the DC coefficients of the Discrete + /// Cosine Transform. + /// + public const int JpegDcAlphabetSize = 12; + + /// + /// Maximum number of DHT "Define Huffman Tables" markers. + /// + public const int MaxDhtMarkers = 512; + + /// + /// Largest value for width OR height. + /// + public const int MaxDimPixels = 65535; + + /// + /// Marker that specifies APP1. + /// + public const int App1 = 0xE1; + + /// + /// Marker that specifies APP2. + /// + public const int App2 = 0xE2; + + /// + /// Gets the tag bytes specifying the ICC profile. + /// + public static ReadOnlySpan IccProfileTag => "ICC_PROFILE\0"u8; + + /// + /// Gets the tag bytes specifying the EXIF profile. + /// + public static ReadOnlySpan ExifTag => "Exif\0\0"u8; + + /// + /// Gets the tag bytes specifying the XMP profile. + /// + public static ReadOnlySpan XmpTag => "http://ns.adobe.com/xap/1.0/\0"u8; + + public static ReadOnlySpan JpegNaturalOrder => + [ + 0, 1, 8, 16, 9, 2, 3, 10, + 17, 24, 32, 25, 18, 11, 4, 5, + 12, 19, 26, 33, 40, 48, 41, 34, + 27, 20, 13, 6, 7, 14, 21, 28, + 35, 42, 49, 56, 57, 50, 43, 36, + 29, 22, 15, 23, 30, 37, 44, 51, + 58, 59, 52, 45, 38, 31, 39, 46, + 53, 60, 61, 54, 47, 55, 62, 63, + 63, 63, 63, 63, 63, 63, 63, 63, + 63, 63, 63, 63, 63, 63, 63, 63 + ]; + + public static ReadOnlySpan JpegZigZagOrder => + [ + 0, 1, 5, 6, 14, 15, 27, 28, + 2, 4, 7, 13, 16, 26, 29, 42, + 3, 8, 12, 17, 25, 30, 41, 43, + 9, 11, 18, 24, 31, 40, 44, 53, + 10, 19, 23, 32, 39, 45, 52, 54, + 20, 22, 33, 38, 46, 51, 55, 60, + 21, 34, 37, 47, 50, 56, 59, 61, + 35, 36, 48, 49, 57, 58, 62, 63 + ]; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Jpeg/README.md b/src/ImageSharp/Formats/Jxl/Processing/Jpeg/README.md index 4b98e0aff0..bde5de389e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Jpeg/README.md +++ b/src/ImageSharp/Formats/Jxl/Processing/Jpeg/README.md @@ -7,3 +7,5 @@ This folder contains logic to: - JXL to JPEG This does not contain a JPEG codec. + +Logic in this folder is used for JPEG<->JXL lossless compression. From 497c27c0d71c8f764a7f04d36fa8dcc2eb084e0e Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 1 Sep 2026 16:46:46 +0400 Subject: [PATCH 115/328] Update folder structure --- .../Jxl/IO/FrameHeader/JxlFrameHeader.cs | 1 + .../Jpeg/Data/JpegAppMarkerType.cs | 2 +- .../Jpeg/Data/JpegDataConstants.cs | 2 +- .../Jxl/IO/Jpeg/Data/JpegQuantizationTable.cs | 29 ++++++++ .../Formats/Jxl/IO/Jpeg/JpegHuffmanDecoder.cs | 38 ++++++++++ .../Jxl/{Processing => IO}/Jpeg/README.md | 0 .../Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs | 2 +- src/ImageSharp/Formats/Jxl/InlineArrays.cs | 9 +++ .../{ => AcStrategy}/IJxlDctAcImage.cs | 4 +- .../{ => AcStrategy}/JxlAcContext.cs | 2 +- .../{ => AcStrategy}/JxlAcStrategy.cs | 2 +- .../{ => AcStrategy}/JxlAcStrategyImage.cs | 2 +- .../{ => AcStrategy}/JxlAcStrategyRow.cs | 2 +- .../{ => AcStrategy}/JxlAcStrategyType.cs | 2 +- .../JxlAlphaBlendingInputLayer.cs | 2 +- .../{ => Blending}/JxlAlphaBlendingOutput.cs | 2 +- .../{ => Blending}/JxlAlphaHelper.cs | 2 +- .../Jxl/Processing/Butteraugli/Butteraugli.cs | 1 + .../Jxl/Processing/{ => Dct}/JxlDct.cs | 2 +- .../Processing/{ => Dct}/JxlDctAcImage{T}.cs | 3 +- .../Processing/{ => Dct}/JxlDctAcPointer.cs | 2 +- .../Jxl/Processing/{ => Dct}/JxlDctAcType.cs | 2 +- .../Jxl/Processing/{ => Dct}/JxlDctOutput.cs | 2 +- .../{ => Dct}/JxlDctQuantWeightParameters.cs | 2 +- .../{ => Dct}/JxlDctReadOnlyAcPointer.cs | 2 +- .../Jxl/Processing/{ => Dct}/JxlDctScales.cs | 2 +- .../Jxl/Processing/{ => Dct}/JxlDctSource.cs | 2 +- .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 1 + .../Jxl/Processing/Decoder/JxlFrameDecoder.cs | 1 + .../Jxl/Processing/Decoder/JxlNoiseDecoder.cs | 1 + .../Decoder/JxlOutputEncodingInfo.cs | 1 + .../Decoder/JxlPassesDecoderState.cs | 1 + .../Processing/Decoder/JxlPatchDictionary.cs | 2 + .../Encoder/Ans/JxlHistogramParameters.cs | 1 + .../AuxiliaryOutput/JxlAuxiliaryOutput.cs | 72 +++++++++++++++++++ .../JxlAuxiliaryOutputConstants.cs | 9 +++ .../Encoder/AuxiliaryOutput/JxlLayerTotals.cs | 22 ++++++ .../Encoder/AuxiliaryOutput/JxlLayerType.cs | 23 ++++++ .../Jxl/Processing/JxlBlockContextMap.cs | 1 + .../Jxl/Processing/JxlCoefficientOrder.cs | 1 + .../Formats/Jxl/Processing/JxlConvolve.cs | 1 + .../Formats/Jxl/Processing/JxlEntropyCoder.cs | 1 + .../Jxl/Processing/JxlImageFeatures.cs | 1 + .../Formats/Jxl/Processing/JxlLoopFilter.cs | 1 + .../Processing/JxlOpsinInverseParameters.cs | 2 + .../Jxl/Processing/JxlPassesSharedState.cs | 2 + .../Formats/Jxl/Processing/JxlTranspose.cs | 2 + .../Processing/{ => Noise}/JxlNoiseHelper.cs | 2 +- .../{ => Noise}/JxlNoiseIndexAndFraction.cs | 2 +- .../Processing/{ => Noise}/JxlNoiseLevel.cs | 2 +- .../{ => Noise}/JxlNoiseParameters.cs | 2 +- .../{ => Primitives}/JxlAspectRatioHelpers.cs | 2 +- .../{ => Primitives}/JxlBitDepth.cs | 2 +- .../{ => Primitives}/JxlBitDepthType.cs | 2 +- .../{ => Primitives}/JxlDataType.cs | 2 +- .../{ => Primitives}/JxlInverseMtf.cs | 2 +- .../{ => Primitives}/JxlLehmerCode.cs | 2 +- .../{ => Primitives}/JxlMatrix3x3.cs | 2 +- .../{ => Primitives}/JxlMatrix3x3F.cs | 2 +- .../{ => Primitives}/JxlOverride.cs | 2 +- .../{ => Primitives}/JxlOverrideHelpers.cs | 2 +- .../{ => Primitives}/JxlPackSigned.cs | 2 +- .../{ => Primitives}/JxlPixelFormat.cs | 2 +- .../{ => Primitives}/JxlSpeedTier.cs | 2 +- .../{ => Primitives}/JxlWeightsSeparable5.cs | 2 +- .../{ => Primitives}/JxlWeightsSymmetric3.cs | 2 +- .../{ => Primitives}/JxlWeightsSymmetric5.cs | 2 +- .../{ => Primitives}/JxlXorShift.cs | 2 +- .../{ => Quantization}/JxlDequantMatrices.cs | 3 +- .../{ => Quantization}/JxlQuantMode.cs | 2 +- .../{ => Quantization}/JxlQuantTable.cs | 2 +- .../{ => Quantization}/JxlQuantWeights.cs | 4 +- .../{ => Quantization}/JxlQuantizer.cs | 3 +- .../JxlQuantizerConstants.cs | 2 +- .../JxlQuantizerEncoding.cs | 4 +- .../JxlQuantizerParameters.cs | 2 +- .../Processing/Splines/JxlQuantizedSpline.cs | 2 + 77 files changed, 285 insertions(+), 50 deletions(-) rename src/ImageSharp/Formats/Jxl/{Processing => IO}/Jpeg/Data/JpegAppMarkerType.cs (89%) rename src/ImageSharp/Formats/Jxl/{Processing => IO}/Jpeg/Data/JpegDataConstants.cs (97%) create mode 100644 src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegQuantizationTable.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegHuffmanDecoder.cs rename src/ImageSharp/Formats/Jxl/{Processing => IO}/Jpeg/README.md (100%) rename src/ImageSharp/Formats/Jxl/Processing/{ => AcStrategy}/IJxlDctAcImage.cs (95%) rename src/ImageSharp/Formats/Jxl/Processing/{ => AcStrategy}/JxlAcContext.cs (96%) rename src/ImageSharp/Formats/Jxl/Processing/{ => AcStrategy}/JxlAcStrategy.cs (98%) rename src/ImageSharp/Formats/Jxl/Processing/{ => AcStrategy}/JxlAcStrategyImage.cs (98%) rename src/ImageSharp/Formats/Jxl/Processing/{ => AcStrategy}/JxlAcStrategyRow.cs (92%) rename src/ImageSharp/Formats/Jxl/Processing/{ => AcStrategy}/JxlAcStrategyType.cs (94%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Blending}/JxlAlphaBlendingInputLayer.cs (83%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Blending}/JxlAlphaBlendingOutput.cs (82%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Blending}/JxlAlphaHelper.cs (98%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Dct}/JxlDct.cs (99%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Dct}/JxlDctAcImage{T}.cs (94%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Dct}/JxlDctAcPointer.cs (87%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Dct}/JxlDctAcType.cs (84%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Dct}/JxlDctOutput.cs (96%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Dct}/JxlDctQuantWeightParameters.cs (92%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Dct}/JxlDctReadOnlyAcPointer.cs (91%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Dct}/JxlDctScales.cs (99%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Dct}/JxlDctSource.cs (96%) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlAuxiliaryOutput.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlAuxiliaryOutputConstants.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlLayerTotals.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlLayerType.cs rename src/ImageSharp/Formats/Jxl/Processing/{ => Noise}/JxlNoiseHelper.cs (92%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Noise}/JxlNoiseIndexAndFraction.cs (82%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Noise}/JxlNoiseLevel.cs (83%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Noise}/JxlNoiseParameters.cs (85%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Primitives}/JxlAspectRatioHelpers.cs (93%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Primitives}/JxlBitDepth.cs (90%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Primitives}/JxlBitDepthType.cs (94%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Primitives}/JxlDataType.cs (90%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Primitives}/JxlInverseMtf.cs (94%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Primitives}/JxlLehmerCode.cs (97%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Primitives}/JxlMatrix3x3.cs (98%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Primitives}/JxlMatrix3x3F.cs (98%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Primitives}/JxlOverride.cs (87%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Primitives}/JxlOverrideHelpers.cs (94%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Primitives}/JxlPackSigned.cs (93%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Primitives}/JxlPixelFormat.cs (93%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Primitives}/JxlSpeedTier.cs (96%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Primitives}/JxlWeightsSeparable5.cs (82%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Primitives}/JxlWeightsSymmetric3.cs (94%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Primitives}/JxlWeightsSymmetric5.cs (97%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Primitives}/JxlXorShift.cs (96%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Quantization}/JxlDequantMatrices.cs (98%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Quantization}/JxlQuantMode.cs (94%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Quantization}/JxlQuantTable.cs (90%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Quantization}/JxlQuantWeights.cs (99%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Quantization}/JxlQuantizer.cs (98%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Quantization}/JxlQuantizerConstants.cs (96%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Quantization}/JxlQuantizerEncoding.cs (98%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Quantization}/JxlQuantizerParameters.cs (95%) diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs index 43d7045041..c1cf766b0e 100644 --- a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs +++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs @@ -9,6 +9,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Processing; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Jpeg/Data/JpegAppMarkerType.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegAppMarkerType.cs similarity index 89% rename from src/ImageSharp/Formats/Jxl/Processing/Jpeg/Data/JpegAppMarkerType.cs rename to src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegAppMarkerType.cs index ba121c80f2..15438464ee 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Jpeg/Data/JpegAppMarkerType.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegAppMarkerType.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Jpeg.Data; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; /// /// Identifies the kind of APP marker in a JPEG file. diff --git a/src/ImageSharp/Formats/Jxl/Processing/Jpeg/Data/JpegDataConstants.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegDataConstants.cs similarity index 97% rename from src/ImageSharp/Formats/Jxl/Processing/Jpeg/Data/JpegDataConstants.cs rename to src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegDataConstants.cs index 7dd73a50c1..e2c36e92ba 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Jpeg/Data/JpegDataConstants.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegDataConstants.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Jpeg.Data; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; /// /// Constants used in parsed JPEG data. diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegQuantizationTable.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegQuantizationTable.cs new file mode 100644 index 0000000000..7386bd5a3f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegQuantizationTable.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; + +/// +/// Representation of quantization values for an 8x8 pixel block. +/// +internal struct JpegQuantizationTable() +{ + /// + /// Quantization values + /// + public InlineArray64 Values; + + public int Precision { get; set; } + + /// + /// Gets or sets the index of the quantization table + /// as it was parsed from the input JPEG. + /// + public int Index { get; set; } + + /// + /// Gets or sets a value indicating whether this table + /// is the last one within its marker segment. + /// + public bool IsLast { get; set; } = true; +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegHuffmanDecoder.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegHuffmanDecoder.cs new file mode 100644 index 0000000000..5cff485be9 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegHuffmanDecoder.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg; + +/// +/// Decodes Huffman codes in a JPEG file for JPEG to JPEG XL encoder. +/// +internal static class JpegHuffmanDecoder +{ + private const int RootTableBits = 8; + private const int LookupSize = 8; + + private static int NextTableBitSize(Span count, int length) + { + int left = 1 << (length - RootTableBits); + while (length < MaxBitLength) + { + left -= count[length]; + + if (left <= 0) + { + break; + } + + length++; + left <<= 1; + } + + return length - RootTableBits; + } + + public struct HuffmanTableEntry() + { + public byte Bits = 0; + public ushort Value = 0xFFFF; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Jpeg/README.md b/src/ImageSharp/Formats/Jxl/IO/Jpeg/README.md similarity index 100% rename from src/ImageSharp/Formats/Jxl/Processing/Jpeg/README.md rename to src/ImageSharp/Formats/Jxl/IO/Jpeg/README.md diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs index a6df675acf..346e374148 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs @@ -5,7 +5,7 @@ using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Fields; -using SixLabors.ImageSharp.Formats.Jxl.Processing; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; diff --git a/src/ImageSharp/Formats/Jxl/InlineArrays.cs b/src/ImageSharp/Formats/Jxl/InlineArrays.cs index acb9c3d413..f61f7490ea 100644 --- a/src/ImageSharp/Formats/Jxl/InlineArrays.cs +++ b/src/ImageSharp/Formats/Jxl/InlineArrays.cs @@ -16,6 +16,15 @@ internal struct InlineArray55 private T first; } +/// +/// Used by JpegQuantizationTable +/// +[InlineArray(64)] +internal struct InlineArray64 +{ + private T first; +} + /// /// Used by JxlCustomTransformData /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/IJxlDctAcImage.cs b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/IJxlDctAcImage.cs similarity index 95% rename from src/ImageSharp/Formats/Jxl/Processing/IJxlDctAcImage.cs rename to src/ImageSharp/Formats/Jxl/Processing/AcStrategy/IJxlDctAcImage.cs index 30969824cd..6d773d7cd3 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/IJxlDctAcImage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/IJxlDctAcImage.cs @@ -1,7 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; /// /// Base DCT AC coefficient image diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAcContext.cs b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcContext.cs similarity index 96% rename from src/ImageSharp/Formats/Jxl/Processing/JxlAcContext.cs rename to src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcContext.cs index 58e5c8b4f4..61893c621b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlAcContext.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcContext.cs @@ -3,7 +3,7 @@ using System.Runtime.CompilerServices; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; /// /// AC context diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategy.cs similarity index 98% rename from src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs rename to src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategy.cs index 6da2926640..36f0b71c5b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategy.cs @@ -5,7 +5,7 @@ using System.Runtime.InteropServices; using static SixLabors.ImageSharp.Formats.Jxl.Processing.JxlFrameDimensions; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; [StructLayout(LayoutKind.Sequential, Pack = 8)] internal struct JxlAcStrategy diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyImage.cs similarity index 98% rename from src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs rename to src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyImage.cs index 236d518140..875cae762f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyImage.cs @@ -3,7 +3,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; internal sealed class JxlAcStrategyImage : IDisposable { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyRow.cs b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyRow.cs similarity index 92% rename from src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyRow.cs rename to src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyRow.cs index b94c565e7b..7a9b1eeafb 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyRow.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyRow.cs @@ -4,7 +4,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; internal sealed class JxlAcStrategyRow { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyType.cs b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyType.cs similarity index 94% rename from src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyType.cs rename to src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyType.cs index e1b935fa2b..317f71ccb3 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyType.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyType.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; internal enum JxlAcStrategyType : ushort { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingInputLayer.cs b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaBlendingInputLayer.cs similarity index 83% rename from src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingInputLayer.cs rename to src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaBlendingInputLayer.cs index 4b49e32c11..9a73f659fb 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingInputLayer.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaBlendingInputLayer.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Blending; internal sealed class JxlAlphaBlendingInputLayer { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingOutput.cs b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaBlendingOutput.cs similarity index 82% rename from src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingOutput.cs rename to src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaBlendingOutput.cs index c5db8cb8a7..62296b20e7 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingOutput.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaBlendingOutput.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Blending; internal sealed class JxlAlphaBlendingOutput { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaHelper.cs b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaHelper.cs similarity index 98% rename from src/ImageSharp/Formats/Jxl/Processing/JxlAlphaHelper.cs rename to src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaHelper.cs index 641205ab21..b3c890f9a2 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaHelper.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaHelper.cs @@ -3,7 +3,7 @@ using System.Runtime.CompilerServices; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Blending; internal sealed class JxlAlphaHelper { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs index 2011c186b3..99f1cf2d88 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs @@ -6,6 +6,7 @@ using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDct.cs b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDct.cs similarity index 99% rename from src/ImageSharp/Formats/Jxl/Processing/JxlDct.cs rename to src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDct.cs index 5e3e76355f..c498c3a6d7 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlDct.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDct.cs @@ -4,7 +4,7 @@ using System.Numerics; using System.Runtime.CompilerServices; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; /// /// Discrete Cosine Transform with SIMD support. diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcImage{T}.cs b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctAcImage{T}.cs similarity index 94% rename from src/ImageSharp/Formats/Jxl/Processing/JxlDctAcImage{T}.cs rename to src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctAcImage{T}.cs index 4f311f8982..6d132c4128 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcImage{T}.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctAcImage{T}.cs @@ -2,8 +2,9 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Jxl.Memory; +using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; internal sealed class JxlDctAcImage : IJxlDctAcImage, IDisposable where T : unmanaged diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcPointer.cs b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctAcPointer.cs similarity index 87% rename from src/ImageSharp/Formats/Jxl/Processing/JxlDctAcPointer.cs rename to src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctAcPointer.cs index 9710712d77..732e8f4a04 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcPointer.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctAcPointer.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; /// /// Pointer to DCT AC coefficients diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcType.cs b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctAcType.cs similarity index 84% rename from src/ImageSharp/Formats/Jxl/Processing/JxlDctAcType.cs rename to src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctAcType.cs index a7e4ede67f..35801b9192 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcType.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctAcType.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; /// /// Bit size for DCT AC coefficient diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctOutput.cs b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctOutput.cs similarity index 96% rename from src/ImageSharp/Formats/Jxl/Processing/JxlDctOutput.cs rename to src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctOutput.cs index 1be3b9adfe..5ef6218908 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlDctOutput.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctOutput.cs @@ -4,7 +4,7 @@ using System.Numerics; using System.Runtime.CompilerServices; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; /// /// Output DCT block. diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctQuantWeightParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctQuantWeightParameters.cs similarity index 92% rename from src/ImageSharp/Formats/Jxl/Processing/JxlDctQuantWeightParameters.cs rename to src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctQuantWeightParameters.cs index 8cd8a16abd..2c0573668c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlDctQuantWeightParameters.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctQuantWeightParameters.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; internal sealed class JxlDctQuantWeightParameters { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctReadOnlyAcPointer.cs b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctReadOnlyAcPointer.cs similarity index 91% rename from src/ImageSharp/Formats/Jxl/Processing/JxlDctReadOnlyAcPointer.cs rename to src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctReadOnlyAcPointer.cs index c80d8eaa64..cbedf64fe8 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlDctReadOnlyAcPointer.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctReadOnlyAcPointer.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; /// /// Pointer to DCT AC coefficients. (Read-only) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctScales.cs b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctScales.cs similarity index 99% rename from src/ImageSharp/Formats/Jxl/Processing/JxlDctScales.cs rename to src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctScales.cs index 0ebcb7a1fe..0785afe128 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlDctScales.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctScales.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; /// /// Read-only cosine lookups for the Discrete Cosine Transform (DCT), diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctSource.cs b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctSource.cs similarity index 96% rename from src/ImageSharp/Formats/Jxl/Processing/JxlDctSource.cs rename to src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctSource.cs index 08dafc11e7..0fe17e92ad 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlDctSource.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctSource.cs @@ -4,7 +4,7 @@ using System.Numerics; using System.Runtime.CompilerServices; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; /// /// Source DCT block. diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index 8dcd47711b..8947b0f142 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -9,6 +9,7 @@ using SixLabors.ImageSharp.Formats.Jxl.IO.Container; using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Metadata.Profiles.Icc; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs index e127354a7b..75b39cbeb0 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs @@ -3,6 +3,7 @@ using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs index 819517551a..1ddb429cba 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs @@ -5,6 +5,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOutputEncodingInfo.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOutputEncodingInfo.cs index bdec666432..616d7ceeb8 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOutputEncodingInfo.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOutputEncodingInfo.cs @@ -4,6 +4,7 @@ using System.Numerics; using SixLabors.ImageSharp.Formats.Jxl.Cms; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs index 5b534ef773..9c5b910c2d 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs @@ -3,6 +3,7 @@ using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchDictionary.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchDictionary.cs index 1b72325667..d9bc0e88f8 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchDictionary.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchDictionary.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; internal sealed class JxlPatchDictionary diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHistogramParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHistogramParameters.cs index 4f2047424c..c6d7526f72 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHistogramParameters.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHistogramParameters.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlAuxiliaryOutput.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlAuxiliaryOutput.cs new file mode 100644 index 0000000000..6a34aabdc4 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlAuxiliaryOutput.cs @@ -0,0 +1,72 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; + +/// +/// Provides statistics gathered during compression or decompression. +/// +internal sealed class JxlAuxiliaryOutput +{ + public JxlLayerTotals[] Layers { get; set; } = new JxlLayerTotals[JxlAuxiliaryOutputConstants.NumberOfImageLayers]; + + public long NumberOfBlocks { get; set; } + + public long NumberOfSmallBlocks { get; set; } + + public long NumberOfDct4x8Blocks { get; set; } + + public long NumberOfAfvBlocks { get; set; } + + public long NumberOfDct8Blocks { get; set; } + + public long NumberOfDct8x16Blocks { get; set; } + + public long NumberOfDct8x32Blocks { get; set; } + + public long NumberOfDct16Blocks { get; set; } + + public long NumberOfDct16x32Blocks { get; set; } + + public long NumberOfDct32Blocks { get; set; } + + public long NumberOfDct32x64Blocks { get; set; } + + public long NumberOfDct64Blocks { get; set; } + + public long NumberOfButteraugliIterations { get; set; } + + public long TotalBits => this.Layers.Sum(x => x.TotalBits); + + public static string GetLayerName(JxlLayerType layer) => layer switch + { + JxlLayerType.Header => "Headers", + JxlLayerType.Toc => "TOC", + JxlLayerType.Dictionary => "Patches", + JxlLayerType.Splines => "Splines", + JxlLayerType.Noise => "Noise", + JxlLayerType.Quant => "Quantizer", + JxlLayerType.ModularTree => "ModularTree", + JxlLayerType.ModularGlobal => "ModularGlobal", + JxlLayerType.Dc => "DC", + JxlLayerType.ModularDcGroup => "ModularDcGroup", + JxlLayerType.ControlFields => "ControlFields", + JxlLayerType.Order => "CoeffOrder", + JxlLayerType.Ac => "ACHistograms", + JxlLayerType.AcTokens => "ACTokens", + JxlLayerType.ModularAcGroup => "ModularAcGroup", + _ => "Invalid", + }; + + public void Assimilate(JxlAuxiliaryOutput victim) + { + for (int i = 0; i < JxlAuxiliaryOutputConstants.NumberOfImageLayers; i++) + { + this.Layers[i].Assimilate(victim.Layers[i]); + } + + this.NumberOfBlocks += victim.NumberOfBlocks; + this.NumberOfSmallBlocks += victim.NumberOfSmallBlocks; + + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlAuxiliaryOutputConstants.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlAuxiliaryOutputConstants.cs new file mode 100644 index 0000000000..1c1b4edd5c --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlAuxiliaryOutputConstants.cs @@ -0,0 +1,9 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; + +internal class JxlAuxiliaryOutputConstants +{ + public const int NumberOfImageLayers = (int)JxlLayerType.ModularAcGroup + 1; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlLayerTotals.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlLayerTotals.cs new file mode 100644 index 0000000000..a62785cbd9 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlLayerTotals.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; + +internal struct JxlLayerTotals +{ + public int NumberOfClusteredHistograms; + public int ExtraBits; + public int HistogramBits; + public int TotalBits; + public double ClusteredEntropy; + + public void Assimilate(in JxlLayerTotals victim) + { + this.NumberOfClusteredHistograms += victim.NumberOfClusteredHistograms; + this.HistogramBits += victim.HistogramBits; + this.ExtraBits += victim.ExtraBits; + this.TotalBits += victim.TotalBits; + this.ClusteredEntropy += victim.ClusteredEntropy; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlLayerType.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlLayerType.cs new file mode 100644 index 0000000000..5852bd654d --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlLayerType.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; + +internal enum JxlLayerType : byte +{ + Header = 0, + Toc, + Dictionary, + Splines, + Noise, + Quant, + ModularTree, + ModularGlobal, + Dc, + ModularDcGroup, + ControlFields, + Order, + Ac, + AcTokens, + ModularAcGroup, +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs index 92dd8d94dd..b47a949871 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs index 4ee1a99587..deb5045f2a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs @@ -4,6 +4,7 @@ using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs index 9bf6a7dcb0..ebe7f5195d 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs @@ -6,6 +6,7 @@ using System.Runtime.InteropServices; using System.Runtime.Intrinsics; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlEntropyCoder.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlEntropyCoder.cs index 1b896638ca..b36517e30b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlEntropyCoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlEntropyCoder.cs @@ -3,6 +3,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlImageFeatures.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlImageFeatures.cs index 142edd0710..63d4025eb1 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlImageFeatures.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlImageFeatures.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Noise; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs index 2edfeca0c1..c7ea5b8365 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs @@ -4,6 +4,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlOpsinInverseParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlOpsinInverseParameters.cs index 322bcd1c64..820b11a5ba 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlOpsinInverseParameters.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlOpsinInverseParameters.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + namespace SixLabors.ImageSharp.Formats.Jxl.Processing; internal static class JxlOpsinInverseParameters diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs index 880c9e32d2..dcc699503b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs @@ -6,6 +6,8 @@ using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlTranspose.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlTranspose.cs index 4b05150a77..99a3a8c984 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlTranspose.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlTranspose.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; + namespace SixLabors.ImageSharp.Formats.Jxl.Processing; /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseHelper.cs b/src/ImageSharp/Formats/Jxl/Processing/Noise/JxlNoiseHelper.cs similarity index 92% rename from src/ImageSharp/Formats/Jxl/Processing/JxlNoiseHelper.cs rename to src/ImageSharp/Formats/Jxl/Processing/Noise/JxlNoiseHelper.cs index f6432e24db..1b6dfa2480 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseHelper.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Noise/JxlNoiseHelper.cs @@ -3,7 +3,7 @@ using System.Runtime.CompilerServices; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Noise; internal static class JxlNoiseHelper { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseIndexAndFraction.cs b/src/ImageSharp/Formats/Jxl/Processing/Noise/JxlNoiseIndexAndFraction.cs similarity index 82% rename from src/ImageSharp/Formats/Jxl/Processing/JxlNoiseIndexAndFraction.cs rename to src/ImageSharp/Formats/Jxl/Processing/Noise/JxlNoiseIndexAndFraction.cs index c3ec1437c8..fe18886a04 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseIndexAndFraction.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Noise/JxlNoiseIndexAndFraction.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Noise; [StructLayout(LayoutKind.Sequential)] internal struct JxlNoiseIndexAndFraction(int index, float fraction) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseLevel.cs b/src/ImageSharp/Formats/Jxl/Processing/Noise/JxlNoiseLevel.cs similarity index 83% rename from src/ImageSharp/Formats/Jxl/Processing/JxlNoiseLevel.cs rename to src/ImageSharp/Formats/Jxl/Processing/Noise/JxlNoiseLevel.cs index d5feebd52b..836a0e3026 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseLevel.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Noise/JxlNoiseLevel.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Noise; [StructLayout(LayoutKind.Sequential)] internal struct JxlNoiseLevel(float noiseLevel, float intensity) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/Noise/JxlNoiseParameters.cs similarity index 85% rename from src/ImageSharp/Formats/Jxl/Processing/JxlNoiseParameters.cs rename to src/ImageSharp/Formats/Jxl/Processing/Noise/JxlNoiseParameters.cs index 3f51f85379..de8072c3ee 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseParameters.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Noise/JxlNoiseParameters.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Noise; internal sealed class JxlNoiseParameters { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAspectRatioHelpers.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlAspectRatioHelpers.cs similarity index 93% rename from src/ImageSharp/Formats/Jxl/Processing/JxlAspectRatioHelpers.cs rename to src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlAspectRatioHelpers.cs index 7fcda36c9a..6c69a8fff7 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlAspectRatioHelpers.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlAspectRatioHelpers.cs @@ -3,7 +3,7 @@ using System.Runtime.CompilerServices; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; internal static class JxlAspectRatioHelpers { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlBitDepth.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlBitDepth.cs similarity index 90% rename from src/ImageSharp/Formats/Jxl/Processing/JxlBitDepth.cs rename to src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlBitDepth.cs index 52b384bb94..a0f6765224 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlBitDepth.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlBitDepth.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; /// /// Describes the interpretation of the input and output diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlBitDepthType.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlBitDepthType.cs similarity index 94% rename from src/ImageSharp/Formats/Jxl/Processing/JxlBitDepthType.cs rename to src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlBitDepthType.cs index e942d57a47..754ac41010 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlBitDepthType.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlBitDepthType.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; /// /// Specifies the kind of bit depth. diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDataType.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlDataType.cs similarity index 90% rename from src/ImageSharp/Formats/Jxl/Processing/JxlDataType.cs rename to src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlDataType.cs index a9c8057e6f..8bc0bf6911 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlDataType.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlDataType.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; /// /// Specifies which data type to use for sample values diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlInverseMtf.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlInverseMtf.cs similarity index 94% rename from src/ImageSharp/Formats/Jxl/Processing/JxlInverseMtf.cs rename to src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlInverseMtf.cs index 8016ec51c0..18f9a55bdb 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlInverseMtf.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlInverseMtf.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; /// /// Inverse Move to Front implementation diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlLehmerCode.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlLehmerCode.cs similarity index 97% rename from src/ImageSharp/Formats/Jxl/Processing/JxlLehmerCode.cs rename to src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlLehmerCode.cs index 4afbcb34fd..2cbd4ea7ab 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlLehmerCode.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlLehmerCode.cs @@ -3,7 +3,7 @@ using System.Runtime.CompilerServices; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; internal static class JxlLehmerCode { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlMatrix3x3.cs similarity index 98% rename from src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3.cs rename to src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlMatrix3x3.cs index a01e2a6216..88454e1f69 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlMatrix3x3.cs @@ -6,7 +6,7 @@ #pragma warning disable IDE0044 // Add readonly modifier #pragma warning disable IDE0051 // Remove unused private members -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; internal struct JxlMatrix3x3 { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3F.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlMatrix3x3F.cs similarity index 98% rename from src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3F.cs rename to src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlMatrix3x3F.cs index b8620c2dd4..2d66df2729 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3F.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlMatrix3x3F.cs @@ -6,7 +6,7 @@ #pragma warning disable IDE0044 // Add readonly modifier #pragma warning disable IDE0052 // Remove unread private members -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; internal struct JxlMatrix3x3F { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlOverride.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlOverride.cs similarity index 87% rename from src/ImageSharp/Formats/Jxl/Processing/JxlOverride.cs rename to src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlOverride.cs index ebd84b7b68..c25f354046 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlOverride.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlOverride.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; /// /// Represents a boolean which can be overriden to be a default diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlOverrideHelpers.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlOverrideHelpers.cs similarity index 94% rename from src/ImageSharp/Formats/Jxl/Processing/JxlOverrideHelpers.cs rename to src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlOverrideHelpers.cs index 85464a16aa..a4e06edc17 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlOverrideHelpers.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlOverrideHelpers.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; /// /// Override utilities. diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlPackSigned.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlPackSigned.cs similarity index 93% rename from src/ImageSharp/Formats/Jxl/Processing/JxlPackSigned.cs rename to src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlPackSigned.cs index f54b77f1d3..a4c2fec411 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlPackSigned.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlPackSigned.cs @@ -3,7 +3,7 @@ using System.Runtime.CompilerServices; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; /// /// Provides PackSigned and UnpackSigned methods. diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlPixelFormat.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlPixelFormat.cs similarity index 93% rename from src/ImageSharp/Formats/Jxl/Processing/JxlPixelFormat.cs rename to src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlPixelFormat.cs index fadb8709fb..be00bd3e76 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlPixelFormat.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlPixelFormat.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; /// /// Data type for the sample values per channel per pixel diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlSpeedTier.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlSpeedTier.cs similarity index 96% rename from src/ImageSharp/Formats/Jxl/Processing/JxlSpeedTier.cs rename to src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlSpeedTier.cs index 363b4a5e4b..8cbc589a1b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlSpeedTier.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlSpeedTier.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; /// /// Defines how quickly or slowly to encode an image. Slower diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlWeightsSeparable5.cs similarity index 82% rename from src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs rename to src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlWeightsSeparable5.cs index 91258b8529..03c16de089 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlWeightsSeparable5.cs @@ -3,7 +3,7 @@ using System.Runtime.CompilerServices; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; internal struct JxlWeightsSeparable5 { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSymmetric3.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlWeightsSymmetric3.cs similarity index 94% rename from src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSymmetric3.cs rename to src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlWeightsSymmetric3.cs index 4d350b3ab8..69cdc0395a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSymmetric3.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlWeightsSymmetric3.cs @@ -4,7 +4,7 @@ using System.Runtime.CompilerServices; using System.Runtime.Intrinsics; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; internal sealed class JxlWeightsSymmetric3 { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSymmetric5.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlWeightsSymmetric5.cs similarity index 97% rename from src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSymmetric5.cs rename to src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlWeightsSymmetric5.cs index 1e2481dbb4..df5fa66b7b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSymmetric5.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlWeightsSymmetric5.cs @@ -4,7 +4,7 @@ using System.Runtime.CompilerServices; using System.Runtime.Intrinsics; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; internal sealed class JxlWeightsSymmetric5 { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlXorShift.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlXorShift.cs similarity index 96% rename from src/ImageSharp/Formats/Jxl/Processing/JxlXorShift.cs rename to src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlXorShift.cs index 78d8900a52..68edb75d5e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlXorShift.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlXorShift.cs @@ -3,7 +3,7 @@ using System.Runtime.CompilerServices; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; internal sealed class JxlXorShift { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDequantMatrices.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlDequantMatrices.cs similarity index 98% rename from src/ImageSharp/Formats/Jxl/Processing/JxlDequantMatrices.cs rename to src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlDequantMatrices.cs index 15b0935415..9649f44203 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlDequantMatrices.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlDequantMatrices.cs @@ -3,8 +3,9 @@ using System.Diagnostics; using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; /// /// Contains matrices used to inverse quantize coefficients. diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantMode.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantMode.cs similarity index 94% rename from src/ImageSharp/Formats/Jxl/Processing/JxlQuantMode.cs rename to src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantMode.cs index 1b974343a4..412dd245d3 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantMode.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantMode.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; /// /// Specifies which algorithm should be used to quantize coefficients. diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantTable.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantTable.cs similarity index 90% rename from src/ImageSharp/Formats/Jxl/Processing/JxlQuantTable.cs rename to src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantTable.cs index 5fc8b49a8e..dca689b88f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantTable.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantTable.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; /// /// Specifies which quantization table to use depending on diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantWeights.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantWeights.cs similarity index 99% rename from src/ImageSharp/Formats/Jxl/Processing/JxlQuantWeights.cs rename to src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantWeights.cs index dfe12b8f30..4986ee7e39 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantWeights.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantWeights.cs @@ -1,7 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; internal static class JxlQuantWeights { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs similarity index 98% rename from src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs rename to src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs index 3b7500fd05..1ea7cfcbd1 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs @@ -6,9 +6,10 @@ using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; /// /// The quantizer for DCT DC/AC coefficients. diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerConstants.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerConstants.cs similarity index 96% rename from src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerConstants.cs rename to src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerConstants.cs index daf28db696..ff6e9c3754 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerConstants.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerConstants.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; /// /// Shared constants used by the quantizer. diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerEncoding.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerEncoding.cs similarity index 98% rename from src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerEncoding.cs rename to src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerEncoding.cs index 8a12e6dbf5..283892392a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerEncoding.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerEncoding.cs @@ -1,7 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; /// /// Specifies weights and quantizer modes. diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerParameters.cs similarity index 95% rename from src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerParameters.cs rename to src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerParameters.cs index 4faa558692..2963d4c329 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerParameters.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerParameters.cs @@ -3,7 +3,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Fields; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; /// /// Represents parameters for the JPEG XL quantizer. diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs index 731c22835b..a3901bf408 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs @@ -3,7 +3,9 @@ using System.Buffers; using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; From 4b9a74d969434ff68cea549dab041575b306351d Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 1 Sep 2026 22:12:57 +0400 Subject: [PATCH 116/328] Add JPEG parser for JXL<->JPEG conversion This isn't a decoder or encoder. It's just a parser/writer so the codec can take some quantization parameters from a JPEG file, as JPEG and JPEG XL are very similar. --- src/ImageSharp/Common/InlineArray.cs | 9 + src/ImageSharp/Common/InlineArray.tt | 2 +- .../Formats/Jxl/IO/Jpeg/Data/JpegComponent.cs | 53 + .../Jxl/IO/Jpeg/Data/JpegComponentScanInfo.cs | 16 + .../Jxl/IO/Jpeg/Data/JpegComponentType.cs | 15 + .../Formats/Jxl/IO/Jpeg/Data/JpegData.cs | 1009 +++++++++++++++++ .../Jxl/IO/Jpeg/Data/JpegExtraZeroRunInfo.cs | 11 + .../Jxl/IO/Jpeg/Data/JpegHuffmanCode.cs | 30 + .../Formats/Jxl/IO/Jpeg/Data/JpegInfo.cs | 17 + .../Jxl/IO/Jpeg/Data/JpegQuantizationTable.cs | 8 +- .../Formats/Jxl/IO/Jpeg/Data/JpegScanInfo.cs | 43 + .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 5 +- .../Formats/Jxl/Processing/JxlImageBundle.cs | 3 +- 13 files changed, 1214 insertions(+), 7 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegComponent.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegComponentScanInfo.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegComponentType.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegData.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegExtraZeroRunInfo.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegHuffmanCode.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegInfo.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegScanInfo.cs diff --git a/src/ImageSharp/Common/InlineArray.cs b/src/ImageSharp/Common/InlineArray.cs index d4cde5f256..a72e3f5394 100644 --- a/src/ImageSharp/Common/InlineArray.cs +++ b/src/ImageSharp/Common/InlineArray.cs @@ -44,6 +44,15 @@ internal struct InlineArray16 private T t; } +/// +/// Represents a safe, fixed sized buffer of 17 elements. +/// +[InlineArray(17)] +internal struct InlineArray17 +{ + private T t; +} + /// /// Represents a safe, fixed sized buffer of 18 elements. /// diff --git a/src/ImageSharp/Common/InlineArray.tt b/src/ImageSharp/Common/InlineArray.tt index 998f8ae105..d15650592a 100644 --- a/src/ImageSharp/Common/InlineArray.tt +++ b/src/ImageSharp/Common/InlineArray.tt @@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp; <#GenerateInlineArrays();#> <#+ -private static int[] Lengths = [4, 8, 14, 16, 18, 19, 26, 32, 33, 36, 256]; +private static int[] Lengths = [4, 8, 14, 16, 17, 18, 19, 26, 32, 33, 36, 256]; void GenerateInlineArrays() { diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegComponent.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegComponent.cs new file mode 100644 index 0000000000..297acacc0a --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegComponent.cs @@ -0,0 +1,53 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; + +// We may need to get a ref to fields, so don't +// make these properties. +#pragma warning disable SA1401 // Fields should be private + +/// +/// Represents one component of a jpeg file. +/// +internal sealed class JpegComponent +{ + /// + /// One-byte id of the component + /// + public int Id; + + /// + /// In interleaved mode, each minimal coded unit (MCU) + /// has horizontal x vertical sample factor DCT blocks + /// from this component. This is the horizontal factor. + /// + public int HorizontalSampleFactor = 1; + + /// + /// In interleaved mode, each minimal coded unit (MCU) + /// has horizontal x vertical sample factor DCT blocks + /// from this component. This is the vertical factor. + /// + public int VerticalSampleFactor = 1; + + /// + /// Index of quantization table used for this component. + /// + public int QuantIndex; + + /// + /// Width measured in 8x8 blocks + /// + public int WidthInBlocks; + + /// + /// Width measured in 8x8 blocks + /// + public int HeightInBlocks; + + /// + /// Gets or sets DCT coefficients. + /// + public List Coefficients { get; set; } = []; +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegComponentScanInfo.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegComponentScanInfo.cs new file mode 100644 index 0000000000..1a3ba3707b --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegComponentScanInfo.cs @@ -0,0 +1,16 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; + +/// +/// Huffman table indexes used for one component of one scan. +/// +// We may need to get a ref to fields, so don't +// make these properties. +internal struct JpegComponentScanInfo +{ + public int ComponentIndex; + public int DcTableIndex; + public int AcTableIndex; +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegComponentType.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegComponentType.cs new file mode 100644 index 0000000000..adf5940c6e --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegComponentType.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; + +internal enum JpegComponentType : byte +{ + Gray, + + YCbCr, + + Rgb, + + Custom +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegData.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegData.cs new file mode 100644 index 0000000000..9ab957be93 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegData.cs @@ -0,0 +1,1009 @@ +// 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.Fields; +using SixLabors.ImageSharp.Formats.Jxl.Processing; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; + +internal sealed class JpegData : IJxlFields +{ + private int restartInterval; + private bool hasZeroPaddingBit; + + public int Width { get; set; } + + public int Height { get; set; } + + public int RestartInterval + { + get => this.restartInterval; + set => this.restartInterval = value; + } + + public bool HasZeroPaddingBit + { + get => this.hasZeroPaddingBit; + set => this.hasZeroPaddingBit = value; + } + + /// + /// Gets or sets raw bytes of APP markers. Types + /// of APP markers are specified by . + /// + public List> AppData { get; set; } = []; + + /// + /// Gets or sets kinds of APP markers for each marker data + /// within . + /// + public List AppMarkerTypes { get; set; } = []; + + /// + /// Gets or sets raw bytes of COM markers. + /// + public List> ComData { get; set; } = []; + + /// + /// Gets or sets quantization tables. + /// + public List Quant { get; set; } = []; + + /// + /// Gets or sets definitions of Huffman codes. + /// + public List HuffmanCodes { get; set; } = []; + + /// + /// Gets or sets JPEG components. + /// + public List Components { get; set; } = []; + + /// + /// Gets or sets scan infos. + /// + public List ScanInfos { get; set; } = []; + + public List MarkerOrder { get; set; } = []; + + public List> InterMarkerData { get; set; } = []; + + public List TailData { get; set; } = []; + + public List PaddingBits { get; set; } = []; + + public void CalculateMcuSize(JpegScanInfo scan, out int mcusPerRow, out int mcuRows) + { + bool isInterleaved = scan.NumComponents > 1; + JpegComponent baseComponent = this.Components[scan.Components[0].ComponentIndex]; + + int horizontalGroup = isInterleaved ? 1 : baseComponent.HorizontalSampleFactor; + int verticalGroup = isInterleaved ? 1 : baseComponent.VerticalSampleFactor; + + int maxHSampFactor = 1; + int maxVSampFactor = 1; + + foreach (JpegComponent component in this.Components) + { + maxHSampFactor = Math.Max(component.HorizontalSampleFactor, maxHSampFactor); + maxVSampFactor = Math.Max(component.VerticalSampleFactor, maxVSampFactor); + } + + mcusPerRow = JxlMath.DivCeil(this.Width * horizontalGroup, 8 * maxHSampFactor); + mcuRows = JxlMath.DivCeil(this.Height * verticalGroup, 8 * maxVSampFactor); + } + + public static void SetJpegDataFromIcc(Span icc, JpegData jpegData) + { + int iccPos = 0; + + for (int i = 0; i < jpegData.AppData.Count; i++) + { + if (jpegData.AppMarkerTypes[i] != JpegAppMarkerType.Icc) + { + continue; + } + + if (jpegData.AppData[i].Count < 17) + { + throw new InvalidOperationException("ICC APP marker too small: " + jpegData.AppData[i].Count); + } + + int len = jpegData.AppData[i].Count - 17; + if (iccPos + len > icc.Length) + { + throw new InvalidOperationException("ICC length is less than APP markers: requested " + len + " more bytes, " + (icc.Length - iccPos) + " available"); + } + + icc.Slice(iccPos, len).CopyTo(CollectionsMarshal.AsSpan(jpegData.AppData[i])[17..]); + iccPos += len; + } + + if (iccPos != icc.Length && iccPos != 0) + { + throw new InvalidOperationException("ICC length > APP markers"); + } + } + + private static bool VisitMarker(ref byte marker, JxlVisitor visitor, ref JpegInfo info) + { + uint marker32 = marker - 0xC0u; + + if (!visitor.Bits(6, 0x00, ref marker32)) + { + return false; + } + + marker = (byte)(marker32 + 0xC0u); + + if ((marker & 0xf0) == 0xe0) + { + info.NumberOfAppMarkers++; + } + + if (marker == 0xfe) + { + info.NumberOfComMarkers++; + } + + if (marker == 0xda) + { + info.NumberOfScans++; + } + + if (marker == 0xff) + { + info.NumberOfIntermarkers++; + } + + if (marker == 0xdd) + { + info.HasDri = true; + } + + return true; + } + + /// + public bool Visit(JxlVisitor visitor) + { + // The following is just JPEG parsing/writing code. + // Nothing different. + bool isGray = this.Components.Count == 1; + + if (!visitor.Boolean(false, ref isGray)) + { + return false; + } + + if (visitor.IsReading) + { + this.Components = new List(isGray ? 1 : 3); + } + + JpegInfo info = default; + + if (visitor.IsReading) + { + byte marker = 0xC0; + + do + { + if (!VisitMarker(ref marker, visitor, ref info)) + { + return false; + } + + this.MarkerOrder.Add(marker); + + if (this.MarkerOrder.Count > 16384) + { + throw new InvalidOperationException("Too many markers: " + this.MarkerOrder.Count); + } + } + while (marker != 0xD9); + } + else + { + if (this.MarkerOrder.Count > 16384) + { + throw new InvalidOperationException("Too many markers: " + this.MarkerOrder.Count); + } + + Span markerData = CollectionsMarshal.AsSpan(this.MarkerOrder); + + for (int i = 0; i < markerData.Length; i++) + { + ref byte marker = ref markerData[i]; + + if (!VisitMarker(ref marker, visitor, ref info)) + { + return false; + } + } + + if (this.MarkerOrder.Count > 0) + { + if (this.MarkerOrder[^1] != 0xD9) + { + throw new InvalidOperationException("Last marker should always be EOI (0xD9) marker"); + } + } + } + + if (info.NumberOfScans == 0) + { + throw new InvalidOperationException("No JPEG scans"); + } + + if (visitor.IsReading) + { + this.AppData = new List>(info.NumberOfAppMarkers); + this.AppMarkerTypes = new List(info.NumberOfAppMarkers); + this.ComData = new List>(info.NumberOfAppMarkers); + this.ScanInfos = new List(info.NumberOfAppMarkers); + } + + if (this.AppData.Count != info.NumberOfAppMarkers || + this.AppMarkerTypes.Count != info.NumberOfAppMarkers || + this.ComData.Count != info.NumberOfComMarkers || + this.ScanInfos.Count != info.NumberOfScans) + { + throw new InvalidOperationException("Mismatch between number of APP markers and the actual APP marker count"); + } + + for (int i = 0; i < this.AppData.Count; i++) + { + uint uMarkerType = (uint)this.AppMarkerTypes[i]; + + if (!visitor.U32( + JxlFieldExpressions.Value(0), + JxlFieldExpressions.Value(1), + JxlFieldExpressions.BitsOffset(1, 2), + JxlFieldExpressions.BitsOffset(2, 4), + 0, + ref uMarkerType)) + { + return false; + } + + this.AppMarkerTypes[i] = (JpegAppMarkerType)uMarkerType; + + if (this.AppMarkerTypes[i] is not JpegAppMarkerType.Unknown and + not JpegAppMarkerType.Icc and + not JpegAppMarkerType.Exif and + not JpegAppMarkerType.Xmp) + { + throw new InvalidOperationException("Unknown APP marker type: " + (uint)this.AppMarkerTypes[i]); + } + + uint len = (uint)this.AppMarkerTypes.Count - 1; + if (!visitor.Bits(16, 0, ref len)) + { + return false; + } + + if (visitor.IsReading) + { + this.AppData[i] = new List((int)len + 1); + if (len + 1 < 3) + { + throw new InvalidOperationException("Marker size is invalid"); + } + } + + if (len + 1 < 3) + { + throw new InvalidOperationException("Marker size is invalid"); + } + } + + for (int i = 0; i < this.ComData.Count; i++) + { + List com = this.ComData[i]; + + uint len = (uint)com.Count - 1; + + if (!visitor.Bits(16, 0, ref len)) + { + return false; + } + + if (len + 1 < 3) + { + throw new InvalidOperationException("Marker size is invalid"); + } + + if (visitor.IsReading) + { + this.ComData[i] = new List((int)len + 1); + + if (len + 1 < 3) + { + throw new InvalidOperationException("Marker size is invalid"); + } + } + + if (len + 1 < 3) + { + throw new InvalidOperationException("Marker size is invalid"); + } + } + + uint numQuantTables = (uint)this.Quant.Count; + + if (!visitor.U32( + JxlFieldExpressions.Value(1), + JxlFieldExpressions.Value(2), + JxlFieldExpressions.Value(3), + JxlFieldExpressions.Value(4), + 2, + ref numQuantTables)) + { + return false; + } + + if (numQuantTables == 4) + { + throw new InvalidOperationException("Invalid number of quant tables"); + } + + if (visitor.IsReading) + { + this.Quant = new List((int)numQuantTables); + } + + Span quantSpan = CollectionsMarshal.AsSpan(this.Quant); + + for (int i = 0; i < numQuantTables; i++) + { + ref JpegQuantizationTable quant = ref quantSpan[i]; + + if (quant.Precision > 1) + { + throw new InvalidOperationException("Quant tables with more than 16 bits are not supported"); + } + + if (!visitor.Bits(1, 0, ref Unsafe.As(ref quant.Precision))) + { + return false; + } + + if (!visitor.Bits(2, (uint)i, ref Unsafe.As(ref quant.Index))) + { + return false; + } + + if (!visitor.Boolean(true, ref quant.IsLast)) + { + return false; + } + } + + Span components = CollectionsMarshal.AsSpan(this.Components); + + JpegComponentType componentType = + components.Length == 1 && components[0].Id == 1 ? JpegComponentType.Gray + : components.Length == 3 && components[0].Id == 1 && + components[1].Id == 2 && components[2].Id == 3 + ? JpegComponentType.YCbCr + : components.Length == 3 && components[0].Id == 'R' && + components[1].Id == 'G' && components[2].Id == 'B' + ? JpegComponentType.Rgb + : JpegComponentType.Custom; + + if (visitor.Bits(2, (uint)JpegComponentType.YCbCr, ref Unsafe.As(ref componentType))) + { + return false; + } + + uint numberOfComponents; + + if (componentType == JpegComponentType.Gray) + { + numberOfComponents = 1; + } + else if (componentType != JpegComponentType.Custom) + { + numberOfComponents = 3; + } + else + { + numberOfComponents = (uint)components.Length; + + if (!visitor.U32( + JxlFieldExpressions.Value(1), + JxlFieldExpressions.Value(2), + JxlFieldExpressions.Value(3), + JxlFieldExpressions.Value(4), + 3, + ref numberOfComponents)) + { + return false; + } + + if (numberOfComponents is not 1 and not 3) + { + throw new InvalidOperationException("Invalid number of components: " + numberOfComponents); + } + } + + if (visitor.IsReading) + { + this.Components = new List((int)numberOfComponents); + + // It's unsafe to assign a new List (or add/remove items to it) + // while keeping a Span to it. + components = CollectionsMarshal.AsSpan(this.Components); + } + + if (componentType == JpegComponentType.Custom) + { + foreach (JpegComponent component in components) + { + if (!visitor.Bits(8, 0, ref Unsafe.As(ref component.Id))) + { + return false; + } + } + } + else if (componentType == JpegComponentType.Gray) + { + components[0].Id = 1; + } + else if (componentType == JpegComponentType.Rgb) + { + components[0].Id = 'R'; + components[1].Id = 'G'; + components[2].Id = 'B'; + } + else + { + components[0].Id = 1; + components[1].Id = 2; + components[2].Id = 3; + } + + uint usedTables = 0; + + for (int i = 0; i < components.Length; i++) + { + if (!visitor.Bits(2, 0, ref Unsafe.As(ref components[i].QuantIndex))) + { + return false; + } + + if (components[i].QuantIndex >= this.Quant.Count) + { + throw new InvalidOperationException("Invalid quant table for component " + components[i].QuantIndex); + } + + usedTables |= 1u << components[i].QuantIndex; + } + + for (int i = 0; i < this.Quant.Count; i++) + { + if ((usedTables & (1 << i)) != 0) + { + continue; + } + + if (i == 0) + { + throw new InvalidOperationException("First quant table unused"); + } + + for (int j = 0; j < 64; j++) + { + if (this.Quant[i].Values[j] != this.Quant[i - 1].Values[j]) + { + throw new InvalidOperationException("Non-trivial unused quant table"); + } + } + } + + uint numHuff = (uint)this.HuffmanCodes.Count; + + if (!visitor.U32( + JxlFieldExpressions.Value(4), + JxlFieldExpressions.BitsOffset(3, 2), + JxlFieldExpressions.BitsOffset(4, 10), + JxlFieldExpressions.BitsOffset(6, 26), + 4, + ref numHuff)) + { + return false; + } + + if (visitor.IsReading) + { + this.HuffmanCodes = new List((int)numHuff); + } + + Span huffs = CollectionsMarshal.AsSpan(this.HuffmanCodes); + + for (int i = 0; i < huffs.Length; i++) + { + ref JpegHuffmanCode hc = ref huffs[i]; + + bool isAc = (hc.SlotId >> 4) != 0; + uint id = (uint)hc.SlotId & 0x0Fu; + + if (!visitor.Boolean(false, ref isAc)) + { + return false; + } + + if (!visitor.Bits(2, 0, ref id)) + { + return false; + } + + hc.SlotId = ((isAc ? 1 : 0) << 4) | (int)id; + + if (!visitor.Boolean(true, ref hc.IsLast)) + { + return false; + } + + int numSymbols = 0; + + for (int j = 0; j <= 16; j++) + { + if (!visitor.U32( + JxlFieldExpressions.Value(0), + JxlFieldExpressions.Value(1), + JxlFieldExpressions.BitsOffset(3, 2), + JxlFieldExpressions.Bits(8), + 0, + ref Unsafe.As(ref hc.Counts[j]))) + { + return false; + } + + numSymbols += hc.Counts[j]; + } + + if (numSymbols == 0) + { + // At least 2 symbols are required, since one of them is EOI. + // This case is used to represent an empty DHT marker. + continue; + } + + if (numSymbols > 17) + { + throw new InvalidOperationException("Huffman code too large (" + numSymbols + ")"); + } + + InlineArray5 valueSlots = default; + + for (int j = 0; j < numSymbols; j++) + { + // Goes up to 256, included. Might have the same symbol appear twice. + if (!visitor.U32( + JxlFieldExpressions.Bits(2), + JxlFieldExpressions.BitsOffset(2, 4), + JxlFieldExpressions.BitsOffset(4, 8), + JxlFieldExpressions.BitsOffset(8, 1), + 0, + ref Unsafe.As(ref hc.Values[j]))) + { + return false; + } + + valueSlots[hc.Values[j] >> 6] |= 1L << (hc.Values[j] & 0x3F); + } + + if (hc.Values[numSymbols - 1] != JpegDataConstants.JpegHuffmanAlphabetSize) + { + throw new InvalidOperationException("Missing EOI symbol"); + } + + if (valueSlots[4] != 1) + { + return false; + } + + int numValues = 1; + + for (int j = 0; j < 4; j++) + { + numValues += BitOperations.PopCount((uint)valueSlots[i]); + } + + if (numValues != numSymbols) + { + throw new InvalidOperationException("Duplicate Huffman symbols"); + } + + if (!isAc) + { + bool onlyDC = ((valueSlots[0] >> JpegDataConstants.JpegDcAlphabetSize) | valueSlots[1] | valueSlots[2] | valueSlots[3]) == 0; + + if (!onlyDC) + { + throw new InvalidOperationException("Huffman symbols out of DC range"); + } + } + } + + foreach (JpegScanInfo scan in this.ScanInfos) + { + if (!visitor.U32( + JxlFieldExpressions.Value(1), + JxlFieldExpressions.Value(2), + JxlFieldExpressions.Value(3), + JxlFieldExpressions.Value(4), + 1, + ref Unsafe.As(ref scan.NumComponents))) + { + return false; + } + + if (scan.NumComponents >= 4) + { + throw new InvalidOperationException("Invalid number of components in SOS marker"); + } + + if (!visitor.Bits(6, 0, ref Unsafe.As(ref scan.Ss))) + { + return false; + } + + if (!visitor.Bits(6, 63, ref Unsafe.As(ref scan.Se))) + { + return false; + } + + if (!visitor.Bits(4, 0, ref Unsafe.As(ref scan.Al))) + { + return false; + } + + if (!visitor.Bits(4, 0, ref Unsafe.As(ref scan.Ah))) + { + return false; + } + + for (int i = 0; i < scan.NumComponents; i++) + { + if (!visitor.Bits(2, 0, ref Unsafe.As(ref scan.Components[i].ComponentIndex))) + { + return false; + } + + if (scan.Components[i].ComponentIndex >= components.Length) + { + throw new InvalidOperationException("Invalid component idx in SOS marker"); + } + + if (!visitor.Bits(2, 0, ref Unsafe.As(ref scan.Components[i].AcTableIndex))) + { + return false; + } + + if (!visitor.Bits(2, 0, ref Unsafe.As(ref scan.Components[i].DcTableIndex))) + { + return false; + } + } + + if (!visitor.U32( + JxlFieldExpressions.Value(0), + JxlFieldExpressions.Value(1), + JxlFieldExpressions.Value(2), + JxlFieldExpressions.BitsOffset(3, 3), + JxlShared.MaximumNumberOfPasses - 1, + ref Unsafe.As(ref scan.LastNeededPass))) + { + return false; + } + } + + if (info.HasDri) + { + if (!visitor.Bits(16, 0, ref Unsafe.As(ref this.restartInterval))) + { + return false; + } + } + + foreach (JpegScanInfo scan in this.ScanInfos) + { + int numResetPoints = scan.ResetPoints.Count; + + if (!visitor.U32( + JxlFieldExpressions.Value(0), + JxlFieldExpressions.BitsOffset(2, 1), + JxlFieldExpressions.BitsOffset(4, 4), + JxlFieldExpressions.BitsOffset(16, 20), + 0, + ref Unsafe.As(ref numResetPoints))) + { + return false; + } + + if (visitor.IsReading) + { + scan.ResetPoints = new List(numResetPoints); + } + + int lastBlockIdx = -1; + foreach (int blk in scan.ResetPoints) + { + int blockIdx = blk - (lastBlockIdx + 1); + + if (!visitor.U32( + JxlFieldExpressions.Value(0), + JxlFieldExpressions.BitsOffset(3, 1), + JxlFieldExpressions.BitsOffset(5, 9), + JxlFieldExpressions.BitsOffset(28, 41), + 0, + ref Unsafe.As(ref blockIdx))) + { + return false; + } + + blockIdx += lastBlockIdx + 1; + if (blockIdx >= (3u << 26)) + { + // At most 8K x 8K x num_channels blocks are possible in a JPEG. + // So valid block indices are below 3 * 2^26. + throw new InvalidOperationException("Invalid block ID: " + blockIdx); + } + + lastBlockIdx = blockIdx; + } + + int numExtraZeroRuns = scan.ExtraZeroRuns.Count; + + if (!visitor.U32( + JxlFieldExpressions.Value(0), + JxlFieldExpressions.BitsOffset(2, 1), + JxlFieldExpressions.BitsOffset(4, 4), + JxlFieldExpressions.BitsOffset(16, 20), + 0, + ref Unsafe.As(ref numExtraZeroRuns))) + { + return false; + } + + if (visitor.IsReading) + { + scan.ExtraZeroRuns = new List(numExtraZeroRuns); + } + + lastBlockIdx = -1; + + Span extraZeroes = CollectionsMarshal.AsSpan(scan.ExtraZeroRuns); + + for (int i = 0; i < extraZeroes.Length; i++) + { + ref JpegExtraZeroRunInfo extraZeroRun = ref extraZeroes[i]; + + ref int block_idx = ref extraZeroRun.BlockIndex; + ref int extra_zero_runs = ref extraZeroRun.NumExtraZeroRuns; + + if (!visitor.U32( + JxlFieldExpressions.Value(1), + JxlFieldExpressions.BitsOffset(2, 2), + JxlFieldExpressions.BitsOffset(4, 5), + JxlFieldExpressions.BitsOffset(8, 20), + 1, + ref Unsafe.As(ref extra_zero_runs))) + { + return false; + } + + block_idx -= lastBlockIdx + 1; + + if (!visitor.U32( + JxlFieldExpressions.Value(0), + JxlFieldExpressions.BitsOffset(3, 1), + JxlFieldExpressions.BitsOffset(5, 9), + JxlFieldExpressions.BitsOffset(28, 41), + 0, + ref Unsafe.As(ref block_idx))) + { + return false; + } + + block_idx += lastBlockIdx + 1; + + if (extra_zero_runs > 4) + { + throw new InvalidOperationException("Invalid number of extra zero runs: " + extra_zero_runs); + } + + if (block_idx > (3u << 26)) + { + throw new InvalidOperationException("Invalid block ID: " + block_idx); + } + + lastBlockIdx = block_idx; + } + } + + List interMarkerDataSizes = new(info.NumberOfIntermarkers); + + for (int i = 0; i < info.NumberOfIntermarkers; ++i) + { + int len = visitor.IsReading ? 0 : this.InterMarkerData[i].Count; + + if (!visitor.Bits(16, 0, ref Unsafe.As(ref len))) + { + return false; + } + + if (visitor.IsReading) + { + interMarkerDataSizes.Add(len); + } + } + + int tail_data_len = this.TailData.Count; + + if (visitor.IsReading && tail_data_len > 4260096) + { + throw new InvalidOperationException("Tail data too large (max size = 4260096, size = " + tail_data_len + ")"); + } + + if (!visitor.U32( + JxlFieldExpressions.Value(0), + JxlFieldExpressions.BitsOffset(8, 1), + JxlFieldExpressions.BitsOffset(16, 257), + JxlFieldExpressions.BitsOffset(22, 65793), + 0, + ref Unsafe.As(ref tail_data_len))) + { + return false; + } + + if (!visitor.Boolean(false, ref this.hasZeroPaddingBit)) + { + return false; + } + + if (this.hasZeroPaddingBit) + { + uint nbit = (uint)this.PaddingBits.Count; + + if (!visitor.Bits(24, 0, ref nbit)) + { + return false; + } + + if (visitor.IsReading) + { + this.PaddingBits = new List((int)Math.Min(1024u, nbit)); + + for (int i = 0; i < nbit; i++) + { + bool bbit = false; + + if (!visitor.Boolean(false, ref bbit)) + { + return false; + } + + this.PaddingBits.Add(bbit ? (byte)1 : (byte)0); + } + } + else + { + Span bits = CollectionsMarshal.AsSpan(this.PaddingBits); + + for (int i = 0; i < bits.Length; i++) + { + ref byte bit = ref bits[i]; + bool bbit = bit != 0; + + if (!visitor.Boolean(false, ref bbit)) + { + return false; + } + + bit = bbit ? (byte)1 : (byte)0; + } + } + } + + int dhtIndex = 0; // index of the Define Huffman Table + int scanIndex = 0; + bool isProgressive = false; + + InlineArray4 acOk = default; + InlineArray4 dcOk = default; + + // All values of acOk, dcOk by default are false. + acOk[0] = acOk[1] = acOk[2] = acOk[3] = false; + dcOk[0] = dcOk[1] = dcOk[2] = dcOk[3] = false; + + Span markerOrderSpan = CollectionsMarshal.AsSpan(this.MarkerOrder); + + for (int i = 0; i < markerOrderSpan.Length; i++) + { + byte marker = markerOrderSpan[i]; + + if (marker == 0xC2) + { + isProgressive = true; + } + else if (marker == 0xC4) + { + Span huffmanCode = CollectionsMarshal.AsSpan(this.HuffmanCodes); + + for (; dhtIndex < huffmanCode.Length;) + { + ref JpegHuffmanCode huff = ref huffmanCode[dhtIndex++]; + int index = huff.SlotId; + if ((index & 0x10) != 0) + { + index -= 0x10; + acOk[index] = true; + } + else + { + dcOk[index] = true; + } + + if (huff.IsLast) + { + break; + } + } + } + else if (marker == 0xDA) + { + Span scanInfo = CollectionsMarshal.AsSpan(this.ScanInfos); + JpegScanInfo si = scanInfo[scanIndex++]; + + for (int j = 0; j < si.NumComponents; ++j) + { + ref JpegComponentScanInfo csi = ref si.Components[j]; + + int dcTableIndex = csi.DcTableIndex; + int acTableIndex = csi.AcTableIndex; + + bool wantDc = !isProgressive || (si.Ss == 0); + + if (wantDc && !dcOk[dcTableIndex]) + { + throw new InvalidOperationException("DC Huffman table used before defined"); + } + + bool wantAc = !isProgressive || (si.Ss != 0) || (si.Se != 0); + + if (wantAc && !acOk[acTableIndex]) + { + throw new InvalidOperationException("AC Huffman table used before defined"); + } + } + } + } + + // Apply postponed actions + if (visitor.IsReading) + { + this.TailData = new List(tail_data_len); + + if (interMarkerDataSizes.Count != info.NumberOfIntermarkers) + { + return false; + } + + this.InterMarkerData = new List>(info.NumberOfIntermarkers); + + for (int i = 0; i < info.NumberOfIntermarkers; ++i) + { + this.InterMarkerData.Add(new List(interMarkerDataSizes[i])); + } + } + + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegExtraZeroRunInfo.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegExtraZeroRunInfo.cs new file mode 100644 index 0000000000..ed2d41eb03 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegExtraZeroRunInfo.cs @@ -0,0 +1,11 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; + +internal struct JpegExtraZeroRunInfo +{ + public int BlockIndex; + + public int NumExtraZeroRuns; +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegHuffmanCode.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegHuffmanCode.cs new file mode 100644 index 0000000000..324ff5f306 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegHuffmanCode.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; + +// We may need to get a ref to fields, so don't +// make these properties. +internal struct JpegHuffmanCode() +{ + /// + /// Bit length histogram + /// + public InlineArray17 Counts; + + /// + /// Symbol values stored by increasing bit lengths. + /// + public InlineArray17 Values; + + /// + /// The index of the code in the current set of Huffman codes. + /// For AC component Huffman codes, 0x10 is added to the index. + /// + public int SlotId; + + /// + /// True if the code is last within its marker segment. + /// + public bool IsLast = true; +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegInfo.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegInfo.cs new file mode 100644 index 0000000000..2454f6bbe9 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegInfo.cs @@ -0,0 +1,17 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; + +internal struct JpegInfo +{ + public int NumberOfAppMarkers { get; set; } + + public int NumberOfComMarkers { get; set; } + + public int NumberOfScans { get; set; } + + public int NumberOfIntermarkers { get; set; } + + public bool HasDri { get; set; } +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegQuantizationTable.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegQuantizationTable.cs index 7386bd5a3f..051835048d 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegQuantizationTable.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegQuantizationTable.cs @@ -6,6 +6,8 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; /// /// Representation of quantization values for an 8x8 pixel block. /// +// We may need to get a ref to fields, so don't +// make these properties. internal struct JpegQuantizationTable() { /// @@ -13,17 +15,17 @@ internal struct JpegQuantizationTable() /// public InlineArray64 Values; - public int Precision { get; set; } + public int Precision; /// /// Gets or sets the index of the quantization table /// as it was parsed from the input JPEG. /// - public int Index { get; set; } + public int Index; /// /// Gets or sets a value indicating whether this table /// is the last one within its marker segment. /// - public bool IsLast { get; set; } = true; + public bool IsLast = true; } diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegScanInfo.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegScanInfo.cs new file mode 100644 index 0000000000..617f0c7357 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegScanInfo.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; + +// We may need to get a ref to fields, so don't +// make these properties. +#pragma warning disable SA1401 // Fields should be private + +internal sealed class JpegScanInfo +{ + // Variables copied from ITU-T T.81 spec + + /// + /// Start of spectral band in zigzag sequence + /// + public int Ss; + + /// + /// End of spectral band in zigzag sequence + /// + public int Se; + + /// + /// Successive approximation bit position. (High) + /// + public int Ah; + + /// + /// Successive approximation bit position. (Low) + /// + public int Al; + + public int NumComponents; + + public InlineArray4 Components; + + public int LastNeededPass; + + public List ResetPoints { get; set; } = []; + + public List ExtraZeroRuns { get; set; } = []; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index 8947b0f142..5b58a19128 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -8,6 +8,7 @@ using SixLabors.ImageSharp.Formats.Jxl.IO; using SixLabors.ImageSharp.Formats.Jxl.IO.Container; using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; +using SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using SixLabors.ImageSharp.IO; @@ -2241,7 +2242,7 @@ public int ProcessBoxes(Stream stream) if (this.reconstructionOutputJpeg == JpegReconstructionStage.SetMetadata && this.JbrdNeedsMoreBoxes()) { - JxlJpegData jpegData = this.imageBundle!.JpegData.GetData(); + JpegData jpegData = this.imageBundle!.JpegData.GetData(); if (this.reconstructionExifSize > 0) { @@ -2580,7 +2581,7 @@ public int ProcessBoxes(Stream stream) if (reconstructionResult == JxlDecoderStatus.JpegReconstruction) { - JxlJpegData jpegData = this.jpegDecoder!.GetJpegData(); + JpegData jpegData = this.jpegDecoder!.GetJpegData(); long numExif = JxlToJpegDecoder.NumExifMarkers(jpegData); long numXmp = JxlToJpegDecoder.NumXmpMarkers(jpegData); diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlImageBundle.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlImageBundle.cs index 48f84175b3..184feb6e74 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlImageBundle.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlImageBundle.cs @@ -3,6 +3,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Cms; using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; +using SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; @@ -79,7 +80,7 @@ public JxlImageBundle() /// /// Gets or sets the JPEG data if the input image was converted to JPEG XL from a JPEG. /// - public JxlJpegData? JpegData { get; set; } + public JpegData? JpegData { get; set; } /// /// Gets a value indicating whether returns the image does or will represent quantized DCT-8 coefficients From bc7e30e32da7a27ac90d9e13f494e2caedb98a24 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 1 Sep 2026 22:27:46 +0400 Subject: [PATCH 117/328] Refactor & optimize --- src/ImageSharp/Common/Helpers/Numerics.cs | 9 ----- .../AuxiliaryOutput/JxlAuxiliaryOutput.cs | 12 +++++- .../Encoder/JxlFastLosslessEncoder.cs | 2 +- .../Formats/Jxl/Processing/JxlMath.cs | 40 ++++++------------- .../Modular/Transforms/JxlPalette.cs | 2 +- .../Modular/Transforms/JxlSqueeze.cs | 23 +++++++---- .../Processing/RenderPipeline/Epf0Stage.cs | 1 - .../RenderPipelineStageConfiguration.cs | 2 +- 8 files changed, 41 insertions(+), 50 deletions(-) diff --git a/src/ImageSharp/Common/Helpers/Numerics.cs b/src/ImageSharp/Common/Helpers/Numerics.cs index a5a5571795..e5a6b45493 100644 --- a/src/ImageSharp/Common/Helpers/Numerics.cs +++ b/src/ImageSharp/Common/Helpers/Numerics.cs @@ -1033,13 +1033,4 @@ public static nuint Vector512Count(this ReadOnlySpan span) public static nuint Vector512Count(int length) where TVector : struct => (uint)length / (uint)Vector512.Count; - - /// - /// Computes the average of two integers. - /// - /// First integer - /// Second integer - /// The average of x, y. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int Average(int x, int y) => (x + y + ((x > y) ? 1 : 0)) >> 1; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlAuxiliaryOutput.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlAuxiliaryOutput.cs index 6a34aabdc4..d39b707bc2 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlAuxiliaryOutput.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlAuxiliaryOutput.cs @@ -67,6 +67,16 @@ public void Assimilate(JxlAuxiliaryOutput victim) this.NumberOfBlocks += victim.NumberOfBlocks; this.NumberOfSmallBlocks += victim.NumberOfSmallBlocks; - + this.NumberOfDct4x8Blocks += victim.NumberOfDct4x8Blocks; + this.NumberOfAfvBlocks += victim.NumberOfAfvBlocks; + this.NumberOfDct8Blocks += victim.NumberOfDct8Blocks; + this.NumberOfDct8x16Blocks += victim.NumberOfDct8x16Blocks; + this.NumberOfDct8x32Blocks += victim.NumberOfDct8x32Blocks; + this.NumberOfDct16Blocks += victim.NumberOfDct16Blocks; + this.NumberOfDct16x32Blocks += victim.NumberOfDct16x32Blocks; + this.NumberOfDct32Blocks += victim.NumberOfDct32Blocks; + this.NumberOfDct32x64Blocks += victim.NumberOfDct32x64Blocks; + this.NumberOfDct64Blocks += victim.NumberOfDct64Blocks; + this.NumberOfButteraugliIterations += victim.NumberOfButteraugliIterations; } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs index 316f1ed12f..3feb60c72a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs @@ -163,7 +163,7 @@ internal interface IFjxlFrameInputSource : IDisposable /// A wrapper over the color data of the channel at the specified /// position. /// - public Span GetColorChannelData(int x, int y, int width, int height, out long rowOffset) + Span GetColorChannelData(int x, int y, int width, int height, out long rowOffset) where T : unmanaged; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs index 0157dba4e1..a0042c9c50 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs @@ -3,7 +3,6 @@ using System.Numerics; using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.Common.Helpers; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; @@ -687,25 +686,7 @@ public static ulong CeilLog2Nonzero(ulong x) /// X /// Y /// Hypotenuse of x and y - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static double Hypot(double x, double y) - { - x = Math.Abs(x); - y = Math.Abs(y); - - if (x < y) - { - RuntimeUtility.Swap(ref x, ref y); - } - - if (x == 0.0) - { - return 0.0; - } - - double ratio = y / x; - return x * Math.Sqrt(1 + (ratio * ratio)); - } + public static float Hypot(float x, float y) => Hypot(x, y); /// /// Computes the hypotenuse of x and y. @@ -713,23 +694,26 @@ public static double Hypot(double x, double y) /// X /// Y /// Hypotenuse of x and y + public static double Hypot(double x, double y) => Hypot(x, y); + [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float Hypot(float x, float y) + private static T Hypot(T x, T y) + where T : INumber, IRootFunctions { - x = MathF.Abs(x); - y = MathF.Abs(y); + x = T.Abs(x); + y = T.Abs(y); if (x < y) { - RuntimeUtility.Swap(ref x, ref y); + (y, x) = (x, y); } - if (x == 0.0f) + if (x == T.Zero) { - return 0.0f; + return T.Zero; } - float ratio = y / x; - return x * MathF.Sqrt(1 + (ratio * ratio)); + T ratio = y / x; + return x * T.Sqrt(T.One + (ratio * ratio)); } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs index bfcd683c5b..4a41bc34c7 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs @@ -492,7 +492,7 @@ private static float ColorDistance(Span a, InlineArray3 b) if (a.Length >= 3) { - ave3 = (a[0] + b[0] + a[1] + b[1] + a[2] + b[2]) * (1.21f / 3.0f); + ave3 = ((a[0] + b[0]) + (a[1] + b[1]) + (a[2] + b[2])) * (1.21f / 3.0f); } float sumA = 0; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs index d21d0438c7..6888015825 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs @@ -27,6 +27,15 @@ internal static class JxlSqueeze { private const int MaxFirstPreviewSize = 8; + /// + /// Computes the average of two integers. + /// + /// First integer + /// Second integer + /// The average of x, y. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Average(int x, int y) => (x + y + ((x > y) ? 1 : 0)) >> 1; + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int SmoothTendency(int b, int a, int n) { @@ -534,10 +543,8 @@ private static void CheckMetaSqueezeParameters(in JxlSqueezeParameters parameter int c1 = parameter.BeginC; int c2 = parameter.BeginC + parameter.NumC - 1; - if (c1 < 0 || - c1 >= numChannels || - c2 < 0 || - c2 >= numChannels || + if ((uint)c1 >= numChannels || + (uint)c2 >= numChannels || c2 < c1) { throw new InvalidOperationException("Invalid channel range"); @@ -654,7 +661,7 @@ public static void ForwardHorizontalSqueeze(Configuration configuration, JxlModu int a = pIn[x2]; int b = pIn[x2 + 1]; - int avg = Numerics.Average(a, b); + int avg = Average(a, b); pOut[x] = avg; int diff = a - b; int nextAvg = avg; @@ -664,7 +671,7 @@ public static void ForwardHorizontalSqueeze(Configuration configuration, JxlModu int c2 = pIn[x2 + 2]; // actually C, but 1. variable 'c' already defined 2. names should be camelCase int d = pIn[x2 + 3]; - nextAvg = Numerics.Average(c2, d); + nextAvg = Average(c2, d); } else if ((inputChannel.Width & 1) != 0) { @@ -711,7 +718,7 @@ public static void ForwardVerticalSqueeze(Configuration configuration, JxlModula { int a = pIn[x]; int b = pIn[x + oneRowInput]; - int avg = Numerics.Average(a, b); + int avg = Average(a, b); pOut[x] = avg; int diff = a - b; int nextAvg = avg; @@ -720,7 +727,7 @@ public static void ForwardVerticalSqueeze(Configuration configuration, JxlModula { int c2 = pIn[x + (2 * oneRowInput)]; // actually C, but 1. variable 'c' already defined 2. names should be camelCase int d = pIn[x + (3 * oneRowInput)]; - nextAvg = Numerics.Average(c2, d); + nextAvg = Average(c2, d); } else if ((inputChannel.Height & 1) != 0) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf0Stage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf0Stage.cs index 56f52fab8a..5df7525ea9 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf0Stage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf0Stage.cs @@ -103,7 +103,6 @@ public override void ProcessRow(Buffer2D> inputRows, Buffer2D vsm = Vector256.Create(sadMul[ix..]); Vector256 inverseSigma = Vector256.Create(rowSigma[bx]) * vsm; - } } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineStageConfiguration.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineStageConfiguration.cs index acd7b8b765..282876b6bc 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineStageConfiguration.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineStageConfiguration.cs @@ -5,7 +5,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; -internal record struct RenderPipelineStageConfiguration(int BorderX, int BorderY, int ShiftX, int ShiftY) +internal readonly record struct RenderPipelineStageConfiguration(int BorderX, int BorderY, int ShiftX, int ShiftY) { [MethodImpl(MethodImplOptions.AggressiveInlining)] public static RenderPipelineStageConfiguration CreateShiftX(int shift, int border) => new(border, 0, shift, 0); From 081384d8136588fe769e0b4b8bb0e6f3f7004411 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 1 Sep 2026 22:48:53 +0400 Subject: [PATCH 118/328] Optimize --- .../Modular/Transforms/JxlPalette.cs | 21 +++++++++++-------- .../Modular/Transforms/JxlTransform.cs | 19 +++++++---------- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs index 4a41bc34c7..63d0c0b081 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs @@ -658,6 +658,11 @@ private static void ForwardPaletteIteration( DebugGuard.MustBeGreaterThanOrEqualTo(beginC, input.MetaChannels, nameof(beginC)); int nb = endC - beginC + 1; // inclusive number of channels + // TODO: if this assert below triggers, increase nb to 6 and update + // the stack allocation for the 'tmp' variable below + // so the size is big enough. + DebugGuard.MustBeLessThanOrEqualTo(nb, 5, nameof(nb)); + JxlModularChannel beginCChannel = input.Channels[beginC]; int w = beginCChannel.Width; int h = beginCChannel.Height; @@ -1036,10 +1041,11 @@ private static void ForwardPaletteIteration( errorRow[2] = new(nb, w + 4); } - Span bestValue = stackalloc int[nb]; - Span idealResidual = stackalloc int[nb]; - Span quantizedValue = stackalloc int[nb]; - Span predictions = stackalloc int[nb]; + Span tmp = stackalloc int[32]; // Power of 2 + Span bestValue = tmp.Slice(0 * nb, nb); + Span idealResidual = tmp.Slice(1 * nb, nb); + Span quantizedValue = tmp.Slice(2 * nb, nb); + Span predictions = tmp.Slice(30 * nb, nb); // This is a temporary buffer, values are copied here. // It is so we can swap spans. Since spans are just a view @@ -1051,7 +1057,7 @@ private static void ForwardPaletteIteration( { for (int c = 0; c < nb; c++) { - p_in[c] = input.channel[begin_c + c].Row(y); + p_in[c] = input.channel[beginC + c].Row(y); if (lossy) p_quant[c] = quantized_input.channel[c].Row(y); } @@ -1076,10 +1082,7 @@ private static void ForwardPaletteIteration( bool best_is_delta = false; float best_distance = float.PositiveInfinity; - bestValue.Clear(); - idealResidual.Clear(); - quantizedValue.Clear(); - predictions.Clear(); + tmp.Clear(); foreach (double diffusion_multiplier in (Span)[0.55, 0.75]) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs index 75ba986086..cf3c2156f7 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics.Tensors; using SixLabors.ImageSharp.Formats.Jxl.Fields; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; @@ -46,19 +47,13 @@ public static void ComputeMinMax(JxlModularChannel channel, out int min, out int for (int y = 0; y < channel.Height; y++) { - Span p = channel.GetRow(y); - for (int x = 0; x < channel.Width; x++) - { - if (p[x] < min) - { - min = p[x]; - } + ReadOnlySpan p = channel.GetRow(y); - if (p[x] > max) - { - max = p[x]; - } - } + int minRow = TensorPrimitives.Min(p); + int maxRow = TensorPrimitives.Max(p); + + min = Math.Min(minRow, min); + max = Math.Max(maxRow, max); } } } From 80f107591e29d3eb63f5869c8415820178b6ca34 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 1 Sep 2026 22:50:50 +0400 Subject: [PATCH 119/328] Make interface an abstract class --- .../Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs index 3feb60c72a..91263989ef 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs @@ -147,8 +147,11 @@ internal sealed class JxlFastLosslessEncoder /// /// Abstracts access to a raster frame data required for encoding. /// - internal interface IFjxlFrameInputSource : IDisposable + internal abstract class IFjxlFrameInputSource : IDisposable { + /// + public abstract void Dispose(); + /// /// Returns a span that wraps over channel color data at the /// specified rectangular position. @@ -163,7 +166,7 @@ internal interface IFjxlFrameInputSource : IDisposable /// A wrapper over the color data of the channel at the specified /// position. /// - Span GetColorChannelData(int x, int y, int width, int height, out long rowOffset) + public abstract Span GetColorChannelData(int x, int y, int width, int height, out long rowOffset) where T : unmanaged; } From fad73d8cfdaa84dded8f2c0501642be08820c15d Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 2 Sep 2026 13:37:00 +0400 Subject: [PATCH 120/328] Add encoder ANS SIMD bit cost estimation --- .../Entropy/JxlAnsHybridUIntConfiguration.cs | 2 +- .../Jxl/Processing/Encoder/Ans/JxlAnsSimd.cs | 275 ++++++++++++++++++ .../Jxl/Processing/JxlCoefficientOrder.cs | 6 +- 3 files changed, 277 insertions(+), 6 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlAnsSimd.cs diff --git a/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHybridUIntConfiguration.cs b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHybridUIntConfiguration.cs index c1d6409076..9ff4306522 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHybridUIntConfiguration.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHybridUIntConfiguration.cs @@ -31,7 +31,7 @@ public JxlAnsHybridUIntConfiguration(uint splitExponent = 4, uint msbInToken = 2 public uint LsbMask => (1u << (int)this.LsbInToken) - 1; - public void Encode(uint value, ref uint token, ref uint bitCount, ref uint bits) + public void Encode(uint value, out uint token, out uint bitCount, out uint bits) { if (value < this.SplitToken) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlAnsSimd.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlAnsSimd.cs new file mode 100644 index 0000000000..e23bc27009 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlAnsSimd.cs @@ -0,0 +1,275 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; + +/// +/// SIMD utilities for ANS entropy encoder +/// +internal static class JxlAnsSimd +{ + private static readonly Vector IotaOffsets = CreateIotaOffsets(); + + private static Vector CreateIotaOffsets() + { + Span values = stackalloc uint[Vector.Count]; + + for (int i = 0; i < values.Length; i++) + { + values[i] = (uint)i; + } + + return new Vector(values); + } + + /// + /// Adds continuously incrementing numbers to the vector. + /// + /// The input vector. + /// vec + [ 1, 2, 3, 4, 5, ... ] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector Iota(uint vec) => Vector.Create(vec) + IotaOffsets; + + private static uint EstimateTokenCostImpl(uint e, uint m, uint l, ref uint values, int len, ref uint output) + { + Vector split = Vector.Create(1u << (int)e); + Vector expOffset = Vector.Create(127u); + Vector ebOffset = Vector.Create(127u + m + l); + Vector @base = Vector.Create((1u << (int)e) - (e << (int)(m + l))); + Vector mulN = Vector.Create(1u << (int)(m + l)); + Vector maskL = Vector.Create((1u << (int)l) - 1); + Vector maskM = Vector.Create(((1u << (int)m) - 1) << (int)l); + Vector largeThreshold = Vector.Create((1u << 2) - 1); + const uint largeShiftVal = 10; + Vector largeShift = Vector.Create(largeShiftVal); + + Vector extraBits = Vector.Zero; + int lastFull = Vector.Count * (len / Vector.Count); + + for (int i = 0; i < lastFull; i += Vector.Count) + { + Vector val = Vector.LoadUnsafe(ref Unsafe.Add(ref values, i)); + Vector isLarge = Vector.GreaterThan(val, largeThreshold); + Vector valShifted = Vector.ShiftRightLogical(val, (int)largeShiftVal); + Vector notLiteral = Vector.GreaterThanOrEqual(val, split); + Vector valFixed = Vector.ConditionalSelect(isLarge, valShifted, val); + Vector L = val & maskL; + Vector exp = Vector.ShiftRightLogical(valFixed, 23); + Vector expFixed = Vector.ConditionalSelect(isLarge, exp + largeShift, exp); + Vector n = expFixed - expOffset; + Vector eb = expFixed - ebOffset; + Vector M = Vector.ShiftRightLogical(valFixed, (int)(23 - m - l)); + Vector a = @base + (n * mulN); + Vector d = M & maskM; + Vector ebFixed = Vector.ConditionalSelect(notLiteral, eb, Vector.Zero); + Vector c = a | L; + extraBits += ebFixed; + Vector t = c | d; + Vector tFixed = Vector.ConditionalSelect(notLiteral, t, val); + tFixed.StoreUnsafe(ref Unsafe.Add(ref output, i)); + } + + if (lastFull < len) + { + Vector stop = Vector.Create((uint)len); + Vector fence = Iota((uint)lastFull); + Vector take = Vector.LessThan(fence, stop); + Vector val = Vector.LoadUnsafe(ref Unsafe.Add(ref values, lastFull)); + Vector isLarge = Vector.GreaterThan(val, largeThreshold); + Vector valShifted = Vector.ShiftRightLogical(val, (int)largeShiftVal); + Vector notLiteral = Vector.GreaterThanOrEqual(val, split); + Vector valFixed = Vector.ConditionalSelect(isLarge, valShifted, val); + Vector L = val | maskL; + Vector exp = Vector.ShiftRightLogical(valFixed, 23); + Vector exp_fixed = Vector.ConditionalSelect(isLarge, exp + largeShift, exp); + Vector n = exp_fixed - expOffset; + Vector eb = exp_fixed - ebOffset; + Vector M = Vector.ShiftRightLogical(valFixed, 23); + Vector a = @base + (n * mulN); + Vector d = M & maskM; + Vector ebFixed = Vector.ConditionalSelect(notLiteral, eb, Vector.Zero); + Vector ebMasked = Vector.ConditionalSelect(take, ebFixed, Vector.Zero); + Vector c = a | L; + extraBits += ebMasked; + Vector t = c | d; + Vector tFixed = Vector.ConditionalSelect(notLiteral, t, val); + tFixed.StoreUnsafe(ref Unsafe.Add(ref output, lastFull)); + } + + return Vector.Sum(extraBits); + } + + public static uint EstimateTokenCost(ref uint values, int len, JxlAnsHybridUIntConfiguration cfg, ref uint tokens) + { + if (!Vector.IsHardwareAccelerated) + { + // No SIMD support + uint extraBits = 0; + + for (int i = 0; i < len; i++) + { + uint v = Unsafe.Add(ref values, i); + cfg.Encode(v, out uint tok, out uint nbits, out _); // Last parameter is bits + extraBits += nbits; + Unsafe.Add(ref tokens, i) = tok; + } + + return extraBits; + } + else + { + // Have SIMD support + if (cfg.SplitExponent == 0) + { + return EstimateTokenCostImpl(0, 0, 0, ref values, len, ref tokens); + } + else if (cfg.SplitExponent == 2) + { + return EstimateTokenCostImpl(2, 0, 1, ref values, len, ref tokens); + } + else if (cfg.SplitExponent == 3) + { + if (cfg.MsbInToken == 1) + { + if (cfg.LsbInToken == 0) + { + return EstimateTokenCostImpl(3, 1, 0, ref values, len, ref tokens); + } + else + { + return EstimateTokenCostImpl(3, 1, 2, ref values, len, ref tokens); + } + } + else + { + if (cfg.LsbInToken == 0) + { + return EstimateTokenCostImpl(3, 2, 0, ref values, len, ref tokens); + } + else + { + return EstimateTokenCostImpl(3, 2, 1, ref values, len, ref tokens); + } + } + } + else if (cfg.SplitExponent == 4) + { + if (cfg.MsbInToken == 1) + { + if (cfg.LsbInToken == 0) + { + return EstimateTokenCostImpl(4, 1, 0, ref values, len, ref tokens); + } + else if (cfg.LsbInToken == 2) + { + return EstimateTokenCostImpl(4, 1, 2, ref values, len, ref tokens); + } + else + { + return EstimateTokenCostImpl(4, 1, 3, ref values, len, ref tokens); + } + } + else + { + if (cfg.LsbInToken == 0) + { + return EstimateTokenCostImpl(4, 2, 0, ref values, len, ref tokens); + } + else if (cfg.LsbInToken == 1) + { + return EstimateTokenCostImpl(4, 2, 1, ref values, len, ref tokens); + } + else + { + return EstimateTokenCostImpl(4, 2, 2, ref values, len, ref tokens); + } + } + } + else if (cfg.SplitExponent == 5) + { + if (cfg.MsbInToken == 1) + { + if (cfg.LsbInToken == 0) + { + return EstimateTokenCostImpl(5, 1, 0, ref values, len, ref tokens); + } + else if (cfg.LsbInToken == 2) + { + return EstimateTokenCostImpl(5, 1, 2, ref values, len, ref tokens); + } + else + { + return EstimateTokenCostImpl(5, 1, 4, ref values, len, ref tokens); + } + } + else + { + if (cfg.LsbInToken == 0) + { + return EstimateTokenCostImpl(5, 2, 0, ref values, len, ref tokens); + } + else if (cfg.LsbInToken == 1) + { + return EstimateTokenCostImpl(5, 2, 1, ref values, len, ref tokens); + } + else if (cfg.LsbInToken == 2) + { + return EstimateTokenCostImpl(5, 2, 2, ref values, len, ref tokens); + } + else + { + return EstimateTokenCostImpl(5, 2, 3, ref values, len, ref tokens); + } + } + } + else if (cfg.SplitExponent == 6) + { + if (cfg.MsbInToken == 0) + { + return EstimateTokenCostImpl(6, 0, 0, ref values, len, ref tokens); + } + else if (cfg.MsbInToken == 1) + { + return EstimateTokenCostImpl(6, 1, 5, ref values, len, ref tokens); + } + else + { + return EstimateTokenCostImpl(6, 2, 4, ref values, len, ref tokens); + } + } + else if (cfg.SplitExponent is >= 7 and <= 12) + { + if (cfg.SplitExponent == 7) + { + return EstimateTokenCostImpl(7, 0, 0, ref values, len, ref tokens); + } + else if (cfg.SplitExponent == 8) + { + return EstimateTokenCostImpl(8, 0, 0, ref values, len, ref tokens); + } + else if (cfg.SplitExponent == 9) + { + return EstimateTokenCostImpl(9, 0, 0, ref values, len, ref tokens); + } + else if (cfg.SplitExponent == 10) + { + return EstimateTokenCostImpl(10, 0, 0, ref values, len, ref tokens); + } + else if (cfg.SplitExponent == 11) + { + return EstimateTokenCostImpl(11, 0, 0, ref values, len, ref tokens); + } + else + { + return EstimateTokenCostImpl(12, 0, 0, ref values, len, ref tokens); + } + } + + return ~0u; + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs index deb5045f2a..4cace39d73 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs @@ -45,11 +45,7 @@ internal static class JxlCoefficientOrder [MethodImpl(MethodImplOptions.AggressiveInlining)] public static uint CoeffOrderContext(uint value) { - uint token = 0; - uint nbits = 0; - uint bits = 0; - - new JxlAnsHybridUIntConfiguration(0, 0, 0).Encode(value, ref token, ref nbits, ref bits); + new JxlAnsHybridUIntConfiguration(0, 0, 0).Encode(value, out uint token, out uint nbits, out uint bits); return Math.Min(token, PermutationContexts - 1u); } From 64bade82be47caca243d0a794734b2562eb788a1 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 2 Sep 2026 17:18:09 +0400 Subject: [PATCH 121/328] Add huffman encoder, inverse Gaborish transform, gamma correction, noise encoder tools, and complete render pipeline EPF 0 stage Files implemented: - enc_gaborish.cc - enc_gaborish.h - enc_gamma_correct.h - enc_huffman.cc - enc_huffman.h - enc_huffman_tree.cc - enc_huffman_tree.h - enc_noise.cc - enc_noise.h - render_pipeline/stage_epf.cc - render_pipeline/stage_epf.h --- .../Formats/Jxl/Memory/JxlImage3{T}.cs | 6 + .../Encoder/Huffman/JxlHuffmanEncoder.cs | 226 ++++++++++ .../Encoder/Huffman/JxlHuffmanTree.cs | 411 ++++++++++++++++++ .../Jxl/Processing/Encoder/JxlGaborish.cs | 69 +++ .../Jxl/Processing/Encoder/JxlGammaCorrect.cs | 51 +++ .../Encoder/Noise/JxlLossFunction.cs | 63 +++ .../Encoder/Noise/JxlNoiseEncoder.cs | 66 +++ .../Encoder/Noise/JxlNoiseHistogram.cs | 87 ++++ .../Formats/Jxl/Processing/JxlConvolve.cs | 19 +- .../Primitives/JxlWeightsSymmetric5.cs | 71 +-- .../Processing/Primitives/RectangleUtils.cs | 25 ++ .../Processing/RenderPipeline/Epf0Stage.cs | 63 ++- 12 files changed, 1116 insertions(+), 41 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Huffman/JxlHuffmanEncoder.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Huffman/JxlHuffmanTree.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGaborish.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGammaCorrect.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlLossFunction.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseEncoder.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseHistogram.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Primitives/RectangleUtils.cs diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs index 91aed2e97a..4442eb6495 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs @@ -94,6 +94,12 @@ private void PlaneRowBoundsCheck(int c, int y) DebugGuard.MustBeLessThan(y, this.YSize, nameof(y)); } + /// + /// Returns the rectangle for this image bounds. + /// + /// A rectangle with x,y=0,0 width,height=XSize,YSize. + public Rectangle GetRectangle() => new(0, 0, this.XSize, this.YSize); + public void Dispose() { foreach (JxlPlane plane in this.planes) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Huffman/JxlHuffmanEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Huffman/JxlHuffmanEncoder.cs new file mode 100644 index 0000000000..7caf854655 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Huffman/JxlHuffmanEncoder.cs @@ -0,0 +1,226 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Huffman; + +/// +/// Derives & writes Huffman codes. +/// +internal static class JxlHuffmanEncoder +{ + private const int CodeLengthCodes = 18; + + private static ReadOnlySpan StorageOrder => [1, 2, 3, 4, 0, 5, 17, 6, 16, 7, 8, 9, 10, 11, 12, 13, 14, 15]; + + private static ReadOnlySpan HuffmanBitLengthHuffmanCodeSymbols => [0, 7, 3, 2, 1, 15]; + + private static ReadOnlySpan HuffmanBitLengthHuffmanCodeBitLengths => [2, 4, 3, 2, 2, 4]; + + public static void StoreHuffmanTreeOfHuffmanTreeToBitMask(int numCodes, Span codeLengthBitDepth, JxlBitWriter writer) + { + int codesToStore = CodeLengthCodes; + if (numCodes > 1) + { + for (; codesToStore > 0; codesToStore--) + { + if (codeLengthBitDepth[StorageOrder[codesToStore - 1]] != 0) + { + break; + } + } + } + + int skipSome = 0; + if (codeLengthBitDepth[StorageOrder[0]] == 0 && codeLengthBitDepth[StorageOrder[1]] == 0) + { + skipSome = 2; // skips two + if (codeLengthBitDepth[StorageOrder[2]] == 0) + { + skipSome = 3; // skips three + } + } + + writer.Write(2, skipSome); + + for (int i = skipSome; i < codesToStore; ++i) + { + int l = codeLengthBitDepth[StorageOrder[i]]; + writer.Write(HuffmanBitLengthHuffmanCodeBitLengths[l], HuffmanBitLengthHuffmanCodeSymbols[l]); + } + } + + public static void StoreHuffmanTreeToBitMask(int huffmanTreeSize, Span huffmanTree, Span huffmanTreeExtraBits, Span codeLengthBitDepth, Span codeLengthBitDepthSymbols, JxlBitWriter writer) + { + for (int i = 0; i < huffmanTreeSize; ++i) + { + int ix = huffmanTree[i]; + writer.Write(codeLengthBitDepth[ix], codeLengthBitDepthSymbols[ix]); + DebugGuard.MustBeLessThan(ix, 17, nameof(ix)); + + // Extra bits + // + // Micro optimization: + // Original: + // switch (ix) + // { + // case 16: + // writer->Write(2, huffman_tree_extra_bits[i]); + // break; + // case 17: + // writer->Write(3, huffman_tree_extra_bits[i]); + // break; + // default: + // // no-op + // break; + // } + if ((ix & 16) != 0) + { + writer.Write(2 + (ix & 1), huffmanTreeExtraBits[i]); + } + } + } + + public static void StoreSimpleHuffmanTree(Span depths, InlineArray4 symbols, int numSymbols, int maxBits, JxlBitWriter writer) + { + writer.Write(2, 1); + writer.Write(2, numSymbols - 1); + + for (int i = 0; i < numSymbols; i++) + { + for (int j = i + 1; j < numSymbols; j++) + { + if (depths[symbols[j]] < depths[symbols[i]]) + { + RuntimeUtility.Swap(ref symbols[j], ref symbols[i]); + } + } + } + + if (numSymbols == 2) + { + writer.Write(maxBits, symbols[0]); + writer.Write(maxBits, symbols[1]); + } + else if (numSymbols == 3) + { + writer.Write(maxBits, symbols[0]); + writer.Write(maxBits, symbols[1]); + writer.Write(maxBits, symbols[2]); + } + else + { + writer.Write(maxBits, symbols[0]); + writer.Write(maxBits, symbols[1]); + writer.Write(maxBits, symbols[2]); + writer.Write(maxBits, symbols[3]); + writer.Write(1, depths[symbols[0]] == 1 ? 1 : 0); + } + } + + public static void StoreHuffmanTree(Span depths, int num, JxlBitWriter writer) + { + Span arena = stackalloc byte[2 * num]; + Span huffmanTree = arena; + Span huffmanTreeExtraBits = arena[num..]; + int huffmanTreeSize = 0; + JxlHuffmanTree.WriteHuffmanTree(depths, num, ref huffmanTreeSize, huffmanTree, huffmanTreeExtraBits); + + Span huffmanTreeHistogram = stackalloc int[CodeLengthCodes]; + huffmanTreeHistogram.Clear(); + + for (int i = 0; i < huffmanTreeSize; ++i) + { + huffmanTreeHistogram[huffmanTree[i]]++; + } + + int numCodes = 0; + int code = 0; + + for (int i = 0; i < CodeLengthCodes; ++i) + { + if (huffmanTreeHistogram[i] != 0) + { + if (numCodes == 0) + { + code = i; + numCodes = 1; + } + else if (numCodes == 1) + { + numCodes = 2; + break; + } + } + } + + Span codeLengthBitDepth = stackalloc byte[CodeLengthCodes]; + Span codeLengthBitDepthSymbols = stackalloc short[CodeLengthCodes]; + codeLengthBitDepth.Clear(); + codeLengthBitDepthSymbols.Clear(); + + JxlHuffmanTree.CreateHuffmanTree(huffmanTreeHistogram, CodeLengthCodes, 5, codeLengthBitDepth); + JxlHuffmanTree.ConvertBitDepthsToSymbols(codeLengthBitDepth, CodeLengthCodes, codeLengthBitDepthSymbols); + + StoreHuffmanTreeOfHuffmanTreeToBitMask(numCodes, codeLengthBitDepth, writer); + + if (numCodes == 1) + { + codeLengthBitDepth[code] = 0; + } + + StoreHuffmanTreeToBitMask(huffmanTreeSize, huffmanTree, huffmanTreeExtraBits, codeLengthBitDepth, codeLengthBitDepthSymbols, writer); + } + + public static void BuildAndStoreHuffmanTree(Span histogram, int length, Span depth, Span bits, JxlBitWriter writer) + { + int count = 0; + InlineArray4 s4 = default; + + for (int i = 0; i < length; i++) + { + if (histogram[i] != 0) + { + if (count < 4) + { + s4[count] = i; + } + else if (count > 4) + { + break; + } + + count++; + } + } + + int maxBitsCounter = length - 1; + int maxBits = 0; + + while (maxBitsCounter != 0) + { + maxBitsCounter >>= 1; + ++maxBits; + } + + if (count <= 1) + { + writer.Write(4, 1); + writer.Write(maxBits, s4[0]); + return; + } + + JxlHuffmanTree.CreateHuffmanTree(histogram, length, 15, depth); + JxlHuffmanTree.ConvertBitDepthsToSymbols(depth, length, bits); + + if (count <= 4) + { + StoreSimpleHuffmanTree(depth, s4, count, maxBits, writer); + } + else + { + StoreHuffmanTree(depth, length, writer); + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Huffman/JxlHuffmanTree.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Huffman/JxlHuffmanTree.cs new file mode 100644 index 0000000000..0e3faa65aa --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Huffman/JxlHuffmanTree.cs @@ -0,0 +1,411 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Numerics.Tensors; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Huffman; + +/// +/// Node of a Huffman tree. +/// +internal struct JxlHuffmanTree(int count, short left, short right) +{ + public int TotalCount = count; + + /// + /// Index of the left node of the tree. + /// + public short IndexLeft = left; + + /// + /// Index of the right node of the tree. If it's missing + /// then this is the value of the node. + /// + public short IndexRightOrValue = right; + + /// + /// Gets a lookup table with pre-reversed 4-bit values. + /// This lookup is used by . + /// + private static ReadOnlySpan ReverseLookup => + [ + 0x0, 0x8, 0x4, 0xc, 0x2, 0xa, 0x6, 0xe, + 0x1, 0x9, 0x5, 0xd, 0x3, 0xb, 0x7, 0xf + ]; + + public static void SetDepth(ref JxlHuffmanTree p, Span pool, Span depth, byte level) + { + if (p.IndexLeft >= 0) + { + level++; + SetDepth(ref pool[p.IndexLeft], pool, depth, level); + SetDepth(ref pool[p.IndexRightOrValue], pool, depth, level); + } + else + { + depth[p.IndexRightOrValue] = level; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Compare(JxlHuffmanTree v0, JxlHuffmanTree v1) + { + if (v0.TotalCount != v1.TotalCount) + { + return v0.TotalCount.CompareTo(v1.TotalCount); + } + + return v0.IndexRightOrValue.CompareTo(v1.IndexRightOrValue); + } + + public static void CreateHuffmanTree(Span data, int length, int treeLimit, Span depth) + { + JxlHuffmanTree[]? pool = null; + + // This is basically the equivalent of List but is fixed-size. + // We don't need an entire collection on the heap. + int desiredTreeItems = (2 * length) + 1; + Span tree = + desiredTreeItems <= 256 + ? stackalloc JxlHuffmanTree[256].Slice(0, desiredTreeItems) + : pool = ArrayPool.Shared.Rent(desiredTreeItems); + + // Number of items in our "fixed List". So in other to + // add to the tree we do 'tree[treeRef++] = ...'. + int treeRef = 0; + + for (int countLimit = 1; ; countLimit *= 2) + { + tree.Clear(); // Always clear on every iteration + + int i = length; + for (; i != 0;) + { + --i; + if (data[i] != 0) + { + int count = Math.Max(data[i], countLimit - 1); + tree[treeRef++] = new JxlHuffmanTree(count, -1, (short)i); + } + } + + if (treeRef == 1) + { + // Fake value; will be fixed on upper level. + depth[tree[0].IndexRightOrValue] = 1; + break; + } + + tree.Sort(Compare); + + JxlHuffmanTree sentinel = new(int.MaxValue, -1, -1); + tree[treeRef++] = sentinel; + tree[treeRef++] = sentinel; // We do this twice, yes + + i = 0; + int j = treeRef + 1; + + for (int k = treeRef - 1; k != 0; --k) + { + int left; + int right; + + if (tree[i].TotalCount <= tree[j].TotalCount) + { + left = i; + i++; + } + else + { + left = j; + j++; + } + + if (tree[i].TotalCount <= tree[j].TotalCount) + { + right = i; + i++; + } + else + { + right = j; + j++; + } + + int j_end = treeRef - 1; + ref JxlHuffmanTree currTree = ref tree[j_end]; + currTree.TotalCount = tree[left].TotalCount + tree[right].TotalCount; + currTree.IndexLeft = (short)left; + currTree.IndexRightOrValue = (short)right; + + tree[treeRef++] = sentinel; + } + + SetDepth(ref tree[(2 * treeRef) - 1], tree, depth, 0); + + if (TensorPrimitives.Max((ReadOnlySpan)depth[..length]) <= treeLimit) + { + break; + } + } + + // Don't forget to return the pooled array + if (pool is not null) + { + ArrayPool.Shared.Return(pool); + } + } + + public static void Reverse(Span v, int start, int end) + { + end--; + while (start < end) + { + RuntimeUtility.Swap(ref v[end], ref v[start]); + start++; + end++; + } + } + + public static void WriteHuffmanTreeRepetitions(byte previousValue, byte value, int repetitions, ref int treeSize, Span tree, Span extraBitsData) + { + DebugGuard.MustBeGreaterThan(repetitions, 0, nameof(repetitions)); + + if (previousValue != value) + { + tree[treeSize] = value; + extraBitsData[treeSize] = 0; + treeSize++; + repetitions--; + } + + if (repetitions == 7) + { + tree[treeSize] = value; + extraBitsData[treeSize] = 0; + treeSize++; + } + + if (repetitions < 3) + { + for (int i = 0; i < repetitions; ++i) + { + tree[treeSize] = value; + extraBitsData[treeSize] = 0; + treeSize++; + } + } + else + { + repetitions -= 3; + int start = treeSize; + while (true) + { + tree[treeSize] = 16; + extraBitsData[treeSize] = (byte)(repetitions & 0x3); + treeSize++; + repetitions >>= 2; + + if (repetitions == 0) + { + break; + } + + repetitions--; + } + + Reverse(tree, start, treeSize); + Reverse(extraBitsData, start, treeSize); + } + } + + public static void WriteHuffmanTreeRepetitionsZeros(int repetitions, ref int treeSize, Span tree, Span extraBitsData) + { + if (repetitions == 11) + { + tree[treeSize] = 0; + extraBitsData[treeSize] = 0; + treeSize++; + repetitions--; + } + + if (repetitions < 3) + { + for (int i = 0; i < repetitions; ++i) + { + tree[treeSize] = 0; + extraBitsData[treeSize] = 0; + treeSize++; + } + } + else + { + repetitions -= 3; + int start = treeSize; + + while (true) + { + tree[treeSize] = 17; + extraBitsData[treeSize] = (byte)(repetitions & 0x7); + treeSize++; + repetitions >>= 3; + + if (repetitions == 0) + { + break; + } + + repetitions--; + } + + Reverse(tree, start, treeSize); + Reverse(extraBitsData, start, treeSize); + } + } + + // Decides whether or not to use Run Length Encoding (RLE). + // Basically that's where, for example, when we have a + // string of repetitive letters "aaaaaa", instead of encoding + // them all separately, it encodes "a times 6". + public static void DecideOverRleUse(Span depth, int length, ref bool useRleForNonZero, ref bool useRleForZero) + { + int totalRepsZero = 0; + int totalRepsNonZero = 0; + int countRepsZero = 1; + int countRepsNonZero = 1; + + for (int i = 0; i < length;) + { + byte value = depth[i]; + int reps = 1; + + for (int k = i + 1; k < length && depth[k] == value; k++) + { + reps++; + } + + if (reps >= 3 && value == 0) + { + totalRepsZero += reps; + countRepsZero++; + } + + if (reps >= 4 && value != 0) + { + totalRepsNonZero += reps; + countRepsNonZero++; + } + + i += reps; + } + + useRleForNonZero = totalRepsNonZero > countRepsNonZero * 2; + useRleForZero = totalRepsZero > countRepsZero * 2; + } + + public static void WriteHuffmanTree(Span depth, int length, ref int treeSize, Span tree, Span extraBitsData) + { + byte previousValue = 8; + int newLength = length; + + for (int i = 0; i < length; i++) + { + if (depth[length - i - 1] == 0) + { + newLength--; + } + else + { + break; + } + } + + bool useRleForNonZeroes = false; + bool useRleForZero = false; + + if (length > 50) + { + DecideOverRleUse(depth, newLength, ref useRleForNonZeroes, ref useRleForZero); + } + + for (int i = 0; i < newLength;) + { + byte value = depth[i]; + int reps = 1; + + if ((value != 0 && useRleForNonZeroes) || (value == 0 && useRleForZero)) + { + for (int k = i + 1; k < newLength && depth[k] == value; k++) + { + reps++; + } + } + + if (value == 0) + { + WriteHuffmanTreeRepetitionsZeros(reps, ref treeSize, tree, extraBitsData); + } + else + { + WriteHuffmanTreeRepetitions(previousValue, value, reps, ref treeSize, tree, extraBitsData); + previousValue = value; + } + + i += reps; + } + } + + public static short ReverseBits(int numBits, short bits) + { + int result = ReverseLookup[bits & 0xf]; + + for (int i = 4; i < numBits; i += 4) + { + result <<= 4; + bits = (short)(bits >> 4); + result |= ReverseLookup[bits & 0xf]; + } + + result >>= -numBits & 0x3; + + return (short)result; + } + + public static void ConvertBitDepthsToSymbols(Span depth, int len, Span bits) + { + // In Brotli, all bit depths are [1..15] + // 0 bit depth means that the symbol does not exist. + const int maxBits = 16; // 0..15 are values for bits + + Span blCount = stackalloc short[maxBits]; + blCount.Clear(); // explicitly cleared from reference + + for (int i = 0; i < len; i++) + { + blCount[depth[i]]++; + } + + blCount[0] = 0; + + Span nextCode = stackalloc short[maxBits]; // not cleared in reference + nextCode[0] = 0; + + int code = 0; + for (int i = 1; i < maxBits; ++i) + { + code = (code + blCount[i - 1]) << 1; + nextCode[i] = (short)code; + } + + for (int i = 0; i < len; ++i) + { + if (depth[i] != 0) + { + bits[i] = ReverseBits(depth[i], nextCode[depth[i]]++); + } + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGaborish.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGaborish.cs new file mode 100644 index 0000000000..191e2c5a4a --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGaborish.cs @@ -0,0 +1,69 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; + +/// +/// Gaborish transform +/// +internal static class JxlGaborish +{ + private static ReadOnlySpan GaborishLookup => [ + -0.09495815671340026f, -0.041031725066768575f, 0.013710004822696948f, + 0.006510206083837737f, -0.0014789063378272242f]; + + public static void InverseGaborish(Configuration configuration, JxlImage3F inOut, Rectangle rect, InlineArray3 mul) + { + InlineArray3 weights = default; + + for (int i = 0; i < 3; ++i) + { + double sum = 1.0 + (mul[i] * 4 * ((GaborishLookup[0] + GaborishLookup[1]) + (GaborishLookup[2] + GaborishLookup[4]) + (2 * GaborishLookup[3]))); + sum = Math.Max(sum, 1e-5); // if (sum < 1e-5) sum = 1e-5 + + float normalize = (float)(1.0f / sum); + float normalizeMul = mul[i] * normalize; + + weights[i] = new JxlWeightsSymmetric5() + { + C = JxlWeightsSymmetric5.CreateVector4(normalize), + R = JxlWeightsSymmetric5.CreateVector4(normalizeMul * GaborishLookup[0]), + R2 = JxlWeightsSymmetric5.CreateVector4(normalizeMul * GaborishLookup[2]), + D = JxlWeightsSymmetric5.CreateVector4(normalizeMul * GaborishLookup[1]), + D2 = JxlWeightsSymmetric5.CreateVector4(normalizeMul * GaborishLookup[4]), + L = JxlWeightsSymmetric5.CreateVector4(normalizeMul * GaborishLookup[3]) + }; + } + + using JxlImageF temp = new(configuration, inOut.Plane(2).XSize, inOut.Plane(2).YSize); + + if (!JxlImageOperations.CopyImage(inOut.Plane(2), temp)) + { + throw new InvalidOperationException("Image copying failed"); + } + + Rectangle xRect = RectangleUtils.Extend(rect, 3, inOut.GetRectangle()); + + if (!JxlConvolve.Symmetric5(inOut.Plane(0), xRect, ref weights[0], inOut.Plane(2), xRect)) + { + throw new InvalidOperationException("Symmetric5 convolution failed"); + } + + if (!JxlConvolve.Symmetric5(inOut.Plane(1), xRect, ref weights[1], inOut.Plane(0), xRect)) + { + throw new InvalidOperationException("Symmetric5 convolution failed"); + } + + if (!JxlConvolve.Symmetric5(temp, xRect, ref weights[2], inOut.Plane(1), xRect)) + { + throw new InvalidOperationException("Symmetric5 convolution failed"); + } + + inOut.Plane(0).Swap(inOut.Plane(1)); + inOut.Plane(0).Swap(inOut.Plane(2)); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGammaCorrect.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGammaCorrect.cs new file mode 100644 index 0000000000..1b60ed38c8 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGammaCorrect.cs @@ -0,0 +1,51 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; + +internal static class JxlGammaCorrect +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double SRgb8ToLinearDirect(double srgb) + { + if (srgb <= 0.0) + { + return 0.0; + } + + if (srgb <= 0.04045) + { + return srgb / 12.92; + } + + if (srgb >= 1.0) + { + return 1.0; + } + + return Math.Pow((srgb + 0.055) / 1.055, 2.4); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double LinearToSRgb8Direct(double linear) + { + if (linear <= 0.0) + { + return 0.0; + } + + if (linear >= 1.0) + { + return 1.0; + } + + if (linear <= 0.0031308) + { + return linear * 12.92; + } + + return (Math.Pow(linear, 1.0 / 2.4) * 1.055) - 0.055; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlLossFunction.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlLossFunction.cs new file mode 100644 index 0000000000..e338862bb0 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlLossFunction.cs @@ -0,0 +1,63 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Processing.Noise; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Noise; + +internal sealed class JxlLossFunction(ReadOnlyMemory noiseLevels) +{ + public double Compute(Span w, Span df, bool skipRegularization = false) + { + const double reg = 0.005; + const double asym = 1.1; + + double lossFunction = 0; + + w.Clear(); + + ReadOnlySpan levels = noiseLevels.Span; + + for (int i = 0; i < levels.Length; i++) + { + JxlNoiseLevel nl = levels[i]; + + JxlNoiseIndexAndFraction pos = JxlNoiseHelper.IndexAndFraction(nl.Intensity); + + double low = w[pos.Index]; + double hi = w[pos.Index + 1]; + double val = (low * (1.0f - pos.Fraction)) + (hi * pos.Fraction); + double dist = val - nl.NoiseLevel; + + if (dist > 0) + { + lossFunction += asym * dist * dist; + df[pos.Index] -= asym * (1.0f - pos.Fraction) * dist; + df[pos.Index + 1] -= asym * pos.Fraction * dist; + } + else + { + lossFunction += dist * dist; + df[pos.Index] -= (1.0f - pos.Fraction) * dist; + df[pos.Index + 1] -= pos.Fraction * dist; + } + } + + if (skipRegularization) + { + return lossFunction; + } + + int levelsSize = levels.Length; + + for (int i = 0; i + 1 < w.Length; i++) + { + double diff = w[i] - w[i + 1]; + lossFunction += reg * levelsSize * diff * diff; + df[i] -= reg * diff * levelsSize; + df[i + 1] += reg * diff * levelsSize; + } + + return lossFunction; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseEncoder.cs new file mode 100644 index 0000000000..026ee73f2f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseEncoder.cs @@ -0,0 +1,66 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Numerics.Tensors; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Noise; + +/// +/// Noise functions for encoder. +/// +internal static class JxlNoiseEncoder +{ + public static float GetScoreSumsOfAbsoluteDifferences(JxlImage3F opsin, int x, int y, int blockSize) + { + const int smallBlockSizeX = 3; + const int smallBlockSizeY = 4; + + int numSAD = (blockSize - smallBlockSizeX) * (blockSize - smallBlockSizeY); + int counter = 0; + const int offset = 2; + + float[]? pooled = null; + + Span sad = numSAD <= 128 + ? stackalloc float[128].Slice(0, numSAD) + : pooled = ArrayPool.Shared.Rent(numSAD); + + for (int yBl = 0; yBl + smallBlockSizeY < blockSize; ++yBl) + { + for (int xBl = 0; xBl + smallBlockSizeX < blockSize; ++xBl) + { + float sadSum = 0; + + for (int cy = 0; cy < smallBlockSizeY; ++cy) + { + for (int cx = 0; cx < smallBlockSizeX; ++cx) + { + float wnd = 0.5f * (opsin.PlaneRow(1, y + yBl + cy)[x + xBl + cx] + opsin.PlaneRow(0, y + yBl + cy)[x + xBl + cx]); + float center = 0.5f * (opsin.PlaneRow(1, y + offset + cy)[x + offset + cx] + opsin.PlaneRow(0, y + offset + cy)[x + offset + cx]); + sadSum += MathF.Abs(center - wnd); + } + } + + sad[counter++] = sadSum; + } + } + + int samples = numSAD / 2; + + // As with ROAD (rank order absolute distance), we keep the smallest half of + // the values in SAD (we use here the more robust patch SAD instead of + // absolute single-pixel differences). + sad.Sort(); + + float totalSadSum = TensorPrimitives.Sum(sad); + + if (pooled is not null) + { + ArrayPool.Shared.Return(pooled); + } + + return totalSadSum / samples; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseHistogram.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseHistogram.cs new file mode 100644 index 0000000000..5cacaa5d02 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseHistogram.cs @@ -0,0 +1,87 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Numerics.Tensors; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Noise; + +internal sealed class JxlNoiseHistogram +{ + private const int Bins = 256; + + private readonly uint[] bins = new uint[Bins]; + + public int Mode + { + get + { + int maxIdx = 0; + + for (int i = 0; i < Bins; i++) + { + if (this.bins[i] > this.bins[maxIdx]) + { + maxIdx = i; + } + } + + return maxIdx; + } + } + + /// + /// Gets the Inter-quartile range. + /// + public double Iqr => this.Quantile(0.75) - this.Quantile(0.25); + + public void Increment(float x) => this.bins[Index(x)]++; + + public uint Get(float x) => this.bins[Index(x)]; + + public uint Bin(int bin) => this.bins[bin]; + + public double Quantile(double q01) + { + long total = 1 + TensorPrimitives.Sum((ReadOnlySpan)this.bins.AsSpan()); + long target = (long)q01 * total; + long sum = 0; + int i = 0; + + for (; i < Bins; i++) + { + sum += this.bins[i]; + + if (sum == target) + { + return i + 0.5; + } + + if (sum > target) + { + break; + } + } + + int next = i + 1; + + while (next < Bins && this.bins[next] == 0) + { + next++; + } + + double excess = target - sum; + double weightNext = this.bins[Index(next)] / excess; + + return ClampX((next * weightNext) + (i * (1.0 - weightNext))); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static T ClampX(T x) + where T : unmanaged, INumber + => T.Clamp(x, T.Zero, T.CreateSaturating(Bins - 1)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Index(float x) => ClampX((int)x); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs index ebe7f5195d..2baad82d6d 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs @@ -5,6 +5,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; @@ -70,7 +71,7 @@ public static Vector WeightedSum( return sum2 + (sum1 + sum0); } - public static float Symmetric5Border(JxlImageF input, Func wrapY, long ix, long iy, JxlWeightsSymmetric5 weights) + public static float Symmetric5Border(JxlImageF input, Func wrapY, long ix, long iy, ref JxlWeightsSymmetric5 weights) { float w0 = weights.GetCVector()[0]; float w1 = weights.GetRVector()[0]; @@ -99,7 +100,7 @@ public static void Symmetric5Interior( Func wrapY, int rix, long iy, - JxlWeightsSymmetric5 weights, + ref JxlWeightsSymmetric5 weights, Span rowOut) { Vector w0 = LoadDuplicate128(weights.GetCVector()); // c @@ -126,7 +127,7 @@ public static void Symmetric5Row( Func wrapY, in Rectangle rect, long iy, - JxlWeightsSymmetric5 weights, + ref JxlWeightsSymmetric5 weights, Span rowOut) { const int radius = 2; @@ -140,25 +141,25 @@ public static void Symmetric5Row( for (; ix < Math.Min(alignedX, xEnd); ix++, rix++) { - rowOut[rix] = Symmetric5Border(image, wrapY, ix, iy, weights); + rowOut[rix] = Symmetric5Border(image, wrapY, ix, iy, ref weights); } for (; ix + n + radius <= xEnd; ix += n, rix += n) { - Symmetric5Interior(image, ix, wrapY, rix, iy, weights, rowOut); + Symmetric5Interior(image, ix, wrapY, rix, iy, ref weights, rowOut); } for (; ix < xEnd; ix++, rix++) { - rowOut[rix] = Symmetric5Border(image, wrapY, ix, iy, weights); + rowOut[rix] = Symmetric5Border(image, wrapY, ix, iy, ref weights); } } public static bool Symmetric5( - JxlImageF input, + JxlPlane input, in Rectangle rectangle, - JxlWeightsSymmetric5 weights, - JxlImageF output, + ref JxlWeightsSymmetric5 weights, + JxlPlane output, Rectangle outputRect) { if (rectangle.Width != outputRect.Width || rectangle.Height != outputRect.Height) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlWeightsSymmetric5.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlWeightsSymmetric5.cs index df5fa66b7b..7fb8977a50 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlWeightsSymmetric5.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlWeightsSymmetric5.cs @@ -6,89 +6,98 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; -internal sealed class JxlWeightsSymmetric5 +internal struct JxlWeightsSymmetric5 { - private InlineArray4 c; + public InlineArray4 C; - private InlineArray4 r; + public InlineArray4 R; - private InlineArray4 r2; + public InlineArray4 R2; - private InlineArray4 d; + public InlineArray4 D; - private InlineArray4 d2; + public InlineArray4 D2; - private InlineArray4 l; + public InlineArray4 L; - public Vector128 GetCVector() + public static InlineArray4 CreateVector4(float x) { - ref float first = ref Unsafe.AsRef(in this.c[0]); + InlineArray4 array = default; + + array[0] = array[1] = array[2] = array[3] = x; + + return array; + } + + public readonly Vector128 GetCVector() + { + ref float first = ref Unsafe.AsRef(in this.C[0]); return Vector128.LoadUnsafe(ref first); } - public Vector128 GetRVector() + public readonly Vector128 GetRVector() { - ref float first = ref Unsafe.AsRef(in this.r[0]); + ref float first = ref Unsafe.AsRef(in this.R[0]); return Vector128.LoadUnsafe(ref first); } - public Vector128 GetR2Vector() + public readonly Vector128 GetR2Vector() { - ref float first = ref Unsafe.AsRef(in this.r2[0]); + ref float first = ref Unsafe.AsRef(in this.R2[0]); return Vector128.LoadUnsafe(ref first); } - public Vector128 GetDVector() + public readonly Vector128 GetDVector() { - ref float first = ref Unsafe.AsRef(in this.d[0]); + ref float first = ref Unsafe.AsRef(in this.D[0]); return Vector128.LoadUnsafe(ref first); } - public Vector128 GetD2Vector() + public readonly Vector128 GetD2Vector() { - ref float first = ref Unsafe.AsRef(in this.d2[0]); + ref float first = ref Unsafe.AsRef(in this.D2[0]); return Vector128.LoadUnsafe(ref first); } - public Vector128 GetLVector() + public readonly Vector128 GetLVector() { - ref float first = ref Unsafe.AsRef(in this.l[0]); + ref float first = ref Unsafe.AsRef(in this.L[0]); return Vector128.LoadUnsafe(ref first); } - public void SetC(Vector128 vec) + public readonly void SetC(Vector128 vec) { - ref float first = ref Unsafe.AsRef(in this.c[0]); + ref float first = ref Unsafe.AsRef(in this.C[0]); vec.StoreUnsafe(ref first); } - public void SetD(Vector128 vec) + public readonly void SetD(Vector128 vec) { - ref float first = ref Unsafe.AsRef(in this.d[0]); + ref float first = ref Unsafe.AsRef(in this.D[0]); vec.StoreUnsafe(ref first); } - public void SetD2(Vector128 vec) + public readonly void SetD2(Vector128 vec) { - ref float first = ref Unsafe.AsRef(in this.d2[0]); + ref float first = ref Unsafe.AsRef(in this.D2[0]); vec.StoreUnsafe(ref first); } - public void SetR(Vector128 vec) + public readonly void SetR(Vector128 vec) { - ref float first = ref Unsafe.AsRef(in this.r[0]); + ref float first = ref Unsafe.AsRef(in this.R[0]); vec.StoreUnsafe(ref first); } - public void SetR2(Vector128 vec) + public readonly void SetR2(Vector128 vec) { - ref float first = ref Unsafe.AsRef(in this.r2[0]); + ref float first = ref Unsafe.AsRef(in this.R2[0]); vec.StoreUnsafe(ref first); } - public void SetL(Vector128 vec) + public readonly void SetL(Vector128 vec) { - ref float first = ref Unsafe.AsRef(in this.l[0]); + ref float first = ref Unsafe.AsRef(in this.L[0]); vec.StoreUnsafe(ref first); } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Primitives/RectangleUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/RectangleUtils.cs new file mode 100644 index 0000000000..072b51983f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/RectangleUtils.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + +internal static class RectangleUtils +{ + public static int X0(in Rectangle rect) => rect.X; + + public static int Y0(in Rectangle rect) => rect.Y; + + public static int X1(in Rectangle rect) => rect.X + rect.Width; + + public static int Y1(in Rectangle rect) => rect.Y + rect.Height; + + public static Rectangle Extend(Rectangle curr, int border, Rectangle parent) + { + int newX0 = X0(in curr) > X0(in parent) + border ? X0(in curr) - border : X0(in parent); + int newY0 = Y0(in curr) > Y0(in parent) + border ? Y0(in curr) - border : Y0(in parent); + int newX1 = X1(in curr) + border > X1(in parent) ? X1(in parent) : X1(in curr) + border; + int newY1 = Y1(in curr) + border > Y1(in parent) ? Y1(in parent) : Y1(in curr) + border; + + return new(newX0, newY0, newX1 - newX0, newY1 - newY0); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf0Stage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf0Stage.cs index 5df7525ea9..28f3dcb34d 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf0Stage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf0Stage.cs @@ -19,6 +19,11 @@ internal sealed class Epf0Stage : RenderPipelineStageBase [0, 1], [0, 2], [1, -1], [1, 0], [1, 1], [2, 0] ]; + private static readonly int[][] PlusOffsets = + [ + [0, 0], [-1, 0], [0, -1], [1, 0], [0, 1] + ]; + private readonly JxlLoopFilter loopFilter; private readonly JxlImageF sigma; @@ -29,12 +34,13 @@ public Epf0Stage(JxlLoopFilter loopFilter, JxlImageF sigma, Configuration config this.Settings = RenderPipelineStageConfiguration.CreateSymmetricBorderOnly(3); } + /// public override string Name => "EPF0"; [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void AddPixel( int row, - InlineArray7>> rows, + InlineArray3>> rows, int x, Vector256 sad, Vector256 inverseSigma, @@ -54,6 +60,7 @@ public static void AddPixel( bOut += (weight * cb) + bOut; } + /// public override void ProcessRow(Buffer2D> inputRows, Buffer2D> outputRows, int xExtraLeft, int xExtraRight, int width, int xPos, int yPos) { Span> sads = stackalloc Vector256[16].Slice(0, 12); @@ -103,6 +110,60 @@ public override void ProcessRow(Buffer2D> inputRows, Buffer2D vsm = Vector256.Create(sadMul[ix..]); Vector256 inverseSigma = Vector256.Create(rowSigma[bx]) * vsm; + + sads.Clear(); + + for (int c = 0; c < 3; c++) + { + Vector256 scale = Vector256.Create(this.loopFilter.EpfChannelScale[c]); + + for (int i = 0; i < 12; i++) + { + Vector256 sad = Vector256.Zero; + + foreach (Span offset in PlusOffsets) + { + Vector256 r11 = Vector256.Create((ReadOnlySpan)rows[c][3 + offset[0]][(x + offset[1])..].Span); + Vector256 c11 = Vector256.Create((ReadOnlySpan)rows[c][3 + SadOffsets[i][0] + offset[0]][(x + SadOffsets[i][1] + offset[1])..].Span); + sad += Vector256.Abs(r11 - c11); + } + + sads[i] = (sad * scale) + sads[i]; + } + } + + Vector256 xCC = Vector256.Create((ReadOnlySpan)rows[0][3 + 0][x..].Span); + Vector256 yCC = Vector256.Create((ReadOnlySpan)rows[1][3 + 0][x..].Span); + Vector256 bCC = Vector256.Create((ReadOnlySpan)rows[2][3 + 0][x..].Span); + + Vector256 w = Vector256.One; + Vector256 X = xCC; + Vector256 Y = yCC; + Vector256 B = bCC; + + for (int i = 0; i < 12; i++) + { + AddPixel(SadOffsets[i][0], rows, x + SadOffsets[i][1], sads[i], 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; } } } From 83456232133ac14d3314f43994c20e37f7698c00 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 2 Sep 2026 17:38:31 +0400 Subject: [PATCH 122/328] Refactor, optimize 1. Reorder the overflow check in JpegData 2. Prefer switch in JpegData instead of multiple if statements 3. Don't explicitly false initialize acOk and dcOk in JpegData (they're already initialized to false). 4. IFjxlFrameInputSource -> FjxlFrameInputSource (it's an abstract class, I prefix is for interfaces) 5. Document that the JxlSqueeze.Average method is specific to the squeeze transform. 6. Prefer RuntimeUtility.Swap over tuple-based swap (micro-optimization) --- .../Formats/Jxl/IO/Jpeg/Data/JpegData.cs | 33 ++++++++----------- .../Encoder/JxlFastLosslessEncoder.cs | 4 +-- .../Formats/Jxl/Processing/JxlMath.cs | 3 +- .../Modular/Transforms/JxlSqueeze.cs | 4 +++ 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegData.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegData.cs index 9ab957be93..4555442bcb 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegData.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegData.cs @@ -113,7 +113,7 @@ public static void SetJpegDataFromIcc(Span icc, JpegData jpegData) } int len = jpegData.AppData[i].Count - 17; - if (iccPos + len > icc.Length) + if (iccPos > icc.Length - len) { throw new InvalidOperationException("ICC length is less than APP markers: requested " + len + " more bytes, " + (icc.Length - iccPos) + " available"); } @@ -144,24 +144,23 @@ private static bool VisitMarker(ref byte marker, JxlVisitor visitor, ref JpegInf info.NumberOfAppMarkers++; } - if (marker == 0xfe) + switch (marker) { - info.NumberOfComMarkers++; - } + case 0xFE: + info.NumberOfComMarkers++; + break; - if (marker == 0xda) - { - info.NumberOfScans++; - } + case 0xDA: + info.NumberOfScans++; + break; - if (marker == 0xff) - { - info.NumberOfIntermarkers++; - } + case 0xFF: + info.NumberOfIntermarkers++; + break; - if (marker == 0xdd) - { - info.HasDri = true; + case 0xDD: + info.HasDri = true; + break; } return true; @@ -919,10 +918,6 @@ ref Unsafe.As(ref tail_data_len))) InlineArray4 acOk = default; InlineArray4 dcOk = default; - // All values of acOk, dcOk by default are false. - acOk[0] = acOk[1] = acOk[2] = acOk[3] = false; - dcOk[0] = dcOk[1] = dcOk[2] = dcOk[3] = false; - Span markerOrderSpan = CollectionsMarshal.AsSpan(this.MarkerOrder); for (int i = 0; i < markerOrderSpan.Length; i++) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs index 91263989ef..bea22c76a3 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs @@ -53,7 +53,7 @@ internal sealed class JxlFastLosslessEncoder /// /// Input frame data is stored here. /// - private readonly IFjxlFrameInputSource input; + private readonly FjxlFrameInputSource input; /// /// Image width of the input image. @@ -147,7 +147,7 @@ internal sealed class JxlFastLosslessEncoder /// /// Abstracts access to a raster frame data required for encoding. /// - internal abstract class IFjxlFrameInputSource : IDisposable + internal abstract class FjxlFrameInputSource : IDisposable { /// public abstract void Dispose(); diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs index a0042c9c50..f0ab04104e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs @@ -3,6 +3,7 @@ using System.Numerics; using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Common.Helpers; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; @@ -705,7 +706,7 @@ private static T Hypot(T x, T y) if (x < y) { - (y, x) = (x, y); + RuntimeUtility.Swap(ref x, ref y); } if (x == T.Zero) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs index 6888015825..11752a92c9 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs @@ -30,6 +30,10 @@ internal static class JxlSqueeze /// /// Computes the average of two integers. /// + /// + /// This method is specific to the Squeeze transform. + /// It is not a generic average method. + /// /// First integer /// Second integer /// The average of x, y. 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 123/328] 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; + } + } +} From 5874c4c9fdbb341b66afcc6c7f326695be564a37 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 2 Sep 2026 19:47:46 +0400 Subject: [PATCH 124/328] Optimize JxlNoiseHistogram - Use double for Clamp - Avoid bounds check in while loop --- .../Jxl/Processing/Encoder/Noise/JxlNoiseHistogram.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseHistogram.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseHistogram.cs index 5cacaa5d02..396021a55d 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseHistogram.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseHistogram.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Numerics; using System.Numerics.Tensors; using System.Runtime.CompilerServices; @@ -66,7 +65,7 @@ public double Quantile(double q01) int next = i + 1; - while (next < Bins && this.bins[next] == 0) + while ((uint)next < this.bins.Length && this.bins[next] == 0) { next++; } @@ -78,10 +77,8 @@ public double Quantile(double q01) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static T ClampX(T x) - where T : unmanaged, INumber - => T.Clamp(x, T.Zero, T.CreateSaturating(Bins - 1)); + private static double ClampX(double x) => Math.Clamp(x, 0, Bins - 1); [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int Index(float x) => ClampX((int)x); + private static int Index(float x) => (int)ClampX((int)x); } From 5f99a6530f7807fd89ba5945c5e783abf8b13bec Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 2 Sep 2026 20:16:57 +0400 Subject: [PATCH 125/328] Further update folder structure, start working on write to output stage (incomplete) --- .../{Processing => IO}/JxlBoxCodingMode.cs | 2 +- .../Primitives => IO}/JxlDataType.cs | 2 +- .../Formats/Jxl/Memory/JxlPlane{T}.cs | 2 +- .../Processing/AcStrategy/JxlAcStrategy.cs | 2 +- .../AcStrategy/JxlAcStrategyImage.cs | 1 + .../Jxl/Processing/Dct/JxlDctAcImage{T}.cs | 1 + .../Decoder/JxlBoxContentDecoder.cs | 1 + .../Jxl/Processing/Decoder/JxlDecoderCache.cs | 14 ++ .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 1 + .../Jxl/Processing/Decoder/JxlFrameDecoder.cs | 1 + .../Decoder/JxlPassesDecoderState.cs | 1 + .../Processing/Decoder/JxlPatchDictionary.cs | 1 + .../Jxl/Processing/Decoder/JxlXybDecoder.cs | 1 + .../Jxl/Processing/Encoder/JxlGaborish.cs | 1 + .../Processing/{ => Image}/JxlImageBundle.cs | 2 +- .../{ => Image}/JxlImageFeatures.cs | 2 +- .../{ => Image}/JxlImageOperations.cs | 3 +- .../Jxl/Processing/JxlChromaFromLuma.cs | 2 +- .../Jxl/Processing/JxlPassesSharedState.cs | 2 + .../Modular/Transforms/JxlPalette.cs | 1 + .../Jxl/Processing/Primitives/Delegates.cs | 25 +++ .../{ => Primitives}/JxlFrameDimensions.cs | 2 +- .../JxlOpsinInverseParameters.cs | 4 +- .../Processing/Primitives/JxlPixelFormat.cs | 2 + .../Quantization/JxlDequantMatrices.cs | 1 + .../Processing/Quantization/JxlQuantizer.cs | 1 + .../Processing/RenderPipeline/Epf0Stage.cs | 2 + .../Processing/RenderPipeline/Epf1Stage.cs | 2 + .../Processing/RenderPipeline/Epf2Stage.cs | 2 + .../RenderPipeline/WriteToOutputStage.cs | 212 ++++++++++++++++++ 30 files changed, 284 insertions(+), 12 deletions(-) rename src/ImageSharp/Formats/Jxl/{Processing => IO}/JxlBoxCodingMode.cs (87%) rename src/ImageSharp/Formats/Jxl/{Processing/Primitives => IO}/JxlDataType.cs (90%) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCache.cs rename src/ImageSharp/Formats/Jxl/Processing/{ => Image}/JxlImageBundle.cs (99%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Image}/JxlImageFeatures.cs (92%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Image}/JxlImageOperations.cs (99%) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Primitives/Delegates.cs rename src/ImageSharp/Formats/Jxl/Processing/{ => Primitives}/JxlFrameDimensions.cs (97%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Primitives}/JxlOpsinInverseParameters.cs (87%) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlBoxCodingMode.cs b/src/ImageSharp/Formats/Jxl/IO/JxlBoxCodingMode.cs similarity index 87% rename from src/ImageSharp/Formats/Jxl/Processing/JxlBoxCodingMode.cs rename to src/ImageSharp/Formats/Jxl/IO/JxlBoxCodingMode.cs index 89f5af5e3f..412d18a8aa 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlBoxCodingMode.cs +++ b/src/ImageSharp/Formats/Jxl/IO/JxlBoxCodingMode.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.IO; /// /// Specifies the type of box content. diff --git a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlDataType.cs b/src/ImageSharp/Formats/Jxl/IO/JxlDataType.cs similarity index 90% rename from src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlDataType.cs rename to src/ImageSharp/Formats/Jxl/IO/JxlDataType.cs index 8bc0bf6911..7370f1b25b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlDataType.cs +++ b/src/ImageSharp/Formats/Jxl/IO/JxlDataType.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +namespace SixLabors.ImageSharp.Formats.Jxl.IO; /// /// Specifies which data type to use for sample values diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs index 2d96ab636f..f8602a1897 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.Processing; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; namespace SixLabors.ImageSharp.Formats.Jxl.Memory; diff --git a/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategy.cs b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategy.cs index 36f0b71c5b..78e97ed16c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategy.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategy.cs @@ -3,7 +3,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using static SixLabors.ImageSharp.Formats.Jxl.Processing.JxlFrameDimensions; +using static SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives.JxlFrameDimensions; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; diff --git a/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyImage.cs b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyImage.cs index 875cae762f..4c751e290c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyImage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyImage.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctAcImage{T}.cs b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctAcImage{T}.cs index 6d132c4128..002342ae57 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctAcImage{T}.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctAcImage{T}.cs @@ -3,6 +3,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs index 93fe598304..9d35ea85a9 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.IO.Compression; +using SixLabors.ImageSharp.Formats.Jxl.IO; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCache.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCache.cs new file mode 100644 index 0000000000..8e50433369 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCache.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +/// +/// Constants used by decoder cache. +/// +internal static class JxlDecoderCache +{ + public const int SigmaBorder = 1; + + public const int SigmaPadding = 2; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index 5b58a19128..6a850d8288 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -10,6 +10,7 @@ using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Metadata.Profiles.Icc; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs index 75b39cbeb0..9b8e9b0f28 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs @@ -3,6 +3,7 @@ using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs index 9c5b910c2d..3e32e94a48 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs @@ -4,6 +4,7 @@ using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchDictionary.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchDictionary.cs index d9bc0e88f8..c990288589 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchDictionary.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchDictionary.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs index 7fd7d54c6e..87f96e97eb 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs @@ -6,6 +6,7 @@ using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.Cms; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGaborish.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGaborish.cs index 191e2c5a4a..4fddd68873 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGaborish.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGaborish.cs @@ -3,6 +3,7 @@ using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlImageBundle.cs b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs similarity index 99% rename from src/ImageSharp/Formats/Jxl/Processing/JxlImageBundle.cs rename to src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs index 184feb6e74..b4ca42d788 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlImageBundle.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs @@ -7,7 +7,7 @@ using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Image; /// /// An image bundle. diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlImageFeatures.cs b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageFeatures.cs similarity index 92% rename from src/ImageSharp/Formats/Jxl/Processing/JxlImageFeatures.cs rename to src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageFeatures.cs index 63d4025eb1..eb0e4581da 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlImageFeatures.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageFeatures.cs @@ -4,7 +4,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Noise; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Image; /// /// Image features for the JPEG XL passes decoder diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs similarity index 99% rename from src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs rename to src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs index e5ddf834f7..0a4ff8f5d7 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs @@ -5,8 +5,9 @@ using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Image; /// /// Provides methods for processing 2D views of memory used by the diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlChromaFromLuma.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlChromaFromLuma.cs index 0121140472..49ed970b84 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlChromaFromLuma.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlChromaFromLuma.cs @@ -2,7 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Jxl.Fields; -using static SixLabors.ImageSharp.Formats.Jxl.Processing.JxlFrameDimensions; +using static SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives.JxlFrameDimensions; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs index dcc699503b..f56689382a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs @@ -7,6 +7,8 @@ using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs index 63d0c0b081..4e306c1f40 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs @@ -4,6 +4,7 @@ using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Primitives/Delegates.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/Delegates.cs new file mode 100644 index 0000000000..e0f7c78a33 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/Delegates.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + +internal delegate void JxlImageOutputCallback( + Span data, + int x, + int y, + int numPixels, + Span pixels); + +internal delegate void JxlImageOutputInitializeCallback( + Span data, + int numThreads, + int numPixelsPerThread); + +internal delegate void JxlImageOutputRunCallback( + Span data, + int x, + int y, + int numPixels, + Span pixels); + +internal delegate void JxlImageOutputDestroyCallback(Span data); diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlFrameDimensions.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlFrameDimensions.cs similarity index 97% rename from src/ImageSharp/Formats/Jxl/Processing/JxlFrameDimensions.cs rename to src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlFrameDimensions.cs index 70a3bff8bb..7bd67326a3 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlFrameDimensions.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlFrameDimensions.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; internal sealed class JxlFrameDimensions { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlOpsinInverseParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlOpsinInverseParameters.cs similarity index 87% rename from src/ImageSharp/Formats/Jxl/Processing/JxlOpsinInverseParameters.cs rename to src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlOpsinInverseParameters.cs index 820b11a5ba..4e51220296 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlOpsinInverseParameters.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlOpsinInverseParameters.cs @@ -1,9 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; internal static class JxlOpsinInverseParameters { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlPixelFormat.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlPixelFormat.cs index be00bd3e76..5e0d3ef22b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlPixelFormat.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlPixelFormat.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.IO; + namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlDequantMatrices.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlDequantMatrices.cs index 9649f44203..e7084a87cd 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlDequantMatrices.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlDequantMatrices.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs index 1ea7cfcbd1..5788160141 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs @@ -8,6 +8,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf0Stage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf0Stage.cs index 28f3dcb34d..a7bcafcc3d 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf0Stage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf0Stage.cs @@ -4,6 +4,8 @@ using System.Runtime.CompilerServices; using System.Runtime.Intrinsics; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf1Stage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf1Stage.cs index 9ad9b3dec8..b42513898c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf1Stage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf1Stage.cs @@ -4,6 +4,8 @@ using System.Runtime.CompilerServices; using System.Runtime.Intrinsics; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf2Stage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf2Stage.cs index ff6198e7b8..7100df484c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf2Stage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf2Stage.cs @@ -4,6 +4,8 @@ using System.Runtime.CompilerServices; using System.Runtime.Intrinsics; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs new file mode 100644 index 0000000000..09426afb2b --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs @@ -0,0 +1,212 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +internal sealed class WriteToOutputStage +{ + private const int ChunkSize = 1024; + + /// + /// Gets the 32x32 blue noise dithering pattern lookup + /// table (from ), + /// scaled to have an average of 0 and be fully contained in (0.49219 + /// to -0.49219). Rows are padded to 48 (32 + 16) to allow SIMD to wrap around + /// horizontally. + /// + private static ReadOnlySpan DitheringPattern => + [ + -0.26057f, 0.32619f, 0.21039f, -0.03281f, -0.10616f, 0.16792f, 0.43042f, -0.48061f, + -0.00965f, -0.31075f, 0.24899f, -0.35322f, -0.02509f, -0.25285f, 0.02895f, 0.10230f, + -0.28373f, -0.00193f, 0.23355f, 0.43428f, -0.23741f, 0.18336f, -0.31847f, -0.11002f, + -0.36094f, 0.26057f, -0.19108f, -0.29531f, 0.40726f, -0.09458f, 0.11002f, -0.48833f, + -0.26057f, 0.32619f, 0.21039f, -0.03281f, -0.10616f, 0.16792f, 0.43042f, -0.48061f, + -0.00965f, -0.31075f, 0.24899f, -0.35322f, -0.02509f, -0.25285f, 0.02895f, 0.10230f, + 0.16020f, -0.35708f, -0.18336f, 0.36094f, -0.28373f, -0.34550f, -0.20267f, 0.07914f, + 0.35708f, -0.41498f, 0.47675f, -0.21811f, -0.12546f, 0.44200f, -0.41884f, -0.17178f, + 0.39954f, 0.33778f, -0.33778f, 0.04053f, -0.46517f, 0.27215f, -0.16792f, 0.39182f, + 0.20653f, -0.43814f, -0.02895f, 0.17950f, -0.41498f, 0.01737f, 0.24899f, 0.49219f, + 0.16020f, -0.35708f, -0.18336f, 0.36094f, -0.28373f, -0.34550f, -0.20267f, 0.07914f, + 0.35708f, -0.41498f, 0.47675f, -0.21811f, -0.12546f, 0.44200f, -0.41884f, -0.17178f, + -0.00965f, 0.08300f, 0.41112f, -0.46903f, 0.04053f, 0.47289f, 0.26057f, -0.05983f, + -0.13704f, 0.14862f, 0.03281f, 0.29531f, -0.45744f, 0.22583f, 0.14862f, -0.09072f, + -0.37638f, 0.19881f, -0.14476f, 0.14476f, -0.09072f, 0.48447f, -0.39954f, 0.06369f, + -0.05983f, -0.26829f, 0.43428f, -0.12546f, 0.28759f, -0.22969f, -0.32619f, -0.15248f, + -0.00965f, 0.08300f, 0.41112f, -0.46903f, 0.04053f, 0.47289f, 0.26057f, -0.05983f, + -0.13704f, 0.14862f, 0.03281f, 0.29531f, -0.45744f, 0.22583f, 0.14862f, -0.09072f, + -0.42270f, 0.23741f, -0.23355f, -0.11774f, 0.18722f, 0.11388f, -0.43814f, -0.24899f, + 0.41884f, 0.21039f, -0.28373f, -0.06756f, 0.07914f, 0.36480f, -0.31075f, 0.30303f, + -0.03281f, 0.07142f, -0.42656f, 0.38024f, -0.27987f, 0.00579f, 0.12546f, -0.22197f, + 0.29917f, 0.36866f, 0.13704f, -0.47289f, 0.09072f, 0.35708f, -0.04825f, 0.38796f, + -0.42270f, 0.23741f, -0.23355f, -0.11774f, 0.18722f, 0.11388f, -0.43814f, -0.24899f, + 0.41884f, 0.21039f, -0.28373f, -0.06756f, 0.07914f, 0.36480f, -0.31075f, 0.30303f, + -0.28759f, -0.07142f, 0.44200f, 0.27601f, -0.38024f, -0.16020f, -0.01737f, 0.30303f, + -0.33006f, -0.40340f, -0.16792f, 0.40726f, -0.36480f, -0.00579f, -0.19108f, 0.41498f, + -0.26443f, 0.46903f, -0.21811f, 0.28759f, -0.04053f, 0.22197f, 0.34550f, -0.44972f, + -0.14476f, -0.34164f, 0.04053f, -0.19494f, 0.45358f, -0.37252f, 0.21425f, 0.05597f, + -0.28759f, -0.07142f, 0.44200f, 0.27601f, -0.38024f, -0.16020f, -0.01737f, 0.30303f, + -0.33006f, -0.40340f, -0.16792f, 0.40726f, -0.36480f, -0.00579f, -0.19108f, 0.41498f, + 0.31075f, 0.14090f, -0.33778f, 0.00579f, 0.34550f, -0.29917f, 0.38796f, 0.13704f, + 0.05983f, -0.10230f, 0.34164f, 0.10616f, -0.23741f, 0.19494f, -0.47675f, 0.04439f, + -0.39568f, 0.24127f, 0.10616f, -0.49219f, -0.17950f, -0.36094f, -0.30303f, 0.45744f, + -0.01351f, 0.24513f, -0.39182f, -0.07528f, 0.18722f, -0.26057f, -0.11002f, -0.45358f, + 0.31075f, 0.14090f, -0.33778f, 0.00579f, 0.34550f, -0.29917f, 0.38796f, 0.13704f, + 0.05983f, -0.10230f, 0.34164f, 0.10616f, -0.23741f, 0.19494f, -0.47675f, 0.04439f, + 0.46903f, -0.17178f, -0.41112f, 0.07528f, -0.09458f, 0.21811f, -0.20267f, -0.48833f, + 0.44972f, 0.00965f, 0.24127f, -0.42656f, 0.48447f, -0.11774f, 0.26443f, 0.14090f, + -0.15634f, -0.07142f, -0.32233f, 0.36094f, 0.42270f, 0.19108f, 0.07142f, -0.11002f, + 0.15634f, 0.38024f, -0.28759f, 0.27987f, -0.00193f, 0.33006f, 0.11388f, -0.21039f, + 0.46903f, -0.17178f, -0.41112f, 0.07528f, -0.09458f, 0.21811f, -0.20267f, -0.48833f, + 0.44972f, 0.00965f, 0.24127f, -0.42656f, 0.48447f, -0.11774f, 0.26443f, 0.14090f, + 0.02123f, 0.17950f, 0.38024f, -0.24127f, -0.44586f, 0.48833f, -0.03667f, 0.26829f, + -0.36866f, -0.22583f, 0.17178f, -0.30689f, 0.29145f, -0.04825f, -0.35322f, 0.43042f, + 0.34936f, 0.00193f, 0.16792f, -0.12932f, 0.03667f, -0.06756f, 0.31847f, -0.40726f, + -0.24513f, 0.09458f, -0.17564f, 0.47675f, -0.43042f, -0.32233f, 0.40340f, 0.26057f, + 0.02123f, 0.17950f, 0.38024f, -0.24127f, -0.44586f, 0.48833f, -0.03667f, 0.26829f, + -0.36866f, -0.22583f, 0.17178f, -0.30689f, 0.29145f, -0.04825f, -0.35322f, 0.43042f, + -0.47675f, -0.12160f, -0.04825f, 0.28759f, 0.10230f, 0.15634f, -0.14862f, -0.27601f, + 0.36094f, -0.12932f, -0.05983f, -0.45358f, -0.17950f, 0.01737f, 0.09458f, -0.29145f, + -0.22969f, -0.43428f, 0.45744f, -0.38796f, -0.27601f, -0.21039f, -0.46131f, 0.22969f, + 0.41112f, -0.05211f, -0.48061f, 0.16406f, 0.05211f, -0.14862f, -0.03281f, -0.36866f, + -0.47675f, -0.12160f, -0.04825f, 0.28759f, 0.10230f, 0.15634f, -0.14862f, -0.27601f, + 0.36094f, -0.12932f, -0.05983f, -0.45358f, -0.17950f, 0.01737f, 0.09458f, -0.29145f, + -0.27215f, 0.34164f, -0.31075f, 0.42656f, -0.38410f, -0.32619f, 0.02895f, 0.19881f, + 0.08300f, 0.42270f, 0.31461f, 0.13318f, 0.45744f, 0.37638f, -0.40726f, 0.31847f, + -0.08686f, 0.21425f, 0.29917f, 0.07914f, 0.26829f, 0.13704f, 0.48447f, -0.15248f, + 0.02509f, -0.34936f, 0.34936f, -0.10230f, 0.42656f, -0.23741f, 0.22583f, 0.09072f, + -0.27215f, 0.34164f, -0.31075f, 0.42656f, -0.38410f, -0.32619f, 0.02895f, 0.19881f, + 0.08300f, 0.42270f, 0.31461f, 0.13318f, 0.45744f, 0.37638f, -0.40726f, 0.31847f, + 0.44972f, 0.20267f, 0.04825f, -0.21425f, 0.24513f, -0.07142f, 0.39954f, -0.46131f, + -0.39568f, -0.01351f, -0.33392f, 0.05597f, -0.26443f, 0.22197f, -0.20653f, 0.15248f, + 0.04439f, -0.46517f, -0.16406f, -0.04439f, -0.34936f, 0.37252f, -0.01351f, -0.30689f, + 0.29917f, 0.20653f, -0.26829f, 0.26443f, 0.13318f, -0.39954f, 0.30303f, -0.08686f, + 0.44972f, 0.20267f, 0.04825f, -0.21425f, 0.24513f, -0.07142f, 0.39954f, -0.46131f, + -0.39568f, -0.01351f, -0.33392f, 0.05597f, -0.26443f, 0.22197f, -0.20653f, 0.15248f, + -0.42656f, 0.12932f, -0.14476f, -0.46903f, -0.00579f, 0.34936f, -0.18722f, 0.28373f, + -0.23741f, 0.22969f, -0.16020f, -0.38024f, -0.08300f, -0.48447f, -0.02123f, -0.14862f, + 0.48061f, -0.31847f, 0.39568f, -0.24899f, 0.18722f, -0.41884f, 0.10230f, -0.08300f, + -0.38796f, 0.06369f, -0.19881f, -0.44972f, 0.00579f, -0.33392f, 0.37252f, -0.19108f, + -0.42656f, 0.12932f, -0.14476f, -0.46903f, -0.00579f, 0.34936f, -0.18722f, 0.28373f, + -0.23741f, 0.22969f, -0.16020f, -0.38024f, -0.08300f, -0.48447f, -0.02123f, -0.14862f, + -0.02509f, -0.35708f, 0.32619f, 0.46517f, 0.17178f, -0.28373f, 0.10616f, 0.47675f, + -0.09458f, 0.15248f, 0.43428f, 0.35322f, 0.17564f, 0.27215f, 0.41112f, -0.36480f, + 0.24899f, 0.11774f, 0.01351f, 0.33006f, -0.11388f, -0.18336f, 0.41884f, -0.23355f, + 0.16406f, 0.46131f, 0.38410f, -0.04825f, -0.15634f, 0.49219f, 0.17564f, 0.03667f, + -0.02509f, -0.35708f, 0.32619f, 0.46517f, 0.17178f, -0.28373f, 0.10616f, 0.47675f, + -0.09458f, 0.15248f, 0.43428f, 0.35322f, 0.17564f, 0.27215f, 0.41112f, -0.36480f, + 0.40726f, 0.23355f, -0.25285f, -0.08300f, -0.41112f, -0.12160f, -0.35708f, 0.05211f, + -0.41884f, -0.29531f, 0.02123f, -0.21425f, 0.09844f, -0.30689f, -0.11388f, 0.34550f, + -0.26443f, -0.07142f, -0.39954f, 0.44586f, 0.05983f, -0.48833f, 0.24127f, 0.34936f, + -0.44200f, -0.12546f, 0.12160f, -0.30303f, 0.27215f, 0.07528f, -0.48447f, -0.29145f, + 0.40726f, 0.23355f, -0.25285f, -0.08300f, -0.41112f, -0.12160f, -0.35708f, 0.05211f, + -0.41884f, -0.29531f, 0.02123f, -0.21425f, 0.09844f, -0.30689f, -0.11388f, 0.34550f, + 0.28373f, -0.17564f, 0.09458f, 0.02123f, 0.30689f, 0.41884f, 0.20653f, -0.03667f, + 0.32233f, 0.25671f, -0.45744f, -0.05597f, 0.46517f, -0.41498f, 0.00965f, 0.07142f, + -0.44586f, 0.16406f, -0.20653f, 0.21811f, -0.29917f, 0.28759f, -0.05597f, 0.03281f, + -0.32619f, -0.00965f, 0.31847f, -0.37252f, 0.18722f, -0.11002f, -0.22969f, -0.06369f, + 0.28373f, -0.17564f, 0.09458f, 0.02123f, 0.30689f, 0.41884f, 0.20653f, -0.03667f, + 0.32233f, 0.25671f, -0.45744f, -0.05597f, 0.46517f, -0.41498f, 0.00965f, 0.07142f, + -0.39568f, 0.36866f, -0.45744f, -0.31847f, 0.14476f, -0.22583f, -0.49219f, 0.37638f, + -0.19494f, -0.13318f, 0.39182f, -0.35322f, 0.29531f, -0.24127f, 0.21039f, -0.18722f, + 0.45358f, 0.31461f, -0.13318f, -0.01737f, -0.36094f, 0.12932f, -0.25671f, 0.43814f, + -0.16792f, 0.23355f, -0.22197f, 0.44972f, -0.42270f, 0.33392f, 0.42656f, 0.11774f, + -0.39568f, 0.36866f, -0.45744f, -0.31847f, 0.14476f, -0.22583f, -0.49219f, 0.37638f, + -0.19494f, -0.13318f, 0.39182f, -0.35322f, 0.29531f, -0.24127f, 0.21039f, -0.18722f, + -0.13318f, 0.19494f, -0.03667f, 0.44972f, 0.24513f, -0.15248f, 0.08300f, -0.33006f, + 0.00579f, 0.12546f, 0.19494f, 0.05983f, -0.15634f, 0.14476f, 0.36480f, -0.04053f, + -0.33006f, 0.25671f, -0.46903f, 0.37252f, 0.48833f, -0.09458f, -0.41112f, 0.19108f, + 0.08686f, -0.46903f, -0.07528f, 0.04053f, -0.26829f, -0.02895f, 0.22197f, -0.34164f, + -0.13318f, 0.19494f, -0.03667f, 0.44972f, 0.24513f, -0.15248f, 0.08300f, -0.33006f, + 0.00579f, 0.12546f, 0.19494f, 0.05983f, -0.15634f, 0.14476f, 0.36480f, -0.04053f, + 0.47289f, -0.21811f, 0.06756f, -0.38410f, -0.27987f, -0.06369f, 0.27987f, 0.43814f, + -0.25671f, -0.39182f, 0.49219f, -0.27601f, -0.07914f, -0.48061f, 0.42656f, -0.38410f, + 0.11002f, 0.03667f, -0.27215f, 0.15634f, 0.07528f, -0.22197f, 0.33006f, 0.38410f, + -0.34936f, 0.27987f, 0.15248f, 0.40340f, 0.09844f, -0.16406f, -0.46131f, 0.03281f, + 0.47289f, -0.21811f, 0.06756f, -0.38410f, -0.27987f, -0.06369f, 0.27987f, 0.43814f, + -0.25671f, -0.39182f, 0.49219f, -0.27601f, -0.07914f, -0.48061f, 0.42656f, -0.38410f, + -0.29531f, 0.31461f, -0.10616f, 0.39954f, 0.01351f, 0.33778f, -0.43814f, 0.17178f, + -0.08686f, 0.23741f, -0.44586f, 0.33778f, -0.00193f, -0.31461f, 0.23741f, -0.12932f, + -0.22583f, -0.06756f, 0.40340f, -0.16792f, -0.43428f, 0.01351f, -0.14476f, -0.04053f, + -0.29145f, 0.46517f, -0.13704f, -0.39182f, -0.32233f, 0.29531f, 0.38410f, 0.16020f, + -0.29531f, 0.31461f, -0.10616f, 0.39954f, 0.01351f, 0.33778f, -0.43814f, 0.17178f, + -0.08686f, 0.23741f, -0.44586f, 0.33778f, -0.00193f, -0.31461f, 0.23741f, -0.12932f, + -0.44200f, 0.26443f, 0.12546f, -0.42270f, 0.21425f, -0.19881f, -0.35708f, 0.04825f, + 0.36480f, -0.02895f, -0.21425f, 0.09072f, 0.41498f, 0.18336f, 0.04439f, 0.29917f, + 0.47675f, -0.40340f, 0.27601f, -0.31461f, 0.31075f, 0.17564f, 0.24899f, -0.45744f, + 0.05597f, -0.19494f, 0.00193f, 0.36094f, 0.24127f, -0.09844f, -0.24513f, -0.00965f, + -0.44200f, 0.26443f, 0.12546f, -0.42270f, 0.21425f, -0.19881f, -0.35708f, 0.04825f, + 0.36480f, -0.02895f, -0.21425f, 0.09072f, 0.41498f, 0.18336f, 0.04439f, 0.29917f, + -0.17564f, -0.05597f, -0.34550f, -0.24899f, 0.48061f, 0.15248f, -0.11388f, 0.45358f, + -0.16406f, -0.32233f, 0.31461f, -0.11774f, -0.36866f, -0.18722f, -0.25671f, -0.44200f, + 0.13318f, -0.02123f, 0.19881f, -0.10616f, 0.43042f, -0.36866f, -0.24899f, 0.41112f, + 0.11002f, 0.21425f, -0.25671f, -0.47675f, -0.04439f, 0.13704f, -0.37252f, 0.43814f, + -0.17564f, -0.05597f, -0.34550f, -0.24899f, 0.48061f, 0.15248f, -0.11388f, 0.45358f, + -0.16406f, -0.32233f, 0.31461f, -0.11774f, -0.36866f, -0.18722f, -0.25671f, -0.44200f, + 0.19108f, 0.03667f, 0.35708f, -0.14090f, 0.08300f, -0.02123f, -0.30303f, -0.48061f, + 0.11774f, 0.20267f, -0.43042f, 0.25285f, 0.14090f, -0.04439f, 0.38796f, 0.34550f, + -0.34164f, -0.19494f, 0.05983f, -0.48447f, 0.09844f, -0.00579f, -0.07914f, 0.33778f, + -0.41498f, -0.10230f, 0.30689f, 0.17178f, 0.48833f, -0.20267f, 0.07914f, 0.33392f, + 0.19108f, 0.03667f, 0.35708f, -0.14090f, 0.08300f, -0.02123f, -0.30303f, -0.48061f, + 0.11774f, 0.20267f, -0.43042f, 0.25285f, 0.14090f, -0.04439f, 0.38796f, 0.34550f, + -0.48833f, -0.30689f, 0.41498f, 0.22969f, -0.44586f, 0.32233f, 0.25285f, 0.39182f, + -0.23355f, 0.01737f, 0.42270f, -0.27987f, 0.46903f, -0.47289f, 0.02123f, -0.09072f, + 0.21811f, 0.44586f, -0.25285f, 0.36480f, -0.29145f, 0.47289f, -0.18722f, 0.14476f, + -0.31461f, 0.43814f, -0.36094f, 0.04439f, -0.29917f, -0.41884f, 0.25285f, -0.11774f, + -0.48833f, -0.30689f, 0.41498f, 0.22969f, -0.44586f, 0.32233f, 0.25285f, 0.39182f, + -0.23355f, 0.01737f, 0.42270f, -0.27987f, 0.46903f, -0.47289f, 0.02123f, -0.09072f, + 0.46131f, 0.11388f, -0.21039f, -0.07528f, -0.38024f, -0.26057f, 0.06369f, -0.05983f, + 0.29145f, -0.40340f, -0.09072f, 0.06756f, -0.16020f, 0.27601f, -0.31075f, 0.10616f, + -0.14090f, -0.43042f, 0.25671f, -0.05211f, -0.13318f, 0.23355f, -0.44972f, 0.02895f, + 0.26829f, -0.02895f, -0.17950f, 0.37252f, -0.13704f, 0.40726f, 0.01351f, -0.26443f, + 0.46131f, 0.11388f, -0.21039f, -0.07528f, -0.38024f, -0.26057f, 0.06369f, -0.05983f, + 0.29145f, -0.40340f, -0.09072f, 0.06756f, -0.16020f, 0.27601f, -0.31075f, 0.10616f, + -0.03281f, -0.40340f, 0.27987f, 0.17564f, 0.02509f, 0.44200f, -0.15248f, -0.34550f, + 0.14862f, -0.19881f, -0.01351f, 0.36866f, -0.38796f, 0.19494f, -0.22197f, 0.32619f, + -0.37638f, 0.00193f, 0.30689f, 0.12160f, -0.39182f, 0.16792f, -0.34550f, 0.39954f, + -0.23355f, 0.09072f, -0.43428f, 0.22969f, -0.06369f, 0.12546f, -0.35322f, 0.30689f, + -0.03281f, -0.40340f, 0.27987f, 0.17564f, 0.02509f, 0.44200f, -0.15248f, -0.34550f, + 0.14862f, -0.19881f, -0.01351f, 0.36866f, -0.38796f, 0.19494f, -0.22197f, 0.32619f, + -0.09844f, 0.06756f, 0.38410f, -0.33392f, -0.18336f, 0.35322f, 0.21039f, -0.42270f, + 0.48833f, 0.33006f, 0.21811f, -0.33392f, 0.12932f, -0.05211f, 0.39568f, 0.04825f, + 0.48061f, 0.17950f, -0.31847f, -0.21811f, 0.38024f, 0.05211f, 0.32233f, -0.06756f, + -0.12546f, 0.46131f, 0.16020f, -0.25285f, 0.29531f, -0.44972f, 0.17950f, -0.16406f, + -0.09844f, 0.06756f, 0.38410f, -0.33392f, -0.18336f, 0.35322f, 0.21039f, -0.42270f, + 0.48833f, 0.33006f, 0.21811f, -0.33392f, 0.12932f, -0.05211f, 0.39568f, 0.04825f, + 0.22583f, -0.46131f, -0.27601f, -0.00579f, 0.12932f, -0.47289f, -0.09844f, 0.10230f, + -0.28759f, -0.12160f, -0.49219f, -0.24127f, 0.44586f, -0.11388f, -0.45358f, -0.27215f, + -0.17178f, -0.07528f, -0.47675f, 0.43042f, -0.02509f, -0.27215f, -0.19108f, 0.19881f, + -0.49219f, -0.37252f, 0.33392f, -0.00193f, -0.33006f, -0.20267f, 0.48061f, 0.34164f, + 0.22583f, -0.46131f, -0.27601f, -0.00579f, 0.12932f, -0.47289f, -0.09844f, 0.10230f, + -0.28759f, -0.12160f, -0.49219f, -0.24127f, 0.44586f, -0.11388f, -0.45358f, -0.27215f, + -0.22969f, 0.42270f, -0.12160f, 0.31075f, 0.46903f, -0.22583f, 0.27215f, -0.02509f, + 0.03281f, 0.40340f, 0.25671f, 0.08686f, 0.00965f, 0.29145f, -0.41112f, 0.14090f, + 0.24513f, 0.34164f, 0.08686f, -0.14862f, 0.27601f, -0.42656f, 0.48447f, 0.09844f, + 0.26443f, -0.27987f, 0.05597f, -0.10230f, 0.43428f, 0.08686f, 0.02895f, -0.38024f, + -0.22969f, 0.42270f, -0.12160f, 0.31075f, 0.46903f, -0.22583f, 0.27215f, -0.02509f, + 0.03281f, 0.40340f, 0.25671f, 0.08686f, 0.00965f, 0.29145f, -0.41112f, 0.14090f, + 0.15634f, 0.09458f, -0.36480f, 0.18336f, -0.05211f, -0.40726f, 0.36866f, -0.33778f, + -0.19881f, 0.16020f, -0.37638f, -0.16020f, -0.29917f, 0.20267f, 0.41884f, -0.01737f, + -0.34936f, -0.24127f, 0.02509f, 0.20653f, -0.36480f, -0.08686f, 0.01737f, -0.33778f, + 0.41498f, -0.03667f, 0.37638f, -0.17178f, -0.47289f, 0.26829f, -0.28759f, -0.05597f, + 0.15634f, 0.09458f, -0.36480f, 0.18336f, -0.05211f, -0.40726f, 0.36866f, -0.33778f, + -0.19881f, 0.16020f, -0.37638f, -0.16020f, -0.29917f, 0.20267f, 0.41884f, -0.01737f, + 0.35708f, 0.00193f, 0.25285f, -0.15634f, -0.30303f, 0.06369f, 0.22197f, 0.45358f, + -0.43814f, 0.30303f, -0.04053f, 0.46517f, 0.35322f, -0.21039f, 0.06756f, -0.14090f, + 0.37638f, -0.43042f, 0.45744f, -0.29531f, 0.39568f, 0.14862f, 0.23741f, -0.13704f, + -0.21425f, 0.16406f, -0.40726f, 0.22583f, 0.13318f, 0.38796f, -0.12932f, -0.43428f, + 0.35708f, 0.00193f, 0.25285f, -0.15634f, -0.30303f, 0.06369f, 0.22197f, 0.45358f, + -0.43814f, 0.30303f, -0.04053f, 0.46517f, 0.35322f, -0.21039f, 0.06756f, -0.14090f, + -0.31461f, -0.20653f, 0.46131f, -0.45358f, 0.39568f, -0.24513f, -0.14090f, 0.11002f, + -0.08300f, -0.26829f, 0.05211f, -0.46517f, -0.09844f, -0.39568f, -0.32619f, -0.06369f, + 0.16792f, 0.28373f, 0.11388f, -0.04439f, -0.18336f, -0.44200f, 0.35322f, -0.26057f, + -0.46517f, 0.31075f, -0.07914f, -0.34164f, -0.24513f, -0.02123f, 0.19108f, 0.44200f, + -0.31461f, -0.20653f, 0.46131f, -0.45358f, 0.39568f, -0.24513f, -0.14090f, 0.11002f, + -0.08300f, -0.26829f, 0.05211f, -0.46517f, -0.09844f, -0.39568f, -0.32619f, -0.06369f, + 0.04825f, -0.07914f, -0.39954f, 0.12160f, 0.29145f, 0.00965f, -0.37638f, 0.32233f, + 0.20267f, -0.17564f, 0.39182f, 0.12160f, 0.18336f, 0.32619f, 0.26057f, 0.49219f, + -0.48447f, -0.20653f, -0.10616f, -0.38796f, 0.31847f, 0.07528f, -0.01737f, 0.44586f, + 0.11774f, 0.02509f, 0.47289f, 0.07142f, 0.33392f, -0.38410f, -0.17950f, 0.28373f, + 0.04825f, -0.07914f, -0.39954f, 0.12160f, 0.29145f, 0.00965f, -0.37638f, 0.32233f, + 0.20267f, -0.17564f, 0.39182f, 0.12160f, 0.18336f, 0.32619f, 0.26057f, 0.49219f + ]; +} From 9ad02108f331abab74d824c16285d31c93d7fdb0 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 2 Sep 2026 20:25:37 +0400 Subject: [PATCH 126/328] Add IJxlImageOutput --- .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 3 +- .../Jxl/Processing/Image/IJxlImageOutput.cs | 54 +++++++++++++++++++ .../Jxl/Processing/Primitives/Delegates.cs | 25 --------- 3 files changed, 55 insertions(+), 27 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Image/IJxlImageOutput.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Primitives/Delegates.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index 6a850d8288..4606b6d338 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -1928,8 +1928,7 @@ public int ProcessCodestream() new PixelCallback( this.imageOutputInitCallback, this.imageOutputRunCallback, - this.imageOutputDestroyCallback, - this.imageOutputInitOpaque), + this.imageOutputDestroyCallback), this.imageOutBuffer, this.imageOutputSize, dimensions.Width, diff --git a/src/ImageSharp/Formats/Jxl/Processing/Image/IJxlImageOutput.cs b/src/ImageSharp/Formats/Jxl/Processing/Image/IJxlImageOutput.cs new file mode 100644 index 0000000000..72d631ff3b --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Image/IJxlImageOutput.cs @@ -0,0 +1,54 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Image; + +/// +/// Abstracts a way for the codec to output images during decoding. +/// +internal interface IJxlImageOutput : IDisposable +{ + /// + /// Gets or sets the pixel format for the output pixels. + /// + public JxlPixelFormat PixelFormat { get; set; } + + /// + /// Gets or sets the output bit depth for unsigned data types. + /// + public int BitsPerSample { get; set; } + + /// + /// Gets or sets the pixel buffer for image output. + /// + public Memory Buffer { get; set; } + + /// + /// Gets or sets length of a row of image buffer in bytes. + /// + public int Stride { get; set; } + + /// + /// Outputs the image. + /// + /// Data where the image should be output. + /// X offset + /// Y offset + /// Pixels to output. + public void Output( + Span data, + int x, + int y, + Span pixels); + + /// + /// Initializes image data. + /// + /// Output image data. + /// Number of pixels a thread processes. + public void Initialize( + Span data, + int numPixelsPerThread); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Primitives/Delegates.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/Delegates.cs deleted file mode 100644 index e0f7c78a33..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Primitives/Delegates.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; - -internal delegate void JxlImageOutputCallback( - Span data, - int x, - int y, - int numPixels, - Span pixels); - -internal delegate void JxlImageOutputInitializeCallback( - Span data, - int numThreads, - int numPixelsPerThread); - -internal delegate void JxlImageOutputRunCallback( - Span data, - int x, - int y, - int numPixels, - Span pixels); - -internal delegate void JxlImageOutputDestroyCallback(Span data); From eead4a93615fc6bd71bfa6563c31a47afe701c7f Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 2 Sep 2026 20:31:31 +0400 Subject: [PATCH 127/328] Add ShouldFlip methods, fix typo --- .../Jxl/IO/Metadata/JxlExifOrientation.cs | 2 +- .../RenderPipeline/WriteToOutputStage.cs | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExifOrientation.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExifOrientation.cs index 406e7e9a1a..fb7d8d7bba 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExifOrientation.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExifOrientation.cs @@ -9,7 +9,7 @@ internal enum JxlExifOrientation : byte FlipHorizontal = 2, Rotate180 = 3, FlipVertical = 4, - Transponse = 5, + Transpose = 5, Rotate90 = 6, AntiTranspose = 7, Rotate270 = 8 diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs index 09426afb2b..e07be6ec06 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs @@ -1,6 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; + namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; internal sealed class WriteToOutputStage @@ -209,4 +212,22 @@ internal sealed class WriteToOutputStage 0.04825f, -0.07914f, -0.39954f, 0.12160f, 0.29145f, 0.00965f, -0.37638f, 0.32233f, 0.20267f, -0.17564f, 0.39182f, 0.12160f, 0.18336f, 0.32619f, 0.26057f, 0.49219f ]; + + private static bool ShouldFlipX(JxlExifOrientation orientation) => + orientation is JxlExifOrientation.FlipHorizontal or + JxlExifOrientation.Rotate180 or + JxlExifOrientation.Rotate270 or + JxlExifOrientation.AntiTranspose; + + private static bool ShouldFlipY(JxlExifOrientation orientation) => + orientation is JxlExifOrientation.FlipVertical or + JxlExifOrientation.Rotate180 or + JxlExifOrientation.Rotate90 or + JxlExifOrientation.AntiTranspose; + + private static bool ShouldTranspose(JxlExifOrientation orientation) => + orientation is JxlExifOrientation.Transpose or + JxlExifOrientation.Rotate90 or + JxlExifOrientation.Rotate270 or + JxlExifOrientation.AntiTranspose; } From a0feb216aecc78cfcdee578380bac221a43accff Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 2 Sep 2026 20:50:24 +0400 Subject: [PATCH 128/328] Add Y'Cb'Cr stage, make Epf1Stage class sealed --- .../Processing/RenderPipeline/Epf1Stage.cs | 2 +- .../RenderPipeline/WriteToOutputStage.cs | 2 + .../Processing/RenderPipeline/YCbCrStage.cs | 67 +++++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/YCbCrStage.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf1Stage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf1Stage.cs index b42513898c..5dc19b6626 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf1Stage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf1Stage.cs @@ -13,7 +13,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; /// /// Edge Preserving Filter (type 1) stage. /// -internal class Epf1Stage : RenderPipelineStageBase +internal sealed class Epf1Stage : RenderPipelineStageBase { private readonly JxlLoopFilter loopFilter; private readonly JxlImageF sigma; diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs index e07be6ec06..ad790ed10a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs @@ -1,8 +1,10 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/YCbCrStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/YCbCrStage.cs new file mode 100644 index 0000000000..6de34c61bf --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/YCbCrStage.cs @@ -0,0 +1,67 @@ +// 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.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +/// +/// SIMD-based conversion from Y'Cb'Cr to RGB pixel buffers. +/// +internal sealed class YCbCrStage : RenderPipelineStageBase +{ + public YCbCrStage(Configuration configuration) + : base(configuration) + { + } + + /// + public override string Name => "YCbCr"; + + /// + public override void ProcessRow(Buffer2D> inputRows, Buffer2D> outputRows, int xExtraLeft, int xExtraRight, int width, int xPos, int yPos) + { + // Vectors for conversion, defined by the ITU + Vector c128 = Vector.Create(128.0f / 255); + Vector crcr = Vector.Create(1.402f); + Vector cgcb = Vector.Create(-0.114f * 1.772f / 0.587f); + Vector cgcr = Vector.Create(-0.299f * 1.402f / 0.587f); + Vector cbcb = Vector.Create(1.772f); + + Span row0 = this.GetInputRow(inputRows, 0, 0); + Span row1 = this.GetInputRow(inputRows, 1, 0); + Span row2 = this.GetInputRow(inputRows, 2, 0); + + // Using refs for better performance + ref float row0Ref = ref MemoryMarshal.GetReference(row0); + ref float row1Ref = ref MemoryMarshal.GetReference(row1); + ref float row2Ref = ref MemoryMarshal.GetReference(row2); + + for (int x = 0; x < width; x += Vector.Count) + { + // Y'Cb'Cr input vectors + Vector yVec = Vector.LoadUnsafe(ref Unsafe.Add(ref row1Ref, x)) + c128; + Vector cbVec = Vector.LoadUnsafe(ref Unsafe.Add(ref row0Ref, x)); + Vector crVec = Vector.LoadUnsafe(ref Unsafe.Add(ref row2Ref, x)); + + // RGB output vectors + Vector rVec = (crcr * crVec) + yVec; + Vector gVec = (cgcr * crVec) + ((cgcb * cbVec) + yVec); + Vector bVec = (cbcb * cbVec) + yVec; + + // Copying to the output... + rVec.StoreUnsafe(ref Unsafe.Add(ref row0Ref, x)); + gVec.StoreUnsafe(ref Unsafe.Add(ref row1Ref, x)); + bVec.StoreUnsafe(ref Unsafe.Add(ref row2Ref, x)); + } + } + + /// + public override RenderPipelineChannelMode GetChannelMode(int channel) => + channel < 3 + ? RenderPipelineChannelMode.InPlace + : RenderPipelineChannelMode.Ignored; +} From 9f3ccfada9e8a33cb18f32cb515499a7452bde8e Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 2 Sep 2026 20:51:53 +0400 Subject: [PATCH 129/328] Use primary constructor --- .../Formats/Jxl/Processing/RenderPipeline/YCbCrStage.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/YCbCrStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/YCbCrStage.cs index 6de34c61bf..7c6ad0362b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/YCbCrStage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/YCbCrStage.cs @@ -11,13 +11,8 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; /// /// SIMD-based conversion from Y'Cb'Cr to RGB pixel buffers. /// -internal sealed class YCbCrStage : RenderPipelineStageBase +internal sealed class YCbCrStage(Configuration configuration) : RenderPipelineStageBase(configuration) { - public YCbCrStage(Configuration configuration) - : base(configuration) - { - } - /// public override string Name => "YCbCr"; From 76ed571b621bfd0f553de107b97cebae5ed481d0 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:06:58 +0400 Subject: [PATCH 130/328] Add Gaborish stage --- .../RenderPipeline/GaborishStage.cs | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/GaborishStage.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/GaborishStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/GaborishStage.cs new file mode 100644 index 0000000000..fbc83ab214 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/GaborishStage.cs @@ -0,0 +1,100 @@ +// 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.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +internal sealed class GaborishStage : RenderPipelineStageBase +{ + private InlineArray9 weights; + + public GaborishStage(Configuration configuration, JxlLoopFilter lf) + : base(configuration) + { + this.Settings = RenderPipelineStageConfiguration.CreateSymmetricBorderOnly(1); + + this.weights[0] = 1; + this.weights[1] = lf.GaborishXWeight1; + this.weights[2] = lf.GaborishXWeight2; + this.weights[3] = 1; + this.weights[4] = lf.GaborishYWeight1; + this.weights[5] = lf.GaborishYWeight2; + this.weights[6] = 1; + this.weights[7] = lf.GaborishBWeight1; + this.weights[8] = lf.GaborishBWeight2; + + // Normalization + for (int c = 0; c < 3; c++) + { + int c3 = c * 3; // prevent repeated multiplication + + float div = this.weights[c3] + (4 * (this.weights[c3 + 1] + this.weights[c3 + 2])); + float mul = 1.0f / div; + + this.weights[c3] *= mul; + this.weights[c3 + 1] *= mul; + this.weights[c3 + 2] *= mul; + } + } + + /// + public override string Name => "Gab"; + + public override void ProcessRow(Buffer2D> inputRows, Buffer2D> outputRows, int xExtraLeft, int xExtraRight, int width, int xPos, int yPos) + { + int xStart = -JxlMath.RoundUpTo(xExtraLeft, Vector.Count); + int xEnd = width + xExtraRight; + + for (int c = 0; c < 3; c++) + { + int c3 = c * 3; // prevent repeated multiplication + + Span rowT = this.GetInputRow(inputRows, c, -1); + Span rowM = this.GetInputRow(inputRows, c, 0); + Span rowB = this.GetInputRow(inputRows, c, 1); + Span rowOut = GetOutputRow(outputRows, c, 0); + + Vector w0 = Vector.Create(this.weights[c3]); + Vector w1 = Vector.Create(this.weights[c3 + 1]); + Vector w2 = Vector.Create(this.weights[c3 + 2]); + + // Ref for performance + ref float refRowT = ref MemoryMarshal.GetReference(rowT); + ref float refRowM = ref MemoryMarshal.GetReference(rowM); + ref float refRowB = ref MemoryMarshal.GetReference(rowB); + ref float refRowOut = ref MemoryMarshal.GetReference(rowOut); + + for (int x = xStart; x < xEnd; x += Vector.Count) + { + Vector t = Vector.LoadUnsafe(ref Unsafe.Add(ref refRowT, x)); + Vector tl = Vector.LoadUnsafe(ref Unsafe.Add(ref refRowT, x - 1)); + Vector tr = Vector.LoadUnsafe(ref Unsafe.Add(ref refRowT, x + 1)); + + Vector m = Vector.LoadUnsafe(ref Unsafe.Add(ref refRowM, x)); + Vector l = Vector.LoadUnsafe(ref Unsafe.Add(ref refRowM, x - 1)); + Vector r = Vector.LoadUnsafe(ref Unsafe.Add(ref refRowM, x + 1)); + + Vector b = Vector.LoadUnsafe(ref Unsafe.Add(ref refRowB, x)); + Vector bl = Vector.LoadUnsafe(ref Unsafe.Add(ref refRowB, x - 1)); + Vector br = Vector.LoadUnsafe(ref Unsafe.Add(ref refRowB, x + 1)); + + Vector sum0 = m; + Vector sum1 = (l + r) + (t + b); + Vector sum2 = (tl + tr) + (bl + br); + + Vector pixels = (sum2 * w2) + ((sum1 * w1) + (sum0 * w0)); + pixels.StoreUnsafe(ref Unsafe.Add(ref refRowOut, x)); + } + } + } + + /// + public override RenderPipelineChannelMode GetChannelMode(int channel) => + channel < 3 + ? RenderPipelineChannelMode.InPlace + : RenderPipelineChannelMode.Ignored; +} From 64edab5721df90ee4adf86b4740444d7b41de2ad Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:20:52 +0400 Subject: [PATCH 131/328] Add patch dictionary stage --- .../Processing/Decoder/JxlPatchDictionary.cs | 2 +- .../RenderPipeline/PatchDictionaryStage.cs | 40 +++++++++++++++++++ .../RenderPipeline/RenderPipelineStageBase.cs | 6 +++ 3 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/PatchDictionaryStage.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchDictionary.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchDictionary.cs index c990288589..d91c41e38e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchDictionary.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchDictionary.cs @@ -53,7 +53,7 @@ public void Decode( this.blendingsStride = (int)(numExtraChannels + 1); List contextMap = []; - var code = new JxlAnsCode(); + JxlAnsCode code = new(); var status = DecodeHistograms( memoryManager, diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/PatchDictionaryStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/PatchDictionaryStage.cs new file mode 100644 index 0000000000..0e6bf82745 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/PatchDictionaryStage.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Drawing; +using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +internal sealed class PatchDictionaryStage(Configuration configuration, JxlPatchDictionary patches, List extraChannelInfos) + : RenderPipelineStageBase(configuration) +{ + /// + public override string Name => "Patches"; + + /// + public override void ProcessRow(Buffer2D> inputRows, Buffer2D> outputRows, int xExtraLeft, int xExtraRight, int width, int xPos, int yPos) + { + int channels = 3 + extraChannelInfos.Count; + + Span> rowPtrs = new Memory[channels]; + + for (int i = 0; i < channels; i++) + { + rowPtrs[i] = this.GetInputRowMemory(inputRows, i, 0, xExtraLeft); + } + + return patches.AddOneRow(rowPtrs, yPos, xPos - xExtraLeft, width + xExtraLeft + xExtraRight, extraChannelInfos); + } + + /// + public override RenderPipelineChannelMode GetChannelMode(int channel) + { + int numChannels = 3 + extraChannelInfos.Count; + return channel < numChannels + ? RenderPipelineChannelMode.InPlace + : RenderPipelineChannelMode.Ignored; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineStageBase.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineStageBase.cs index b02b62db7a..07cb0f59db 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineStageBase.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineStageBase.cs @@ -71,9 +71,15 @@ public virtual void SetInputSizes(Span inputSizes) public Span GetInputRow(Buffer2D> inputRows, int c, int offset) => inputRows[c, this.Settings.BorderY + offset].Span[RenderPipelineXOffset..]; + public Span GetInputRow(Buffer2D> inputRows, int c, int offset, int xExtraLeft) + => inputRows[c, this.Settings.BorderY + offset].Span[(RenderPipelineXOffset - xExtraLeft)..]; + public Memory GetInputRowMemory(Buffer2D> inputRows, int c, int offset) => inputRows[c, this.Settings.BorderY + offset][RenderPipelineXOffset..]; + public Memory GetInputRowMemory(Buffer2D> inputRows, int c, int offset, int xExtraLeft) + => inputRows[c, this.Settings.BorderY + offset][(RenderPipelineXOffset - xExtraLeft)..]; + public static Span GetOutputRow(Buffer2D> outputRows, int c, int offset) => outputRows[c, offset].Span[RenderPipelineXOffset..]; From e0305d78c5dc0986a860034e3c5d859c1a406528 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:58:14 +0400 Subject: [PATCH 132/328] Partial adaptive quantizer encoder --- .../Encoder/JxlAdaptiveQuantization.cs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs new file mode 100644 index 0000000000..00150b3dd3 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs @@ -0,0 +1,44 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; + +/// +/// Adaptive quantization encoder +/// +internal static class JxlAdaptiveQuantization +{ + // Scaling differences between JPEG XL and Butteraugli + private const float SGMul = 226.77216153508914f; + private const float SGMul2 = 1f / 73.377132366608819f; + + // Includes correlation factor for std::log -> log2 + private const float SGRetMul = SGMul2 * 18.6580932135f * JxlMath.InverseLog2E; + private const float SGVOffset = 7.7825991679894591f; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float ComputeMaskForAcStrategyUse(float outputValue) + { + const float multiplier = 1f; + const float offset = 0.001f; + + return multiplier / (outputValue + offset); + } + + public static float RatioOfDerivativesOfCubicRootToSimpleGamma(float v, bool invert = false) + { + float epsilon = 1e-2f; + v = Math.Max(0, v); // cannot be < 0 + const float numMul = SGRetMul * 3 * SGMul; + float voffset = (SGVOffset * JxlMath.InverseLog2E) + epsilon; + const float denMul = JxlMath.InverseLog2E * SGMul; + + float v2 = v * v; + float num = (numMul * v2) + epsilon; + float den = ((denMul * v) * v2) + voffset; + + return invert ? num / den : den / num; + } +} From 5de2701e1c1061be684d95bf4044899539b135cb Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 2 Sep 2026 22:23:34 +0400 Subject: [PATCH 133/328] Add JxlToJpegDecoder static methods --- .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 13 +- .../Processing/Decoder/JxlToJpegDecoder.cs | 150 ++++++++++++++++++ 2 files changed, 154 insertions(+), 9 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlToJpegDecoder.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index 4606b6d338..9fefcde018 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -363,11 +363,6 @@ internal sealed class JxlDecoderCore : ImageDecoderCore, IDisposable /// private readonly JxlBoxContentDecoder? boxContentDecoder; - /// - /// Decodes JPEG XL to JPEG. - /// - private JxlToJpegDecoder? jpegDecoder; - private readonly JxlBoxContentDecoder? metadataDecoder; /// @@ -2246,12 +2241,12 @@ public int ProcessBoxes(Stream stream) if (this.reconstructionExifSize > 0) { - JxlToJpegDecoder.SetExif(this.exifMetadata!.Memory, jpegData); + JxlToJpegDecoder.TrySetExif(this.exifMetadata!.Memory.Span, jpegData); } if (this.reconstructionXmpSize > 0) { - JxlToJpegDecoder.SetXmp(this.xmpMetadata!.Memory, jpegData); + JxlToJpegDecoder.TrySetXmp(this.xmpMetadata!.Memory.Span, jpegData); } this.reconstructionOutputJpeg = JpegReconstructionStage.Output; @@ -2592,7 +2587,7 @@ public int ProcessBoxes(Stream stream) throw new InvalidOperationException("Only one EXIF marker for JPEG reconstruction can be present"); } - if (JxlToJpegDecoder.ExifBoxContentSize(jpegData, ref this.reconstructionExifSize) != Success) + if (!JxlToJpegDecoder.ExifBoxContentSize(jpegData, ref this.reconstructionExifSize)) { throw new InvalidOperationException("Invalid jbrd EXIF size"); } @@ -2605,7 +2600,7 @@ public int ProcessBoxes(Stream stream) throw new InvalidOperationException("Only one XMP marker for JPEG reconstruction can be present"); } - if (JxlToJpegDecoder.XmlBoxContentSize(jpegData, ref this.reconstructionXmpSize) != Success) + if (!JxlToJpegDecoder.XmpBoxContentSize(jpegData, ref this.reconstructionXmpSize)) { throw new InvalidOperationException("Invalid jbrd XMP size"); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlToJpegDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlToJpegDecoder.cs new file mode 100644 index 0000000000..995b74852a --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlToJpegDecoder.cs @@ -0,0 +1,150 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +internal class JxlToJpegDecoder +{ + /// + /// Returns the number of EXIF markers in the JPEG file. + /// + /// Input parsed JPEG data + /// Number of EXIFs in the JPEG + public static int NumExifMarkers(JpegData jpegData) => jpegData.AppMarkerTypes.Count(x => x == JpegAppMarkerType.Exif); + + /// + /// Returns the number of XMP markers in the JPEG file. + /// + /// Input parsed JPEG data + /// Number of XMPs in the JPEG + public static int NumXmpMarkers(JpegData jpegData) => jpegData.AppMarkerTypes.Count(x => x == JpegAppMarkerType.Xmp); + + /// + /// Attempts to set EXIF data in the JPEG file. + /// + /// EXIF data + /// JPEG file for EXIF data + /// If EXIF data was set, true; returns false if no EXIF marker is present, or is present but not enough data + public static bool TrySetExif(Span data, JpegData jpegData) + { + int size = data.Length; + ReadOnlySpan exifTag = JpegDataConstants.ExifTag; + int exifTagSize = exifTag.Length; + + for (int i = 0; i < jpegData.AppData.Count; ++i) + { + if (jpegData.AppMarkerTypes[i] == JpegAppMarkerType.Exif) + { + Span dataSpan = CollectionsMarshal.AsSpan(jpegData.AppData[i]); + + if (dataSpan.Length != size + 3 + exifTagSize - 4) + { + return false; + } + + // The first 9 bytes are used for JPEG marker header. + dataSpan[0] = 0xE1; + + // The second and third byte are already filled in correctly + exifTag.CopyTo(dataSpan[3..]); + + // The first 4 bytes are the TIFF header from the box contents, and are + // not included in the JPEG + data[4..].CopyTo(dataSpan[(3 + exifTagSize)..]); + + return true; + } + } + + return false; + } + + /// + /// Attempts to set XMP data in the JPEG file. + /// + /// XMP data + /// JPEG file for XMP data + /// If XMP data was set, true; returns false if no XMP marker is present, or is present but not enough data + public static bool TrySetXmp(Span data, JpegData jpegData) + { + int size = data.Length; + ReadOnlySpan xmpTag = JpegDataConstants.XmpTag; + int xmpTagSize = xmpTag.Length; + + for (int i = 0; i < jpegData.AppData.Count; ++i) + { + if (jpegData.AppMarkerTypes[i] == JpegAppMarkerType.Xmp) + { + Span dataSpan = CollectionsMarshal.AsSpan(jpegData.AppData[i]); + + if (dataSpan.Length != size + 3 + xmpTagSize) + { + return false; + } + + // The first 9 bytes are used for JPEG marker header. + dataSpan[0] = 0xE1; + + // The second and third byte are already filled in correctly + xmpTag.CopyTo(dataSpan[3..]); + + data.CopyTo(dataSpan[(3 + xmpTagSize)..]); + + return true; + } + } + + return false; + } + + public static bool ExifBoxContentSize(JpegData jpegData, ref long size) + { + size = 0; + int exifTagLength = JpegDataConstants.ExifTag.Length; + + for (int i = 0; i < jpegData.AppData.Count; ++i) + { + if (jpegData.AppMarkerTypes[i] == JpegAppMarkerType.Exif) + { + if (jpegData.AppData[i].Count < 3 + exifTagLength) + { + // too small for app marker header + return false; + } + + // The first 4 bytes are the TIFF header from the box contents, and are + // not included in the JPEG + size = jpegData.AppData[i].Count + 4 - 3 - exifTagLength; + return true; + } + } + + return false; + } + + public static bool XmpBoxContentSize(JpegData jpegData, ref long size) + { + size = 0; + int xmpTagLength = JpegDataConstants.XmpTag.Length; + + for (int i = 0; i < jpegData.AppData.Count; ++i) + { + if (jpegData.AppMarkerTypes[i] == JpegAppMarkerType.Xmp) + { + if (jpegData.AppData[i].Count < 3 + xmpTagLength) + { + // too small for app marker header + return false; + } + + size = jpegData.AppData[i].Count - 3 - xmpTagLength; + return true; + } + } + + return false; + } +} From 4476abd507ccecee159ad1a5f5e510c11b4bf2d2 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 2 Sep 2026 23:40:18 +0400 Subject: [PATCH 134/328] Add 3 missing methods to JxlFrameDecoder, add spline and spotcolor render pipeline stages --- .../Jxl/Processing/Decoder/JxlFrameDecoder.cs | 25 +++++++++++++ .../Processing/RenderPipeline/SplineStage.cs | 21 +++++++++++ .../RenderPipeline/SpotColorStage.cs | 37 +++++++++++++++++++ 3 files changed, 83 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/SplineStage.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/SpotColorStage.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs index 9b8e9b0f28..5e0eefc9ef 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.IO; using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; @@ -133,4 +134,28 @@ public static void DecodeFrame(JxlPassesDecoderState decoderState, Stream stream frameDecoder.FinalizeFrame(); } + + private static int BytesPerChannel(JxlDataType dataType) => + dataType == JxlDataType.Byte ? 1 + : dataType == JxlDataType.Single + ? 4 + : 2; + + private int GetStorageLocation(int thread, int task) => this.useTaskId ? task : thread; + + private void PrepareStorage(int numThreads, int numTasks) + { + int storageSize = Math.Min(numThreads, numTasks); + if (storageSize > this.groupDecoderCaches.Count) + { + this.groupDecoderCaches = [.. this.groupDecoderCaches.Take(storageSize)]; + } + + this.useTaskId = numThreads > numTasks; + bool useNoise = (this.frameHeader.Flags & (int)JxlFrameHeaderFlags.Noise) != 0; + bool useGroupIds = this.modularFrameDecoder.UsesFullImage && (this.frameHeader.Encoding == JxlFrameEncoding.VarDct || useNoise); + + this.decoderState.RenderPipeline?.PrepareForThreads(storageSize, useGroupIds); + this.decoderState.Upsampler8x.PrepareForThreads(numThreads); + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/SplineStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/SplineStage.cs new file mode 100644 index 0000000000..39864265d0 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/SplineStage.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +internal sealed class SplineStage(Configuration configuration, JxlSplines splines) : RenderPipelineStageBase(configuration) +{ + public override string Name => "Splines"; + + public override void ProcessRow(Buffer2D> inputRows, Buffer2D> outputRows, int xExtraLeft, int xExtraRight, int width, int xPos, int yPos) + { + Span rowX = this.GetInputRow(inputRows, 0, 0, xExtraLeft); + Span rowY = this.GetInputRow(inputRows, 1, 0, xExtraLeft); + Span rowB = this.GetInputRow(inputRows, 2, 0, xExtraLeft); + splines.AddToRow(rowX, rowY, rowB, yPos, xPos - xExtraLeft, xPos + width + xExtraRight); + } + + public override RenderPipelineChannelMode GetChannelMode(int channel) => channel < 3 ? RenderPipelineChannelMode.InPlace : RenderPipelineChannelMode.Ignored; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/SpotColorStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/SpotColorStage.cs new file mode 100644 index 0000000000..6b0fc97ed8 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/SpotColorStage.cs @@ -0,0 +1,37 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +internal sealed class SpotColorStage(Configuration configuration, int spotColorOffset, Memory spotColor) + : RenderPipelineStageBase(configuration) +{ + private readonly int spotC = 3 + spotColorOffset; + + public override string Name => "Spot"; + + public override void ProcessRow(Buffer2D> inputRows, Buffer2D> outputRows, int xExtraLeft, int xExtraRight, int width, int xPos, int yPos) + { + Span spotColors = spotColor.Span; + + float scale = 0; + for (int c = 0; c < 3; c++) + { + Span p = this.GetInputRow(inputRows, c, 0); + Span s = this.GetInputRow(inputRows, this.spotC, 0); + + for (int x = 0; x < width; x++) + { + float mix = scale * s[x]; + p[x] = (mix * spotColors[c]) + ((1.0f - mix) * p[x]); + } + } + } + + public override RenderPipelineChannelMode GetChannelMode(int channel) + => channel < 3 ? RenderPipelineChannelMode.InPlace + : channel == this.spotC ? RenderPipelineChannelMode.Input + : RenderPipelineChannelMode.Ignored; +} From 57392184374eb043c0342415380f3ec322795935 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 3 Sep 2026 21:23:22 +0400 Subject: [PATCH 135/328] Add a few CMS transfer functions (incomplete) --- .../JxlBt709TransferFunction.cs | 20 +++ .../JxlHybridLogGammaTransferFunction.cs | 53 ++++++++ .../JxlHybridLogGammaTransferFunctionBase.cs | 114 ++++++++++++++++++ ...ceptualQuantizationTransferFunctionBase.cs | 54 +++++++++ .../Formats/Jxl/Processing/JxlSimdUtils.cs | 8 ++ .../RenderPipeline/PatchDictionaryStage.cs | 1 - .../RenderPipeline/WriteToOutputStage.cs | 3 - 7 files changed, 249 insertions(+), 4 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlBt709TransferFunction.cs create mode 100644 src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlHybridLogGammaTransferFunction.cs create mode 100644 src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlHybridLogGammaTransferFunctionBase.cs create mode 100644 src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlPerceptualQuantizationTransferFunctionBase.cs diff --git a/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlBt709TransferFunction.cs b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlBt709TransferFunction.cs new file mode 100644 index 0000000000..99b6548b60 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlBt709TransferFunction.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms.TransferFunctions; + +/// +/// ITU-R BT.709 transfer function +/// +internal static class JxlBt709TransferFunction +{ + public static double EncodedFromDisplay(double d) + { + if (d < Threshold) + { + return MulLow * d; + } + + return (MulHi * Math.Pow(d, PowHi)) + Sub; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlHybridLogGammaTransferFunction.cs b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlHybridLogGammaTransferFunction.cs new file mode 100644 index 0000000000..3957c3ac7b --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlHybridLogGammaTransferFunction.cs @@ -0,0 +1,53 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Formats.Jxl.Processing; + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms.TransferFunctions; + +internal sealed class JxlHybridLogGammaTransferFunction : JxlHybridLogGammaTransferFunctionBase +{ + private const float HiAdd = B * Inverse12; + private const float HiMul = 0.003639807079052639f; // MathF.Exp(-C * RA) * Inverse12 + private const float HiPow = 8.067285659607931f; // RA * JxlMath.InverseLog2E + + /// + /// Initializes a new instance of the class. + /// + /// + /// Use static methods. Don't instantiate this class. + /// + private JxlHybridLogGammaTransferFunction() + { + } + + public static Vector EncodedFromDisplay(Vector x) + { + Vector sign = Vector.Create(0x80000000u).As(); + Vector originalSign = x & sign; + x = Vector.AndNot(sign, x); + Vector belowInverse12 = Vector.LessThan(x, Vector.Create(Inverse12)); + + Vector lo = Vector.SquareRoot(Vector.Create(3.0f) * x); + Vector hi = (Vector.Create(A * JxlMath.InverseLog2E) * Vector.Log2((Vector.Create(12f) * x) + Vector.Create(-B))) + Vector.Create(C); + Vector magnitude = Vector.ConditionalSelect(belowInverse12, lo, hi); + return Vector.AndNot(sign, magnitude) | originalSign; + } + + public static Vector DisplayFromEncoded(Vector x) + { + Vector sign = Vector.Create(0x80000000u).As(); + Vector originalSign = x & sign; + x = Vector.AndNot(sign, x); + Vector below05 = Vector.LessThan(x, Vector.Create(0.5f)); + + Vector lo = x * (x * Vector.Create(1f / 3f)); + Vector hi = (Pow2(x * Vector.Create(HiPow)) * Vector.Create(HiMul)) + Vector.Create(HiAdd); + Vector magnitude = Vector.ConditionalSelect(below05, lo, hi); + + return Vector.AndNot(sign, magnitude) | originalSign; + } + + private static Vector Pow2(Vector x) => x * x; +} diff --git a/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlHybridLogGammaTransferFunctionBase.cs b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlHybridLogGammaTransferFunctionBase.cs new file mode 100644 index 0000000000..795db106c3 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlHybridLogGammaTransferFunctionBase.cs @@ -0,0 +1,114 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms.TransferFunctions; + +/// +/// Base class for HLG transfer function. +/// +internal abstract class JxlHybridLogGammaTransferFunctionBase +{ + // Shared constants used by transfer functions + protected const float A = 0.17883277f; + protected const float RA = 1.0f / A; + protected const float B = 1 - (4 * A); + protected const float C = 0.5599107295f; + protected const float Inverse12 = 1.0f / 12.0f; + + /// + /// Converts encoded signal to display signal. + /// + /// The encoded signal + /// The display signal + protected static double DisplayFromEncoded(double encoded) => Ootf(InverseOotf(encoded)); + + /// + /// Converts display signal to encoded signal. + /// + /// The display signal + /// The encoded signal + protected static double EncodedFromDisplay(double display) => Oetf(InverseOetf(display)); + + /// + /// Opto-Electronic Transfer Function - converts + /// real-world scene light (s) into a digital video + /// signal inside a camera. + /// + /// + /// + /// + /// Scene light + /// Digital video signal + private static double Oetf(double s) + { + if (s == 0) + { + return 0; + } + + double originalSign = s; + + s = Math.Abs(s); + + if (s <= Inverse12) + { + return Math.CopySign(Math.Sqrt(3.0 * s), originalSign); + } + + double e = (A * Math.Log((12 * s) - B)) + C; + DebugGuard.MustBeGreaterThan(e, 0.0, nameof(e)); + + return Math.CopySign(e, originalSign); + } + + /// + /// Inverse Opto-Electronic Transfer Function - converts + /// digital video signal into a real-world scene light. + /// + /// + /// + /// + /// Digital video signal + /// Scene light + private static double InverseOetf(double e) + { + if (e == 0) + { + return 0; + } + + double originalSign = e; + + e = Math.Abs(e); + + if (e <= 0.5) + { + return Math.CopySign(e * e * (1.0 / 3), originalSign); + } + + double s = (Math.Exp((e - C) * RA) + B) * Inverse12; + DebugGuard.MustBeGreaterThan(s, 0.0, nameof(s)); + + return Math.CopySign(s, originalSign); + } + + /// + /// Opto-Optical Transfer Function - as-is. + /// + /// + /// + /// + /// Input signal + /// Digital video signal + private static double Ootf(double s) => s; + + /// + /// Inverse Opto-Optical Transfer Function - as-is. + /// + /// + /// + /// + /// Digital video signal + /// Scene light + private static double InverseOotf(double s) => s; +} diff --git a/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlPerceptualQuantizationTransferFunctionBase.cs b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlPerceptualQuantizationTransferFunctionBase.cs new file mode 100644 index 0000000000..eea7b06560 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlPerceptualQuantizationTransferFunctionBase.cs @@ -0,0 +1,54 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms.TransferFunctions; + +/// +/// Base class for PQ transfer function. +/// +internal abstract class JxlPerceptualQuantizationTransferFunctionBase +{ + private const double M1 = 2610.0 / 16384; + private const double M2 = (2523.0 / 4096) * 128; + private const double C1 = 3424.0 / 4096; + private const double C2 = (2413.0 / 4096) * 32; + private const double C3 = (2392.0 / 4096) * 32; + + protected static double DisplayFromEncoded(float displayIntensityTarget, double e) + { + if (e == 0.0) + { + return 0.0; + } + + double originalSign = e; + + e = Math.Abs(e); + + double xp = Math.Pow(e, 1.0 / M2); + double num = Math.Max(xp - C1, 0.0); + double den = C2 - (C3 * xp); + double d = Math.Pow(num / den, 1.0 / M1); + + return Math.CopySign(d * (10000.0 / displayIntensityTarget), originalSign); + } + + protected static double EncodedFromDisplay(float displayIntensityTarget, double d) + { + if (d == 0.0) + { + return 0.0; + } + + double originalSign = d; + + d = Math.Abs(d); + + double xp = Math.Pow(d * (displayIntensityTarget * (1 / 10000)), M1); + double num = C1 + (xp * C2); + double den = 1.0 + (xp * C3); + double e = Math.Pow(num / den, M2); + + return Math.CopySign(e, originalSign); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs index dd8e7314ee..8da98bbf4f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; @@ -101,4 +102,11 @@ public static void Transpose8x8Block(Span fromSpan, Span toSpan, int s } } } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector Pow(Vector @base, Vector exponent) + { + Vector vec = Vector.Log2(@base) * exponent; + return vec * vec; + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/PatchDictionaryStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/PatchDictionaryStage.cs index 0e6bf82745..f2f2f0054f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/PatchDictionaryStage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/PatchDictionaryStage.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Drawing; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; using SixLabors.ImageSharp.Memory; diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs index ad790ed10a..d442faeac6 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs @@ -1,10 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Numerics; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; -using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; -using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; From 7880fb290e94f14fb1734f773c748487fa2a0cf4 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 3 Sep 2026 22:07:56 +0400 Subject: [PATCH 136/328] Add SIMD float->Half conversion This is needed for WriteToOutputStage which uses demote from float to Half --- .../Jxl/Processing/Primitives/JxlHalfUtils.cs | 178 ++++++++++++++++++ .../RenderPipeline/WriteToOutputStage.cs | 5 + 2 files changed, 183 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlHalfUtils.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlHalfUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlHalfUtils.cs new file mode 100644 index 0000000000..49faab821a --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlHalfUtils.cs @@ -0,0 +1,178 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + +internal static class JxlHalfUtils +{ + /* + * SIMD version of the following bit-twiddling implementation + * of float to Half conversion, because Vector doesn't + * have a method like this (only reinterpretation, we want + * conversion). + public static ushort ConvertSingleToHalf(float f) + { + uint x = BitConverter.SingleToUInt32Bits(f); + + uint sign = (x >> 16) & 0x8000; // sign bit + int exp = (int)((x >> 23) & 0xFF) - 127 + 15; // exponent rebias + uint mantissa = x & 0x007FFFFF; // mantissa bits + + if (exp <= 0) + { + // Subnormal or zero + if (exp < -10) + { + return (ushort)sign; + } + + mantissa = (mantissa | 0x00800000) >> (1 - exp); + return (ushort)(sign | (mantissa >> 13)); + } + else if (exp >= 31) + { + // Inf or NaN + return (ushort)(sign | 0x7C00 | (mantissa >> 13)); + } + else + { + // Normalized + return (ushort)(sign | ((uint)exp << 10) | (mantissa >> 13)); + } + } + */ + public static Vector ConvertSingleToHalf(Vector f) + { + Vector x = f.As(); + + Vector sign = (x >> 16) & Vector.Create(0x8000u); + Vector exponent = ((x >> 23) & Vector.Create(0xFFu)).As() - Vector.Create(127) + Vector.Create(15); + Vector mantissa = x & Vector.Create(0x007FFFFFu); + + Vector results = Vector.Zero; + + Vector lessThanMinus10 = Vector.LessThan(exponent, Vector.Create(-10)).As(); + Vector gte31 = Vector.GreaterThanOrEqual(exponent, Vector.Create(31)).As(); + Vector notLtMinus10OrGte31 = ~(lessThanMinus10 | gte31); + + // Subnormal or zero (exp < -10) = sign + results = Vector.ConditionalSelect(lessThanMinus10, sign, results); + + // exp <= 0 = (ushort)(sign | (((mantissa | 0x00800000) >> (1 - exp)) >> 13)); + results = Vector.ConditionalSelect( + Vector.LessThanOrEqual(exponent, Vector.Zero).As(), + sign | (ShiftRightAll(mantissa.As() | Vector.Create(0x00800000), Vector.One - exponent) >> 13).As(), + results); + + // exp >= 31 = (ushort)(sign | 0x7C00 | (mantissa >> 13)) + results = Vector.ConditionalSelect( + gte31, + sign | Vector.Create(0x7C00u) | (mantissa >> 13), + results); + + // anything else - normalized + results = Vector.ConditionalSelect( + notLtMinus10OrGte31, + sign | (exponent.As() << 10) | (mantissa >> 13), + results); + + return VectorUInt32ToUInt16(results); + } + + private static Vector VectorUInt32ToUInt16(Vector uint32) + { + Vector clipped = uint32 & Vector.Create(0xFFFFu); + return clipped.As(); + } + + /// + /// Vectors don't support shifting right using shift value as vector. + /// This is a hack to do this. + /// + /// Kind of vector + /// Input vector + /// Vectors with shift value for each corresponding item. + /// Shifted vectors + private static Vector ShiftRightAll(Vector vec, Vector shiftVec) + where T : unmanaged, IShiftOperators + { + Span firstVec = stackalloc T[Vector.Count]; + Span secondVec = stackalloc T[Vector.Count]; + + vec.CopyTo(firstVec); + shiftVec.CopyTo(secondVec); + + ref T firstRef = ref MemoryMarshal.GetReference(firstVec); + ref T secondRef = ref MemoryMarshal.GetReference(secondVec); + + int count = Vector.Count; + + if (count == 1) + { + return Vector.Create(firstRef >>> secondRef); + } + else if (count == 2) + { + firstRef >>>= secondRef; + Unsafe.Add(ref firstRef, 1) >>>= Unsafe.Add(ref secondRef, 1); + return Vector.Create(firstVec); + } + else if (count == 4) + { + firstRef >>>= secondRef; + Unsafe.Add(ref firstRef, 1) >>>= Unsafe.Add(ref secondRef, 1); + Unsafe.Add(ref firstRef, 2) >>>= Unsafe.Add(ref secondRef, 2); + Unsafe.Add(ref firstRef, 3) >>>= Unsafe.Add(ref secondRef, 3); + return Vector.Create(firstVec); + } + else if (count == 8) + { + firstRef >>>= secondRef; + Unsafe.Add(ref firstRef, 1) >>>= Unsafe.Add(ref secondRef, 1); + Unsafe.Add(ref firstRef, 2) >>>= Unsafe.Add(ref secondRef, 2); + Unsafe.Add(ref firstRef, 3) >>>= Unsafe.Add(ref secondRef, 3); + Unsafe.Add(ref firstRef, 4) >>>= Unsafe.Add(ref secondRef, 4); + Unsafe.Add(ref firstRef, 5) >>>= Unsafe.Add(ref secondRef, 5); + Unsafe.Add(ref firstRef, 6) >>>= Unsafe.Add(ref secondRef, 6); + Unsafe.Add(ref firstRef, 7) >>>= Unsafe.Add(ref secondRef, 7); + return Vector.Create(firstVec); + } + else if (count == 16) + { + firstRef >>>= secondRef; + Unsafe.Add(ref firstRef, 1) >>>= Unsafe.Add(ref secondRef, 1); + Unsafe.Add(ref firstRef, 2) >>>= Unsafe.Add(ref secondRef, 2); + Unsafe.Add(ref firstRef, 3) >>>= Unsafe.Add(ref secondRef, 3); + Unsafe.Add(ref firstRef, 4) >>>= Unsafe.Add(ref secondRef, 4); + Unsafe.Add(ref firstRef, 5) >>>= Unsafe.Add(ref secondRef, 5); + Unsafe.Add(ref firstRef, 6) >>>= Unsafe.Add(ref secondRef, 6); + Unsafe.Add(ref firstRef, 7) >>>= Unsafe.Add(ref secondRef, 7); + Unsafe.Add(ref firstRef, 8) >>>= Unsafe.Add(ref secondRef, 8); + Unsafe.Add(ref firstRef, 9) >>>= Unsafe.Add(ref secondRef, 9); + Unsafe.Add(ref firstRef, 10) >>>= Unsafe.Add(ref secondRef, 10); + Unsafe.Add(ref firstRef, 11) >>>= Unsafe.Add(ref secondRef, 11); + Unsafe.Add(ref firstRef, 12) >>>= Unsafe.Add(ref secondRef, 12); + Unsafe.Add(ref firstRef, 13) >>>= Unsafe.Add(ref secondRef, 13); + Unsafe.Add(ref firstRef, 14) >>>= Unsafe.Add(ref secondRef, 14); + Unsafe.Add(ref firstRef, 15) >>>= Unsafe.Add(ref secondRef, 15); + return Vector.Create(firstVec); + } + else + { + // Vector too large, use a (slightly) slower loop + // instead of duplicating too much. + firstRef >>>= secondRef; + + for (int i = 1; i < count; i++) + { + Unsafe.Add(ref firstRef, i) >>>= Unsafe.Add(ref secondRef, i); + } + + return Vector.Create(firstVec); + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs index d442faeac6..2d1b989c9a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs @@ -1,7 +1,12 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; From 5fe70bac5b609eefd693f9cf1829005304152349 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 3 Sep 2026 22:32:28 +0400 Subject: [PATCH 137/328] Add photon noise encoder --- .../Encoder/Noise/JxlPhotonNoise.cs | 60 +++++++++++++++++++ .../Jxl/Processing/Noise/JxlNoiseConstants.cs | 13 ++++ .../Processing/Noise/JxlNoiseParameters.cs | 2 +- 3 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlPhotonNoise.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Noise/JxlNoiseConstants.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlPhotonNoise.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlPhotonNoise.cs new file mode 100644 index 0000000000..ce48a39e32 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlPhotonNoise.cs @@ -0,0 +1,60 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Jxl.Cms; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Noise; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Noise; + +internal static class JxlPhotonNoise +{ + /// + /// Assumes a daylight-like spectrum. + /// + private const float PhotonsPerLxSPerUm2 = 11260; + + /// + /// Order of magnitude for cameras in the 2010-2020 decade, + /// taking the CFA into account. + /// + private const float EffectiveQuantumEfficiency = 0.20f; + + private const float PhotoResponseNonUniformity = 0.005f; + + private const float InputReferredReadNoise = 3; + + private const float SensorAreaUm2 = 36000f * 24000; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float Square(float x) => x * x; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float Cube(float x) => x * x * x; + + public static JxlNoiseParameters SimulatePhotonNoise(int xSize, int ySize, float iso) + { + float opsinAbsorbanceBiasCbrt = MathF.Cbrt(JxlOpsinConstants.OpsinAbsorbanceBias1); + float h18 = 10f / iso; + float pixelAreaUm2 = SensorAreaUm2 / (xSize * ySize); + float electronsPerPixel18 = EffectiveQuantumEfficiency * PhotonsPerLxSPerUm2 * h18 * pixelAreaUm2; + JxlNoiseParameters parameters = new(); + Span lookup = parameters.Lookup; // Faster than float[] + + for (int i = 0; i < JxlNoiseParameters.NoisePoints; ++i) + { + float scaledIndex = i / (JxlNoiseParameters.NoisePoints - 2f); + float y = 2 * scaledIndex; + float linear = MathF.Max(0f, Cube(y - opsinAbsorbanceBiasCbrt) + JxlOpsinConstants.OpsinAbsorbanceBias1); + float electronsPerPixel = electronsPerPixel18 * (linear / 0.18f); + float noise = MathF.Sqrt(Square(InputReferredReadNoise) + electronsPerPixel + Square(PhotoResponseNonUniformity * electronsPerPixel)); + float linearNoise = noise * (0.18f / electronsPerPixel18); + float opsinDerivative = (1f / 3) / Square(MathF.Sqrt(linear - JxlOpsinConstants.OpsinAbsorbanceBias1)); + float opsinNoise = linearNoise * opsinDerivative; + + lookup[i] = Math.Clamp(opsinNoise / (0.22f * MathF.Sqrt(2f) * 1.13f), 0f, JxlNoiseConstants.NoiseLutMax); + } + + return parameters; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Noise/JxlNoiseConstants.cs b/src/ImageSharp/Formats/Jxl/Processing/Noise/JxlNoiseConstants.cs new file mode 100644 index 0000000000..87027aad94 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Noise/JxlNoiseConstants.cs @@ -0,0 +1,13 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Noise; + +/// +/// Constants used by noise processing. +/// +internal static class JxlNoiseConstants +{ + public const float Precision = 1024f; + public const float NoiseLutMax = 1023.4999f / Precision; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Noise/JxlNoiseParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/Noise/JxlNoiseParameters.cs index de8072c3ee..9fef114a25 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Noise/JxlNoiseParameters.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Noise/JxlNoiseParameters.cs @@ -11,5 +11,5 @@ internal sealed class JxlNoiseParameters public bool ContainsAny => this.Lookup.Any(x => MathF.Abs(x) > 1e-3f); - public void Clear() => Array.Fill(this.Lookup, 0f); + public void Clear() => this.Lookup.AsSpan().Clear(); } From bedf8eccac278da36e1defee3e6d794d3a0fe4e1 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Fri, 4 Sep 2026 21:57:14 +0400 Subject: [PATCH 138/328] Add tests, complete splines, SIMD-accelerate transpose, add a few SIMD methods, add SIMD-based quantization, XorShift constructor, heuristics (incomplete), spline encoder, v256/v128 support to DCT output/source, JxlBlending (blending.cc, blending.h) --- src/ImageSharp/Common/Helpers/SimdUtils.cs | 39 +- .../Common/Helpers/Vector128Utilities.cs | 40 ++ .../Common/Helpers/Vector256Utilities.cs | 40 ++ .../Formats/Jxl/Memory/JxlImage3{T}.cs | 33 ++ .../Processing/AcStrategy/JxlAcStrategyRow.cs | 10 +- .../Blending/JxlAlphaBlendingInputLayer.cs | 10 +- .../Blending/JxlAlphaBlendingOutput.cs | 10 +- .../Jxl/Processing/Blending/JxlBlending.cs | 368 ++++++++++++++++ .../Jxl/Processing/Dct/JxlDctOutput.cs | 19 + .../Jxl/Processing/Dct/JxlDctSource.cs | 21 + .../Jxl/Processing/Encoder/Ans/JxlToken.cs | 13 + .../Jxl/Processing/Encoder/JxlHeuristics.cs | 170 ++++++++ .../Processing/Encoder/JxlSplineEncoder.cs | 83 ++++ .../Jxl/Processing/JxlCoefficientOrder.cs | 2 +- ...JxlSimdUtils.StoreInterleaved.Generated.cs | 165 ++++---- .../JxlSimdUtils.StoreInterleaved.tt | 5 +- .../Formats/Jxl/Processing/JxlSimdUtils.cs | 332 ++++++++++++++- .../Formats/Jxl/Processing/JxlTranspose.cs | 113 ++++- .../Processing/Primitives/JxlLehmerCode.cs | 14 +- .../Jxl/Processing/Primitives/JxlXorShift.cs | 2 +- .../Quantization/JxlQuantizerSimd.cs | 28 ++ .../Formats/Jxl/Processing/Splines/Dct32.cs | 15 + .../Processing/Splines/JxlQuantizedSpline.cs | 4 +- .../Jxl/Processing/Splines/JxlSpline.cs | 4 +- .../Jxl/Processing/Splines/JxlSplineUtils.cs | 394 +++++++++++++++++ .../Jxl/Processing/Splines/JxlSplines.cs | 289 +++++++++++++ .../Formats/Jxl/Processing/AnsCommonTests.cs | 48 +++ .../Encoder/GammaCorrectionTests.cs | 37 ++ .../Encoder/Noise/PhotonNoiseTests.cs | 43 ++ .../Processing/Primitives/LehmerCodeTests.cs | 88 ++++ .../Processing/Primitives/XorShiftTests.cs | 396 ++++++++++++++++++ tests/ImageSharp.Tests/Formats/Jxl/README.md | 5 + tests/ImageSharp.Tests/Formats/Jxl/Rng.cs | 87 ++++ 33 files changed, 2816 insertions(+), 111 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlending.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlToken.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlHeuristics.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlSplineEncoder.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerSimd.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Splines/Dct32.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineUtils.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplines.cs create mode 100644 tests/ImageSharp.Tests/Formats/Jxl/Processing/AnsCommonTests.cs create mode 100644 tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/GammaCorrectionTests.cs create mode 100644 tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/Noise/PhotonNoiseTests.cs create mode 100644 tests/ImageSharp.Tests/Formats/Jxl/Processing/Primitives/LehmerCodeTests.cs create mode 100644 tests/ImageSharp.Tests/Formats/Jxl/Processing/Primitives/XorShiftTests.cs create mode 100644 tests/ImageSharp.Tests/Formats/Jxl/README.md create mode 100644 tests/ImageSharp.Tests/Formats/Jxl/Rng.cs diff --git a/src/ImageSharp/Common/Helpers/SimdUtils.cs b/src/ImageSharp/Common/Helpers/SimdUtils.cs index 2b8f58b086..3e0abcd6ef 100644 --- a/src/ImageSharp/Common/Helpers/SimdUtils.cs +++ b/src/ImageSharp/Common/Helpers/SimdUtils.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Diagnostics; @@ -71,6 +71,43 @@ internal static Vector FastRound(this Vector v) return val_2p23_f32 | sign; } + /// + /// Estimates the reciprocal of this vector. + /// + /// The vector to get reciprocal estimate of. + /// An estimated reciprocal of each element in the vector. + internal static Vector ReciprocalEstimate(this Vector v) + { + // TODO: System.Runtime.Intrinsics.Arm has Sve and Sve2 + // support but is for evaluation purposes only; add SVE/SVE2 + // support when possible + if (Avx512F.IsSupported && Vector.Count == 16) + { + // x86 + return Avx512F.Reciprocal14(v.AsVector512()).AsVector(); + } + else if (Avx.IsSupported && Vector.Count == 8) + { + // x86 + return Avx.Reciprocal(v.AsVector256()).AsVector(); + } + else if (AdvSimd.IsSupported && Vector.Count == 4) + { + // ARM + return AdvSimd.ReciprocalEstimate(v.AsVector128()).AsVector(); + } + else if (Sse.IsSupported && Vector.Count == 4) + { + // x86 + return Sse.Reciprocal(v.AsVector128()).AsVector(); + } + else + { + // Exact reciprocal fallback (slower) + return Vector.One / v; + } + } + [Conditional("DEBUG")] private static void DebugVerifySpanInput(ReadOnlySpan source, ReadOnlySpan dest, int shouldBeDivisibleBy) { diff --git a/src/ImageSharp/Common/Helpers/Vector128Utilities.cs b/src/ImageSharp/Common/Helpers/Vector128Utilities.cs index 6b4c6ad63c..0e9a40dd1b 100644 --- a/src/ImageSharp/Common/Helpers/Vector128Utilities.cs +++ b/src/ImageSharp/Common/Helpers/Vector128Utilities.cs @@ -899,4 +899,44 @@ public static Vector128 InterleaveUpper(Vector128 a, Vector128 b) return (shuffledA & maskA) | (shuffledB & maskB); } + + /// + /// Interleaves the lower half of the vector. + /// + /// First vector + /// Second vector + /// + /// { a[0], b[0], a[1], b[1] } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 InterleaveLower(Vector128 a, Vector128 b) + { + Vector128 shuffledA = Vector128.Shuffle(a, Vector128.Create(0, 0, 1, 1)); + Vector128 shuffledB = Vector128.Shuffle(b, Vector128.Create(0, 0, 1, 1)); + + Vector128 maskA = Vector128.Create(-1, 0, -1, 0).AsSingle(); + Vector128 maskB = Vector128.Create(0, -1, 0, -1).AsSingle(); + + return (shuffledA & maskA) | (shuffledB & maskB); + } + + /// + /// Interleaves the upper half of the vector. + /// + /// First vector + /// Second vector + /// + /// { a[2], b[2], a[3], b[3] } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 InterleaveUpper(Vector128 a, Vector128 b) + { + Vector128 shuffledA = Vector128.Shuffle(a, Vector128.Create(2, 2, 3, 3)); + Vector128 shuffledB = Vector128.Shuffle(b, Vector128.Create(2, 2, 3, 3)); + + Vector128 maskA = Vector128.Create(-1, 0, -1, 0).AsSingle(); + Vector128 maskB = Vector128.Create(0, -1, 0, -1).AsSingle(); + + return (shuffledA & maskA) | (shuffledB & maskB); + } } diff --git a/src/ImageSharp/Common/Helpers/Vector256Utilities.cs b/src/ImageSharp/Common/Helpers/Vector256Utilities.cs index 4bd78b88fd..bb11108869 100644 --- a/src/ImageSharp/Common/Helpers/Vector256Utilities.cs +++ b/src/ImageSharp/Common/Helpers/Vector256Utilities.cs @@ -563,4 +563,44 @@ public static Vector256 InterleaveUpper(Vector256 a, Vector256 b) return (shuffledA & maskA) | (shuffledB & maskB); } + + /// + /// Interleaves the lower half of the vector. + /// + /// First vector + /// Second vector + /// + /// { a[0], b[0], a[1], b[1], a[2], b[2], a[3], b[3] } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 InterleaveLower(Vector256 a, Vector256 b) + { + Vector256 shuffledA = Vector256.Shuffle(a, Vector256.Create(0, 0, 1, 1, 2, 2, 3, 3)); + Vector256 shuffledB = Vector256.Shuffle(b, Vector256.Create(0, 0, 1, 1, 2, 2, 3, 3)); + + Vector256 maskA = Vector256.Create(-1, 0, -1, 0, -1, 0, -1, 0).AsSingle(); + Vector256 maskB = Vector256.Create(0, -1, 0, -1, 0, -1, 0, -1).AsSingle(); + + return (shuffledA & maskA) | (shuffledB & maskB); + } + + /// + /// Interleaves the upper half of the vector. + /// + /// First vector + /// Second vector + /// + /// { a[4], b[4], a[5], b[5], a[6], b[6], a[7], b[7] } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 InterleaveUpper(Vector256 a, Vector256 b) + { + Vector256 shuffledA = Vector256.Shuffle(a, Vector256.Create(4, 4, 5, 5, 6, 6, 7, 7)); + Vector256 shuffledB = Vector256.Shuffle(b, Vector256.Create(4, 4, 5, 5, 6, 6, 7, 7)); + + Vector256 maskA = Vector256.Create(-1, 0, -1, 0, -1, 0, -1, 0).AsSingle(); + Vector256 maskB = Vector256.Create(0, -1, 0, -1, 0, -1, 0, -1).AsSingle(); + + return (shuffledA & maskA) | (shuffledB & maskB); + } } diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs index 4442eb6495..882c1098bb 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace SixLabors.ImageSharp.Formats.Jxl.Memory; @@ -9,6 +11,21 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Memory; internal class JxlImage3 : IDisposable where T : unmanaged { + private sealed class TypeChangingMemoryManager(Memory memory) : MemoryManager + where TTarget : unmanaged + { + public override Span GetSpan() => MemoryMarshal.Cast(memory.Span); + + // we don't use these + public override MemoryHandle Pin(int elementIndex = 0) => throw new NotImplementedException(); + + public override void Unpin() => throw new NotImplementedException(); + + protected override void Dispose(bool disposing) + { + } + } + private const int PlaneCount = 3; private JxlPlane[] planes = new JxlPlane[3]; @@ -46,6 +63,22 @@ public Span PlaneRow(int plane, int row) return rowSpan; } + // This method performs minor allocations! + public Memory PlaneRowMemory(int plane, int row) + { + this.PlaneRowBoundsCheck(plane, row); + + int rowOffset = row * this.planes[0].BytesPerRow; + Memory rowMemoryBytes = this.planes[plane].Bytes[rowOffset..]; + + // we have to allocate a utility class so we can reinterpret + // a Memory. + // Unsafe.As is truly unsafe because, f.e. what if there are + // 400 bytes but T is 4 bytes? the length will remain as 400. + TypeChangingMemoryManager reinterpreter = new(rowMemoryBytes); + return reinterpreter.Memory; + } + public Span PlaneRow(Rectangle rectangle, int c, int y) { DebugGuard.MustBeGreaterThanOrEqualTo(y + rectangle.Top, 0, nameof(y)); diff --git a/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyRow.cs b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyRow.cs index 7a9b1eeafb..60900dc578 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyRow.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyRow.cs @@ -6,17 +6,13 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; -internal sealed class JxlAcStrategyRow +internal readonly struct JxlAcStrategyRow(ReadOnlyMemory row) { - private readonly ReadOnlyMemory row; - - public JxlAcStrategyRow(ReadOnlyMemory row) => this.row = row; - - public JxlAcStrategy this[int x] + public readonly JxlAcStrategy this[int x] { get { - ReadOnlySpan span = this.row.Span; + ReadOnlySpan span = row.Span; DebugGuard.MustBeLessThan(x * 8, span.Length, "x overflows"); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaBlendingInputLayer.cs b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaBlendingInputLayer.cs index 9a73f659fb..b749a5ba26 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaBlendingInputLayer.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaBlendingInputLayer.cs @@ -3,13 +3,13 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Blending; -internal sealed class JxlAlphaBlendingInputLayer +internal ref struct JxlAlphaBlendingInputLayer(ReadOnlySpan singleSpan) { - public ReadOnlyMemory R { get; set; } + public ReadOnlySpan R = singleSpan; - public ReadOnlyMemory G { get; set; } + public ReadOnlySpan G = singleSpan; - public ReadOnlyMemory B { get; set; } + public ReadOnlySpan B = singleSpan; - public ReadOnlyMemory A { get; set; } + public ReadOnlySpan A = singleSpan; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaBlendingOutput.cs b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaBlendingOutput.cs index 62296b20e7..76a10d2158 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaBlendingOutput.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaBlendingOutput.cs @@ -3,13 +3,13 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Blending; -internal sealed class JxlAlphaBlendingOutput +internal ref struct JxlAlphaBlendingOutput(Span singleSpan) { - public Memory R { get; set; } + public Span R = singleSpan; - public Memory G { get; set; } + public Span G = singleSpan; - public Memory B { get; set; } + public Span B = singleSpan; - public Memory A { get; set; } + public Span A = singleSpan; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlending.cs b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlending.cs new file mode 100644 index 0000000000..7ddad4714e --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlending.cs @@ -0,0 +1,368 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; +using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Blending; + +internal static class JxlBlending +{ + public static bool NeedsBlending(JxlFrameHeader header) + { + if (header.FrameType is not JxlFrameType.RegularFrame and not JxlFrameType.SkipProgressive) + { + return false; + } + + JxlBlendingInfo? blendingInfo = header.BlendingInfo; + if (blendingInfo is null) + { + return false; + } + + bool replaceAll = blendingInfo.BlendMode == JxlBlendMode.Replace; + + foreach (JxlBlendingInfo info in header.ExtraChannelBlendingInfo) + { + if (info.BlendMode != JxlBlendMode.Replace) + { + replaceAll = false; + } + } + + if (!header.CustomSizeOrOrigin && replaceAll) + { + return false; + } + + return true; + } + + public static void PerformBlending( + Configuration configuration, + Buffer2D bg, + Buffer2D fg, + Buffer2D output, + int x0, + int xsize, + JxlPatchBlending colorBlending, + Span ecBlending, + List extraChannelInfo) + { + bool hasAlpha = extraChannelInfo.Any(x => x.Type == JxlExtraChannel.Alpha); + + int numEc = extraChannelInfo.Count; + using JxlImageF tmp = new(configuration, xsize, 3 + numEc); + + for (int i = 0; i < numEc; i++) + { + int i3 = 3 + i; + + switch (ecBlending[i].Mode) + { + case JxlPatchBlendMode.Add: + { + Span row = tmp.GetRow(i3); + for (int x = 0; x < xsize; x++) + { + row[x] = bg[i3, x + x0] + fg[i3, x + x0]; + } + + continue; + } + + case JxlPatchBlendMode.BlendAbove: + { + int alpha = ecBlending[i].AlphaChannel; + bool isPremultiplied = extraChannelInfo[alpha].AlphaAssociated; + + Span bgSpan3 = bg.DangerousGetRowSpan(i3)[x0..]; + Span bgSpan3Alpha = bg.DangerousGetRowSpan(3 + alpha)[x0..]; + Span fgSpan3 = fg.DangerousGetRowSpan(i3)[x0..]; + Span fgSpan3Alpha = fg.DangerousGetRowSpan(3 + alpha)[x0..]; + + JxlAlphaHelper.PerformAlphaBlending( + bgSpan3, + bgSpan3Alpha, + fgSpan3, + fgSpan3Alpha, + tmp.GetRow(i3), + xsize, + isPremultiplied, + ecBlending[i].Clamp); + + continue; + } + + case JxlPatchBlendMode.BlendBelow: + { + int alpha = ecBlending[i].AlphaChannel; + bool isPremultiplied = extraChannelInfo[alpha].AlphaAssociated; + + Span bgSpan3 = bg.DangerousGetRowSpan(i3)[x0..]; + Span bgSpan3Alpha = bg.DangerousGetRowSpan(3 + alpha)[x0..]; + Span fgSpan3 = fg.DangerousGetRowSpan(i3)[x0..]; + Span fgSpan3Alpha = fg.DangerousGetRowSpan(3 + alpha)[x0..]; + + JxlAlphaHelper.PerformAlphaBlending( + bgSpan3, + bgSpan3Alpha, + fgSpan3, + fgSpan3Alpha, + tmp.GetRow(3 + i), + xsize, + isPremultiplied, + ecBlending[i].Clamp); + + continue; + } + + case JxlPatchBlendMode.AlphaWeightedAddAbove: + { + int alpha = ecBlending[i].AlphaChannel; + + Span bgSpan3 = bg.DangerousGetRowSpan(i3)[x0..]; + Span bgSpan3Alpha = bg.DangerousGetRowSpan(3 + alpha)[x0..]; + Span fgSpan3 = fg.DangerousGetRowSpan(i3)[x0..]; + + JxlAlphaHelper.PerformAlphaWeightedAdd( + bgSpan3, + fgSpan3, + bgSpan3Alpha, + tmp.GetRow(3 + i), + xsize, + ecBlending[i].Clamp); + + continue; + } + + case JxlPatchBlendMode.AlphaWeightedAddBelow: + { + int alpha = ecBlending[i].AlphaChannel; + + Span bgSpan3 = bg.DangerousGetRowSpan(i3)[x0..]; + Span bgSpan3Alpha = bg.DangerousGetRowSpan(3 + alpha)[x0..]; + Span fgSpan3 = fg.DangerousGetRowSpan(i3)[x0..]; + + JxlAlphaHelper.PerformAlphaWeightedAdd( + fgSpan3, + bgSpan3, + bgSpan3Alpha, + tmp.GetRow(3 + i), + xsize, + ecBlending[i].Clamp); + + continue; + } + + case JxlPatchBlendMode.Multiply: + { + Span bgSpan3 = bg.DangerousGetRowSpan(i3)[x0..]; + Span fgSpan3 = fg.DangerousGetRowSpan(i3)[x0..]; + + JxlAlphaHelper.PerformMultiplyBlending( + bgSpan3, + fgSpan3, + tmp.GetRow(i3), + xsize, + ecBlending[i].Clamp); + + continue; + } + + case JxlPatchBlendMode.Replace: + if (xsize > 0) + { + Span fgSpan3 = fg.DangerousGetRowSpan(i3)[x0..]; + fgSpan3.Slice(0, xsize).CopyTo(tmp.GetRow(i3)); + } + + continue; + + case JxlPatchBlendMode.None: + if (xsize > 0) + { + Span bgSpan3 = bg.DangerousGetRowSpan(i3)[x0..]; + bgSpan3.Slice(0, xsize).CopyTo(tmp.GetRow(i3)); + } + + continue; + } + } + + int colorBlendingAlpha = colorBlending.AlphaChannel; + + void Add() + { + for (int p = 0; p < 3; p++) + { + Span output = tmp.GetRow(p); + Span bgSpan = bg.DangerousGetRowSpan(p); + Span fgSpan = fg.DangerousGetRowSpan(p); + + for (int x = 0; x < xsize; x++) + { + int xPlusX0 = x + x0; + + output[x] = bgSpan[xPlusX0] + fgSpan[xPlusX0]; + } + } + } + + void BlendWeighted(Span bottom, Span top) + { + bool isPremultiplied = extraChannelInfo[colorBlendingAlpha].AlphaAssociated; + + JxlAlphaHelper.PerformAlphaBlending( + new JxlAlphaBlendingInputLayer() + { + R = bottom[x0..], + G = bottom[(x0 + 1)..], + B = bottom[(2 + x0)..], + A = bottom[(3 + colorBlendingAlpha + x0)..] + }, + new JxlAlphaBlendingInputLayer() + { + R = top[x0..], + G = top[(x0 + 1)..], + B = top[(x0 + 2)..], + A = top[(3 + colorBlendingAlpha + x0)..] + }, + new JxlAlphaBlendingOutput() + { + R = tmp.GetRow(0), + G = tmp.GetRow(1), + B = tmp.GetRow(2), + A = tmp.GetRow(3) + }, + xsize, + isPremultiplied, + colorBlending.Clamp); + } + + void AddWeighted(Span bottom, Span top) + { + for (int c = 0; c < 3; c++) + { + JxlAlphaHelper.PerformAlphaWeightedAdd(bottom[(c + x0)..], top[(c + x0)..], top[(3 + colorBlendingAlpha + x0)..], tmp.GetRow(c), xsize, colorBlending.Clamp); + } + } + + void Copy(Span src) + { + for (int p = 0; p < 3; p++) + { + src.Slice(p + x0, xsize).CopyTo(tmp.GetRow(p)); + } + } + + switch (colorBlending.Mode) + { + case JxlPatchBlendMode.Add: + { + Add(); + break; + } + + case JxlPatchBlendMode.AlphaWeightedAddAbove: + { + if (hasAlpha) + { + AddWeighted(bg.DangerousGetSingleSpan(), fg.DangerousGetSingleSpan()); + } + else + { + Add(); + } + + break; + } + + case JxlPatchBlendMode.AlphaWeightedAddBelow: + { + if (hasAlpha) + { + AddWeighted(fg.DangerousGetSingleSpan(), bg.DangerousGetSingleSpan()); + } + else + { + Add(); + } + + break; + } + + case JxlPatchBlendMode.BlendAbove: + { + if (hasAlpha) + { + BlendWeighted(bg.DangerousGetSingleSpan(), fg.DangerousGetSingleSpan()); + } + else + { + Copy(fg.DangerousGetSingleSpan()); + } + + break; + } + + case JxlPatchBlendMode.BlendBelow: + { + if (hasAlpha) + { + BlendWeighted(fg.DangerousGetSingleSpan(), bg.DangerousGetSingleSpan()); + } + else + { + Copy(fg.DangerousGetSingleSpan()); + } + + break; + } + + case JxlPatchBlendMode.Multiply: + { + Span bgSpan = bg.DangerousGetSingleSpan(); + Span fgSpan = fg.DangerousGetSingleSpan(); + + for (int p = 0; p < 3; p++) + { + JxlAlphaHelper.PerformMultiplyBlending( + bgSpan[(p + x0)..], + fgSpan[(p + x0)..], + tmp.GetRow(p), + xsize, + colorBlending.Clamp); + } + + break; + } + + case JxlPatchBlendMode.Replace: + { + Copy(fg.DangerousGetSingleSpan()); + break; + } + + case JxlPatchBlendMode.None: + { + Copy(bg.DangerousGetSingleSpan()); + break; + } + } + + if (xsize != 0) + { + Span outputSpan = output.DangerousGetSingleSpan(); + + for (int i = 0; i < 3; i++) + { + tmp.GetRow(i).Slice(0, xsize).CopyTo(outputSpan[(i + x0)..]); + } + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctOutput.cs b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctOutput.cs index 5ef6218908..5a1d5c9581 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctOutput.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctOutput.cs @@ -3,6 +3,7 @@ using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; @@ -49,4 +50,22 @@ internal ref struct JxlDctOutput(Span data, int stride) /// The offset. [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly void StorePart(Vector value, int row, int index) => value.CopyTo(this.Address(row, index)); + + /// + /// Stores the vector into the data at the specified row and offset. + /// + /// The vector to write. + /// The row index. + /// The offset. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly void StorePart256(Vector256 value, int row, int index) => value.CopyTo(this.Address(row, index)); + + /// + /// Stores the vector into the data at the specified row and offset. + /// + /// The vector to write. + /// The row index. + /// The offset. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly void StorePart128(Vector128 value, int row, int index) => value.CopyTo(this.Address(row, index)); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctSource.cs b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctSource.cs index 0fe17e92ad..ba99dd5f67 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctSource.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctSource.cs @@ -3,6 +3,7 @@ using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; @@ -52,4 +53,24 @@ internal readonly ref struct JxlDctSource(Span data, int stride) /// Vector at that row and offset. /// public Vector LoadPart(int row, int i) => new(this.Address(row, i)); + + /// + /// Loads a vector at the specified row and offset. + /// + /// The row index. + /// The offset. + /// + /// Vector at that row and offset. + /// + public Vector256 LoadPart256(int row, int i) => Vector256.Create(this.Address(row, i)); + + /// + /// Loads a vector at the specified row and offset. + /// + /// The row index. + /// The offset. + /// + /// Vector at that row and offset. + /// + public Vector128 LoadPart128(int row, int i) => Vector128.Create(this.Address(row, i)); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlToken.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlToken.cs new file mode 100644 index 0000000000..2b07fb14d9 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlToken.cs @@ -0,0 +1,13 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; + +internal struct JxlToken(JxlSplineEntropyContext c, uint value) +{ + public bool IsLz77Length; + public JxlSplineEntropyContext Context = c; + public uint Value = value; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlHeuristics.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlHeuristics.cs new file mode 100644 index 0000000000..846f791f69 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlHeuristics.cs @@ -0,0 +1,170 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; + +internal static class JxlHeuristics +{ + private static ReadOnlySpan SimpleContextMap => + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + ]; + + public static void FindBestBlockEntropyModel(JxlCompressParameters cparameters, JxlImageI rqf, JxlAcStrategyImage acStrategy, JxlBlockContextMap blockCtxMap) + { + if (cparameters.DecodingSpeedTier >= 1) + { + SimpleContextMap.CopyTo(blockCtxMap.ContextMap.AsSpan()); + blockCtxMap.ContextCount = 2; + blockCtxMap.DcContextCount = 1; + return; + } + + if (cparameters.SpeedTier >= JxlSpeedTier.Falcon) + { + return; + } + + int total = rqf.XSize * rqf.YSize; + int sizeForContextModel = (1 << 10) * cparameters.ButteraugliDistance; + + if (total < sizeForContextModel) + { + return; + } + + OccCounters counters = new(rqf, acStrategy); + int sizeForQfSplit = (1 << 13) * cparameters.ButteraugliDistance; + int numQfSegments = total < sizeForQfSplit ? 1 : 2; + List qft = blockCtxMap.QfThresholds; + qft.Clear(); + int cumulativeSum = 0; + int next = 1; + int lastCut = 256; + int cut = total * next / numQfSegments; + + for (int j = 0; j < 256; j++) + { + cumulativeSum += counters.QfCounts[j]; + + if (cumulativeSum > cut) + { + if (j != 0) + { + qft.Add((uint)j); + } + + lastCut = j; + + while (cumulativeSum > cut) + { + next++; + cut = total * next / numQfSegments; + } + } + else if (next > qft.Count + 1) + { + if (j - 1 == lastCut && j != 0) + { + qft.Add((uint)j); + } + } + } + + int[]? pooledCounts = null; + int[]? pooledRemap = null; + int[]? pooledClusters = null; + int countsLength = JxlForwardCoefficientOrder.OrderCount * (qft.Count + 1); + + Span counts = + countsLength <= 128 + ? stackalloc int[128].Slice(0, countsLength) + : pooledCounts = ArrayPool.Shared.Rent(countsLength); + + Span remap = + countsLength <= 128 + ? stackalloc int[128].Slice(0, countsLength) + : pooledRemap = ArrayPool.Shared.Rent(countsLength); + + Span clusters = + countsLength <= 128 + ? stackalloc int[128].Slice(0, countsLength) + : pooledClusters = ArrayPool.Shared.Rent(countsLength); + + int qftPos = 0; + + for (int j = 0; j < 256; j++) + { + if (qftPos < qft.Count && j == qft[qftPos]) + { + qftPos++; + } + + for (int i = 0; i < JxlForwardCoefficientOrder.OrderCount; i++) + { + counts[qftPos + (i * (qft.Count + 1))] += counters.QfOrdCounts[i, j]; + } + } + + JxlSimdUtils.Iota(remap, 0); + remap.CopyTo(clusters); + + int numClusters = Math.Clamp(total / sizeForContextModel / 2, 2, 9); + int numClustersChroma = Math.Clamp(total / sizeForContextModel / 3, 1, 5); + + // TODO: method incomplete + // do not forget to ArrayPool.Shared.Return pooledCounts, pooledRemap, pooledClusters if needed + } + + private sealed class OccCounters : IDisposable + { + private readonly int[] qfCounts; + private readonly int[] dataForQfOrdCounts; + private readonly int[] ordCounts; + + public OccCounters(JxlImageI rqf, JxlAcStrategyImage acStrategy) + { + this.qfCounts = ArrayPool.Shared.Rent(256); + this.dataForQfOrdCounts = ArrayPool.Shared.Rent(256 * JxlForwardCoefficientOrder.OrderCount); + this.ordCounts = ArrayPool.Shared.Rent(JxlForwardCoefficientOrder.OrderCount); + + this.QfOrdCounts = new(JxlForwardCoefficientOrder.OrderCount, 256, this.dataForQfOrdCounts); + + for (int y = 0; y < rqf.YSize; y++) + { + Span qfRow = rqf.GetRow(y); + JxlAcStrategyRow acsRow = acStrategy.GetRow(y); + + for (int x = 0; x < rqf.XSize; x++) + { + int ord = JxlCoefficientOrder.StrategyOrder[acsRow[x].RawStrategy]; + int qf = qfRow[x] - 1; + this.qfCounts[qf]++; + this.QfOrdCounts[ord, qf]++; + this.ordCounts[ord]++; + } + } + } + + public Span QfCounts => this.qfCounts.AsSpan(); + + public DenseMatrix QfOrdCounts { get; } + + public Span OrdCounts => this.ordCounts.AsSpan(); + + public void Dispose() + { + ArrayPool.Shared.Return(this.qfCounts); + ArrayPool.Shared.Return(this.dataForQfOrdCounts); + ArrayPool.Shared.Return(this.ordCounts); + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlSplineEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlSplineEncoder.cs new file mode 100644 index 0000000000..949b43870e --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlSplineEncoder.cs @@ -0,0 +1,83 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; + +internal sealed class JxlSplineEncoder +{ + private static void Tokenize(JxlQuantizedSpline spline, List tokens) + { + tokens.Add(new(JxlSplineEntropyContext.NumControlPoints, (uint)spline.ControlPoints.Length)); + + foreach (JxlControlPoint point in spline.ControlPoints.Span) + { + tokens.Add(new(JxlSplineEntropyContext.ControlPoints, JxlPackSigned.PackUnsigned(point.First))); + tokens.Add(new(JxlSplineEntropyContext.ControlPoints, JxlPackSigned.PackUnsigned(point.Second))); + } + + void EncodeDCT(Span dct) + { + for (int i = 0; i < 32; i++) + { + tokens.Add(new(JxlSplineEntropyContext.Dct, JxlPackSigned.PackUnsigned(dct[i]))); + } + } + + foreach (Span dct in spline.ColorDct) + { + EncodeDCT(dct); + } + + EncodeDCT(spline.SigmaDct); + } + + public static void EncodeAllStartingPoints(Span points, List tokens) + { + long lastX = 0; + long lastY = 0; + + for (int i = 0; i < points.Length; i++) + { + long x = (long)MathF.Round(points[i].X, MidpointRounding.AwayFromZero); + long y = (long)MathF.Round(points[i].Y, MidpointRounding.AwayFromZero); + + if (i == 0) + { + tokens.Add(new(JxlSplineEntropyContext.StartingPosition, (uint)x)); + tokens.Add(new(JxlSplineEntropyContext.StartingPosition, (uint)y)); + } + else + { + tokens.Add(new(JxlSplineEntropyContext.StartingPosition, JxlPackSigned.PackUnsigned((int)(x - lastX)))); + tokens.Add(new(JxlSplineEntropyContext.StartingPosition, JxlPackSigned.PackUnsigned((int)(y - lastY)))); + } + + lastX = x; + lastY = y; + } + } + + public static void EncodeSplines(JxlSplines splines, JxlBitWriter writer, JxlLayerType layer, JxlHistogramParameters histogramParameters, JxlAuxiliaryOutput auxOut) + { + Span quantizedSplines = splines.QuantizedSplines; + List> tokens = [[]]; + tokens[0].Add(new(JxlSplineEntropyContext.NumSplineContexts, (uint)(quantizedSplines.Length - 1))); + + EncodeAllStartingPoints(splines.StartingPoints, tokens[0]); + + tokens[0].Add(new(JxlSplineEntropyContext.QuantizationAdjustment, JxlPackSigned.PackUnsigned(splines.QuantizationAdjustment))); + + foreach (JxlQuantizedSpline spline in quantizedSplines) + { + Tokenize(spline, tokens[0]); + } + + _ = BuildAndEncodeHistograms(writer, histogramParameters, JxlSplineEntropyContext.NumSplineContexts, tokens, out JxlEntropyEncodingData codes, writer, layer, auxOut); + WriteTokens(tokens[0], codes, 0, writer, layer, auxOut); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs index 4cace39d73..00c162be9a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs @@ -50,7 +50,7 @@ public static uint CoeffOrderContext(uint value) return Math.Min(token, PermutationContexts - 1u); } - public static bool ReadPermutation(int skip, int size, Span order, JxlBitReader bitReader, JxlAnsSymbolReader reader, Span contextMap) + public static bool ReadPermutation(int skip, int size, Span order, JxlBitReader bitReader, JxlAnsSymbolReader reader, Span contextMap) { DebugGuard.MustBeLessThanOrEqualTo(size, 65536, nameof(size)); diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.Generated.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.Generated.cs index b03c77eced..8e4495e0f0 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.Generated.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.Generated.cs @@ -10,124 +10,139 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing; internal static partial class JxlSimdUtils { - public static void StoreInterleaved(Vector v1, Vector v2, ref T memory) + public static unsafe void StoreInterleaved(Vector v1, Vector v2, ref T memory) + where T : unmanaged { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); + v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); } - public static void StoreInterleaved(Vector v1, Vector v2, Vector v3, ref T memory) + public static unsafe void StoreInterleaved(Vector v1, Vector v2, Vector v3, ref T memory) + where T : unmanaged { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); + v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); } - public static void StoreInterleaved(Vector v1, Vector v2, Vector v3, Vector v4, ref T memory) + public static unsafe void StoreInterleaved(Vector v1, Vector v2, Vector v3, Vector v4, ref T memory) + where T : unmanaged { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); - v4.StoreUnsafe(ref Unsafe.Add(ref memory, 3)); + v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 3)); } - public static void StoreInterleaved(Vector v1, Vector v2, Vector v3, Vector v4, Vector v5, ref T memory) + public static unsafe void StoreInterleaved(Vector v1, Vector v2, Vector v3, Vector v4, Vector v5, ref T memory) + where T : unmanaged { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); - v4.StoreUnsafe(ref Unsafe.Add(ref memory, 3)); - v5.StoreUnsafe(ref Unsafe.Add(ref memory, 4)); + v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 3)); + v5.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 4)); } - public static void StoreInterleaved(Vector v1, Vector v2, Vector v3, Vector v4, Vector v5, Vector v6, ref T memory) + public static unsafe void StoreInterleaved(Vector v1, Vector v2, Vector v3, Vector v4, Vector v5, Vector v6, ref T memory) + where T : unmanaged { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); - v4.StoreUnsafe(ref Unsafe.Add(ref memory, 3)); - v5.StoreUnsafe(ref Unsafe.Add(ref memory, 4)); - v6.StoreUnsafe(ref Unsafe.Add(ref memory, 5)); + v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 3)); + v5.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 4)); + v6.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 5)); } - public static void StoreInterleaved(Vector128 v1, Vector128 v2, ref T memory) + public static unsafe void StoreInterleaved(Vector128 v1, Vector128 v2, ref T memory) + where T : unmanaged { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); + v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); } - public static void StoreInterleaved(Vector128 v1, Vector128 v2, Vector128 v3, ref T memory) + public static unsafe void StoreInterleaved(Vector128 v1, Vector128 v2, Vector128 v3, ref T memory) + where T : unmanaged { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); + v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); } - public static void StoreInterleaved(Vector128 v1, Vector128 v2, Vector128 v3, Vector128 v4, ref T memory) + public static unsafe void StoreInterleaved(Vector128 v1, Vector128 v2, Vector128 v3, Vector128 v4, ref T memory) + where T : unmanaged { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); - v4.StoreUnsafe(ref Unsafe.Add(ref memory, 3)); + v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 3)); } - public static void StoreInterleaved(Vector128 v1, Vector128 v2, Vector128 v3, Vector128 v4, Vector128 v5, ref T memory) + public static unsafe void StoreInterleaved(Vector128 v1, Vector128 v2, Vector128 v3, Vector128 v4, Vector128 v5, ref T memory) + where T : unmanaged { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); - v4.StoreUnsafe(ref Unsafe.Add(ref memory, 3)); - v5.StoreUnsafe(ref Unsafe.Add(ref memory, 4)); + v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 3)); + v5.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 4)); } - public static void StoreInterleaved(Vector128 v1, Vector128 v2, Vector128 v3, Vector128 v4, Vector128 v5, Vector128 v6, ref T memory) + public static unsafe void StoreInterleaved(Vector128 v1, Vector128 v2, Vector128 v3, Vector128 v4, Vector128 v5, Vector128 v6, ref T memory) + where T : unmanaged { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); - v4.StoreUnsafe(ref Unsafe.Add(ref memory, 3)); - v5.StoreUnsafe(ref Unsafe.Add(ref memory, 4)); - v6.StoreUnsafe(ref Unsafe.Add(ref memory, 5)); + v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 3)); + v5.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 4)); + v6.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 5)); } - public static void StoreInterleaved(Vector256 v1, Vector256 v2, ref T memory) + public static unsafe void StoreInterleaved(Vector256 v1, Vector256 v2, ref T memory) + where T : unmanaged { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); + v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); } - public static void StoreInterleaved(Vector256 v1, Vector256 v2, Vector256 v3, ref T memory) + public static unsafe void StoreInterleaved(Vector256 v1, Vector256 v2, Vector256 v3, ref T memory) + where T : unmanaged { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); + v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); } - public static void StoreInterleaved(Vector256 v1, Vector256 v2, Vector256 v3, Vector256 v4, ref T memory) + public static unsafe void StoreInterleaved(Vector256 v1, Vector256 v2, Vector256 v3, Vector256 v4, ref T memory) + where T : unmanaged { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); - v4.StoreUnsafe(ref Unsafe.Add(ref memory, 3)); + v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 3)); } - public static void StoreInterleaved(Vector256 v1, Vector256 v2, Vector256 v3, Vector256 v4, Vector256 v5, ref T memory) + public static unsafe void StoreInterleaved(Vector256 v1, Vector256 v2, Vector256 v3, Vector256 v4, Vector256 v5, ref T memory) + where T : unmanaged { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); - v4.StoreUnsafe(ref Unsafe.Add(ref memory, 3)); - v5.StoreUnsafe(ref Unsafe.Add(ref memory, 4)); + v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 3)); + v5.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 4)); } - public static void StoreInterleaved(Vector256 v1, Vector256 v2, Vector256 v3, Vector256 v4, Vector256 v5, Vector256 v6, ref T memory) + public static unsafe void StoreInterleaved(Vector256 v1, Vector256 v2, Vector256 v3, Vector256 v4, Vector256 v5, Vector256 v6, ref T memory) + where T : unmanaged { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); - v4.StoreUnsafe(ref Unsafe.Add(ref memory, 3)); - v5.StoreUnsafe(ref Unsafe.Add(ref memory, 4)); - v6.StoreUnsafe(ref Unsafe.Add(ref memory, 5)); + v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 3)); + v5.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 4)); + v6.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 5)); } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.tt b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.tt index 41a5b9c90d..d4dff7ee41 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.tt +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.tt @@ -33,10 +33,11 @@ internal static partial class JxlSimdUtils } string inlineParameters = string.Join(", ", vectorParameters) + ", "; #> - public static void StoreInterleaved(<#= inlineParameters #>ref T memory) + public static unsafe void StoreInterleaved(<#= inlineParameters #>ref T memory) + where T : unmanaged { <# for (int j = 0; j < i; j++) { #> - v<#= j + 1 #>.StoreUnsafe(ref Unsafe.Add(ref memory, <#= j #>)); + v<#= j + 1 #>.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * <#= j #>)); <# } #> } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs index 8da98bbf4f..fdcc932c67 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs @@ -15,10 +15,12 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing; internal static partial class JxlSimdUtils { [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector256 ConcatLowerLower(Vector256 a, Vector256 b) => Vector256.Create(a.GetLower(), b.GetLower()); + public static Vector256 ConcatLowerLower(Vector256 a, Vector256 b) + where T : unmanaged => Vector256.Create(a.GetLower(), b.GetLower()); [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector256 ConcatUpperUpper(Vector256 a, Vector256 b) => Vector256.Create(a.GetUpper(), b.GetUpper()); + public static Vector256 ConcatUpperUpper(Vector256 a, Vector256 b) + where T : unmanaged => Vector256.Create(a.GetUpper(), b.GetUpper()); public static void Transpose8x8Block(Span fromSpan, Span toSpan, int stride) { @@ -109,4 +111,330 @@ public static Vector Pow(Vector @base, Vector exponent) Vector vec = Vector.Log2(@base) * exponent; return vec * vec; } + + /// + /// + /// Fills the span so its first value is equal to + /// and subsequent values increment by one. For example, with start=5, + /// the span's values will be: + /// + /// { start, start+1, start+2, start+3, start+4, ... to the end of the span } + /// + /// + /// + /// or, more precisely: + /// + /// { 5, 6, 7, 8, 9, 10, 11, ... to the end of the span } + /// + /// + /// + /// + /// The span where the values are filled. + /// Initial value. + public static void Iota(Span span, int start) + { + ref int spanRef = ref MemoryMarshal.GetReference(span); + + // Using fixed-size vectors so we can construct an + // incrementMask more easily. + if (Vector512.IsHardwareAccelerated) + { + Vector512 incrementMask = Vector512.Create(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16); + Vector512 v = Vector512.Create(start) + (incrementMask - Vector512.One); + + if ((span.Length % Vector512.Count) == 0) + { + // Aligned length + for (int i = 0; i < span.Length; i += Vector512.Count) + { + v.StoreUnsafe(ref Unsafe.Add(ref spanRef, i)); + v += incrementMask; + } + } + else + { + // We will need a scalar remainder + int vectorLength = span.Length - (span.Length % Vector512.Count); + + int i; + for (i = 0; i < vectorLength; i += Vector512.Count) + { + v.StoreUnsafe(ref Unsafe.Add(ref spanRef, i)); + v += incrementMask; + } + + int val = v.ToScalar(); + for (; i < span.Length; i++) + { + Unsafe.Add(ref spanRef, i) = val++; + } + } + } + else if (Vector256.IsHardwareAccelerated) + { + Vector256 incrementMask = Vector256.Create(1, 2, 3, 4, 5, 6, 7, 8); + Vector256 v = Vector256.Create(start) + (incrementMask - Vector256.One); + + if ((span.Length % Vector256.Count) == 0) + { + // Aligned length + for (int i = 0; i < span.Length; i += Vector256.Count) + { + v.StoreUnsafe(ref Unsafe.Add(ref spanRef, i)); + v += incrementMask; + } + } + else + { + // We will need a scalar remainder + int vectorLength = span.Length - (span.Length % Vector256.Count); + + int i; + for (i = 0; i < vectorLength; i += Vector256.Count) + { + v.StoreUnsafe(ref Unsafe.Add(ref spanRef, i)); + v += incrementMask; + } + + int val = v.ToScalar(); + for (; i < span.Length; i++) + { + Unsafe.Add(ref spanRef, i) = val++; + } + } + } + else if (Vector128.IsHardwareAccelerated) + { + Vector128 incrementMask = Vector128.Create(1, 2, 3, 4); + Vector128 v = Vector128.Create(start) + (incrementMask - Vector128.One); + + if ((span.Length % Vector128.Count) == 0) + { + // Aligned length + for (int i = 0; i < span.Length; i += Vector128.Count) + { + v.StoreUnsafe(ref Unsafe.Add(ref spanRef, i)); + v += incrementMask; + } + } + else + { + // We will need a scalar remainder + int vectorLength = span.Length - (span.Length % Vector128.Count); + + int i; + for (i = 0; i < vectorLength; i += Vector128.Count) + { + v.StoreUnsafe(ref Unsafe.Add(ref spanRef, i)); + v += incrementMask; + } + + int val = v.ToScalar(); + for (; i < span.Length; i++) + { + Unsafe.Add(ref spanRef, i) = val++; + } + } + } + else if (Vector64.IsHardwareAccelerated) + { + Vector64 incrementMask = Vector64.Create(1, 2); + Vector64 v = Vector64.Create(start) + (incrementMask - Vector64.One); + + if ((span.Length % Vector64.Count) == 0) + { + // Aligned length + for (int i = 0; i < span.Length; i += Vector64.Count) + { + v.StoreUnsafe(ref Unsafe.Add(ref spanRef, i)); + v += incrementMask; + } + } + else + { + // We will need a scalar remainder + int vectorLength = span.Length - (span.Length % Vector64.Count); + + int i; + for (i = 0; i < vectorLength; i += Vector64.Count) + { + v.StoreUnsafe(ref Unsafe.Add(ref spanRef, i)); + v += incrementMask; + } + + int val = v.ToScalar(); + for (; i < span.Length; i++) + { + Unsafe.Add(ref spanRef, i) = val++; + } + } + } + else + { + // No SIMD + int value = start; + spanRef = value; + value++; + for (int i = 1; i < span.Length; i++) + { + Unsafe.Add(ref spanRef, i) = value++; + } + } + } + + /// + /// + /// Fills the span so its first value is equal to + /// and subsequent values increment by one. For example, with start=5, + /// the span's values will be: + /// + /// { start, start+1, start+2, start+3, start+4, ... to the end of the span } + /// + /// + /// + /// or, more precisely: + /// + /// { 5, 6, 7, 8, 9, 10, 11, ... to the end of the span } + /// + /// + /// + /// + /// The span where the values are filled. + /// Initial value. + public static void Iota(Span span, T start) + where T : unmanaged, INumber + { + // Slightly slower than the int variant + ref T spanRef = ref MemoryMarshal.GetReference(span); + + if (Vector.IsSupported && Vector.IsHardwareAccelerated) + { + Vector incrementMask = IotaMask.IncrementMask; + Vector v = Vector.Create(start) + (incrementMask - Vector.One); + + if ((span.Length % Vector.Count) == 0) + { + // Aligned length + for (int i = 0; i < span.Length; i += Vector.Count) + { + v.StoreUnsafe(ref Unsafe.Add(ref spanRef, i)); + v += incrementMask; + } + } + else + { + // Remainder needed + int vectorLength = span.Length - (span.Length % Vector.Count); + + int i; + for (i = 0; i < vectorLength; i += Vector.Count) + { + v.StoreUnsafe(ref Unsafe.Add(ref spanRef, i)); + v += incrementMask; + } + + T val = v.ToScalar(); + for (; i < span.Length; i++) + { + Unsafe.Add(ref spanRef, i) = val++; + } + } + } + else + { + // Scalar (slow) + T value = start; + spanRef = value; + value++; + for (int i = 1; i < span.Length; i++) + { + Unsafe.Add(ref spanRef, i) = value++; + } + } + } + + public static Vector Iota(T start) + where T : unmanaged, INumber + => IotaMask.IncrementMask + Vector.Create(start); + + /// + /// Vectorized floating-point error function (precise approximate). + /// + /// Vector to compute error of. + /// Vector whose each item is an error (similar to std::erf). + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector FastErff(Vector x) + { + Vector zero = Vector.Zero; + Vector one = Vector.One; + + Vector xle0 = Vector.LessThanOrEqual(x, zero); + Vector absx = Vector.Abs(x); + + Vector denom1 = (absx * new Vector(0.0777394369f)) + new Vector(0.000205260015f); + Vector denom2 = (denom1 * absx) + new Vector(0.232120216f); + Vector denom3 = (denom2 * absx) + new Vector(0.277820801f); + Vector denom4 = (denom3 * absx) + one; + Vector denom5 = denom4 * denom4; + Vector invDenom5 = one / denom5; + Vector result = one - Vector.Multiply(invDenom5, invDenom5); + + // Change sign if x <= 0. + return Vector.ConditionalSelect(xle0, -result, result); + } + + /// + /// Scalar floating-point error function (precise approximate). + /// + /// Value to compute error of. + /// A scalar error value (similar to std::erf). + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float FastErff(float x) + { + float zero = 0.0f; + float one = 1.0f; + + bool xle0 = x <= zero; + float absx = MathF.Abs(x); + + float denom1 = (absx * 0.0777394369f) + 0.000205260015f; + float denom2 = (denom1 * absx) + 0.232120216f; + float denom3 = (denom2 * absx) + 0.277820801f; + float denom4 = (denom3 * absx) + one; + float denom5 = denom4 * denom4; + float invDenom5 = one / denom5; + float result = one - (invDenom5 * invDenom5); + + // Change sign if x <= 0. + return xle0 ? -result : result; + } + + /// + /// Incrementing values to compute the Iota function. + /// + /// + /// Creating a Vector<T> incrementing values would be + /// slow as Vector<T> is not a fixed-size vector, leaving + /// no other option but a slow loop. This class caches these + /// vectors for significantly better performance, though still + /// not as fast as an int variant. + /// + /// Type of the vector. + private static class IotaMask + where T : unmanaged, INumber + { + public static readonly Vector IncrementMask; + + static IotaMask() + { + Span values = stackalloc T[Vector.Count]; + + for (int i = 0; i < Vector.Count; i++) + { + values[i] = T.CreateSaturating(i + 1); + } + + IncrementMask = Vector.Create(values); + } + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlTranspose.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlTranspose.cs index 99a3a8c984..5797553f54 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlTranspose.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlTranspose.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; @@ -10,9 +12,29 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing; /// internal static class JxlTranspose { - // TODO: SIMD public static void Transpose(int r, int c, JxlDctSource from, JxlDctOutput to) { + if (Vector256.IsHardwareAccelerated) + { + if (((r | c) & 7) == 0) // equivalent to (r % 8 == 0 && c % 8 == 0); micro-optimization, reduces one branch + { + // we can use SIMD + TransposeSimd256(r, c, from, to, r, c); + return; + } + } + else if (Vector128.IsHardwareAccelerated) + { + if (((r | c) & 3) == 0) // equivalent to (r % 4 == 0 && c % 4 == 0); micro-optimization, reduces one branch + { + // we can use SIMD + TransposeSimd128(r, c, from, to, r, c); + return; + } + } + + // fallback: can't use SIMD (block size isn't aligned or + // there's no v128/v256 support) for (int n = 0; n < r; n++) { for (int m = 0; m < c; m++) @@ -21,4 +43,93 @@ public static void Transpose(int r, int c, JxlDctSource from, JxlDctOutput to) } } } + + private static void TransposeSimd256(int rowsOr0, int colsOr0, JxlDctSource from, JxlDctOutput to, int rowsP, int colsP) + { + int rows = rowsOr0 == 0 ? rowsP : rowsOr0; + int cols = colsOr0 == 0 ? colsP : colsOr0; + + for (int n = 0; n < rows; n += 8) + { + for (int m = 0; m < cols; m += 8) + { + Vector256 i0 = from.LoadPart256(n, m); + Vector256 i1 = from.LoadPart256(n + 1, m); + Vector256 i2 = from.LoadPart256(n + 2, m); + Vector256 i3 = from.LoadPart256(n + 3, m); + Vector256 i4 = from.LoadPart256(n + 4, m); + Vector256 i5 = from.LoadPart256(n + 5, m); + Vector256 i6 = from.LoadPart256(n + 6, m); + Vector256 i7 = from.LoadPart256(n + 7, m); + + Vector256 q0 = Vector256_.InterleaveLower(i0, i2); + Vector256 q1 = Vector256_.InterleaveLower(i1, i3); + Vector256 q2 = Vector256_.InterleaveUpper(i0, i2); + Vector256 q3 = Vector256_.InterleaveUpper(i1, i3); + Vector256 q4 = Vector256_.InterleaveLower(i4, i6); + Vector256 q5 = Vector256_.InterleaveLower(i5, i7); + Vector256 q6 = Vector256_.InterleaveUpper(i4, i6); + Vector256 q7 = Vector256_.InterleaveUpper(i5, i7); + + Vector256 r0 = Vector256_.InterleaveLower(q0, q1); + Vector256 r1 = Vector256_.InterleaveUpper(q0, q1); + Vector256 r2 = Vector256_.InterleaveLower(q2, q3); + Vector256 r3 = Vector256_.InterleaveUpper(q2, q3); + Vector256 r4 = Vector256_.InterleaveLower(q4, q5); + Vector256 r5 = Vector256_.InterleaveUpper(q4, q5); + Vector256 r6 = Vector256_.InterleaveLower(q6, q7); + Vector256 r7 = Vector256_.InterleaveUpper(q6, q7); + + i0 = JxlSimdUtils.ConcatLowerLower(r4, r0); + i1 = JxlSimdUtils.ConcatLowerLower(r5, r1); + i2 = JxlSimdUtils.ConcatLowerLower(r6, r2); + i3 = JxlSimdUtils.ConcatLowerLower(r7, r3); + i4 = JxlSimdUtils.ConcatUpperUpper(r4, r0); + i5 = JxlSimdUtils.ConcatUpperUpper(r5, r1); + i6 = JxlSimdUtils.ConcatUpperUpper(r6, r2); + i7 = JxlSimdUtils.ConcatUpperUpper(r7, r3); + + to.StorePart256(i0, m, n); + to.StorePart256(i1, m + 1, n); + to.StorePart256(i2, m + 2, n); + to.StorePart256(i3, m + 3, n); + to.StorePart256(i4, m + 4, n); + to.StorePart256(i5, m + 5, n); + to.StorePart256(i6, m + 6, n); + to.StorePart256(i7, m + 7, n); + } + } + } + + private static void TransposeSimd128(int rowsOr0, int colsOr0, JxlDctSource from, JxlDctOutput to, int rowsP, int colsP) + { + int rows = rowsOr0 == 0 ? rowsP : rowsOr0; + int cols = colsOr0 == 0 ? colsP : colsOr0; + + for (int n = 0; n < rows; n += 4) + { + for (int m = 0; m < cols; m += 4) + { + Vector128 p0 = from.LoadPart128(n, m); + Vector128 p1 = from.LoadPart128(n + 1, m); + Vector128 p2 = from.LoadPart128(n + 2, m); + Vector128 p3 = from.LoadPart128(n + 3, m); + + Vector128 q0 = Vector128_.InterleaveLower(p0, p2); + Vector128 q1 = Vector128_.InterleaveLower(p1, p3); + Vector128 q2 = Vector128_.InterleaveUpper(p0, p2); + Vector128 q3 = Vector128_.InterleaveUpper(p1, p3); + + Vector128 r0 = Vector128_.InterleaveLower(q0, q1); + Vector128 r1 = Vector128_.InterleaveUpper(q0, q1); + Vector128 r2 = Vector128_.InterleaveLower(q2, q3); + Vector128 r3 = Vector128_.InterleaveUpper(q2, q3); + + to.StorePart128(r0, m, n); + to.StorePart128(r1, m + 1, n); + to.StorePart128(r2, m + 2, n); + to.StorePart128(r3, m + 3, n); + } + } + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlLehmerCode.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlLehmerCode.cs index 2cbd4ea7ab..83e591fa9e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlLehmerCode.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlLehmerCode.cs @@ -10,16 +10,16 @@ internal static class JxlLehmerCode [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int ValueOfLowest1Bit(int n) => n & -n; - public static bool ComputeLehmerCode(ReadOnlySpan permutation, Span temp, int n, Span code) + public static bool ComputeLehmerCode(ReadOnlySpan permutation, Span temp, int n, Span code) { temp[(n + 1)..].Clear(); for (int idx = 0; idx < n; idx++) { - int s = permutation[idx]; + uint s = permutation[idx]; uint penalty = 0u; - uint i = (uint)s + 1u; + uint i = s + 1u; while (i != 0u) { @@ -32,8 +32,8 @@ public static bool ComputeLehmerCode(ReadOnlySpan permutation, Span t return false; } - code[idx] = (uint)s - penalty; - i = (uint)s + 1u; + code[idx] = s - penalty; + i = s + 1u; while (i < n + 1u) { @@ -45,7 +45,7 @@ public static bool ComputeLehmerCode(ReadOnlySpan permutation, Span t return true; } - public static bool DecodeLehmerCode(ReadOnlySpan code, Span temp, int n, Span permutation) + public static bool DecodeLehmerCode(ReadOnlySpan code, Span temp, int n, Span permutation) { if (n == 0) { @@ -91,7 +91,7 @@ public static bool DecodeLehmerCode(ReadOnlySpan code, Span temp, in } } - permutation[i] = next; + permutation[i] = unchecked((uint)next); next++; while (next <= paddedN) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlXorShift.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlXorShift.cs index 68edb75d5e..8f04197ee5 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlXorShift.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlXorShift.cs @@ -12,7 +12,7 @@ internal sealed class JxlXorShift private readonly ulong[] s0 = new ulong[8]; private readonly ulong[] s1 = new ulong[8]; - public void XorShift128Plus(ulong seed) + public JxlXorShift(ulong seed) { this.s0[0] = SplitMix64(seed + 0x9E3779B97F4A7C15L); this.s1[0] = SplitMix64(this.s0[0]); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerSimd.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerSimd.cs new file mode 100644 index 0000000000..66d92467cb --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerSimd.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; + +/// +/// SIMD utilities used by the quantizer. +/// +internal static class JxlQuantizerSimd +{ + public static Vector AdjustQuantBias(int c, Vector quantI, Span biases) + { + Vector quant = quantI.As(); + Vector constSign = Vector.Create(int.MinValue).As(); + Vector sign = quant & constSign; + Vector absoluteQuant = Vector.AndNot(constSign, quant); + + Vector is01 = Vector.LessThan(absoluteQuant, Vector.Create(1.125f)); + Vector not0 = Vector.GreaterThan(absoluteQuant, Vector.One); + + Vector oneBias = Vector.ConditionalSelect(not0, Vector.Create(biases[c]) ^ sign, Vector.Zero); + Vector bias = -(Vector.Create(biases[3]) * quant.ReciprocalEstimate()) + quant; + + return Vector.ConditionalSelect(is01, oneBias, bias); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/Dct32.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/Dct32.cs new file mode 100644 index 0000000000..e7509e8c05 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/Dct32.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; + +/// +/// Storage for 32 DCT coefficients (floating-point). +/// +[InlineArray(32)] +internal struct Dct32 +{ + private float first; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs index a3901bf408..0a06b60da7 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs @@ -224,7 +224,7 @@ public bool Dequantize( for (int i = 0; i < 32; i++) { - float inverseDctFactor = (i == 0) ? Sqrt05 : 1.0f; + float inverseDctFactor = (i == 0) ? JxlDctScales.Sqrt05 : 1.0f; result.SigmaDct[i] = this.SigmaDct[i] * inverseDctFactor * ChannelWeight[3] * inverseQuant; float weightF = MathF.Ceiling(inverseQuant * MathF.Abs(this.SigmaDct[i])); long weight = (long)Math.Min(weightLimit, Math.Max(1.0f, weightF)); @@ -240,7 +240,7 @@ public bool Dequantize( return true; } - public bool Decode( + public bool TryDecode( Configuration configuration, Span contextMap, JxlAnsSymbolReader decoder, diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSpline.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSpline.cs index f95886b268..e3c35d5e09 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSpline.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSpline.cs @@ -11,9 +11,9 @@ internal sealed class JxlSpline : IDisposable public Memory ControlPoints { get; private set; } - public JxlDct32[] ColorDct { get; set; } = []; + public Dct32[] ColorDct { get; set; } = []; - public JxlDct32 SigmaDct { get; set; } + public Dct32 SigmaDct { get; set; } public void ClearControlPoints() { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineUtils.cs new file mode 100644 index 0000000000..89b446e1f0 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineUtils.cs @@ -0,0 +1,394 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; + +internal static class JxlSplineUtils +{ + public const float DesiredRenderingDistance = 1f; + + private static ReadOnlySpan ContinuousIDCTMultipliers => + [ + MathF.PI / 32 * 0, MathF.PI / 32 * 1, MathF.PI / 32 * 2, MathF.PI / 32 * 3, MathF.PI / 32 * 4, + MathF.PI / 32 * 5, MathF.PI / 32 * 6, MathF.PI / 32 * 7, MathF.PI / 32 * 8, MathF.PI / 32 * 9, + MathF.PI / 32 * 10, MathF.PI / 32 * 11, MathF.PI / 32 * 12, MathF.PI / 32 * 13, MathF.PI / 32 * 14, + MathF.PI / 32 * 15, MathF.PI / 32 * 16, MathF.PI / 32 * 17, MathF.PI / 32 * 18, MathF.PI / 32 * 19, + MathF.PI / 32 * 20, MathF.PI / 32 * 21, MathF.PI / 32 * 22, MathF.PI / 32 * 23, MathF.PI / 32 * 24, + MathF.PI / 32 * 25, MathF.PI / 32 * 26, MathF.PI / 32 * 27, MathF.PI / 32 * 28, MathF.PI / 32 * 29, + MathF.PI / 32 * 30, MathF.PI / 32 * 31, + ]; + + public static float ContinuousInverseDCT(in Dct32 dct, float t) + { + ref float multipliers = ref MemoryMarshal.GetReference(ContinuousIDCTMultipliers); + ReadOnlySpan dctData = dct; + ref float dctDataRef = ref MemoryMarshal.GetReference(dctData); + + if (Vector.Count <= 32 && Vector.IsHardwareAccelerated) + { + Vector result = Vector.Zero; + Vector tandhalf = Vector.Create(t + 0.5f); + + for (int i = 0; i < 32; i += Vector.Count) + { + Vector cosArg = Vector.LoadUnsafe(ref Unsafe.Add(ref multipliers, i)) * tandhalf; + Vector cos = Vector.Cos(cosArg); + Vector localRes = Vector.LoadUnsafe(ref Unsafe.Add(ref dctDataRef, i)) * cos; + result = (Vector.Create(JxlDctScales.Sqrt2) * localRes) + result; + } + + return Vector.Sum(result); + } + + // Might have SIMD support but Vector > 32 (e.g. on some CPUs), + // so let's try different fixed-size vectors first. + else if (Vector512.IsHardwareAccelerated) + { + Vector512 result = Vector512.Zero; + Vector512 tandhalf = Vector512.Create(t + 0.5f); + + for (int i = 0; i < 32; i += Vector512.Count) + { + Vector512 cosArg = Vector512.LoadUnsafe(ref Unsafe.Add(ref multipliers, i)) * tandhalf; + Vector512 cos = Vector512.Cos(cosArg); + Vector512 localRes = Vector512.LoadUnsafe(ref Unsafe.Add(ref dctDataRef, i)) * cos; + result = (Vector512.Create(JxlDctScales.Sqrt2) * localRes) + result; + } + + return Vector512.Sum(result); + } + else if (Vector256.IsHardwareAccelerated) + { + Vector256 result = Vector256.Zero; + Vector256 tandhalf = Vector256.Create(t + 0.5f); + + for (int i = 0; i < 32; i += Vector256.Count) + { + Vector256 cosArg = Vector256.LoadUnsafe(ref Unsafe.Add(ref multipliers, i)) * tandhalf; + Vector256 cos = Vector256.Cos(cosArg); + Vector256 localRes = Vector256.LoadUnsafe(ref Unsafe.Add(ref dctDataRef, i)) * cos; + result = (Vector256.Create(JxlDctScales.Sqrt2) * localRes) + result; + } + + return Vector256.Sum(result); + } + else if (Vector128.IsHardwareAccelerated) + { + Vector128 result = Vector128.Zero; + Vector128 tandhalf = Vector128.Create(t + 0.5f); + + for (int i = 0; i < 32; i += Vector128.Count) + { + Vector128 cosArg = Vector128.LoadUnsafe(ref Unsafe.Add(ref multipliers, i)) * tandhalf; + Vector128 cos = Vector128.Cos(cosArg); + Vector128 localRes = Vector128.LoadUnsafe(ref Unsafe.Add(ref dctDataRef, i)) * cos; + result = (Vector128.Create(JxlDctScales.Sqrt2) * localRes) + result; + } + + return Vector128.Sum(result); + } + else if (Vector64.IsHardwareAccelerated) + { + Vector64 result = Vector64.Zero; + Vector64 tandhalf = Vector64.Create(t + 0.5f); + + for (int i = 0; i < 32; i += Vector64.Count) + { + Vector64 cosArg = Vector64.LoadUnsafe(ref Unsafe.Add(ref multipliers, i)) * tandhalf; + Vector64 cos = Vector64.Cos(cosArg); + Vector64 localRes = Vector64.LoadUnsafe(ref Unsafe.Add(ref dctDataRef, i)) * cos; + result = (Vector64.Create(JxlDctScales.Sqrt2) * localRes) + result; + } + + return Vector64.Sum(result); + } + else + { + // Scalar fallback. + float result = 0f; + float tandhalf = t + 0.5f; + + for (int i = 0; i < 32; i++) + { + float cosArg = Unsafe.Add(ref multipliers, i) * tandhalf; + float cos = MathF.Cos(cosArg); + float localRes = Unsafe.Add(ref dctDataRef, i) * cos; + result = (JxlDctScales.Sqrt2 * localRes) + result; + } + + return result; + } + } + + // SIMD version + private static void DrawSegmentPacked(ref JxlSplineSegment segment, bool add, int y, int x, int x0, InlineArray3> rows) + { + Vector inverseSigma = Vector.Create(segment.InverseSigma); + Vector half = Vector.Create(0.5f); + Vector oneOver2s2 = Vector.Create(0.353553391f); + Vector sigmaOver4TimesIntensity = Vector.Create(segment.SigmaOver4TimesIntensity); + + Vector dx = JxlSimdUtils.Iota(x + x0).As() - Vector.Create(segment.Center.X); + Vector dy = Vector.Create(y - segment.Center.Y); + + Vector sqd = (dx * dx) + (dy * dy); + Vector distance = Vector.SquareRoot(sqd); + + Vector oneDimensionalFactor = + JxlSimdUtils.FastErff(((distance * half) + oneOver2s2) * inverseSigma) + - JxlSimdUtils.FastErff(((distance * half) - oneOver2s2) * inverseSigma); + + Vector localIntensity = sigmaOver4TimesIntensity * (oneDimensionalFactor * oneDimensionalFactor); + + for (int c = 0; c < 3; c++) + { + Span currRow = rows[c].Span; + ref float currRowRef = ref MemoryMarshal.GetReference(currRow); + + // TODO: move the add branch outside the loop and duplicate the + // loops twice? this removes the branch + Vector cm = Vector.Create(add ? segment.Color[c] : -segment.Color[c]); + + Vector @in = Vector.LoadUnsafe(ref Unsafe.Add(ref currRowRef, x)); + ((cm * localIntensity) + @in).StoreUnsafe(ref Unsafe.Add(ref currRowRef, x)); + } + } + + // Scalar version (for remaining items left to process) + private static void DrawSegmentScalar(ref JxlSplineSegment segment, bool add, int y, int x, int x0, InlineArray3> rows) + { + float inverseSigma = segment.InverseSigma; + float half = 0.5f; + float oneOver2s2 = 0.353553391f; + float sigmaOver4TimesIntensity = segment.SigmaOver4TimesIntensity; + + float dx = (x + x0) - segment.Center.X; + float dy = y - segment.Center.Y; + + float sqd = (dx * dx) + (dy * dy); + float distance = MathF.Sqrt(sqd); + + float oneDimensionalFactor = + JxlSimdUtils.FastErff(((distance * half) + oneOver2s2) * inverseSigma) + - JxlSimdUtils.FastErff(((distance * half) - oneOver2s2) * inverseSigma); + + float localIntensity = sigmaOver4TimesIntensity * (oneDimensionalFactor * oneDimensionalFactor); + + for (int c = 0; c < 3; c++) + { + Span currRow = rows[c].Span; + float cm = add ? segment.Color[c] : -segment.Color[c]; + currRow[x] = (cm * localIntensity) + currRow[x]; + } + } + + public static void DrawSegment(ref JxlSplineSegment segment, bool add, int y, int x0, int x1, InlineArray3> rows) + { + int start = (int)MathF.Round(segment.Center.X - segment.MaximumDistance, MidpointRounding.AwayFromZero); + int end = (int)MathF.Round(segment.Center.X + segment.MaximumDistance, MidpointRounding.AwayFromZero); + + if (end < x0 || start >= x1) + { + return; // span does not intersect scan + } + + int spanX0 = Math.Max(x0, start) - x0; + int spanX1 = Math.Min(x1, end + 1) - x0; + + int x = spanX0; + for (; x + Vector.Count <= spanX1; x += Vector.Count) + { + DrawSegmentPacked(ref segment, add, y, x, x0, rows); + } + + for (; x < spanX1; ++x) + { + DrawSegmentScalar(ref segment, add, y, x, x0, rows); + } + } + + public static void ComputeSegments(int imageYSize, PointF center, float intensity, InlineArray3 color, float sigma, List segments, List segmentSpans) + { + if (!(float.IsFinite(sigma) && sigma != 0.0f && float.IsFinite(1.0f / sigma) && float.IsFinite(intensity))) + { + return; + } + + // This is about 30% faster, but for higher precision + // one can change this to 5 instead. + const float distanceExp = 3f; + + float maxColor = MathF.Max(0.01f, MathF.Abs(color[0] * intensity)); + maxColor = MathF.Max(maxColor, MathF.Abs(color[1] * intensity)); + maxColor = MathF.Max(maxColor, MathF.Abs(color[2] * intensity)); + + float maximumDistance = MathF.Sqrt(-2.0f * sigma * sigma * ((MathF.Log(0.1f) * distanceExp) - MathF.Log(maxColor))); + + int y0 = (int)MathF.Round(center.Y - maximumDistance, MidpointRounding.AwayFromZero); + y0 = Math.Max(y0, 0); + + int y1 = (int)MathF.Round(center.Y + maximumDistance, MidpointRounding.AwayFromZero) + 1; + y1 = Math.Min(y1, imageYSize); + + if (y1 <= y0) + { + return; + } + + JxlSplineSegment segment = new() + { + Center = center, + InverseSigma = 1.0f / sigma, + SigmaOver4TimesIntensity = 0.25f * sigma * intensity, + MaximumDistance = maximumDistance, + Color = color + }; + + segments.Add(segment); + segmentSpans.Add(new JxlSplineSegmentSpan(y0, y1)); + } + + public static void DrawSegments(Memory rowX, Memory rowY, Memory rowB, int y, int x0, int x1, bool add, Span segments, Span segmentIndices, Span segmentYStart) + { + InlineArray3> rows = default; + rows[0] = rowX; + rows[1] = rowY; + rows[2] = rowB; + + for (int i = segmentYStart[y]; i < segmentYStart[y + 1]; i++) + { + DrawSegment(ref segments[segmentIndices[i]], add, y, x0, x1, rows); + } + } + + public static void SegmentsFromPoints(int imageYSize, JxlSpline spline, List<(PointF Point, float Multiplier)> pointsToDraw, float arcLength, List segments, List segmentsSpans) + { + float inverseArcLength = 1.0f / arcLength; + int k = 0; + + foreach ((PointF point, float multiplier) in pointsToDraw) + { + float progressAlongArc = MathF.Min(1.0f, (k++ * DesiredRenderingDistance) * inverseArcLength); + + InlineArray3 color = default; + color[0] = ContinuousInverseDCT(spline.ColorDct[0], (32 - 1) * progressAlongArc); + color[1] = ContinuousInverseDCT(spline.ColorDct[1], (32 - 1) * progressAlongArc); + color[2] = ContinuousInverseDCT(spline.ColorDct[2], (32 - 1) * progressAlongArc); + + float sigma = ContinuousInverseDCT(spline.SigmaDct, (32 - 1) * progressAlongArc); + ComputeSegments(imageYSize, point, multiplier, color, sigma, segments, segmentsSpans); + } + } + + public static void DrawCentripetalCatmullRomSpline(Span points, List result) + { + if (points.Length == 0) + { + return; + } + + if (points.Length == 1) + { + result.Add(points[0]); + return; + } + + List pointsCopy = []; + for (int i = 0; i < points.Length; i++) + { + pointsCopy.Add(points[i]); + } + + const int numPoints = 16; + pointsCopy.Insert(0, pointsCopy[0] + (pointsCopy[0] - pointsCopy[1])); + pointsCopy.Add(pointsCopy[^1] + (pointsCopy[^1] - pointsCopy[^2])); + + for (int start = 0; start < pointsCopy.Count - 3; start++) + { + Span p = CollectionsMarshal.AsSpan(pointsCopy)[start..]; + result.Add(p[1]); + + InlineArray3 d = default; + InlineArray4 t = default; + + for (int k = 0; k < 3; ++k) + { + d[k] = MathF.Sqrt(JxlMath.Hypot(p[k + 1].X - p[k].X, p[k + 1].Y - p[k].Y)); + t[k + 1] = t[k] + d[k]; + } + + for (int i = 1; i < numPoints; ++i) + { + float tt = d[0] + (((float)i / numPoints) * d[1]); + InlineArray3 a = default; + + for (int k = 0; k < 3; ++k) + { + a[k] = p[k] + (((tt - t[k]) / d[k]) * (p[k + 1] - p[k])); + } + + InlineArray3 b = default; + + for (int k = 0; k < 2; ++k) + { + b[k] = a[k] + (((tt - t[k]) / (d[k] + d[k + 1])) * (a[k + 1] - a[k])); + } + + result.Add(b[0] + (((tt - t[1]) / d[1]) * (b[1] - b[0]))); + } + } + + result.Add(pointsCopy[^2]); + } + + public static void ForEachEquallySpacedPoint(Span points, Action functor) + { + PointF current = points[0]; + functor(current, DesiredRenderingDistance); + + ref PointF next = ref points[0]; + ref PointF end = ref points[^1]; // last + + while (!Unsafe.AreSame(ref next, ref end)) + { + ref PointF previous = ref current; + float arcLengthFromPrevious = 0f; + + while (true) + { + if (next == end) + { + functor(previous, arcLengthFromPrevious); + return; + } + + float arcLengthToNext = MathF.Sqrt(SquaredNorm(next - previous)); + + if (arcLengthFromPrevious + arcLengthToNext >= DesiredRenderingDistance) + { + current = previous + (((DesiredRenderingDistance - arcLengthFromPrevious) / arcLengthToNext) * (next - previous)); + functor(current, DesiredRenderingDistance); + break; + } + + arcLengthFromPrevious += arcLengthToNext; + previous = ref next; + next = ref Unsafe.Add(ref next, 1); + } + } + } + + private static float SquaredNorm(PointF pointF) + { + float x = pointF.X; + float y = pointF.Y; + + return (x * x) + (y * y); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplines.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplines.cs new file mode 100644 index 0000000000..52e83b4bb4 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplines.cs @@ -0,0 +1,289 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; + +internal sealed class JxlSplines +{ + private List splinesStorage = []; + private readonly List startingPointsStorage = []; + private JxlSplineDataView data = new(); + private readonly List segments = []; + private IMemoryOwner segmentIndices = EmptyMemoryOwner.Instance; + private IMemoryOwner segmentYStart = EmptyMemoryOwner.Instance; + + public bool HasAny => this.data.HasAny; + + public Span QuantizedSplines => CollectionsMarshal.AsSpan(this.data.Splines); + + public Span StartingPoints => CollectionsMarshal.AsSpan(this.data.StartingPoints); + + public int QuantizationAdjustment { get; private set; } + + public void SetData(JxlSplineDataView data) + { + this.Clear(); + this.data = data; + } + + public void Clear() + { + this.QuantizationAdjustment = 0; + this.splinesStorage.Clear(); + this.startingPointsStorage.Clear(); + this.data = new(); + this.segments.Clear(); + this.segmentIndices = EmptyMemoryOwner.Instance; + this.segmentYStart = EmptyMemoryOwner.Instance; + } + + public void Decode(Configuration configuration, JxlBitReader reader, int numPixels) + { + List contextMap = []; + + JxlAnsReader.DecodeHistograms(reader, NumSplineContexts, out JxlAnsCode code, contextMap); + JxlAnsSymbolReader decoder = new(code, reader); + + int numSplines = decoder.ReadHybridUint(NumSplinesContext, reader, contextMap); + int maxControlPoints = Math.Min(MaxNumControlPoints, numPixels / MaxNumControlPointsPerPixelRatio); + + if (numSplines > maxControlPoints || numSplines + 1 > maxControlPoints) + { + throw new InvalidOperationException("Too many splines: " + numSplines); + } + + numSplines++; + + DecodeAllStartingPoints(this.startingPointsStorage, reader, decoder, contextMap, numSplines); + + this.QuantizationAdjustment = JxlPackSigned.UnpackSigned(decoder.ReadHybridUint(QuantizationAdjustmentContext, reader, contextMap)); + this.splinesStorage = new List(numSplines); + + int numControlPoints = numSplines; + + for (int i = 0; i < numSplines; ++i) + { + JxlQuantizedSpline spline = new(); + if (!spline.TryDecode( + configuration, + CollectionsMarshal.AsSpan(contextMap), + decoder, + reader, + maxControlPoints, + ref numControlPoints)) + { + throw new InvalidOperationException("Could not decode quantized spline. Index of the quantized spline: " + i); + } + + this.splinesStorage.Add(spline); + } + + if (!decoder.CheckAnsFinalState()) + { + throw new InvalidOperationException("Not ANS final state"); + } + + this.data = new JxlSplineDataView() + { + Splines = this.splinesStorage, + StartingPoints = this.startingPointsStorage + }; + + if (!this.HasAny) + { + throw new InvalidOperationException("Decoded splines but got none"); + } + } + + public void InitializeDrawCache(Configuration configuration, int imageXSize, int imageYSize, JxlColorCorrelation colorCorrelation) + { + this.segments.Clear(); + this.segmentIndices = EmptyMemoryOwner.Instance; + this.segmentYStart = EmptyMemoryOwner.Instance; + + List segmentsSpans = []; + List intermediatePoints = []; + List splines = []; + long totalEstimatedAreaReached = 0; + + for (int i = 0; i < this.data.Splines.Count; i++) + { + JxlSpline spline = new(); + + if (!this.data.Splines[i].Dequantize( + configuration, + this.data.StartingPoints[i], + this.QuantizationAdjustment, + colorCorrelation.YToXRatio(0), + colorCorrelation.YToBRatio(0), + imageXSize * imageYSize, + ref totalEstimatedAreaReached, + spline)) + { + throw new InvalidOperationException("Could not dequantize a quantized spline"); + } + + if (AdjacentFind(spline.ControlPoints.Span) != spline.ControlPoints.Length - 1) + { + throw new InvalidOperationException("Identical successive control points in spline " + i); + } + + splines.Add(spline); + } + +#if JPEG_XL_THROW_ON_LARGE_SPLINE_AREA + if (totalEstimatedAreaReached > Math.Min((8 * imageXSize * imageYSize) + (1 << 25), 1 << 30)) + { + throw new InvalidOperationException("Total spline area is too large"); + } +#endif + + foreach (JxlSpline spline in splines) + { + List<(PointF Point, float Multiplier)> pointsToDraw = []; + + void AddPoint(PointF point, float multiplier) => pointsToDraw.Add((point, multiplier)); + + intermediatePoints.Clear(); + + JxlSplineUtils.DrawCentripetalCatmullRomSpline(spline.ControlPoints.Span, intermediatePoints); + JxlSplineUtils.ForEachEquallySpacedPoint(CollectionsMarshal.AsSpan(intermediatePoints), AddPoint); + + float arcLength = ((pointsToDraw.Count - 2) * JxlSplineUtils.DesiredRenderingDistance) + pointsToDraw[^1].Multiplier; + if (arcLength <= 0f) + { + // This spline wouldn't have any effect. + continue; + } + + JxlSplineUtils.SegmentsFromPoints(imageYSize, spline, pointsToDraw, arcLength, this.segments, segmentsSpans); + } + + int segmentYStartNumBytes = (imageYSize + 2) * 4; + this.segmentYStart = configuration.MemoryAllocator.Allocate(segmentYStartNumBytes); + + Span segmentYStart = this.segmentYStart.Memory.Span; + segmentYStart.Clear(); + + Span population = segmentYStart[1..]; + + foreach (JxlSplineSegmentSpan segmentSpan in segmentsSpans) + { + population[segmentSpan.StartInclusive]++; + population[segmentSpan.EndInclusive]--; + } + + int total = 0; + int coverage = 0; + + for (int y = 0; y < imageYSize; y++) + { + if (population[y] < 0) + { + if (coverage < -population[y]) + { + throw new InvalidOperationException("Coverage is invalid"); + } + } + + coverage += population[y]; + population[y] = (byte)total; + total += coverage; + } + + this.segmentIndices = configuration.MemoryAllocator.Allocate(total * 4); + Span segmentIndices = MemoryMarshal.Cast(this.segmentIndices.Memory.Span); + + for (int i = 0; i < this.segments.Count; i++) + { + JxlSplineSegmentSpan segmentSpan = segmentsSpans[i]; + + for (int y = segmentSpan.StartInclusive; y < segmentSpan.EndInclusive; y++) + { + segmentIndices[population[y]++] = i; + } + } + } + + private static int AdjacentFind(Span span) + where T : IEquatable + { + for (int i = 0; i < span.Length - 1; i++) + { + if (span[i].Equals(span[i + 1])) + { + return i; + } + } + + return span.Length; + } + + public void AddTo(JxlImage3F opsin, Rectangle opsinRect) => this.Apply(add: true, opsin, opsinRect); + + public void AddToRow(Memory rowX, Memory rowY, Memory rowB, int y, int x0, int x1) + => this.ApplyToRow(add: true, rowX, rowY, rowB, y, x0, x1); + + public void SubtractFrom(JxlImage3F opsin) => this.Apply(add: false, opsin, opsin.GetRectangle()); + + private void ApplyToRow(bool add, Memory rowX, Memory rowY, Memory rowB, int y, int x0, int x1) + { + if (this.segments.Count == 0) + { + return; + } + + JxlSplineUtils.DrawSegments( + rowX, + rowY, + rowB, + y, + x0, + x1, + add, + CollectionsMarshal.AsSpan(this.segments), + MemoryMarshal.Cast(this.segmentIndices.Memory.Span), + MemoryMarshal.Cast(this.segmentYStart.Memory.Span)); + } + + private void Apply(bool add, JxlImage3F opsin, Rectangle opsinRect) + { + if (this.segments.Count == 0) + { + return; + } + + int y0 = RectangleUtils.Y0(in opsinRect); + int x0 = RectangleUtils.X0(in opsinRect); + int x1 = RectangleUtils.X1(in opsinRect); + + for (int y = 0; y < opsinRect.Height; y++) + { + this.ApplyToRow( + add, + opsin.PlaneRowMemory(0, y0 + y)[x0..], + opsin.PlaneRowMemory(1, y0 + y)[x0..], + opsin.PlaneRowMemory(2, y0 + y)[x0..], + y0 + y, + x0, + x1); + } + } + + private sealed class EmptyMemoryOwner : IMemoryOwner + { + public static readonly EmptyMemoryOwner Instance = new(); + + public Memory Memory => Memory.Empty; + + public void Dispose() + { + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/AnsCommonTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/AnsCommonTests.cs new file mode 100644 index 0000000000..87b2fc6153 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/AnsCommonTests.cs @@ -0,0 +1,48 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; + +public class AnsCommonTests +{ + private static void VerifyAliasDistribution(Span distribution, uint logRange) + { + const int logAlphaSize = 8; + + Span table = stackalloc JxlAnsEntry[1 << logAlphaSize]; + bool success = JxlAnsHelper.InitAliasTable(distribution, logRange, logAlphaSize, table); + Assert.True(success); + + uint range = 1u << (int)logRange; + List[] offsets = new List[distribution.Length]; + + for (int i = 0; i < range; i++) + { + JxlAnsSymbol s = JxlAnsHelper.Lookup(table, i, JxlAnsConstants.AnsLogTableSize - 8, (1 << (JxlAnsConstants.AnsLogTableSize - 8)) - 1); + + offsets[s.Value] ??= []; + offsets[s.Value].Add(s.Offset); + } + + for (int i = 0; i < distribution.Length; i++) + { + Assert.Equal(distribution[i], offsets[i].Count); + offsets[i].Sort(); + + for (int j = 0; j < offsets[i].Count; j++) + { + Assert.Equal(offsets[i][j], j); + } + } + } + + [Fact] + public void AliasDistributionSmoke() + { + VerifyAliasDistribution([JxlAnsConstants.AnsTableSize / 2, JxlAnsConstants.AnsTableSize / 2], JxlAnsConstants.AnsLogTableSize); + VerifyAliasDistribution([JxlAnsConstants.AnsTableSize], JxlAnsConstants.AnsLogTableSize); + VerifyAliasDistribution([0, 0, 0, JxlAnsConstants.AnsTableSize, 0], JxlAnsConstants.AnsLogTableSize); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/GammaCorrectionTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/GammaCorrectionTests.cs new file mode 100644 index 0000000000..edb9751cae --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/GammaCorrectionTests.cs @@ -0,0 +1,37 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing.Encoder; + +public class GammaCorrectionTests +{ + [Fact] + public void TestLinearToSRgbEdgeCases() + { + Assert.Equal(0, JxlGammaCorrect.LinearToSRgb8Direct(0.0)); + + Assert.True(new TolerantMath(2E-5).AreEqual(0, JxlGammaCorrect.LinearToSRgb8Direct(1E-6))); + + Assert.Equal(0, JxlGammaCorrect.LinearToSRgb8Direct(-1E-6)); + Assert.Equal(0, JxlGammaCorrect.LinearToSRgb8Direct(-1E6)); + + Assert.True(new TolerantMath(1E-5).AreEqual(1, JxlGammaCorrect.LinearToSRgb8Direct(1 - 1E-6))); + + Assert.Equal(1, JxlGammaCorrect.LinearToSRgb8Direct(1 + 1E-6)); + Assert.Equal(1, JxlGammaCorrect.LinearToSRgb8Direct(1E6)); + } + + [Fact] + public void TestRoundTrip() + { + for (double linear = 0.0; linear <= 1.0; linear += 1E-7) + { + double srgb = JxlGammaCorrect.LinearToSRgb8Direct(linear); + double linear2 = JxlGammaCorrect.SRgb8ToLinearDirect(srgb); + + Assert.True(Math.Abs(linear - linear2) < 2E-13, $"Linear = {linear}, Linear2 = {linear2}"); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/Noise/PhotonNoiseTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/Noise/PhotonNoiseTests.cs new file mode 100644 index 0000000000..4f1eb5e5a9 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/Noise/PhotonNoiseTests.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Noise; + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing.Encoder.Noise; + +/// +/// Tests for the simulation of photon noise in the JPEG XL encoder routines. +/// +public class PhotonNoiseTests +{ + [Fact] + public void TestPhotonNoiseEncoder() + { + ApproximateFloatComparer comparer = new(1e-6f); + + Assert.Equal( + JxlPhotonNoise.SimulatePhotonNoise(xSize: 6000, ySize: 4000, iso: 100).Lookup, + [0.00259652f, 0.0139648f, 0.00681551f, 0.00632582f, + 0.00694917f, 0.00803922f, 0.00934574f, 0.0107607f], + comparer); + + Assert.Equal( + JxlPhotonNoise.SimulatePhotonNoise(xSize: 6000, ySize: 4000, iso: 800).Lookup, + [0.02077220f, 0.0420923f, 0.01820690f, 0.01439020f, + 0.01293670f, 0.01254030f, 0.01277390f, 0.0134161f], + comparer); + + Assert.Equal( + JxlPhotonNoise.SimulatePhotonNoise(xSize: 6000, ySize: 4000, iso: 6400).Lookup, + [0.1661770f, 0.1691120f, 0.05309080f, 0.03963960f, + 0.03357410f, 0.03001650f, 0.02776740f, 0.0263478f], + comparer); + + Assert.Equal( + JxlPhotonNoise.SimulatePhotonNoise(xSize: 4000, ySize: 3000, iso: 6400).Lookup, + [0.0830886f, 0.1008720f, 0.0367748f, 0.0280305f, 0.0240236f, + 0.0218040f, 0.0205771f, 0.0200058f], + comparer); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/Primitives/LehmerCodeTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Primitives/LehmerCodeTests.cs new file mode 100644 index 0000000000..66bd1f4a53 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Primitives/LehmerCodeTests.cs @@ -0,0 +1,88 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Processing; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing.Primitives; + +public class LehmerCodeTests +{ + private sealed class WorkingSet(int maxN) + { + public int PaddedN { get; } = maxN << JxlMath.CeilLog2Nonzero(maxN + 1); + + public uint[] Permutation { get; } = new uint[maxN]; + + public uint[] Temporary { get; } = new uint[maxN]; + + public uint[] LehmerCodes { get; } = new uint[maxN]; + + public uint[] Decoded { get; } = new uint[maxN]; + } + + private static void RoundTrip(int n, WorkingSet ws) + { + Assert.NotEqual(0, n); + int paddedN = 1 << JxlMath.CeilLog2Nonzero(n); + + Rng rng = new(((ulong)n * 65537) + 13); + Assert.True(n < 1 << (sizeof(uint) * 8)); + + Span permutationsSpan = ws.Permutation.AsSpan(); + JxlSimdUtils.Iota(permutationsSpan[..n], 0u); + + for (int rep = 0; rep < 3; rep++) + { + rng.Shuffle(permutationsSpan[..n]); + + Assert.True( + JxlLehmerCode.ComputeLehmerCode(permutationsSpan, ws.Temporary, n, ws.LehmerCodes), + "Could not compute Lehmer code"); + + ws.Temporary.AsSpan()[..(paddedN * 4)].Clear(); + + Assert.True( + JxlLehmerCode.DecodeLehmerCode(ws.LehmerCodes.AsSpan(), ws.Temporary.AsSpan(), n, ws.Decoded.AsSpan()), + "Could not decode Lehmer code"); + + for (int i = 0; i < n; ++i) + { + Assert.Equal(permutationsSpan[i], ws.Decoded[i]); + } + } + } + + private static void RoundTripSizeRange(int begin, int end) + { + Assert.NotEqual(0, begin); + List workingSets = []; + + int numThreads = Environment.ProcessorCount; + + // initialization + for (int i = 0; i < numThreads; i++) + { + workingSets.Add(new WorkingSet(end - 1)); + } + + // loop + Parallel.For( + begin, + end, + () => new WorkingSet(end - 1), + (n, _, workingSet) => + { + RoundTrip(n, workingSet); + return workingSet; + }, + _ => { }); + } + + [Fact] + public void TestLehmerCodes() + { + RoundTripSizeRange(1, 1026); + RoundTripSizeRange(65536, 65540); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/Primitives/XorShiftTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Primitives/XorShiftTests.cs new file mode 100644 index 0000000000..756ed5668e --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Primitives/XorShiftTests.cs @@ -0,0 +1,396 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; +using System.Numerics; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using static SixLabors.ImageSharp.Tests.TestImages; + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing.Primitives; + +public class XorShiftTests +{ + private const int Vectors = 64; + + private static readonly ulong[][] ExpectedVectors = + [ + [0x6E901576D477CBB1uL, 0xE9E53789195DA2A2uL, 0xB681F6DDA5E0AE99uL, + 0x8EFD18CE21FD6896uL, 0xA898A80DF75CF532uL, 0x50CEB2C9E2DE7E32uL, + 0x3CA7C2FEB25C0DD0uL, 0xA4D0866B80B4D836uL], + [0x8CD6A1E6233D3A26uL, 0x3D4603ADE98B112DuL, 0xDC427AF674019E36uL, + 0xE28B4D230705AC53uL, 0x7297E9BBA88783DDuL, 0x34D3D23CFCD9B41AuL, + 0x5A223615ADBE96B8uL, 0xE5EB529027CFBD01uL], + [ + 0xC1894CF00DFAC6A2uL, 0x18EDF8AE9085E404uL, 0x8E936625296B4CCDuL, + 0x31971EF3A14A899BuL, 0xBE87535FCE0BF26AuL, 0x576F7A752BC6649FuL, + 0xA44CBADCE0C6B937uL, 0x3DBA819BB17A353AuL], + [ + 0x27CE38DFCC1C5EB6uL, 0x920BEB5606340256uL, 0x3986CBC40C9AFC2CuL, + 0xE22BCB3EEB1E191EuL, 0x6E1FCDD3602A8FBAuL, 0x052CB044E5415A29uL, + 0x46266646EFB9ECD7uL, 0x8F44914618D29335uL], + [ + 0xDD30AEDF72A362C5uL, 0xBC1D824E16BB98F4uL, 0x9EA6009C2AA3D2F1uL, + 0xF65C0FBBE17AF081uL, 0x22424D06A8738991uL, 0x8A62763F2B7611D2uL, + 0x2F3E89F722637939uL, 0x84D338BEF50AFD50uL], + [ + 0x00F46494898E2B0BuL, 0x81239DC4FB8E8003uL, 0x414AD93EC5773FE7uL, + 0x791473C450E4110FuL, 0x87F127BF68C959ACuL, 0x6429282D695EF67BuL, + 0x661082E11546CBA8uL, 0x5815D53FA5436BFDuL], + [ + 0xB3DEADAB9BE6E0F9uL, 0xAA1B7B8F7CED0202uL, 0x4C5ED437699D279EuL, + 0xA4471727F1CB39D3uL, 0xE439DA193F802F70uL, 0xF89401BB04FA6493uL, + 0x3B08045A4FE898BAuL, 0x32137BFE98227950uL], + [ + 0xFBAE4A092897FEF3uL, 0x0639F6CE56E71C8EuL, 0xF0AD6465C07F0C1EuL, + 0xFF8E28563361DCE5uL, 0xC2013DB7F86BC6B9uL, 0x8EFCC0503330102FuL, + 0x3F6B767EA5C4DA40uL, 0xB9864B950B2232E1uL], + [ + 0x76EB58DE8E5EC22AuL, 0x9BBBF49A18B32F4FuL, 0xC8405F02B2B2FAB9uL, + 0xC3E122A5F146BC34uL, 0xC90BB046660F5765uL, 0xB933981310DBECCFuL, + 0x5A2A7BFC9126FD1CuL, 0x8BB388C94DF87901uL], + [ + 0x753EB89AD63EF3C3uL, 0xF24AAF40C89D65ADuL, 0x23F68931C1A6AA6DuL, + 0xF47E79BF702C6DD0uL, 0xA3AD113244EE7EAEuL, 0xD42CBEA28F793DC3uL, + 0xD896FCF1820F497CuL, 0x042B86D2818948C1uL], + [ + 0x8F2A4FC5A4265763uL, 0xEC499E6F95EAA10CuL, 0xE3786D4ECCD0DEB5uL, + 0xC725C53D3AC4CC43uL, 0x065A4ACBBF83610EuL, 0x35C61C9FEF167129uL, + 0x7B720AEAA7D70048uL, 0x14206B841377D039uL], + [ + 0xAD27D78BF96055F6uL, 0x5F43B20FF47ADCD4uL, 0xE184C2401E2BF71EuL, + 0x30B263D78990045DuL, 0xC22F00EBFF9BA201uL, 0xAE7F86522B53A562uL, + 0x2853312BC039F0A4uL, 0x868D619E6549C3C8uL], + [ + 0xFD5493D8AE9A8371uL, 0x773D5E224DF61B3BuL, 0x5377C54FBB1A8280uL, + 0xCAD4DE3B8265CAFAuL, 0xCDF3F19C91EBD5F6uL, 0xC8EA0F182D73BD78uL, + 0x220502D593433FF1uL, 0xB81205E612DC31B1uL], + [ + 0x8F32A39EAEDA4C70uL, 0x1D4B0914AA4DAC7FuL, 0x56EF1570F3A8B405uL, + 0x29812CB17404A592uL, 0x97A2AAF69CAE90F2uL, 0x12BF5E02778BBFE5uL, + 0x9D4B55AD42A05FD2uL, 0x06C2BAB5E6086620uL], + [ + 0x8DB4B9648302B253uL, 0xD756AD9E3AEA12C7uL, 0x68709B7F11D4B188uL, + 0x7CC299DDCD707A4BuL, 0x97B860C370A7661DuL, 0xCECD314FC20E64F5uL, + 0x55F412CDFB4C7EC3uL, 0x55EE97591193B525uL], + [ + 0xCF70F3ACA96E6254uL, 0x022FEDECA2E09F46uL, 0x686823DB60AE1ECFuL, + 0xFD36190D3739830EuL, 0x74E1C09027F68120uL, 0xB5883A835C093842uL, + 0x93E1EFB927E9E4E3uL, 0xB2721E249D7E5EBEuL], + [ + 0x69B6E21C44188CB8uL, 0x5D6CFB853655A7AAuL, 0x3E001A0B425A66DCuL, + 0x8C57451103A5138FuL, 0x7BF8B4BE18EAB402uL, 0x494102EB8761A365uL, + 0xB33796A9F6A81F0EuL, 0x10005AB3BCCFD960uL], + [ + 0xB2CF25740AE965DCuL, 0x6F7C1DF7EF53D670uL, 0x648DD6087AC2251EuL, + 0x040955D9851D487DuL, 0xBD550FC7E21A7F66uL, 0x57408F484DEB3AB5uL, + 0x481E24C150B506C1uL, 0x72C0C3EAF91A40D6uL], + [ + 0x1997A481858A5D39uL, 0x539718F4BEF50DC1uL, 0x2EC4DC4787E7E368uL, + 0xFF1CE78879419845uL, 0xE219A93DD6F6DD30uL, 0x85328618D02FEC1AuL, + 0xC86E02D969181B20uL, 0xEBEC8CD8BBA34E6EuL], + [ + 0x28B55088A16CE947uL, 0xDD25AC11E6350195uL, 0xBD1F176694257B1CuL, + 0x09459CCF9FCC9402uL, 0xF8047341E386C4E4uL, 0x7E8E9A9AD984C6C0uL, + 0xA4661E95062AA092uL, 0x70A9947005ED1152uL], + [ + 0x4C01CF75DBE98CCDuL, 0x0BA076CDFC7373B9uL, 0x6C5E7A004B57FB59uL, + 0x336B82297FD3BC56uL, 0x7990C0BE74E8D60FuL, 0xF0275CC00EC5C8C8uL, + 0x6CF29E682DFAD2E9uL, 0xFA4361524BD95D72uL], + [ + 0x631D2A19FF62F018uL, 0x41C43863B985B3FAuL, 0xE052B2267038EFD9uL, + 0xE2A535FAC575F430uL, 0xE004EEA90B1FF5B8uL, 0x42DFE2CA692A1F26uL, + 0x90FB0BFC9A189ECCuL, 0x4484102BD3536BD0uL], + [ + 0xD027134E9ACCA5A5uL, 0xBBAB4F966D476A9BuL, 0x713794A96E03D693uL, + 0x9F6335E6B94CD44AuL, 0xC5090C80E7471617uL, 0x6D9C1B0C87B58E33uL, + 0x1969CE82E31185A5uL, 0x2099B97E87754EBEuL], + [ + 0x60EBAF4ED934350FuL, 0xC26FBF0BA5E6ECFFuL, 0x9E54150F0312EC57uL, + 0x0973B48364ED0041uL, 0x800A523241426CFCuL, 0x03AB5EC055F75989uL, + 0x8CF315935DEEB40AuL, 0x83D3FC0190BD1409uL], + [ + 0x26D35394CF720A51uL, 0xCE9EAA15243CBAFEuL, 0xE2B45FBAF21B29E0uL, + 0xDB92E98EDE73F9E0uL, 0x79B16F5101C26387uL, 0x1AC15959DE88C86FuL, + 0x387633AEC6D6A580uL, 0xA6FC05807BFC5EB8uL], + [ + 0x2D26C8E47C6BADA9uL, 0x820E6EC832D52D73uL, 0xB8432C3E0ED0EE5BuL, + 0x0F84B3C4063AAA87uL, 0xF393E4366854F651uL, 0x749E1B4D2366A567uL, + 0x805EACA43480D004uL, 0x244EBF3AA54400A5uL], + [ + 0xBFDC3763AA79F75AuL, 0x9E3A74CC751F41DBuL, 0xF401302A149DBC55uL, + 0x6B25F7973D7BF7BCuL, 0x13371D34FDBC3DAEuL, 0xC5E1998C8F484DCDuL, + 0x7031B8AE5C364464uL, 0x3847F0C4F3DA2C25uL], + [ + 0x24C6387D2C0F1225uL, 0x77CCE960255C67A4uL, 0x21A0947E497B10EBuL, + 0xBB5DB73A825A9D7EuL, 0x26294A41999E553DuL, 0x3953E0089F87D925uL, + 0x3DAE6E5D4E5EAAFEuL, 0x74B545460341A7AAuL], + [ + 0x710E5EB08A7DB820uL, 0x7E43C4E77CAEA025uL, 0xD4C91529C8B060C1uL, + 0x09AE26D8A7B0CA29uL, 0xAB9F356BB360A772uL, 0xB68834A25F19F6E9uL, + 0x79B8D9894C5734E2uL, 0xC6847E7C8FFD265FuL], + [ + 0x10C4BCB06A5111E6uL, 0x57CB50955B6A2516uL, 0xEF53C87798B6995FuL, + 0xAB38E15BBD8D0197uL, 0xA51C6106EFF73C93uL, 0x83D7F0E2270A7134uL, + 0x0923FD330397FCE5uL, 0xF9DE54EDFE58FB45uL], + [ + 0x07D44833ACCD1A94uL, 0xAAD3C9E945E2F9F3uL, 0xABF4C879B876AA37uL, + 0xF29C69A21B301619uL, 0x2DDCE959111C788BuL, 0x7CEDB48F8AC1729BuL, + 0x93F3BA9A02B659BEuL, 0xF20A87FF17933CBEuL], + [ + 0x8E96EBE93180CFE6uL, 0x94CAA12873937079uL, 0x05F613D9380D4189uL, + 0xBCAB40C1DC79F38AuL, 0x0AD8907B7C61D19EuL, 0x88534E189D103910uL, + 0x2DB2FAABA160AB8FuL, 0xA070E7506B06F15CuL], + [ + 0x6FB1FCDAFFEF87A9uL, 0xE735CF25337A090DuL, 0x172C6EDCEFEF1825uL, + 0x76957EA49EF0542DuL, 0x819BF4CD250F7C49uL, 0xD6FF23E4AD00C4D4uL, + 0xE79673C1EC358FF0uL, 0xAC9C048144337938uL], + [ + 0x4C5387FF258B3AF4uL, 0xEDB68FAEC2CB1AA3uL, 0x02A624E67B4E1DA4uL, + 0x5C44797A38E08AF2uL, 0x36546A70E9411B4BuL, 0x47C17B24D2FD9675uL, + 0x101957AAA020CA26uL, 0x47A1619D4779F122uL], + [ + 0xF84B8BCDC92D9A3CuL, 0x951D7D2C74B3066BuL, 0x7AC287C06EDDD9B2uL, + 0x4C38FC476608D38FuL, 0x224D793B19CB4BCDuL, 0x835A255899BF1A41uL, + 0x4AD250E9F62DB4ABuL, 0xD9B44F4B58781096uL], + [ + 0xABBAF99A8EB5C6B8uL, 0xFB568E900D3A9F56uL, 0x11EDF63D23C5DF11uL, + 0xA9C3011D3FA7C5A8uL, 0xAEDD3CF11AFFF725uL, 0xABCA472B5F1EDD6BuL, + 0x0600B6BB5D879804uL, 0xDB4DE007F22191A0uL], + [ + 0xD76CC9EFF0CE9392uL, 0xF5E0A772B59BA49AuL, 0x7D1AE1ED0C1261B5uL, + 0x79224A33B5EA4F4AuL, 0x6DD825D80C40EA60uL, 0x47FC8E747E51C953uL, + 0x695C05F72888BF98uL, 0x1A012428440B9015uL], + [ + 0xD754DD61F9B772BFuL, 0xC4A2FCF4C0F9D4EBuL, 0x461167CDF67A24A2uL, + 0x434748490EBCB9D4uL, 0x274DD9CDCA5781DEuL, 0x36BAC63BA9A85209uL, + 0x30324DAFDA36B70FuL, 0x337570DB4FE6DAB3uL], + [ + 0xF46CBDD57C551546uL, 0x8E02507E676DA3E3uL, 0xD826245A8C15406DuL, + 0xDFB38A5B71113B72uL, 0x5EA38454C95B16B5uL, 0x28C054FB87ABF3E1uL, + 0xAA2724C0BA1A8096uL, 0xECA83EC980304F2FuL], + [ + 0x6AA76EC294EB3303uL, 0x42D4CDB2A8032E3BuL, 0x7999EDF75DCD8735uL, + 0xB422BFFE696CCDCCuL, 0x8F721461FD7CCDFEuL, 0x148E1A5814FDE253uL, + 0x4DC941F4375EF8FFuL, 0x27B2A9E0EB5B49CFuL], + [ + 0xCEA592EF9343EBE1uL, 0xF7D38B5FA7698903uL, 0x6CCBF352203FEAB6uL, + 0x830F3095FCCDA9C5uL, 0xDBEEF4B81B81C8F4uL, 0x6D7EB9BCEECA5CF9uL, + 0xC58ABB0FBE436C69uL, 0xE4B97E6DB2041A4BuL], + [ + 0x7E40FC772978AF14uL, 0xCDDA4BBAE28354A1uL, 0xE4F993B832C32613uL, + 0xD3608093C68A4B35uL, 0x9A3B60E01BEE3699uL, 0x03BEF248F3288713uL, + 0x70B9294318F3E9B4uL, 0x8D2ABB913B8610DEuL], + [ + 0x37F209128E7D8B2CuL, 0x81D2AB375BD874BCuL, 0xA716A1B7373F7408uL, + 0x0CEE97BEC4706540uL, 0xA40C5FD9CDBC1512uL, 0x73CAF6C8918409E7uL, + 0x45E11BCEDF0BBAA1uL, 0x612C612BFF6E6605uL], + [ + 0xF8ECB14A12D0F649uL, 0xDA683CD7C01BA1ACuL, 0xA2203F7510E124C1uL, + 0x7F83E52E162F3C78uL, 0x77D2BB73456ACADBuL, 0x37FC34FC840BBA6FuL, + 0x3076BC7D4C6EBC1FuL, 0x4F514123632B5FA9uL], + [ + 0x44D789DED935E884uL, 0xF8291591E09FEC9FuL, 0xD9CED2CF32A2E4B7uL, + 0x95F70E1EB604904AuL, 0xDE438FE43C14F6ABuL, 0x4C8D23E4FAFCF8D8uL, + 0xC716910A3067EB86uL, 0x3D6B7915315095D3uL], + [ + 0x3170FDBADAB92095uL, 0x8F1963933FC5650BuL, 0x72F94F00ABECFEABuL, + 0x6E3AE826C6AAB4CEuL, 0xA677A2BF31068258uL, 0x9660CDC4F363AF10uL, + 0xD81A15A152379EF1uL, 0x5D7D285E1080A3F9uL], + [ + 0xDAD5DDFF9A2249B3uL, 0x6F9721D926103FAEuL, 0x1418CBB83FFA349AuL, + 0xE71A30AD48C012B2uL, 0xBE76376C63751132uL, 0x3496467ACA713AE6uL, + 0x8D7EC01369F991A3uL, 0xD8C73A88B96B154EuL], + [ + 0x8B5D9C74AEB4833AuL, 0xF914FB3F867B912FuL, 0xB894EA034936B1DCuL, + 0x8A16D21BE51C4F5BuL, 0x31FF048ED582D98EuL, 0xB95AB2F4DC65B820uL, + 0x04082B9170561AF7uL, 0xA215610A5DC836FAuL], + [ + 0xB2ADE592C092FAACuL, 0x7A1E683BCBF13294uL, 0xC7A4DBF86858C096uL, + 0x3A49940F97BFF316uL, 0xCAE5C06B82C46703uL, 0xC7F413A0F951E2BDuL, + 0x6665E7BB10EB5916uL, 0x86F84A5A94EDE319uL], + [ + 0x4EA199D8FAA79CA3uL, 0xDFA26E5BF1981704uL, 0x0F5E081D37FA4E01uL, + 0x9CB632F89CD675CDuL, 0x4A09DB89D48C0304uL, 0x88142742EA3C7672uL, + 0xAC4F149E6D2E9BDBuL, 0x6D9E1C23F8B1C6C6uL], + [ + 0xD58BE47B92DEC0E9uL, 0x8E57573645E34328uL, 0x4CC094CCB5FB5126uL, + 0x5F1D66AF6FB40E3CuL, 0x2BA15509132D3B00uL, 0x0D6545646120E567uL, + 0x3CF680C45C223666uL, 0x96B28E32930179DAuL], + [ + 0x5900C45853AC7990uL, 0x61881E3E8B7FF169uL, 0x4DE5F835DF2230FFuL, + 0x4427A9E7932F73FFuL, 0x9B641BAD379A8C8DuL, 0xDF271E5BF98F4E5CuL, + 0xDFDA16DB830FF5EEuL, 0x371C7E7CFB89C0E9uL], + [ + 0x4410A8576247A250uL, 0x6AD2DA12B45AC0D9uL, 0x18DFC72AAC85EECCuL, + 0x06FC8BB2A0EF25C8uL, 0xEB287619C85E6118uL, 0x19553ECA67F25A2CuL, + 0x3B9557F1DCEC5BAAuL, 0x7BAD9E8B710D1079uL], + [ + 0x34F365D66BD22B28uL, 0xE6E124B9F10F835DuL, 0x0573C38ABF2B24DCuL, + 0xD32E6AF10A0125AEuL, 0x383590ACEA979519uL, 0x8376ED7A39E28205uL, + 0xF0B7F184DCBDA435uL, 0x062A203390E31794uL], + [ + 0xA2AFFD7E41918760uL, 0x7F90FC1BD0819C86uL, 0x5033C08E5A969533uL, + 0x2707AF5C6D039590uL, 0x57BBD5980F17DF9CuL, 0xD3FE6E61D763268AuL, + 0x9E0A0AE40F335A3BuL, 0x43CF4EB0A99613C5uL], + [ + 0xD4D2A397CE1A7C2EuL, 0x3DF7CE7CC3212DADuL, 0x0880F0D5D356C75AuL, + 0xA8AFC44DD03B1346uL, 0x79263B46C13A29E0uL, 0x11071B3C0ED58E7AuL, + 0xED46DC9F538406BFuL, 0x2C94974F2B94843DuL], + [ + 0xE246E13C39AB5D5EuL, 0xAC1018489D955B20uL, 0x8601B558771852B8uL, + 0x110BD4C06DB40173uL, 0x738FC8A18CCA0EBBuL, 0x6673E09BE0EA76E5uL, + 0x024BC7A0C7527877uL, 0x45E6B4652E2EC34EuL], + [ + 0xD1ED26A1A375CDC8uL, 0xAABC4E896A617CB8uL, 0x0A9C9E8E57D753C6uL, + 0xA3774A75FEB4C30EuL, 0x30B816C01C93E49EuL, 0xF405BABC06D2408CuL, + 0xCC0CE6B4CE788ABCuL, 0x75E7922D0447956CuL], + [ + 0xD07C1676A698BC95uL, 0x5F9AEA4840E2D860uL, 0xD5FC10D58BDF6F02uL, + 0xF190A2AD4BC2EEA7uL, 0x0C24D11F51726931uL, 0xDB646899A16B6512uL, + 0x7BC10670047B1DD8uL, 0x2413A5ABCD45F092uL], + [ + 0x4E66892190CFD923uL, 0xF10162440365EC8EuL, 0x158ACA5A6A2280AEuL, + 0x0D60ED11C0224166uL, 0x7CD2E9A71B9D7488uL, 0x450D7289706AB2A3uL, + 0x88FAE34EC9A0D7DCuL, 0x96FF9103575A97DAuL], + [ + 0x77990FAC6046C446uL, 0xB174B5FB30C76676uL, 0xE352CE3EB56CF82AuL, + 0xC6039B6873A9A082uL, 0xE3F80F3AE333148AuL, 0xB853BA24BA3539B9uL, + 0xE8863E52ECCB0C74uL, 0x309B4CC1092CC245uL], + [ + 0xBC2B70BEE8388D9FuL, 0xE48D92AE22216DCEuL, 0xF15F3BF3E2C15D8FuL, + 0x1DD964D4812D8B24uL, 0xD56AF02FB4665E4CuL, 0x98002200595BD9A3uL, + 0x049246D50BB8FA12uL, 0x1B542DF485B579B9uL], + [ + 0x2347409ADFA8E497uL, 0x36015C2211D62498uL, 0xE9F141F32EB82690uL, + 0x1F839912D0449FB9uL, 0x4E4DCFFF2D02D97CuL, 0xF8A03AB4C0F625C9uL, + 0x0605F575795DAC5CuL, 0x4746C9BEA0DDA6B1uL], + [ + 0xCA5BB519ECE7481BuL, 0xFD496155E55CA945uL, 0xF753B9DBB1515F81uL, + 0x50549E8BAC0F70E7uL, 0x8614FB0271E21C60uL, 0x60C72947EB0F0070uL, + 0xA6511C10AEE742B6uL, 0x48FB48F2CACCB43EuL] + ]; + + [Fact] + public void TestGolden() + { + JxlXorShift rng = new(12345); + Span lanes = stackalloc ulong[JxlXorShift.Generators]; + + for (ulong vector = 0; vector < Vectors; vector++) + { + rng.Fill(lanes); + + for (int i = 0; i < JxlXorShift.Generators; i++) + { + Assert.Equal(ExpectedVectors[(int)vector][i], lanes[i]); + } + } + } + + [Fact] + public void TestSeedChanges() + { + Span lanes = stackalloc ulong[JxlXorShift.Generators]; + const int numberOfSeeds = 16384; + + List first = new(numberOfSeeds); + for (int seed = 0; seed < numberOfSeeds; seed++) + { + JxlXorShift xs128Plus = new((ulong)seed); + xs128Plus.Fill(lanes); + first.Add(lanes[0]); + } + + Assert.Equal(numberOfSeeds, first.Count); + first.Sort(); + first = [.. first.Distinct()]; + Assert.Equal(numberOfSeeds, first.Count); + } + + [Fact] + public void TestFloat() + { +#if ALLOW_JPEGXL_SLOW_TESTS + const int seedMax = 4096; +#else + const int seedMax = 256; +#endif + + Parallel.For(0, seedMax, seed => + { + const int VecCap = 16; + + JxlXorShift rng = new((ulong)seed); + + Span batch64 = stackalloc ulong[JxlXorShift.Generators]; + Span batch32 = stackalloc uint[2 * JxlXorShift.Generators]; + + Span lanes = stackalloc float[Vector.Count]; + + int count = 0; + const int reps = 32000; + double sum = 0.0; + while (count < reps) + { + rng.Fill(batch64); + + MemoryMarshal.Cast(batch64).CopyTo(batch32); + + for (int i = 0; i < VecCap; i += Vector.Count) + { + Vector bits = new(batch32.Slice(i, Vector.Count)); + + // (bits >> 9) | 0x3F800000 + Vector shifted = bits >> 9; + Vector mantissa = shifted | new Vector(0x3F800000); + + Vector rand12 = Vector.AsVectorSingle(mantissa); + + rand12.CopyTo(lanes); + + for (int j = 0; j < Vector.Count; j++) + { + float lane = lanes[j]; + sum += lane; + count++; + + Assert.True(lane < 2.0f); + Assert.True(lane >= 1.0f); + } + } + } + }); + } + + [Fact] + public void TestNotZero() + { +#if ALLOW_JPEGXL_SLOW_TESTS + const int seedMax = 2000; +#else + const int seedMax = 500; +#endif + + Parallel.For(0, seedMax, task => + { + Span lanes = stackalloc ulong[JxlXorShift.Generators]; + JxlXorShift rng = new((ulong)task); + int numZero = 0; + + for (int vectors = 0; vectors < 10000; vectors++) + { + rng.Fill(lanes); + for (int i = 0; i < lanes.Length; i++) + { + if (lanes[i] == 0) + { + numZero++; + } + } + } + + Assert.True(numZero < 1, "There should not be any 0 values produced by the RNG"); + }); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Jxl/README.md b/tests/ImageSharp.Tests/Formats/Jxl/README.md new file mode 100644 index 0000000000..4eba637344 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/README.md @@ -0,0 +1,5 @@ +# JPEG XL Tests + +### Preprocessor directives +Enable a ALLOW_JPEGXL_SLOW_TESTS preprocessor directive to allow +slower tests that test the library more extensively. diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Rng.cs b/tests/ImageSharp.Tests/Formats/Jxl/Rng.cs new file mode 100644 index 0000000000..b862171520 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/Rng.cs @@ -0,0 +1,87 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl; + +/// +/// Deterministic random number generator used for compatibility +/// with JPEG XL tests. +/// +internal struct Rng +{ + private ulong s0; + private ulong s1; + + public Rng(ulong seed) + { + this.s0 = 0x94D049BB133111EBUL; + this.s1 = 0xBF58476D1CE4E5B9UL + seed; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ulong Next() + { + ulong s1 = this.s0; + ulong s0 = this.s1; + ulong bits = s1 + s0; + this.s0 = s0; + + s1 ^= s1 << 23; + s1 ^= s0 ^ (s1 >> 18) ^ (s0 >> 5); + + this.s1 = s1; + + return bits; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public long UniformI(long begin, long end) => (long)(this.Next() % (ulong)(end - begin)) + begin; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ulong UniformU(ulong begin, ulong end) => (this.Next() % (end - begin)) + begin; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float UniformF(float begin, float end) + { + uint u = (uint)(this.Next() >> (64 - 23)) | 0x3F800000u; + float f = BitConverter.UInt32BitsToSingle(u); + + return ((end - begin) * (f - 1.0f)) + begin; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Bernoulli(float p) => this.UniformF(0, 1) < p; + + internal readonly struct GeometricDistribution + { + public readonly float Value { get; } + + public GeometricDistribution(float value) => this.Value = value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static GeometricDistribution Make(float p) => new(1.0f / MathF.Log(1.0f - p)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint Geometric(in GeometricDistribution dist) + { + float f = this.UniformF(0, 1); + float invLog1mp = dist.Value; + + float log = MathF.Log(1.0f - f) * invLog1mp; + + return (uint)log; + } + + public void Shuffle(Span span) + { + for (nuint i = 0; i + 1 < (nuint)span.Length; i++) + { + nuint a = (nuint)this.UniformU(i, (nuint)span.Length); + RuntimeUtility.Swap(ref span[(int)a], ref span[(int)i]); + } + } +} From eacb94302dbd15ce49076a3dc2badbd56ad5acc8 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Fri, 4 Sep 2026 22:02:46 +0400 Subject: [PATCH 139/328] Reduce errors --- .../Jxl/Processing/Blending/JxlAlphaHelper.cs | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaHelper.cs b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaHelper.cs index b3c890f9a2..c9a5482fbb 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaHelper.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaHelper.cs @@ -22,23 +22,20 @@ public static void PerformAlphaBlending( bool alphaIsPremultiplied, bool clamp) { - // Store all channels from all parameters into spans, because - // creating a Span from Memory is expensive, especially - // in a loop. - ReadOnlySpan fgR = foreground.R.Span; - ReadOnlySpan fgG = foreground.G.Span; - ReadOnlySpan fgB = foreground.B.Span; - ReadOnlySpan fgA = foreground.A.Span; + ReadOnlySpan fgR = foreground.R; + ReadOnlySpan fgG = foreground.G; + ReadOnlySpan fgB = foreground.B; + ReadOnlySpan fgA = foreground.A; - ReadOnlySpan bgR = background.R.Span; - ReadOnlySpan bgG = background.G.Span; - ReadOnlySpan bgB = background.B.Span; - ReadOnlySpan bgA = background.A.Span; + ReadOnlySpan bgR = background.R; + ReadOnlySpan bgG = background.G; + ReadOnlySpan bgB = background.B; + ReadOnlySpan bgA = background.A; - Span outR = output.R.Span; - Span outG = output.G.Span; - Span outB = output.B.Span; - Span outA = output.A.Span; + Span outR = output.R; + Span outG = output.G; + Span outB = output.B; + Span outA = output.A; if (alphaIsPremultiplied) { From 4e929f3ca877b3adcf1fb93431163d8f5ca8fce8 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Fri, 4 Sep 2026 22:03:10 +0400 Subject: [PATCH 140/328] Move comment --- .../Formats/Jxl/Processing/Blending/JxlAlphaHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaHelper.cs b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaHelper.cs index c9a5482fbb..1393e5cfa6 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaHelper.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaHelper.cs @@ -5,9 +5,9 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Blending; +// TODO: SIMD support internal sealed class JxlAlphaHelper { - // TODO: SIMD support private const float SmallAlpha = 1f / (1 << 26); // Force x to stay within the range of 0 through 1 From 58c803edcd9e5f8e104666737e71f6e10134733b Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 5 Sep 2026 00:07:40 +0400 Subject: [PATCH 141/328] Complete Butteraugli Just the tests for Butteraugli are remaining --- .../Jxl/Processing/Butteraugli/Butteraugli.cs | 558 ++++++++++++++---- .../Butteraugli/ButteraugliBlurTemp.cs | 24 + .../Butteraugli/ButteraugliComparator.cs | 284 ++++++++- .../Butteraugli/ButteraugliPsychoImage.cs | 23 + .../Processing/Image/JxlImageOperations.cs | 9 + .../Formats/Jxl/Processing/JxlToc.cs | 11 +- 6 files changed, 776 insertions(+), 133 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliBlurTemp.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliPsychoImage.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs index 99f1cf2d88..f65a66df92 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs @@ -6,6 +6,7 @@ using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; @@ -43,6 +44,26 @@ internal static class Butteraugli private const float GlobalScale = 1.0f / InternalGoodQualityThreshold; +#pragma warning disable // Just indentation warnings + /// + /// Underlying data for . + /// + private static readonly float[,] HeatmapData = + { + {0, 0, 0}, {0, 0, 1}, + {0, 1, 1}, {0, 1, 0}, // Good level + {1, 1, 0}, {1, 0, 0}, // Bad level + {1, 0, 1}, {0.5f, 0.5f, 1.0f}, + {1.0f, 0.5f, 0.5f}, // Pastel colors for the very bad quality range. + {1.0f, 1.0f, 0.5f}, {1, 1, 1}, + {1, 1, 1}, // Last color repeated to have a solid range of white. + }; +#pragma warning restore + + private static readonly DenseMatrix Heatmap; + + static Butteraugli() => Heatmap = new(HeatmapData); + public static ReadOnlySpan Wmul => [ 400.0f, 1.50815703118f, 0f, @@ -72,7 +93,7 @@ public static ReadOnlySpan ComputeKernel(float sigma) } public static void ConvolveBorderColumn( - JxlImageF input, + JxlPlane input, ReadOnlySpan kernel, int x, Span rowOut) @@ -83,6 +104,7 @@ public static void ConvolveBorderColumn( int maxX = Math.Min(input.XSize - 1, x + offset); float weight = 0.0f; + for (int j = minX; j <= maxX; j++) { weight += kernel[j - x + offset]; @@ -106,9 +128,9 @@ public static void ConvolveBorderColumn( } public static bool ConvolutionWithTranspose( - JxlImageF input, + JxlPlane input, ReadOnlySpan kernel, - JxlImageF output) + JxlPlane output) { if (output.XSize != input.YSize) { @@ -286,11 +308,11 @@ public static bool ConvolutionWithTranspose( } private static bool Blur( - JxlImageF input, + Configuration configuration, + JxlPlane input, float sigma, - in ButteraugliParameters parameters, ButteraugliBlurTemp temp, - JxlImageF output) + JxlPlane output) { ReadOnlySpan kernel = ComputeKernel(sigma); @@ -323,10 +345,7 @@ private static bool Blur( return true; } - if (!temp.GetTransposed(input, out JxlImageF tempT)) - { - return false; - } + JxlPlane tempT = temp.GetTransposed(configuration, input); if (!ConvolutionWithTranspose(input, kernel, tempT)) { @@ -447,9 +466,9 @@ public static void XybLowFrequencyToValues(JxlImage3F xybLf) } } - public static bool SuppressXByY(JxlImageF inY, JxlImageF inOutX) + public static bool SuppressXByY(JxlPlane inY, JxlPlane inOutX) { - if (!SameSize(inOutX, inY)) + if (!JxlImageOperations.SameSize(inOutX, inY)) { return false; } @@ -506,7 +525,7 @@ public static void Subtract(JxlPlane a, JxlPlane b, JxlPlane hf, - BlurTemp blurTemp) + ref InlineArray2> hf, + ButteraugliBlurTemp blurTemp) { const float sigmaHf = 3.22489901262f; int xSize = mf.XSize; int ySize = mf.YSize; - hf[0] = new JxlImageF(configuration, xSize, ySize); - hf[1] = new JxlImageF(configuration, xSize, ySize); + hf[0] = JxlPlane.Create(configuration, xSize, ySize); + hf[1] = JxlPlane.Create(configuration, xSize, ySize); int lanes = Vector.Count; @@ -558,7 +576,7 @@ public static bool SeparateMfAndHf( { if (i == 2) { - if (!Blur(mf.Plane(i), sigmaHf, parameters, blurTemp, mf.Plane(i))) + if (!Blur(configuration, mf.Plane(i), sigmaHf, blurTemp, mf.Plane(i))) { return false; } @@ -578,7 +596,7 @@ public static bool SeparateMfAndHf( } } - if (!Blur(mf.Plane(i), sigmaHf, parameters, blurTemp, mf.Plane(i))) + if (!Blur(configuration, mf.Plane(i), sigmaHf, blurTemp, mf.Plane(i))) { return false; } @@ -631,18 +649,18 @@ public static bool SeparateMfAndHf( } public static bool SeparateHFAndUHF( - in ButteraugliParameters parameters, - JxlImageF[] hf, - JxlImageF[] uhf, - JxlBlurTemp blurTemp) + Configuration configuration, + InlineArray2> hf, + InlineArray2> uhf, + ButteraugliBlurTemp blurTemp) { const float sigmaUhf = 1.56416327805f; int xSize = hf[0].XSize; int ySize = hf[0].YSize; - uhf[0] = new JxlImageF(xSize, ySize); - uhf[1] = new JxlImageF(xSize, ySize); + uhf[0] = JxlPlane.Create(configuration, xSize, ySize); + uhf[1] = JxlPlane.Create(configuration, xSize, ySize); int lanes = Vector.Count; @@ -659,7 +677,7 @@ public static bool SeparateHFAndUHF( } } - if (!Blur(hf[i], sigmaUhf, parameters, blurTemp, hf[i])) + if (!Blur(configuration, hf[i], sigmaUhf, blurTemp, hf[i])) { return false; } @@ -727,34 +745,36 @@ public static bool SeparateHFAndUHF( return true; } - public static void DeallocateHFAndUHF(InlineArray2 hf, InlineArray2 uhf) + public static void DeallocateHFAndUHF(InlineArray2> hf, InlineArray2> uhf) { for (int i = 0; i < 2; i++) { - hf[i] = new JxlImageF(); - uhf[i] = new JxlImageF(); + hf[i].Dispose(); + uhf[i].Dispose(); + + hf[i] = new JxlPlane(); + uhf[i] = new JxlPlane(); } } public static bool SeparateFrequencies( Configuration configuration, - in ButteraugliParameters parameters, - BlurTemp blurTemp, + ButteraugliBlurTemp blurTemp, JxlImage3F xyb, - PsychoImage ps) + ButteraugliPsychoImage ps) { - ps.Lf = JxlImage3F.Create( + ps.Lf = new JxlImage3F( configuration, xyb.XSize, xyb.YSize); - ps.Mf = JxlImage3F.Create( + ps.Mf = new JxlImage3F( configuration, xyb.XSize, xyb.YSize); if (!SeparateLFAndMF( - parameters, + configuration, xyb, ps.Lf, ps.Mf, @@ -764,16 +784,16 @@ public static bool SeparateFrequencies( } if (!SeparateMfAndHf( - parameters, + configuration, ps.Mf, - ps.Hf, + ref ps.Hf, blurTemp)) { return false; } if (!SeparateHFAndUHF( - parameters, + configuration, ps.Hf, ps.Uhf, blurTemp)) @@ -1177,7 +1197,7 @@ static Vector Load(ReadOnlySpan row, int index) [MethodImpl(MethodImplOptions.AggressiveInlining)] private static float PaddedMaltaUnit( - JxlImageF diffs, + JxlPlane diffs, int x0, int y0, bool isLF) @@ -1238,17 +1258,43 @@ private static float PaddedMaltaUnit( public static bool MaltaDiffMap( bool isLf, - JxlImageF lum0, - JxlImageF lum1, + JxlPlane lum0, + JxlPlane lum1, + float w0Gt1, + float w0Lt1, + float norm1, + JxlPlane diffs, + JxlPlane blockDiffAc) + { + if (isLf) + { + const float len = 3.75f; + const float mulli = 0.39905817637f; + + return MaltaDiffMap(true, lum0, lum1, w0Gt1, w0Lt1, norm1, len, mulli, diffs, blockDiffAc); + } + else + { + const float len = 3.75f; + const float mulli = 0.611612573796f; + + return MaltaDiffMap(false, lum0, lum1, w0Gt1, w0Lt1, norm1, len, mulli, diffs, blockDiffAc); + } + } + + public static bool MaltaDiffMap( + bool isLf, + JxlPlane lum0, + JxlPlane lum1, float w0Gt1, float w0Lt1, float norm1, float len, float mulli, - JxlImageF diffs, - JxlImageF blockDiffAc) + JxlPlane diffs, + JxlPlane blockDiffAc) { - if (!SameSize(lum0, lum1) || !SameSize(lum0, diffs)) + if (!JxlImageOperations.SameSize(lum0, lum1) || !JxlImageOperations.SameSize(lum0, diffs)) { return false; } @@ -1400,13 +1446,13 @@ public static bool MaltaDiffMap( } public static bool MaltaDiffMapLf( - JxlImageF lum0, - JxlImageF lum1, + JxlPlane lum0, + JxlPlane lum1, float w0Gt1, float w0Lt1, float norm1, - JxlImageF diffs, - JxlImageF blockDiffAc) + JxlPlane diffs, + JxlPlane blockDiffAc) { const float len = 3.75f; const float mulli = 0.611612573796f; @@ -1425,9 +1471,9 @@ public static bool MaltaDiffMapLf( } public static void CombineChannelsForMasking( - InlineArray2 hf, - InlineArray2 uhf, - JxlImageF output) + InlineArray2> hf, + InlineArray2> uhf, + JxlPlane output) { // Only X and Y components are involved in masking. ReadOnlySpan muls = @@ -1614,8 +1660,7 @@ public static bool Mask( Configuration configuration, JxlImageF mask0, JxlImageF mask1, - in ButteraugliParameters parameters, - JxlBlurTemp blurTemp, + ButteraugliBlurTemp blurTemp, JxlImageF? diffAc, out JxlImageF mask) { @@ -1636,14 +1681,14 @@ public static bool Mask( DiffPrecompute(mask0, mul, bias, diff0); DiffPrecompute(mask1, mul, bias, diff1); - if (!Blur(diff0, radius, parameters, blurTemp, blurred0)) + if (!Blur(configuration, diff0, radius, blurTemp, blurred0)) { return false; } FuzzyErosion(blurred0, diff0); - if (!Blur(diff1, radius, parameters, blurTemp, blurred1)) + if (!Blur(configuration, diff1, radius, blurTemp, blurred1)) { return false; } @@ -1661,7 +1706,7 @@ public static bool Mask( { maskRow[x] = diff0Row[x]; - if (diffRow != null) + if (diffRow.Length > 0) { const float maskToErrorMul = 10.0f; float diff = blur0Row[x] - blur1Row[x]; @@ -1673,16 +1718,15 @@ public static bool Mask( return true; } - public static bool MaskPsychoImage( + public static bool MaskButteraugliPsychoImage( Configuration configuration, ButteraugliPsychoImage pi0, ButteraugliPsychoImage pi1, int width, int height, - in ButteraugliParameters parameters, - BlurTemp blurTemp, + ButteraugliBlurTemp blurTemp, JxlImageF mask, - JxlImageF? diffAc) + out JxlImageF? diffAc) { JxlImageF mask0 = new(configuration, width, height); JxlImageF mask1 = new(configuration, width, height); @@ -1698,12 +1742,12 @@ public static bool MaskPsychoImage( mask1); return Mask( + configuration, mask0, mask1, - parameters, blurTemp, mask, - diffAc); + out diffAc); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -1741,11 +1785,11 @@ public static float MaskColor( public static bool CombineChannelsToDiffmap( JxlImageF mask, JxlImage3F blockDiffDc, - JxlImage3F blockDiffAc, + JxlImage3 blockDiffAc, float xmul, JxlImageF result) { - if (!SameSize(mask, result)) + if (!JxlImageOperations.SameSize(mask, result)) { return false; } @@ -1786,10 +1830,10 @@ public static bool CombineChannelsToDiffmap( } public static void L2Diff( - JxlImageF i0, - JxlImageF i1, + JxlPlane i0, + JxlPlane i1, float w, - JxlImageF diffmap) + JxlPlane diffmap) { if (w == 0) { @@ -1811,10 +1855,10 @@ public static void L2Diff( } public static void SetL2Diff( - JxlImageF i0, - JxlImageF i1, + JxlPlane i0, + JxlPlane i1, float w, - JxlImageF diffmap) + JxlPlane diffmap) { if (w == 0) { @@ -1836,11 +1880,11 @@ public static void SetL2Diff( } public static void L2DiffAsymmetric( - JxlImageF i0, - JxlImageF i1, + JxlPlane i0, + JxlPlane i1, float w0gt1, float w0lt1, - JxlImageF diffmap) + JxlPlane diffmap) { if (w0gt1 == 0 && w0lt1 == 0) { @@ -1942,11 +1986,27 @@ public static void OpsinAbsorbance( } } + // A simple HDR-compatible gamma function + private static Vector Gamma(Vector v) + { + Vector kRetMul = Vector.Create(19.245013259874995f * JxlMath.InverseLog2E); + Vector kRetAdd = Vector.Create(-23.16046239805755f); + + // if (value < 0) value = 0; + v = Vector.ConditionalSelect(Vector.LessThan(v, Vector.Zero), Vector.Zero, v); + + Vector biased = v + Vector.Create(9.9710635769299145f); + Vector log = Vector.Log2(biased); + + return (kRetMul * log) + kRetAdd; + } + public static bool OpsinDynamicsImage( + Configuration configuration, JxlImage3F rgb, in ButteraugliParameters parameters, JxlImage3F blurred, - BlurTemp blurTemp, + ButteraugliBlurTemp blurTemp, JxlImage3F xyb) { if (blurred == null) @@ -1954,19 +2014,19 @@ public static bool OpsinDynamicsImage( return false; } - const double sigma = 1.2; + const float sigma = 1.2f; - if (!Blur(rgb.Plane(0), sigma, parameters, blurTemp, blurred.Plane(0))) + if (!Blur(configuration, rgb.Plane(0), sigma, blurTemp, blurred.Plane(0))) { return false; } - if (!Blur(rgb.Plane(1), sigma, parameters, blurTemp, blurred.Plane(1))) + if (!Blur(configuration, rgb.Plane(1), sigma, blurTemp, blurred.Plane(1))) { return false; } - if (!Blur(rgb.Plane(2), sigma, parameters, blurTemp, blurred.Plane(2))) + if (!Blur(configuration, rgb.Plane(2), sigma, blurTemp, blurred.Plane(2))) { return false; } @@ -2058,16 +2118,16 @@ public static bool ButteraugliDiffmapInPlace( int xSize = image0.XSize; int ySize = image0.YSize; - using var blurTemp = new JxlBlurTemp(); + using ButteraugliBlurTemp blurTemp = new(); using (JxlImage3F temp = new(configuration, xSize, ySize)) { - if (!OpsinDynamicsImage(image0, parameters, temp, blurTemp, image0)) + if (!OpsinDynamicsImage(configuration, image0, parameters, temp, blurTemp, image0)) { return false; } - if (!OpsinDynamicsImage(image1, parameters, temp, blurTemp, image1)) + if (!OpsinDynamicsImage(configuration, image1, parameters, temp, blurTemp, image1)) { return false; } @@ -2080,12 +2140,12 @@ public static bool ButteraugliDiffmapInPlace( using (JxlImage3F lf0 = new(configuration, xSize, ySize)) using (JxlImage3F lf1 = new(configuration, xSize, ySize)) { - if (!SeparateLFAndMF(parameters, image0, lf0, image0, blurTemp)) + if (!SeparateLFAndMF(configuration, image0, lf0, image0, blurTemp)) { return false; } - if (!SeparateLFAndMF(parameters, image1, lf1, image1, blurTemp)) + if (!SeparateLFAndMF(configuration, image1, lf1, image1, blurTemp)) { return false; } @@ -2100,15 +2160,15 @@ public static bool ButteraugliDiffmapInPlace( } } - InlineArray2 hf0 = default; - InlineArray2 hf1 = default; + InlineArray2> hf0 = default; + InlineArray2> hf1 = default; - if (!SeparateMfAndHf(parameters, image0, hf0, blurTemp)) + if (!SeparateMfAndHf(configuration, image0, ref hf0, blurTemp)) { return false; } - if (!SeparateMfAndHf(parameters, image1, hf1, blurTemp)) + if (!SeparateMfAndHf(configuration, image1, ref hf1, blurTemp)) { return false; } @@ -2158,15 +2218,15 @@ public static bool ButteraugliDiffmapInPlace( image0.Dispose(); image1.Dispose(); - InlineArray2 uhf0 = default; - InlineArray2 uhf1 = default; + InlineArray2> uhf0 = default; + InlineArray2> uhf1 = default; - if (!SeparateHFAndUHF(parameters, hf0, uhf0, blurTemp)) + if (!SeparateHFAndUHF(configuration, hf0, uhf0, blurTemp)) { return false; } - if (!SeparateHFAndUHF(parameters, hf1, uhf1, blurTemp)) + if (!SeparateHFAndUHF(configuration, hf1, uhf1, blurTemp)) { return false; } @@ -2175,7 +2235,7 @@ public static bool ButteraugliDiffmapInPlace( using (JxlImageF diffs = new(configuration, xSize, ySize)) { - MaltaDiffMap( + _ = MaltaDiffMap( false, uhf0[1], uhf1[1], @@ -2185,19 +2245,19 @@ public static bool ButteraugliDiffmapInPlace( diffs, blockDiffAc); - MaltaDiffMap( + _ = MaltaDiffMap( false, uhf0[0], uhf1[0], - wUhfMaltaX * hfAsymmetry, - wUhfMaltaX / hfAsymmetry, - norm1UhfX, + WUhfMaltaX * hfAsymmetry, + WUhfMaltaX / hfAsymmetry, + Norm1UhfX, diffs, blockDiffAc); float sqrtAsym = MathF.Sqrt(hfAsymmetry); - MaltaDiffMap( + _ = MaltaDiffMap( true, hf0[1], hf1[1], @@ -2207,7 +2267,7 @@ public static bool ButteraugliDiffmapInPlace( diffs, blockDiffAc); - MaltaDiffMap( + _ = MaltaDiffMap( true, hf0[0], hf1[0], @@ -2239,15 +2299,15 @@ public static bool ButteraugliDiffmapInPlace( DeallocateHFAndUHF(hf0, uhf0); DeallocateHFAndUHF(hf1, uhf1); - if (!Mask(mask0, mask1, parameters, blurTemp, mask, blockDiffAc)) + if (!Mask(configuration, mask0, mask1, blurTemp, mask, out JxlImageF newBlockDiffAc)) { return false; } for (int y = 0; y < ySize; y++) { - ReadOnlySpan dc = blockDiffDc.GetRow(y); - ReadOnlySpan ac = blockDiffAc.GetRow(y); + ReadOnlySpan dc = newBlockDiffAc.GetRow(y); + ReadOnlySpan ac = newBlockDiffAc.GetRow(y); Span output = diffmap.GetRow(y); ReadOnlySpan maskRow = mask.GetRow(y); @@ -2255,10 +2315,7 @@ public static bool ButteraugliDiffmapInPlace( { float m = maskRow[x]; - output[x] = - MathF.Sqrt( - (dc[x] * (float)MaskDcY(m)) + - (ac[x] * (float)MaskY(m))); + output[x] = MathF.Sqrt((dc[x] * (float)MaskDcY(m)) + (ac[x] * (float)MaskY(m))); } } @@ -2272,7 +2329,7 @@ public static JxlImage3F SubSample2x(Configuration configuration, JxlImage3F inp int xs = (input.XSize + 1) / 2; int ys = (input.YSize + 1) / 2; - JxlImage3F retval = new(Configuration, xs, ys); + JxlImage3F retval = new(configuration, xs, ys); for (int c = 0; c < 3; ++c) { @@ -2293,8 +2350,7 @@ public static JxlImage3F SubSample2x(Configuration configuration, JxlImage3F inp for (int x = 0; x < input.XSize; ++x) { - retval.PlaneRow(c, y / 2)[x / 2] += - 0.25f * srcRow[x]; + retval.PlaneRow(c, y / 2)[x / 2] += 0.25f * srcRow[x]; } } @@ -2335,4 +2391,286 @@ public static void AddSupersampled2x(JxlImageF src, float w, JxlImageF dest) } } } + + private static void ScoreToRgb(float score, float goodThreshold, float badThreshold, ref InlineArray3 rgb) + { + if (score < goodThreshold) + { + score = (score / goodThreshold) * 0.3f; + } + else if (score < badThreshold) + { + score = 0.3f + ((score - goodThreshold) / (badThreshold - goodThreshold) * 0.15f); + } + else + { + score = 0.45f + ((score - badThreshold) / (badThreshold * 12) * 0.5f); + } + + int tableSize = HeatmapData.GetLength(0); + score = Math.Clamp(score * (tableSize - 1), 0f, tableSize - 2); + + int ix = (int)score; + ix = Math.Clamp(ix, 0, tableSize - 2); // handle NaN + float mix = score - ix; + + for (int i = 0; i < 3; ++i) + { + float v = (mix * Heatmap[ix + 1, i]) + ((1 - mix) * Heatmap[ix, i]); + rgb[i] = MathF.Pow(v, 0.5f); + } + } + + public static JxlImage3F CreateHeatMapImage(Configuration configuration, JxlImageF distmap, float goodThreshold, float badThreshold) + { + // Do not dispose (this is the return value; it is caller's responsibility to dispose this + // or keep it) + JxlImage3F heatmap = new(configuration, distmap.XSize, distmap.YSize); + + for (int y = 0; y < distmap.YSize; y++) + { + Span row_distmap = distmap.GetRow(y); + Span row_h0 = heatmap.PlaneRow(0, y); + Span row_h1 = heatmap.PlaneRow(1, y); + Span row_h2 = heatmap.PlaneRow(2, y); + + for (int x = 0; x < distmap.XSize; ++x) + { + float d = row_distmap[x]; + InlineArray3 rgb = default; + ScoreToRgb(d, goodThreshold, badThreshold, ref rgb); + row_h0[x] = rgb[0]; + row_h1[x] = rgb[1]; + row_h2[x] = rgb[2]; + } + } + + return heatmap; + } + + public static float ButteraugliFuzzyInverse(float seek) + { + float pos = 0f; + + for (float range = 1.0f; range >= 1e-10f; range *= 0.5f) + { + float cur = ButteraugliFuzzyClass(pos); + + if (cur < seek) + { + pos -= range; + } + else + { + pos += range; + } + } + + // Normalization (pos) can be printed if seek == 1.0, for example + // when debugging. + return pos; + } + + public static float ButteraugliFuzzyClass(float score) + { + const float fuzzyWidthUp = 4.8f; + const float fuzzyWidthDown = 4.8f; + const float m0 = 2.0f; + const float scaler = 0.7777f; + + float val; + + if (score < 1.0) + { + val = m0 / (1.0f + MathF.Exp((score - 1.0f) * fuzzyWidthDown)); + val -= 1.0f; + val *= 2.0f - scaler; + val += scaler; + } + else + { + val = m0 / (1.0f + MathF.Exp((score - 1.0f) * fuzzyWidthUp)); + val *= scaler; + } + + return val; + } + + public static bool ButteraugliInterfaceInPlace(Configuration configuration, JxlImage3F rgb0, JxlImage3F rgb1, ButteraugliParameters parameters, JxlImageF diffmap, out double diffvalue) + { + diffvalue = 0; + + int xsize = rgb0.XSize; + int ysize = rgb0.YSize; + + if ((xsize & ysize) == 0) // equivalent to xsize == 0 || ysize == 0, minus one branch + { + throw new InvalidOperationException("Zero-sized image"); + } + + if (!JxlImageOperations.SameSize(rgb0, rgb1)) + { + throw new InvalidOperationException("Size mismatch"); + } + + const int max = 8; + + if (xsize < max || ysize < max) + { + bool ok = ButteraugliDiffmapSmall(configuration, max, rgb0, rgb1, parameters, out diffmap); + diffvalue = ButteraugliScoreFromDiffmap(diffmap, parameters); + return ok; + } + + JxlImageF subDiffmap = new(); + + if (xsize >= 15 && ysize >= 15) + { + using JxlImage3F rgb0Sub = SubSample2x(configuration, rgb0); + using JxlImage3F rgb1Sub = SubSample2x(configuration, rgb1); + + if (!ButteraugliDiffmapInPlace(configuration, rgb0Sub, rgb1Sub, parameters, diffmap)) + { + return false; + } + } + + if (!ButteraugliDiffmapInPlace(configuration, rgb0, rgb1, parameters, diffmap)) + { + return false; + } + + if (xsize >= 15 && ysize >= 15) + { + AddSupersampled2x(subDiffmap, 0.5f, diffmap); + } + + diffvalue = ButteraugliScoreFromDiffmap(diffmap, parameters); + return true; + } + + public static bool ButteraugliInterface(Configuration configuration, JxlImage3F rgb0, JxlImage3F rgb1, ButteraugliParameters parameters, JxlImageF diffmap, out double diffValue) + { + diffValue = 0; + + if (!ButteraugliDiffmap(configuration, rgb0, rgb1, parameters, out diffmap)) + { + return false; + } + + diffValue = ButteraugliScoreFromDiffmap(diffmap, parameters); + return true; + } + + public static bool ButteraugliInterface(Configuration configuration, JxlImage3F rgb0, JxlImage3F rgb1, float hfAsymmetry, float xmul, JxlImageF diffmap, out double diffValue) + { + ButteraugliParameters parameters = new() + { + HfAsymmetry = hfAsymmetry, + XMultiplier = xmul + }; + + return ButteraugliInterface(configuration, rgb0, rgb1, parameters, diffmap, out diffValue); + } + + public static bool ButteraugliDiffmap(Configuration configuration, JxlImage3F rgb0, JxlImage3F rgb1, ButteraugliParameters parameters, out JxlImageF diffmap) + { + diffmap = new(); + + int xsize = rgb0.XSize; + int ysize = rgb0.YSize; + + if ((xsize & ysize) == 0) // equivalent to xsize == 0 || ysize == 0, minus one branch + { + throw new InvalidOperationException("Zero-sized image"); + } + + if (!JxlImageOperations.SameSize(rgb0, rgb1)) + { + throw new InvalidOperationException("Size mismatch"); + } + + const int max = 8; + + if (xsize < max || ysize < max) + { + if (!ButteraugliDiffmapSmall(configuration, max, rgb0, rgb1, parameters, out diffmap)) + { + return false; + } + } + + ButteraugliComparator butteraugli = ButteraugliComparator.Make(configuration, rgb0, parameters); + + return butteraugli.Diffmap(configuration, rgb1, diffmap); + } + + public static bool ButteraugliDiffmapSmall(Configuration configuration, int max, JxlImage3F rgb0, JxlImage3F rgb1, ButteraugliParameters parameters, out JxlImageF diffmap) + { + int xsize = rgb0.XSize; + int ysize = rgb0.YSize; + + int xborder = xsize < max ? (max - xsize) / 2 : 0; + int yborder = ysize < max ? (max - ysize) / 2 : 0; + int xscaled = Math.Max(max, xsize); + int yscaled = Math.Max(max, ysize); + + using JxlImage3F scaled0 = new(configuration, xscaled, yscaled); + using JxlImage3F scaled1 = new(configuration, xscaled, yscaled); + + for (int i = 0; i < 3; ++i) + { + for (int y = 0; y < yscaled; y++) + { + for (int x = 0; x < xscaled; x++) + { + int x2 = Math.Min(xsize - 1, x > xborder ? x - xborder : 0); + int y2 = Math.Min(ysize - 1, y > yborder ? y - yborder : 0); + + scaled0.PlaneRow(i, y)[x] = rgb0.PlaneRow(i, y2)[x2]; + scaled1.PlaneRow(i, y)[x] = rgb1.PlaneRow(i, y2)[x2]; + } + } + } + + JxlImageF diffmapScaled = new(); + bool ok = ButteraugliDiffmap(configuration, scaled0, scaled1, parameters, out diffmapScaled); + + diffmap = new(configuration, xsize, ysize); + + for (int y = 0; y < ysize; y++) + { + Span diffmapRow = diffmap.GetRow(y); + Span diffmapScaledRow = diffmapScaled.GetRow(y + yborder); + + for (int x = 0; x < xsize; x++) + { + diffmapRow[x] = diffmapScaledRow[x + xborder]; + } + } + + return ok; + } + + public static float ButteraugliScoreFromDiffmap(JxlImageF diffmap, ButteraugliParameters parameters) + { + float retval = 0.0f; + + for (int y = 0; y < diffmap.YSize; ++y) + { + Span row = diffmap.GetRow(y); + for (int x = 0; x < diffmap.XSize; ++x) + { + retval = Math.Max(retval, row[x]); + } + } + + return retval; + } + + public static bool MaltaDiffMap(JxlPlane lum0, JxlPlane lum1, float w0Gt1, float w0Lt1, float norm1, JxlPlane diffs, JxlImage3 blockDiffAc, int c) + => MaltaDiffMap(isLf: false, lum0, lum1, w0Gt1, w0Lt1, norm1, diffs, blockDiffAc.Plane(c)); + + public static bool MaltaDiffMapLf(JxlPlane lum0, JxlPlane lum1, float w0Gt1, float w0Lt1, float norm1, JxlPlane diffs, JxlImage3 blockDiffAc, int c) + => MaltaDiffMap(isLf: true, lum0, lum1, w0Gt1, w0Lt1, norm1, diffs, blockDiffAc.Plane(c)); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliBlurTemp.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliBlurTemp.cs new file mode 100644 index 0000000000..26aa8372ce --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliBlurTemp.cs @@ -0,0 +1,24 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; + +internal sealed class ButteraugliBlurTemp : IDisposable +{ + public JxlPlane TransposedTemp { get; set; } = new(); + + public JxlPlane GetTransposed(Configuration configuration, JxlPlane input) + { + if (this.TransposedTemp.XSize == 0) + { + // Yes, YSize and XSize are swapped + this.TransposedTemp = JxlPlane.Create(configuration, input.YSize, input.XSize); + } + + return this.TransposedTemp; + } + + public void Dispose() => this.TransposedTemp.Dispose(); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs index be5837534b..d290268da6 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs @@ -1,46 +1,300 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; -internal sealed class ButteraugliComparator +internal class ButteraugliComparator : IDisposable { - private int xSize; - private int ySize; + private readonly int xSize; + private readonly int ySize; private ButteraugliParameters parameters; - private readonly ButteraugliPsychoImage pi0; - private readonly JxlImage3F temp; - private bool tempInUse; - private readonly ButteraugliBlurTemp blurTemp; + private readonly ButteraugliPsychoImage pi0 = new(); + private JxlImage3F? temp; + private readonly ButteraugliBlurTemp blurTemp = new(); + private ButteraugliComparator? sub; + + public ButteraugliComparator(int xsize, int ysize, ButteraugliParameters parameters) + { + this.xSize = xsize; + this.ySize = ysize; + this.parameters = parameters; + } + + public JxlImage3F Temp => this.temp ??= new(); + + public static ButteraugliComparator Make(Configuration configuration, JxlImage3F rgb0, ButteraugliParameters parameters) + { + int xSize = rgb0.XSize; + int ySize = rgb0.YSize; + + ButteraugliComparator result = new(xSize, ySize, parameters) + { + temp = new JxlImage3F(configuration, xSize, ySize) + }; + + if (xSize < 8 || ySize < 8) + { + return result; + } + + JxlImage3F xyb0 = new(configuration, xSize, ySize); + + if (!Butteraugli.OpsinDynamicsImage(configuration, rgb0, parameters, result.Temp, result.blurTemp, xyb0)) + { + throw new InvalidOperationException("OpsinDynamicsImage failed"); + } + + result.ReleaseTemp(); + + if (!Butteraugli.SeparateFrequencies(configuration, result.blurTemp, xyb0, result.pi0)) + { + throw new InvalidOperationException("Could not separate frequencies"); + } + + JxlImage3F subsampledRgb0 = Butteraugli.SubSample2x(configuration, rgb0); + result.sub = Make(configuration, subsampledRgb0, parameters); + + return result; + } + + public void ReleaseTemp() + { + this.temp?.Dispose(); + this.temp = null; + } /// /// Computes the butteraugli map between the original image given in the constructor and the distorted image given here. /// - public bool Diffmap(JxlImage3F rgb1, JxlImageF result) + public virtual bool Diffmap(Configuration configuration, JxlImage3F rgb1, JxlImageF result) { - throw new NotImplementedException(); + if (this.xSize < 8 || this.ySize < 8) + { + result.Clear(); + return true; + } + + JxlImage3F xyb1 = new(configuration, this.xSize, this.ySize); + + if (!Butteraugli.OpsinDynamicsImage(configuration, rgb1, this.parameters, this.Temp, this.blurTemp, xyb1)) + { + return false; + } + + this.ReleaseTemp(); + + if (!this.DiffmapOpsinDynamicsImage(configuration, xyb1, out result)) + { + return false; + } + + if (this.sub is not null) + { + if (this.sub.xSize < 8 || this.sub.ySize < 8) + { + return true; + } + + JxlImage3F subXyb = new(configuration, this.sub.xSize, this.sub.ySize); + JxlImage3F subsampledRgb1 = Butteraugli.SubSample2x(configuration, rgb1); + + if (!Butteraugli.OpsinDynamicsImage(configuration, subsampledRgb1, this.parameters, this.sub.Temp, this.sub.blurTemp, subXyb)) + { + return false; + } + + this.sub.ReleaseTemp(); + + if (!this.DiffmapOpsinDynamicsImage(configuration, subXyb, out JxlImageF subResult)) + { + return false; + } + + Butteraugli.AddSupersampled2x(subResult, 0.5f, result); + } + + return true; } /// /// Same as Diffmap but OpsinDynamicsImage() was already applied. /// - public bool DiffmapOpsinDynamicsImage(JxlImage3F xyb1, JxlImageF result) + public bool DiffmapOpsinDynamicsImage(Configuration configuration, JxlImage3F xyb1, out JxlImageF result) { - throw new NotImplementedException(); + result = new(); + + if (this.xSize < 8 || this.ySize < 8) + { + result.Clear(); + return true; + } + + ButteraugliPsychoImage pi1 = new(); + + if (!Butteraugli.SeparateFrequencies(configuration, this.blurTemp, xyb1, pi1)) + { + return false; + } + + result = new(configuration, this.xSize, this.ySize); + return this.DiffmapPsychoImage(configuration, pi1, result); } /// /// Same as above but the frequency decomposition was already applied. /// - public bool DiffmapPsychoImage(ButteraugliPsychoImage pi1, JxlImageF diffmap) + public bool DiffmapPsychoImage(Configuration configuration, ButteraugliPsychoImage pi1, JxlImageF diffmap) { - throw new NotImplementedException(); + if (this.xSize < 8 || this.ySize < 8) + { + diffmap.Clear(); + return true; + } + + float hfAsymmetry = this.parameters.HfAsymmetry; + float xmul = this.parameters.XMultiplier; + + JxlImageF diffs = new(configuration, this.xSize, this.ySize); + JxlImage3 blockDiffAc = new(configuration, this.xSize, this.ySize); + + JxlImageOperations.ZeroFillImage(blockDiffAc); + + if (!Butteraugli.MaltaDiffMap( + this.pi0.Uhf[1], + pi1.Uhf[1], + Butteraugli.WUhfMalta * hfAsymmetry, + Butteraugli.WUhfMalta / hfAsymmetry, + Butteraugli.Norm1Uhf, + diffs, + blockDiffAc, + 1)) + { + return false; + } + + if (!Butteraugli.MaltaDiffMap( + this.pi0.Uhf[0], + pi1.Uhf[0], + Butteraugli.WUhfMaltaX * hfAsymmetry, + Butteraugli.WUhfMaltaX / hfAsymmetry, + Butteraugli.Norm1UhfX, + diffs, + blockDiffAc, + 0)) + { + return false; + } + + if (!Butteraugli.MaltaDiffMapLf( + this.pi0.Hf[1], + pi1.Hf[1], + Butteraugli.WHfMalta * MathF.Sqrt(hfAsymmetry), + Butteraugli.WHfMalta / MathF.Sqrt(hfAsymmetry), + Butteraugli.Norm1Hf, + diffs, + blockDiffAc, + 1)) + { + return false; + } + + if (!Butteraugli.MaltaDiffMapLf( + this.pi0.Hf[0], + pi1.Hf[0], + Butteraugli.WHfMaltaX * MathF.Sqrt(hfAsymmetry), + Butteraugli.WHfMaltaX / MathF.Sqrt(hfAsymmetry), + Butteraugli.Norm1HfX, + diffs, + blockDiffAc, + 0)) + { + return false; + } + + if (!Butteraugli.MaltaDiffMapLf( + this.pi0.Mf!.Plane(1), + pi1.Mf!.Plane(1), + Butteraugli.WHfMalta, + Butteraugli.WMfMalta, + Butteraugli.Norm1Mf, + diffs, + blockDiffAc, + 1)) + { + return false; + } + + if (!Butteraugli.MaltaDiffMapLf( + this.pi0.Mf!.Plane(0), + pi1.Mf!.Plane(0), + Butteraugli.WHfMaltaX, + Butteraugli.WMfMaltaX, + Butteraugli.Norm1MfX, + diffs, + blockDiffAc, + 0)) + { + return false; + } + + JxlImage3F blockDiffDc = new(configuration, this.xSize, this.ySize); + + for (int c = 0; c < 3; c++) + { + if (c < 2) + { + Butteraugli.L2DiffAsymmetric( + this.pi0.Hf[c], + pi1.Hf[c], + Butteraugli.Wmul[c] * hfAsymmetry, + Butteraugli.Wmul[c] / hfAsymmetry, + blockDiffAc.Plane(c)); + } + + Butteraugli.L2Diff( + this.pi0.Mf.Plane(c), + pi1.Mf.Plane(c), + Butteraugli.Wmul[3 + c], + blockDiffAc.Plane(c)); + + Butteraugli.SetL2Diff( + this.pi0.Lf!.Plane(c), + pi1.Lf!.Plane(c), + Butteraugli.Wmul[6 + c], + blockDiffDc.Plane(c)); + } + + JxlImageF mask = new(); + + if (!Butteraugli.MaskButteraugliPsychoImage( + configuration, + this.pi0, + pi1, + this.xSize, + this.ySize, + this.blurTemp, + mask, + out JxlImageF? diffAc)) + { + return false; + } + + diffAc?.BytesSpan.CopyTo(blockDiffAc.Plane(1).BytesSpan); + + return Butteraugli.CombineChannelsToDiffmap(mask, blockDiffDc, blockDiffAc, xmul, diffmap); } - public bool Mask(JxlImageF mask) + public virtual bool Mask(Configuration configuration, JxlImageF mask) + => Butteraugli.MaskButteraugliPsychoImage(configuration, this.pi0, this.pi0, this.xSize, this.ySize, this.blurTemp, mask, out _); + + public void Dispose() { - throw new NotImplementedException(); + this.temp?.Dispose(); + this.temp = null; + this.blurTemp.Dispose(); } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliPsychoImage.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliPsychoImage.cs new file mode 100644 index 0000000000..de1166cf8e --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliPsychoImage.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Jxl.Memory; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; + +// Public fields because InlineArray2 values +// cannot be mutated if it's a property +#pragma warning disable SA1401 // Fields should be private + +internal sealed class ButteraugliPsychoImage +{ + public InlineArray2> Uhf; // XY + + public InlineArray2> Hf; // XY + + public JxlImage3F? Mf { get; set; } // XYB + + public JxlImage3F? Lf { get; set; } // XYB +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs index 0a4ff8f5d7..b4e98c5ca4 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs @@ -23,6 +23,15 @@ internal static class JxlImageOperations /// True if width and height is equal. public static bool SameSize(JxlPlaneBase a, JxlPlaneBase b) => a.XSize == b.XSize && a.YSize == b.YSize; + /// + /// Returns true if first image has same width and height as the second image. + /// + /// First image + /// Second image + /// True if width and height is equal. + public static bool SameSize(JxlImage3 a, JxlImage3 b) + where T : unmanaged => a.XSize == b.XSize && a.YSize == b.YSize; + /// /// Copies everything from one plane to another. /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlToc.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlToc.cs index 21fa5d23b2..b6dfd4e39f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlToc.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlToc.cs @@ -31,6 +31,7 @@ public static int NumberOfTocEntries(int numGroups, int numDcGroups, int numPass } private const int BitsPerByte = 8; + private const int MaxTocEntries = 65536; public static bool ReadToc( @@ -98,10 +99,7 @@ bool CheckBitBudget(int numEntries) } } - if (!reader.JumpToByteBoundary()) - { - return false; - } + reader.JumpToByteBoundary(); if (!CheckBitBudget(tocEntries)) { @@ -113,10 +111,7 @@ bool CheckBitBudget(int numEntries) sizes[i] = JxlU32Coder.Read(TocDistribution, reader); } - if (!reader.JumpToByteBoundary()) - { - return false; - } + reader.JumpToByteBoundary(); return CheckBitBudget(0); } From b0f0d34a2eb14ba3b0fa2a4fda8c270fe9ea3e19 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 5 Sep 2026 00:35:43 +0400 Subject: [PATCH 142/328] Complete Butteraugli and its tests --- .../Processing/Image/JxlImageOperations.cs | 35 +++++ .../Jxl/Processing/ButteraugliTests.cs | 139 ++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs index b4e98c5ca4..af665db380 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs @@ -64,6 +64,41 @@ public static bool CopyImage(JxlPlane from, JxlPlane to) return true; } + /// + /// Copies everything from one image to another. + /// + /// The type of image to copy. + /// Source image (read-only) + /// Destination image (write-only) + /// + /// Status of the copy operation + /// + public static bool CopyImage(JxlImage3 from, JxlImage3 to) + where T : unmanaged + { + if (!SameSize(from, to)) + { + return false; + } + + if (from.XSize == 0 || from.YSize == 0) + { + return true; + } + + for (int c = 0; c < 3; c++) + { + for (int y = 0; y < from.YSize; y++) + { + Span rowFrom = from.PlaneRow(c, y); + Span rowTo = to.PlaneRow(c, y); + rowFrom.CopyTo(rowTo); + } + } + + return true; + } + /// /// Within bounds specified by input rectangles, copies everything from one plane to another. /// diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs new file mode 100644 index 0000000000..8adabd83e7 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs @@ -0,0 +1,139 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using Microsoft.Diagnostics.Runtime.Interop; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; + +/// +/// Tests for Google Butteraugli (C# implementation), a tool +/// used for comparing images in a more sophisticated way +/// (the way humans may notice differences). For example, +/// if every pixel was off by 1, PSNR, which operates on a +/// pixel-by-pixel basis, would report a large difference, +/// while Butteraugli would report barely any differences, +/// as us humans wouldn't notice anything if every pixel +/// was just off by one. +/// +public class ButteraugliTests +{ + private static JxlImage3F SinglePixelImage(float r, float g, float b) + { + JxlImage3F img = new(TestEnvironment.Configuration, 1, 1); + img.PlaneRow(0, 0)[0] = r; + img.PlaneRow(1, 0)[0] = r; + img.PlaneRow(2, 0)[0] = r; + return img; + } + + private static void AddUniformNoise(JxlImage3F img, float d, ulong seed) + { + Rng generator = new(seed); + + for (int y = 0; y < img.YSize; ++y) + { + for (int c = 0; c < 3; ++c) + { + Span planeRow = img.PlaneRow(c, y); + + for (int x = 0; x < img.XSize; ++x) + { + planeRow[x] += generator.UniformF(-d, d); + } + } + } + } + + private static void AddEdge(JxlImage3F image, float d, int x0, int y0) + { + int h = Math.Min(image.YSize - y0, 100); + int w = Math.Min(image.XSize - x0, 5); + + for (int dy = 0; dy < h; ++dy) + { + Span planeRow = image.PlaneRow(1, y0 + dy); + + for (int dx = 0; dx < w; ++dx) + { + planeRow[x0 + dx] += d; + } + } + } + + [Fact] + public void TestSinglePixel() + { + JxlImage3F rgb0 = SinglePixelImage(0.5f, 0.5f, 0.5f); + JxlImage3F rgb1 = SinglePixelImage(0.5f, 0.49f, 0.5f); + + ButteraugliParameters butteraugliParameters = new(); + JxlImageF diffmap = new(); + + Assert.True( + Butteraugli.ButteraugliInterface(TestEnvironment.Configuration, rgb0, rgb1, butteraugliParameters, diffmap, out double diffval), + "Butteraugli initialization failed"); + + Assert.True(new TolerantMath(0.5).AreEqual(diffval, 2.5), $"Diff value isn't even close to 2.5 (it's {diffval})"); + + JxlImageF diffmap2 = new(); + Assert.True(Butteraugli.ButteraugliInterfaceInPlace( + TestEnvironment.Configuration, + rgb0, + rgb1, + butteraugliParameters, + diffmap2, + out double diffval2)); + + Assert.True(new TolerantMath(1e-10).AreEqual(diffval, diffval2), $"Diff value isn't even close to diffval2 (diffval={diffval}, diffval2={diffval2})"); + } + + // TODO: we need to port test image stuff from libjxl + [Fact] + public void TestLargeImage() + { + const int xSize = 1024; + const int ySize = 1024; + + JxlTestImage img = new(); + img.SetDimensions(xSize, ySize); + + JxlTestFrame frame = img.AddFrame(); + frame.RandomFill(777); + + JxlImage3F rgb0 = GetColorImage(img.Ppf); + JxlImage3F rgb1 = new(TestEnvironment.Configuration, xSize, ySize); + JxlImageOperations.CopyImage(rgb0, rgb1); + + AddUniformNoise(rgb1, 0.02f, 7777uL); + AddEdge(rgb1, 0.1f, xSize / 2, xSize / 2); + + ButteraugliParameters butteraugliParameters = new(); + JxlImageF diffmap = new(); + Assert.True( + Butteraugli.ButteraugliInterface(TestEnvironment.Configuration,, rgb0, rgb1, butteraugliParameters, diffmap, out double diffval), + "Couldn't initialize Butteraugli"); + + double distp = Butteraugli.ComputeDistanceP(diffmap, butteraugliParameters, 3.0); + Assert.True(new TolerantMath(0.5).AreEqual(diffval, 4.0), $"Diff isn't even close to 4.0 (diffval={diffval})"); + Assert.True(new TolerantMath(0.5).AreEqual(distp, 1.5), $"Distance isn't even close to 4.0 (distp={distp})"); + + JxlImageF diffmap2 = new(); + Assert.True( + Butteraugli.ButteraugliInterfaceInPlace( + TestEnvironment.Configuration, + rgb0, + rgb1, + butteraugliParameters, + diffmap2, + out double diffval2), + "Butteraugli in-place interface initialization failed"); + + double distp2 = Butteraugli.ComputeDistanceP(diffmap2, butteraugliParameters, 3.0); + + Assert.True(new TolerantMath(5e-7).AreEqual(diffval, diffval2), $"Diffval != diffval2 (diffval={diffval}, diffval2={diffval2})"); + Assert.True(new TolerantMath(1e-7).AreEqual(distp, distp2), $"Distp != distp2 (distp={distp}, distp2={distp2})"); + } +} From ceb282cc11344de422d1e5d0f07197cae7234705 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 5 Sep 2026 14:38:48 +0400 Subject: [PATCH 143/328] Refactor, add MA encoding (decoder-only), finish quantization weights, add bits tests Files implemented: quant_weights.cc, quant_weights.h, modular/encoding/encoding.cc, modular/encoding/encoding.h, ma_common.h, bits_test.cc --- .../Formats/Jxl/Memory/JxlPlaneBase.cs | 18 + .../Formats/Jxl/Memory/JxlPlane{T}.cs | 16 + .../Jxl/Processing/Butteraugli/Butteraugli.cs | 4 +- .../Formats/Jxl/Processing/JxlScopeGuard.cs | 26 + .../Formats/Jxl/Processing/JxlSimdUtils.cs | 96 ++ .../ContextPrediction/JxlMaTreeLookup.cs | 4 +- .../Modular/Encoding/JxlGroupHeader.cs | 66 ++ .../Modular/Encoding/JxlMaConstants.cs | 16 + .../Modular/Encoding/JxlMaTreeContext.cs | 19 + .../Modular/Encoding/JxlModularEncoding.cs | 866 ++++++++++++++++++ .../Encoding/JxlPropertyDecisionNode.cs | 46 + .../Processing/Modular/Encoding/JxlTreeLut.cs | 13 + .../Processing/Modular/JxlModularChannel.cs | 4 + .../Quantization/JxlDequantMatrices.cs | 4 +- .../Quantization/JxlQuantWeights.cs | 700 ++++++++++++++ .../Quantization/JxlQuantizerConstants.cs | 8 + .../Jxl/Processing/Splines/JxlSplineUtils.cs | 16 +- .../Formats/Jxl/Processing/BitsTests.cs | 89 ++ tests/ImageSharp.Tests/Formats/Jxl/README.md | 2 +- .../Jxl/TestUtils/ColorEncodingDescriptor.cs | 19 + 20 files changed, 2017 insertions(+), 15 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlScopeGuard.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlGroupHeader.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaConstants.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaTreeContext.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncoding.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlPropertyDecisionNode.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlTreeLut.cs create mode 100644 tests/ImageSharp.Tests/Formats/Jxl/Processing/BitsTests.cs create mode 100644 tests/ImageSharp.Tests/Formats/Jxl/TestUtils/ColorEncodingDescriptor.cs diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs index 1b63eb43f2..5d2300c2a0 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs @@ -157,6 +157,24 @@ protected Span GetRowBase(int y) return MemoryMarshal.Cast(row); } + protected Span GetRowMinusBase(int y, int minus) + where T : unmanaged + { + DebugGuard.MustBeLessThan(y, this.YSize, nameof(y)); + + Span row = this.Bytes.Span[((y * this.BytesPerRow) - minus)..]; + return MemoryMarshal.Cast(row); + } + + protected Span GetRowPlusBase(int y, int plus) + where T : unmanaged + { + DebugGuard.MustBeLessThan(y, this.YSize, nameof(y)); + + Span row = this.Bytes.Span[((y * this.BytesPerRow) + plus)..]; + return MemoryMarshal.Cast(row); + } + /// /// Swaps properties & data of this image with the specified image. /// diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs index f8602a1897..489b529221 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs @@ -64,6 +64,22 @@ public static JxlPlane Create(Configuration configuration, int xSize, int ySi /// A span which covers memory for the specified row. public Span GetRow(int y) => this.GetRowBase(y); + /// + /// Returns a span for the specified row, but backwards by specified number of elements. + /// + /// The row index. + /// Once the offset of the row was derived, this is how much to subtract the offset. + /// A span which covers memory for the specified row. + public Span GetRowMinus(int y, int minus) => this.GetRowMinusBase(y, minus); + + /// + /// Returns a span for the specified row, but with offset by specified number of elements. + /// + /// The row index. + /// Once the offset of the row was derived, this is the additional offset. + /// A span which covers memory for the specified row. + public Span GetRowPlus(int y, int plus) => this.GetRowMinusBase(y, plus); + /// /// Returns a span for the specified row within the specified rectangle bounds. /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs index f65a66df92..aad9e1972b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs @@ -60,9 +60,7 @@ internal static class Butteraugli }; #pragma warning restore - private static readonly DenseMatrix Heatmap; - - static Butteraugli() => Heatmap = new(HeatmapData); + private static readonly DenseMatrix Heatmap = new(HeatmapData); public static ReadOnlySpan Wmul => [ diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlScopeGuard.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlScopeGuard.cs new file mode 100644 index 0000000000..de553f17cd --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlScopeGuard.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Conditionally invokes the specified method when +/// appropriate and when it goes out of scope. +/// +internal struct JxlScopeGuard(Action action) : IDisposable +{ + /// + /// When true the action will be invoked when Dispose() is called. + /// + private bool isArmed = true; + + public void Disarm() => this.isArmed = false; + + public readonly void Dispose() + { + if (this.isArmed) + { + action(); + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs index fdcc932c67..042406649d 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs @@ -409,6 +409,102 @@ public static float FastErff(float x) return xle0 ? -result : result; } + // Raises 2 to the power of x + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 FastPow2f(Vector128 x) + { + Vector128 floorX = Vector128.Floor(x); + Vector128 exponent = Vector128.ConvertToInt32(floorX) + Vector128.Create(127); + + Vector128 exp = exponent.AsSingle() << 23; + Vector128 frac = x - floorX; + Vector128 num = frac + Vector128.Create(1.01749063e+01f); + + num = Vector128.FusedMultiplyAdd( + num, + frac, + Vector128.Create(4.88687798e+01f)); + + num = Vector128.FusedMultiplyAdd( + num, + frac, + Vector128.Create(9.85506591e+01f)); + + num *= exp; + + Vector128 den = + Vector128.FusedMultiplyAdd( + frac, + Vector128.Create(2.10242958e-01f), + Vector128.Create(-2.22328856e-02f)); + + den = Vector128.FusedMultiplyAdd( + den, + frac, + Vector128.Create(-1.94414990e+01f)); + + den = Vector128.FusedMultiplyAdd( + den, + frac, + Vector128.Create(9.85506633e+01f)); + + return num / den; + } + + // Raises 2 to the power of x + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector FastPow2f(Vector x) + { + Vector floorX = Vector.Floor(x); + Vector exponent = Vector.ConvertToInt32(floorX) + Vector.Create(127); + + Vector exp = exponent.As() << 23; + Vector frac = x - floorX; + Vector num = frac + Vector.Create(1.01749063e+01f); + + num = Vector.FusedMultiplyAdd( + num, + frac, + Vector.Create(4.88687798e+01f)); + + num = Vector.FusedMultiplyAdd( + num, + frac, + Vector.Create(9.85506591e+01f)); + + num *= exp; + + Vector den = + Vector.FusedMultiplyAdd( + frac, + Vector.Create(2.10242958e-01f), + Vector.Create(-2.22328856e-02f)); + + den = Vector.FusedMultiplyAdd( + den, + frac, + Vector.Create(-1.94414990e+01f)); + + den = Vector.FusedMultiplyAdd( + den, + frac, + Vector.Create(9.85506633e+01f)); + + return num / den; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 FastPowf( + Vector128 @base, + Vector128 exponent) + => FastPow2f(Vector128.Log2(@base) * exponent); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector FastPowf( + Vector @base, + Vector exponent) + => FastPow2f(Vector.Log2(@base) * exponent); + /// /// Incrementing values to compute the Iota function. /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlMaTreeLookup.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlMaTreeLookup.cs index ef42bf8c76..6496cc7d1b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlMaTreeLookup.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlMaTreeLookup.cs @@ -3,7 +3,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; -internal sealed class JxlMaTreeLookup(JxlFlatDecisionNode[] nodes) +internal sealed class JxlMaTreeLookup(List nodes) { public JxlMaTreeLookupResult Lookup(Span properties) { @@ -12,7 +12,7 @@ public JxlMaTreeLookupResult Lookup(Span properties) { for (int i = 0; i < 2; i++) { - JxlFlatDecisionNode node = nodes[pos]; + JxlFlatDecisionNode node = nodes[(int)pos]; if (node.Property0 < 0) { return new(node.ChildID, node.Predictor, node.PredictorOffset, node.Multiplier); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlGroupHeader.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlGroupHeader.cs new file mode 100644 index 0000000000..e702708258 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlGroupHeader.cs @@ -0,0 +1,66 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding; + +internal sealed class JxlGroupHeader : IJxlFields +{ + private bool useGlobalTree; + + public JxlGroupHeader() => JxlBundle.Init(this); + + public bool UseGlobalTree + { + get => this.useGlobalTree; + set => this.useGlobalTree = value; + } + + internal JxlModularHeader WeightedHeader { get; set; } = new(); + + internal List Transforms { get; private set; } = []; + + public bool Visit(JxlVisitor visitor) + { + if (!visitor.Boolean(false, ref this.useGlobalTree)) + { + return false; + } + + if (!visitor.VisitNested(this.WeightedHeader)) + { + return false; + } + + uint numTransforms = (uint)this.Transforms.Count; + + _ = visitor.U32( + JxlFieldExpressions.Value(0), + JxlFieldExpressions.Value(1), + JxlFieldExpressions.BitsOffset(4, 2), + JxlFieldExpressions.BitsOffset(8, 18), + 0, + ref numTransforms); + + if (visitor.IsReading) + { + this.Transforms = new List((int)numTransforms); + } + + Span sp = CollectionsMarshal.AsSpan(this.Transforms); + + for (int i = 0; i < numTransforms; i++) + { + if (!visitor.VisitNested(sp[i])) + { + return false; + } + } + + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaConstants.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaConstants.cs new file mode 100644 index 0000000000..db4f1be9cb --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaConstants.cs @@ -0,0 +1,16 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding; + +internal static class JxlMaConstants +{ + /// + /// Total number of MA tree contexts. + /// + public const int NumTreeContexts = 6; + + public const int MaxTreeSize = 1 << 22; + + public const int PropertyRangeFast = 512 << 4; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaTreeContext.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaTreeContext.cs new file mode 100644 index 0000000000..2c06e80ba7 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaTreeContext.cs @@ -0,0 +1,19 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding; + +internal enum JxlMaTreeContext +{ + SplitValue, + + Property, + + Predictor, + + Offset, + + MultiplierLog, + + MultiplierBits +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncoding.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncoding.cs new file mode 100644 index 0000000000..df0afa4da2 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncoding.cs @@ -0,0 +1,866 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using FlatTree = System.Collections.Generic.List; +using Tree = System.Collections.Generic.List; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding; + +internal static class JxlModularEncoding +{ + public static bool TreeToLookupTable(FlatTree tree, JxlTreeLut lut) + { + bool hasOffsets = lut.Offsets.Length > 0; + bool hasMultipliers = lut.Multipliers.Length > 0; + + Stack ranges = []; + ranges.Push(new(-JxlMaConstants.PropertyRangeFast - 1, JxlMaConstants.PropertyRangeFast - 1, 0)); + while (ranges.Count > 0) + { + TreeRange cur = ranges.Peek(); + _ = ranges.Pop(); + + if (cur.Begin < -JxlMaConstants.PropertyRangeFast - 1 || cur.begin >= JxlMaConstants.PropertyRangeFast - 1 || cur.end > JxlMaConstants.PropertyRangeFast - 1) + { + // Tree is outside the allowed range, exit. + return false; + } + + JxlMaNode node = tree[cur.Pos]; + + // Leaf. + if (node.Property0 == -1) + { + if (node.PredictorOffset is < sbyte.MinValue or > sbyte.MaxValue) + { + return false; + } + + if (node.Multiplier is < sbyte.MinValue or > sbyte.MaxValue) + { + return false; + } + + if (!hasMultipliers && node.Multiplier != 1) + { + return false; + } + + if (!hasOffsets && node.PredictorOffset != 0) + { + return false; + } + + for (int i = cur.Begin + 1; i < cur.End + 1; i++) + { + lut.ContextLookup[i + JxlMaConstants.PropertyRangeFast] = node.ChildID; + + if (hasMultipliers) + { + lut.Multipliers[i + JxlMaConstants.PropertyRangeFast] = node.Multiplier; + } + + if (hasOffsets) + { + lut.Offsets[i + JxlMaConstants.PropertyRangeFast] = node.PredictorOffset; + } + } + + continue; + } + + if (node.Properties[0] >= NumStaticProperties) + { + ranges.Push(new(node.SplitValues[0], cur.End, node.ChildID)); + ranges.Push(new(node.SplitValue0, node.SplitValues[0], node.ChildID + 1)); + } + else + { + ranges.Push(new(node.SplitValue0, cur.End, node.ChildID)); + } + + // <= side + if (node.Properties[1] >= NumStaticProperties) + { + ranges.Push( + new(node.SplitValues[1], node.SplitValue0, node.ChildID + 2)); + ranges.Push( + new(cur.Begin, node.SplitValues[1], node.ChildID + 3)); + } + else + { + ranges.Push(new(cur.Begin, node.SplitValue0, node.ChildID + 2)); + } + } + + return true; + } + + public static FlatTree FilterTree(Tree globalTree, Span staticProps, ref int numProps, ref bool useWp, ref bool wpOnly, ref bool gradientOnly) + { + numProps = 0; + bool hasWeightedPrediction = false; + bool hasNonWeighted = false; + gradientOnly = true; + + void MarkProperty(int p, ref bool gradientOnly) + { + if (p == WpProp) + { + hasWeightedPrediction = true; + } + else if (p >= NumStaticProperties) + { + hasNonWeighted = true; + } + + if (p >= NumStaticProperties && p != GradientProp) + { + gradientOnly = false; + } + } + + FlatTree output = []; + Queue nodes = []; + nodes.Enqueue(0); + + while (nodes.Count > 0) + { + int cur = nodes.Peek(); + _ = nodes.Dequeue(); + + while (globalTree[cur].Property < NumStaticProperties && globalTree[cur].Property != -1) + { + if (staticProps[globalTree[cur].Property] > globalTree[cur].SplitValue) + { + cur = globalTree[cur].LeftChild; + } + else + { + cur = globalTree[cur].RightChild; + } + } + + JxlFlatDecisionNode flat = default; + + if (globalTree[cur].Property == -1) + { + flat.Property0 = -1; + flat.ChildID = (uint)globalTree[cur].LeftChild; + flat.Predictor = globalTree[cur].Predictor; + flat.PredictorOffset = (int)globalTree[cur].PredictorOffset; + flat.Multiplier = globalTree[cur].Multiplier; + + gradientOnly &= flat.Predictor == JxlPredictor.Gradient; + + hasWeightedPrediction |= flat.Predictor == JxlPredictor.Weighted; + hasNonWeighted |= flat.Predictor != JxlPredictor.Weighted; + output.Add(flat); + continue; + } + + flat.ChildID = (uint)(output.Count + nodes.Count + 1); + + flat.Property0 = globalTree[cur].Property; + numProps = Math.Max(flat.Property0 + 1, numProps); + flat.SplitValue0 = globalTree[cur].SplitValue; + + for (int i = 0; i < 2; i++) + { + int currentChild = i == 0 ? globalTree[cur].LeftChild : globalTree[cur].RightChild; + + while (globalTree[currentChild].Property < kNumStaticProperties && globalTree[currentChild].Property != -1) + { + if (staticProps[globalTree[currentChild].Property] > + globalTree[currentChild].SplitValue) + { + currentChild = globalTree[currentChild].LeftChild; + } + else + { + currentChild = globalTree[currentChild].RightChild; + } + } + + if (globalTree[currentChild].Property == -1) + { + flat.Properties[i] = 0; + flat.SplitValues[i] = 0; + nodes.Enqueue(currentChild); + nodes.Enqueue(currentChild); + } + else + { + flat.Properties[i] = globalTree[currentChild].Property; + flat.SplitValues[i] = globalTree[currentChild].SplitValue; + nodes.Enqueue(globalTree[currentChild].LeftChild); + nodes.Enqueue(globalTree[currentChild].RightChild); + + numProps = Math.Max(flat.Properties[i] + 1, numProps); + } + } + + for (int i = 0; i < 2; i++) + { + short property = flat.Properties[i]; + MarkProperty(property, ref gradientOnly); + } + + MarkProperty(flat.Property0, ref gradientOnly); + + output.Add(flat); + } + + if (numProps > JxlMaConstants.NumTreeContexts) + { + numProps = (JxlMath.DivCeil(numProps - JxlMaConstants.NumTreeContexts, kExtraPropsPerChannel) * kExtraPropsPerChannel) + JxlMaConstants.NumTreeContexts; + } + else + { + numProps = JxlMaConstants.NumTreeContexts; + } + + useWp = hasWeightedPrediction; + wpOnly = hasWeightedPrediction && !hasNonWeighted; + + return output; + } + + public static void DecodeModularChannelMAANS(Configuration configuration, bool usesLz77, JxlBitReader bitReader, JxlAnsSymbolReader reader, List contextMap, Tree globalTree, JxlModularHeader wpHeader, int channelIdx, int groupId, JxlTreeLut treeLookup, JxlModularImage image, ref uint flRun, ref uint flV) + { + JxlModularChannel channel = image.Channels[channelIdx]; + + InlineArray2 staticProps = default; + staticProps[0] = channelIdx; + staticProps[1] = groupId; + + if ((channel.Width & channel.Height) == 0) // equivalent to (channel.Width == 0 || channel.Height == 0) + { + return; + } + + bool treeHasWeightedPredictionPropOrPred = false; + bool isWeightedPredictionOnly = false; + bool isGradientOnly = false; + int numProps = 0; + + FlatTree tree = FilterTree(globalTree, staticProps, ref numProps, ref treeHasWeightedPredictionPropOrPred, ref isWeightedPredictionOnly, ref isGradientOnly); + Span span = CollectionsMarshal.AsSpan(tree); + + for (int i = 0; i < span.Length; i++) + { + ref JxlFlatDecisionNode node = ref span[i]; + + if (node.Property0 == -1) + { + node.ChildID = contextMap[(int)node.ChildID]; + } + } + + int MakePixel(uint v, int multiplier, int offset) + { + int value = JxlPackSigned.UnpackSigned(v); + return (value * multiplier) + offset; + } + + bool GlobalTreeIsAllGradientNoOp() + { + foreach (JxlPropertyDecisionNode n in globalTree) + { + if (n.Property == -1) + { + if (n.Predictor != JxlPredictor.Gradient || n.PredictorOffset != 0 || n.Multiplier != 1) + { + return false; + } + } + else if (n.Property >= NumStaticProperties) + { + return false; + } + } + + return true; + } + + if (tree.Count == 1) + { + JxlPredictor predictor = tree[0].Predictor; + int offset = tree[0].PredictorOffset; + int multiplier = tree[0].Multiplier; + uint ctx_id = tree[0].ChildID; + + if (predictor == JxlPredictor.Zero) + { + if (reader.IsSingleValueAndAdvance(ctx_id, channel.Width * channel.Height, out uint value)) + { + int v = MakePixel(value, multiplier, offset); + + for (int y = 0; y < channel.Height; y++) + { + Span r = channel.GetRow(y); + r[..channel.Width].Fill(v); + } + } + else + { + if (multiplier == 1 && offset == 0) + { + for (int y = 0; y < channel.Height; y++) + { + Span r = channel.GetRow(y); + + for (int x = 0; x < channel.Width; x++) + { + uint v = reader.ReadHybridUintClusteredInlined(usesLz77, ctx_id, bitReader); + + r[x] = JxlPackSigned.UnpackSigned(v); + } + } + } + else + { + for (int y = 0; y < channel.Height; y++) + { + Span r = channel.GetRow(y); + + for (int x = 0; x < channel.Width; x++) + { + uint v = reader.ReadHybridUintClusteredMaybeInlined(usesLz77, ctx_id, bitReader); + + r[x] = MakePixel(v, multiplier, offset); + } + } + } + } + + return; + } + else if (usesLz77 && reader.IsHuffRleOnly && GlobalTreeIsAllGradientNoOp()) + { + int sv = JxlPackSigned.UnpackSigned(flV); + + for (int y = 0; y < channel.Height; y++) + { + Span r = channel.GetRow(y); + Span rtop = y > 0 ? channel.GetRow(y - 1) : channel.GetRowMinus(y, 1); + Span rtopleft = y > 0 ? channel.GetRowMinus(y - 1, 1) : channel.GetRowMinus(y, 1); + int guess0 = y > 0 ? rtop[0] : 0; + + if (flRun == 0) + { + reader.ReadHybridUintClusteredHuffRleOnly(ctx_id, bitReader, ref flV, ref flRun); + sv = JxlPackSigned.UnpackSigned(flV); + } + else + { + flRun--; + } + + r[0] = sv + guess0; + + for (int x = 1; x < channel.Width; x++) + { + int left = r[x - 1]; + int top = rtop[x]; + int topleft = rtopleft[x]; + int guess = JxlContextPrediction.ClampedGradient(top, left, topleft); + + if (flRun == 0) + { + reader.ReadHybridUintClusteredHuffRleOnly(ctx_id, bitReader, ref flV, ref flRun); + sv = JxlPackSigned.UnpackSigned(flV); + } + else + { + flRun--; + } + + r[x] = sv + guess; + } + } + + return; + } + else if (predictor == JxlPredictor.Gradient && offset == 0 && multiplier == 1) + { + int onerow = channel.Plane.PixelsPerRow; + + for (int y = 0; y < channel.Height; y++) + { + Span r = channel.GetRow(y); + + for (int x = 0; x < channel.Width; x++) + { + // Neighbors + int left = x > 0 ? r[x - 1] : y > 0 ? channel.GetRowMinus(y, x - onerow)[0] : 0; + int top = y > 0 ? channel.GetRowMinus(y, x - onerow)[0] : left; + int topleft = x > 0 && y > 0 ? channel.GetRowMinus(y, x - 1 - onerow)[0] : left; + + int guess = JxlContextPrediction.ClampedGradient(top, left, topleft); + uint v = reader.ReadHybridUintClusteredMaybeInlined>( + usesLz77, + ctx_id, + bitReader); + + r[x] = MakePixel(v, 1, guess); + } + } + + return; + } + } + + if (isWeightedPredictionOnly) + { + isWeightedPredictionOnly = TreeToLookupTable(tree, treeLookup); + } + + if (isGradientOnly) + { + isGradientOnly = TreeToLookupTable(tree, treeLookup); + } + + if (isGradientOnly) + { + int onerow = channel.Plane.PixelsPerRow; + + for (int y = 0; y < channel.Height; y++) + { + Span r = channel.GetRow(y); + + for (int x = 0; x < channel.Width; x++) + { + // Neighbors + int left = x > 0 ? channel.GetRowPlus(y, x - 1)[0] : y > 0 ? channel.GetRowPlus(y, x - onerow)[0] : 0; + int top = y > 0 ? channel.GetRowPlus(y, x - onerow)[0] : left; + int topleft = x > 0 && y > 0 ? channel.GetRowPlus(y, x - 1 - onerow)[0] : left; + + int guess = JxlContextPrediction.ClampedGradient(top, left, topleft); + int pos = + JxlMaConstants.PropertyRangeFast + + Math.Min( + Math.Max(-JxlMaConstants.PropertyRangeFast, top + left - topleft), + JxlMaConstants.PropertyRangeFast - 1); + + uint ctx_id = treeLookup.ContextLookup[pos]; + uint v = reader.ReadHybridUintClusteredMaybeInlined(usesLz77, ctx_id, br); + r[x] = MakePixel(v, 1, guess); + } + } + } + else if (!usesLz77 && isWeightedPredictionOnly && channel.Width > 8) + { + JxlModularState wpState = new(wpHeader, channel.Width); + Span properties = [0]; + + for (int y = 0; y < channel.Height; y++) + { + Span r = channel.GetRow(y); + Span rtop = y > 0 ? channel.GetRow(y - 1) : channel.GetRowMinus(y, 1); + Span rtoptop = y > 1 ? channel.GetRow(y - 2) : rtop; + Span rtopleft = y > 0 ? channel.GetRowMinus(y - 1, 1) : channel.GetRowMinus(y, 1); + Span rtopright = y > 0 ? channel.GetRow(y - 1)[1..] : channel.GetRowMinus(y, 1); + + int x = 0; + { + int offset = 0; + int left = y > 0 ? rtop[x] : 0; + int toptop = y > 0 ? rtoptop[x] : 0; + int topright = x + 1 < channel.Width && y > 0 ? rtop[x + 1] : left; + + int guess = (int)wpState.Predict(true, x, y, channel.Width, left, left, topright, left, toptop, properties, offset); + int pos = JxlMaConstants.PropertyRangeFast + Math.Clamp(properties[0], -JxlMaConstants.PropertyRangeFast, JxlMaConstants.PropertyRangeFast - 1); + + uint ctx_id = treeLookup.ContextLookup[pos]; + uint v = reader.ReadHybridUintClusteredInlined(usesLz77, ctx_id, bitReader); + + r[x] = MakePixel(v, 1, guess); + wpState.UpdatePredictionErrors(r[x], x, y, channel.Width); + } + + for (x = 1; x + 1 < channel.Width; x++) + { + int offset = 0; + int guess = (int)wpState.Predict(true, x, y, channel.Width, rtop[x], r[x - 1], rtopright[x], rtopleft[x], rtoptop[x], properties, offset); + int pos = JxlMaConstants.PropertyRangeFast + Math.Clamp(properties[0], -JxlMaConstants.PropertyRangeFast, JxlMaConstants.PropertyRangeFast - 1); + + int ctx_id = treeLookup.ContextLookup[pos]; + uint v = reader.ReadHybridUintClusteredInlined(usesLz77, ctx_id, bitReader); + + r[x] = MakePixel(v, 1, guess); + wpState.UpdatePredictionErrors(r[x], x, y, channel.Width); + } + + { + int offset = 0; + int guess = (int)wpState.Predict(true, x, y, channel.Width, rtop[x], r[x - 1], rtop[x], rtopleft[x], rtoptop[x], properties, offset); + int pos = JxlMaConstants.PropertyRangeFast + Math.Clamp(properties[0], -JxlMaConstants.PropertyRangeFast, JxlMaConstants.PropertyRangeFast - 1); + + int ctx_id = treeLookup.ContextLookup[pos]; + uint v = reader.ReadHybridUintClusteredInlined(usesLz77, ctx_id, bitReader); + + r[x] = MakePixel(v, 1, guess); + wpState.UpdatePredictionErrors(r[x], x, y, channel.Width); + } + } + } + else if (!treeHasWeightedPredictionPropOrPred) + { + JxlMaTreeLookup tree_lookup = new(tree); + Span properties = stackalloc int[numProps]; + int onerow = channel.Plane.PixelsPerRow; + + JxlModularChannel references = new(configuration, properties.Length - NumNonrefProperties, channel.Width, 0, 0); + + for (int y = 0; y < channel.Height; y++) + { + Span p = channel.GetRow(y); + PrecomputeReferences(channel, y, image, channelIdx, references); + InitPropsRow(properties, staticProps, y); + + if (y > 1 && channel.Width > 8 && references.Width == 0) + { + for (int x = 0; x < 2; x++) + { + JxlPredictionResult res = JxlContextPrediction.PredictTreeNoWeightedPrediction(properties, channel.Width, ref p[x], onerow, x, y, tree_lookup, references); + uint v = reader.ReadHybridUintClustered(usesLz77, res.Context, bitReader); + p[x] = MakePixel(v, res.Multiplier, res.Guess); + } + + for (int x = 2; x < channel.Width - 2; x++) + { + JxlPredictionResult res = JxlContextPrediction.PredictTreeNoWeightedPredictionNoEdgeCases(properties, channel.Width, ref p[x], onerow, x, y, tree_lookup, references); + uint v = reader.ReadHybridUintClusteredInlined(usesLz77, res.Context, bitReader); + p[x] = MakePixel(v, res.Multiplier, res.Guess); + } + + for (int x = channel.Width - 2; x < channel.Width; x++) + { + JxlPredictionResult res = JxlContextPrediction.PredictTreeNoWeightedPrediction(properties, channel.Width, ref p[x], onerow, x, y, tree_lookup, references); + uint v = reader.ReadHybridUintClustered(usesLz77, res.Context, bitReader); + p[x] = MakePixel(v, res.Multiplier, res.Guess); + } + } + else + { + for (int x = 0; x < channel.Width; x++) + { + JxlPredictionResult res = JxlContextPrediction.PredictTreeNoWeightedPrediction(properties, channel.Width, ref p[x], onerow, x, y, tree_lookup, references); + uint v = reader.ReadHybridUintClusteredMaybeInlined(usesLz77, res.Context, bitReader); + p[x] = MakePixel(v, res.Multiplier, res.Guess); + } + } + } + } + else + { + JxlMaTreeLookup tree_lookup = new(tree); + Span properties = stackalloc int[numProps]; + int onerow = channel.Plane.PixelsPerRow; + + JxlModularChannel references = new(configuration, properties.Length - NumNonrefProperties, channel.Width, 0, 0); + JxlModularState wpState = new(wpHeader, channel.Width); + + for (int y = 0; y < channel.Height; y++) + { + Span p = channel.GetRow(y); + InitPropsRow(properties, staticProps, y); + PrecomputeReferences(channel, y, image, channelIdx, references); + + if (!usesLz77 && y > 1 && channel.Width > 8 && references.Width == 0) + { + for (int x = 0; x < 2; x++) + { + JxlPredictionResult res = JxlContextPrediction.PredictTreeWeightedPrediction(properties, channel.Width, ref p[x], onerow, x, y, tree_lookup, references, wpState); + uint v = reader.ReadHybridUintClustered(usesLz77, res.Context, bitReader); + + p[x] = MakePixel(v, res.Multiplier, res.Guess); + wpState.UpdatePredictionErrors(p[x], x, y, channel.Width); + } + + for (int x = 2; x < channel.Width - 2; x++) + { + JxlPredictionResult res = JxlContextPrediction.PredictTreeWeightedPredictionNoEdgeCases(properties, channel.Width, ref p[x], onerow, x, y, tree_lookup, references, wpState); + uint v = reader.ReadHybridUintClusteredInlined(usesLz77, res.Context, bitReader); + + p[x] = MakePixel(v, res.Multiplier, res.Guess); + wpState.UpdatePredictionErrors(p[x], x, y, channel.Width); + } + + for (int x = channel.Width - 2; x < channel.Width; x++) + { + JxlPredictionResult res = JxlContextPrediction.PredictTreeWeightedPrediction(properties, channel.Width, ref p[x], onerow, x, y, tree_lookup, references, wpState); + uint v = reader.ReadHybridUintClustered(usesLz77, res.Context, bitReader); + + p[x] = MakePixel(v, res.Multiplier, res.Guess); + wpState.UpdatePredictionErrors(p[x], x, y, channel.Width); + } + } + else + { + for (int x = 0; x < channel.Width; x++) + { + JxlPredictionResult res = JxlContextPrediction.PredictTreeWeightedPrediction(properties, channel.Width, ref p[x], onerow, x, y, tree_lookup, references, wpState); + uint v = reader.ReadHybridUintClustered(usesLz77, res.Context, bitReader); + + p[x] = MakePixel(v, res.Multiplier, res.Guess); + wpState.UpdatePredictionErrors(p[x], x, y, channel.Width); + } + } + } + } + } + + public static void DecodeModularChannelMAANS(JxlBitReader bitReader, JxlAnsSymbolReader reader, List contextMap, Tree globalTree, JxlModularHeader wpHeader, int channelIdx, int groupId, JxlTreeLut treeLut, JxlModularImage image, ref uint flRun, ref uint flV) + => DecodeModularChannelMAANS(reader.UsesLz77, bitReader, reader, contextMap, globalTree, wpHeader, channelIdx, groupId, treeLut, image, ref flRun, ref flV); + + public static void ValidateChannelDimensions(JxlModularImage image, JxlModularOptions options) + { + int nbChannels = image.Channels.Count; + + foreach (bool isDc in (bool[])[true, false]) + { + int groupDimensions = options.group_dim * (isDc ? JxlFrameDimensions.BlockDimensions : 1); + int c = image.MetaChannels; + + for (; c < nbChannels; c++) + { + JxlModularChannel ch = image.Channels[c]; + if (ch.Width > options.group_dim || ch.Height > options.group_dim) + { + break; + } + } + + for (; c < nbChannels; c++) + { + JxlModularChannel ch = image.Channels[c]; + if (ch.Width == 0 || ch.Height == 0) + { + continue; + } + + bool isDcChannel = Math.Min(ch.HorizontalShift, ch.VerticalShift) >= 3; + if (isDcChannel != isDc) + { + continue; + } + + int tileDimensions = groupDimensions >> Math.Max(ch.HorizontalShift, ch.VerticalShift); + if (tileDimensions == 0) + { + throw new InvalidOperationException("Inconsistent transforms"); + } + } + } + } + + public static bool ModularDecode(Configuration configuration, JxlBitReader br, JxlModularImage image, JxlGroupHeader header, int groupId, JxlModularOptions options, Tree globalTree, JxlAnsCode globalCode, List globalContextMap, bool allowTruncatedGroup) + { + if (image.Channels.Count == 0) + { + return true; + } + + if (!JxlBundle.Read(br, header) && !allowTruncatedGroup) + { + throw new InvalidOperationException("Could not read a bundle"); + } + + image.Transforms = header.Transforms; + + foreach (JxlTransform transform in image.Transforms) + { + transform.MetaApply(image); + } + + if (image.IsInvalid) + { + throw new InvalidOperationException("Corrupt file. Aborting."); + } + + ValidateChannelDimensions(image, options); + + int numberOfChannels = image.Channels.Count; + int numChannels = 0; + int distanceMultiplier = 0; + + for (int i = 0; i < numberOfChannels; i++) + { + JxlModularChannel channel = image.Channels[i]; + if (i >= image.MetaChannels && (channel.Width > options.MaxChannelSize || channel.Height > options.MaxChannelSize)) + { + break; + } + + if (channel.Width == 0 || channel.Height == 0) + { + continue; // skip empty channels + } + + if (channel.Width > distanceMultiplier) + { + distanceMultiplier = channel.Width; + } + + numChannels++; + } + + if (numChannels == 0) + { + return true; + } + + int nextChannel = 0; + using JxlScopeGuard clearGuard = new(() => + { + for (int c = nextChannel; c < image.Channels.Count; c++) + { + image.Channels[c].Plane.Clear(); + } + }); + + if (allowTruncatedGroup) + { + clearGuard.Disarm(); + } + + Tree treeStorage = []; + List contextMapStorage = []; + JxlAnsCode codeStorage = new(); + + Tree tree = treeStorage; + JxlAnsCode code = codeStorage; + List contextMap = contextMapStorage; + + if (!header.UseGlobalTree) + { + ulong maxTreeSize = 1024; + + for (int i = 0; i < numberOfChannels; i++) + { + JxlModularChannel channel = image.Channels[i]; + if (i >= image.MetaChannels && (channel.Width > options.MaxChannelSize || channel.Height > options.MaxChannelSize)) + { + break; + } + + ulong pixels = (ulong)channel.Width * (ulong)channel.Height; + maxTreeSize += pixels; + } + + maxTreeSize = Math.Min(1uL << 20, maxTreeSize); + DecodeTree(configuration, br, treeStorage, maxTreeSize)); + DecodeHistograms( + configuration, + br, + (treeStorage.Count + 1) / 2, + codeStorage, + contextMapStorage); + } + else + { + if (globalTree.Count == 0) + { + throw new InvalidOperationException("No global tree available but one was requested"); + } + + tree = globalTree; + code = globalCode; + contextMap = globalContextMap; + } + + JxlAnsSymbolReader reader = new(code, br, distanceMultiplier); + JxlTreeLut treeLut = new(false, false); + + uint flRun = 0; + uint flV = 0; + + for (; nextChannel < numberOfChannels; nextChannel++) + { + JxlModularChannel channel = image.Channels[nextChannel]; + if (nextChannel >= image.MetaChannels && + (channel.Width > options.MaxChannelSize || + channel.Height > options.MaxChannelSize)) + { + break; + } + + if (channel.Width == 0 || channel.Height == 0) + { + continue; // skip empty channels + } + + DecodeModularChannelMAANS(br, reader, contextMap, tree, header.WeightedHeader, nextChannel, groupId, treeLut, image, ref flRun, ref flV); + } + + clearGuard.Disarm(); + + if (!reader.CheckAnsFinalState()) + { + throw new InvalidOperationException("ANS decode final state failed"); + } + + return true; + } + + public static bool ModularGenericDecompress(Configuration configuration. JxlBitReader br, JxlModularImage image, JxlGroupHeader header, int groupId, JxlModularOptions options, bool undoTransforms, Tree tree, JxlAnsCode code, List contextMap, bool allowTruncatedGroup) + { + List reqSizes = new(capacity: image.Channels.Count); + + foreach (JxlModularChannel c in image.Channels) + { + reqSizes.Add(new(c.Width, c.Height)); + } + + header ??= new JxlGroupHeader(); + + bool decStatus = ModularDecode(configuration, br, image, header, groupId, options, tree, code, contextMap, allowTruncatedGroup); + + if (!allowTruncatedGroup) + { + if (!decStatus) + { + return false; + } + } + + if (undoTransforms) + { + image.UndoTransforms(header.WeightedHeader); + } + + if (image.IsInvalid) + { + throw new InvalidOperationException("Corrupt file. Aborting"); + } + + if (undoTransforms) + { + if (image.Channels.Count != reqSizes.Count) + { + return false; + } + + for (int c = 0; c < reqSizes.Count; c++) + { + if (reqSizes[c].Width != image.Channels[c].Width || reqSizes[c].Height != image.Channels[c].Height) + { + return false; + } + } + } + + return true; + } + + private readonly record struct TreeRange(int Begin, int End, int Pos); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlPropertyDecisionNode.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlPropertyDecisionNode.cs new file mode 100644 index 0000000000..b0fda7efd9 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlPropertyDecisionNode.cs @@ -0,0 +1,46 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding; + +internal struct JxlPropertyDecisionNode +{ + public int SplitValue; + public int Property; + public int LeftChild; + public int RightChild; + public JxlPredictor Predictor; + public long PredictorOffset; + public int Multiplier; + + public JxlPropertyDecisionNode(int property, int splitValue, int leftChild, int rightChild, JxlPredictor predictor, long predictorOffset, int multiplier) + { + this.SplitValue = splitValue; + this.Property = property; + this.LeftChild = leftChild; + this.RightChild = rightChild; + this.Predictor = predictor; + this.PredictorOffset = predictorOffset; + this.Multiplier = multiplier; + } + + public JxlPropertyDecisionNode() + : this(0, -1, 0, 0, JxlPredictor.Zero, 0, 1) + { + } + + public static JxlPropertyDecisionNode Leaf(JxlPredictor predictor, long offset = 0, int multiplier = 1) + => new(-1, 0, 0, 0, predictor, offset, multiplier); + + public static JxlPropertyDecisionNode Split(int p, int splitValue, int leftChild, int rightChild = -1) + { + if (rightChild == -1) + { + rightChild = leftChild + 1; + } + + return new(p, splitValue, leftChild, rightChild, JxlPredictor.Zero, 0, 1); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlTreeLut.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlTreeLut.cs new file mode 100644 index 0000000000..5b33e6ad7d --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlTreeLut.cs @@ -0,0 +1,13 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding; + +internal sealed class JxlTreeLut(bool hasOffsets, bool hasMultipliers) +{ + public T[] ContextLookup { get; } = new T[2 * JxlMaConstants.PropertyRangeFast]; + + public byte[] Offsets { get; } = new byte[hasOffsets ? (2 * JxlMaConstants.PropertyRangeFast) : 0]; + + public byte[] Multipliers { get; } = new byte[hasMultipliers ? (2 * JxlMaConstants.PropertyRangeFast) : 0]; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularChannel.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularChannel.cs index acf3b9532c..02581f15fe 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularChannel.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularChannel.cs @@ -74,4 +74,8 @@ public void Shrink(Configuration configuration, int newWidth, int newHeight) } public Span GetRow(int y) => this.Plane.GetRow(y); + + public Span GetRowMinus(int y, int minus) => this.Plane.GetRowMinus(y, minus); + + public Span GetRowPlus(int y, int plus) => this.Plane.GetRowPlus(y, plus); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlDequantMatrices.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlDequantMatrices.cs index e7084a87cd..8841f0ceac 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlDequantMatrices.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlDequantMatrices.cs @@ -112,12 +112,12 @@ public JxlDequantMatrices() /// /// Gets a lookup which represents required widths for each quantizer. /// - private static ReadOnlySpan RequiredSizeX => [1, 1, 1, 1, 2, 4, 1, 1, 2, 1, 1, 8, 4, 16, 8, 32, 16]; + public static ReadOnlySpan RequiredSizeX => [1, 1, 1, 1, 2, 4, 1, 1, 2, 1, 1, 8, 4, 16, 8, 32, 16]; /// /// Gets a lookup which represents required heights for each quantizer. /// - private static ReadOnlySpan RequiredSizeY => [1, 1, 1, 1, 2, 4, 2, 4, 4, 1, 1, 8, 8, 16, 16, 32, 32]; + public static ReadOnlySpan RequiredSizeY => [1, 1, 1, 1, 2, 4, 2, 4, 4, 1, 1, 8, 8, 16, 16, 32, 32]; /// /// Returns the default library with quantizer encodings for all transforms diff --git a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantWeights.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantWeights.cs index 4986ee7e39..94aab143af 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantWeights.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantWeights.cs @@ -1,7 +1,16 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; +using System.Numerics; +using System.Numerics.Tensors; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; @@ -15,6 +24,8 @@ internal static class JxlQuantWeights public const int Log2NumQuantModes = 3; + private const float AlmostZero = 1e-8f; + /// /// DCT quantizer encoding. (6 distance bands) /// @@ -526,4 +537,693 @@ internal static class JxlQuantWeights ] ], 8)); + + private static ReadOnlySpan AfvFrequencies => + [ + 0xBAD, + 0xBAD, + 0.8517778890324296f, + 5.37778436506804f, + 0xBAD, + 0xBAD, + 4.734747904497923f, + 5.449245381693219f, + 1.6598270267479331f, + 4, + 7.275749096817861f, + 10.423227632456525f, + 2.662932286148962f, + 7.630657783650829f, + 8.962388608184032f, + 12.97166202570235f, + ]; + + private static Vector128 Gather(ReadOnlySpan data, Vector128 indices) + { + Vector128 result = Vector128.Zero; + + for (int i = 0; i < 4; i++) + { + result = result.WithElement(i, data[indices[i]]); + } + + return result; + } + + public static void GetQuantWeightsDCT2(float[][] dct2Weights, Span weights) + { + for (int c = 0; c < 3; c++) + { + int start = c * 64; + + weights[start] = 0xBAD; + weights[start + 1] = weights[start + 8] = dct2Weights[c][0]; + weights[start + 9] = dct2Weights[c][1]; + + for (int y = 0; y < 2; y++) + { + for (int x = 0; x < 2; x++) + { + weights[start + (y * 8) + x + 2] = dct2Weights[c][2]; + weights[start + ((y + 2) * 8) + x] = dct2Weights[c][2]; + } + } + + for (int y = 0; y < 2; y++) + { + for (int x = 0; x < 2; x++) + { + weights[start + ((y + 2) * 8) + x + 2] = dct2Weights[c][3]; + } + } + + for (int y = 0; y < 4; y++) + { + for (int x = 0; x < 4; x++) + { + weights[start + (y * 8) + x + 4] = dct2Weights[c][4]; + weights[start + ((y + 4) * 8) + x] = dct2Weights[c][4]; + } + } + + for (int y = 0; y < 4; y++) + { + for (int x = 0; x < 4; x++) + { + weights[start + ((y + 4) * 8) + x + 4] = dct2Weights[c][5]; + } + } + } + } + + public static void GetQuantWeightsIdentity(float[][] idWeights, Span weights) + { + for (int c = 0; c < 3; c++) + { + int c64 = 64 * c; + + for (int i = 0; i < 64; i++) + { + weights[c64 + i] = idWeights[c][0]; + } + + weights[c64 + 1] = idWeights[c][1]; + weights[c64 + 8] = idWeights[c][1]; + weights[c64 + 9] = idWeights[c][2]; + } + } + + public static float Interpolate(float pos, float max, Span array, int len) + { + float scaledPos = pos * (len - 1) / max; + int idx = (int)scaledPos; + float a = array[idx]; + float b = array[idx + 1]; + return a * MathF.Pow(b / a, scaledPos - idx); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Mult(float v) => v > 0f + ? 1.0f + v + : 1.0f / (1.0f - v); + + public static Vector128 InterpolateVec(Vector128 scaledPos, ReadOnlySpan array) + { + Vector128 idx = Vector128.ConvertToInt32(scaledPos); + Vector128 frac = scaledPos - Vector128.ConvertToSingle(idx); + + Vector128 a = Gather(array, idx); + Vector128 b = Gather(array[1..], idx); + + return a * JxlSimdUtils.FastPowf(b / a, frac); + } + + public static bool GetQuantWeights( + int rows, + int cols, + float[][] distanceBands, + int numBands, + Span output) + { + Span bands = stackalloc float[JxlQuantizerConstants.MaxDistanceBands]; + + for (int c = 0; c < 3; c++) + { + bands[0] = distanceBands[c][0]; + + if (bands[0] < AlmostZero) + { + return false; + } + + for (int i = 1; i < numBands; i++) + { + bands[i] = bands[i - 1] * distanceBands[c][i]; + + if (bands[i] < AlmostZero) + { + return false; + } + } + + float scale = (numBands - 1) / (JxlDctScales.Sqrt2 + 1e-6f); + float rcpCol = scale / (cols - 1); + float rcpRow = scale / (rows - 1); + + for (int y = 0; y < rows; y++) + { + float dy = y * rcpRow; + float dy2 = dy * dy; + + for (int x = 0; x < cols; x += 4) + { + Vector128 dx = + Vector128.Create((float)x, x + 1, x + 2, x + 3) + * Vector128.Create(rcpCol); + + Vector128 scaledDistance = + Vector128.Sqrt( + (dx * dx) + Vector128.Create(dy2)); + + Vector128 weight = + numBands == 1 + ? Vector128.Create(bands[0]) + : InterpolateVec(scaledDistance, bands); + + weight.CopyTo(output[((c * cols * rows) + (y * cols) + x)..]); + } + } + } + + return true; + } + + public static bool ComputeQuantTable(JxlQuantizerEncoding encoding, Span table, Span inverseTable, int tableNum, JxlQuantTable kind, ref int pos) + { + const int n = JxlFrameDimensions.BlockDimensions; + + int quantSizeTable = (int)kind; + int wrows = 8 * JxlDequantMatrices.RequiredSizeX[quantSizeTable]; + int wcols = 8 * JxlDequantMatrices.RequiredSizeY[quantSizeTable]; + int num = wrows * wcols; + + Span weights = stackalloc float[3 * num]; + + switch (encoding.Mode) + { + case JxlQuantMode.Library: + { + // Library and copy quant encoding should get replaced by the actual + // parameters by the caller. + return false; + } + + case JxlQuantMode.Id: + { + if (num != JxlFrameDimensions.DctBlockSize) + { + return false; + } + + GetQuantWeightsIdentity(encoding.IdWeights!, weights); + break; + } + + case JxlQuantMode.Dct2: + { + if (num != JxlFrameDimensions.DctBlockSize) + { + return false; + } + + GetQuantWeightsDCT2(encoding.Dct2Weights!, weights); + break; + } + + case JxlQuantMode.Dct4: + { + if (num != JxlFrameDimensions.DctBlockSize) + { + return false; + } + + Span weights4x4 = stackalloc float[3 * 4 * 4]; + + // Always use 4x4 GetQuantWeights for DCT4 quantization tables. + if (!GetQuantWeights(4, 4, encoding.DctParameters!.DistanceBands, encoding.DctParameters.NumDistanceBands, weights4x4)) + { + return false; + } + + for (int c = 0; c < 3; c++) + { + for (int y = 0; y < JxlFrameDimensions.BlockDimensions; y++) + { + for (int x = 0; x < JxlFrameDimensions.BlockDimensions; x++) + { + weights[(c * num) + (y * JxlFrameDimensions.BlockDimensions) + x] = weights4x4[(c * 16) + ((y / 2) * 4) + (x / 2)]; + } + } + + weights[(c * num) + 1] /= encoding.Dct4Multipliers![c][0]; + weights[(c * num) + n] /= encoding.Dct4Multipliers[c][0]; + weights[(c * num) + n + 1] /= encoding.Dct4Multipliers[c][1]; + } + + break; + } + + case JxlQuantMode.Dct4x8: + { + if (num != JxlFrameDimensions.DctBlockSize) + { + return false; + } + + Span weights4x8 = stackalloc float[3 * 4 * 8]; + + if (!GetQuantWeights(4, 8, encoding.DctParameters!.DistanceBands, encoding.DctParameters.NumDistanceBands, weights4x8)) + { + return false; + } + + for (int c = 0; c < 3; c++) + { + for (int y = 0; y < JxlFrameDimensions.BlockDimensions; y++) + { + for (int x = 0; x < JxlFrameDimensions.BlockDimensions; x++) + { + weights[(c * num) + (y * JxlFrameDimensions.BlockDimensions) + x] = weights4x8[(c * 32) + ((y / 2) * 8) + x]; + } + } + + weights[(c * num) + n] /= encoding.Dct4x8Multipliers![c]; + } + + break; + } + + case JxlQuantMode.Dct: + { + if (!GetQuantWeights(wrows, wcols, encoding.DctParameters!.DistanceBands, encoding.DctParameters.NumDistanceBands, weights)) + { + return false; + } + + break; + } + + case JxlQuantMode.Raw: + { + if (encoding.QuantizationTable is null || encoding.QuantizationTable.Length != 3 * num) + { + throw new InvalidOperationException("Invalid raw quantizer table encoding"); + } + + Span qtable = encoding.QuantizationTable.AsSpan(); + + for (int i = 0; i < 3 * num; i++) + { + weights[i] = 1f / (encoding.QuantizationTableDenominator * qtable[i]); + } + + break; + } + + case JxlQuantMode.Afv: + { + Span weights4x8 = stackalloc float[3 * 4 * 8]; + + if (!GetQuantWeights(4, 8, encoding.DctParameters!.DistanceBands, encoding.DctParameters.NumDistanceBands, weights4x8)) + { + return false; + } + + Span weights4x4 = stackalloc float[3 * 4 * 4]; + + if (!GetQuantWeights(4, 4, encoding.DctParametersAfv4x4!.DistanceBands, encoding.DctParametersAfv4x4.NumDistanceBands, weights4x4)) + { + return false; + } + + const float lo = 0.8517778890324296f; + const float hi = 12.97166202570235f - lo + 1e-6f; + + Span bands = [0, 0, 0, 0]; + + for (int c = 0; c < 3; c++) + { + bands[0] = encoding.AfvWeights![c][5]; + + if (bands[0] < AlmostZero) + { + throw new InvalidOperationException("Invalid AFV bands"); + } + + for (int i = 1; i < 4; i++) + { + bands[i] = bands[i - 1] * Mult(encoding.AfvWeights[c][i + 5]); + + if (bands[i] < AlmostZero) + { + throw new InvalidOperationException("Invalid AFV bands"); + } + } + + int start = c * 64; + + void SetWeight(int x, int y, float value, Span weights) => weights[start + (y * 8) + x] = value; + + weights[start] = 1; + + SetWeight(0, 1, encoding.AfvWeights[c][0], weights); + SetWeight(1, 0, encoding.AfvWeights[c][1], weights); + SetWeight(0, 2, encoding.AfvWeights[c][2], weights); + SetWeight(2, 0, encoding.AfvWeights[c][3], weights); + SetWeight(2, 2, encoding.AfvWeights[c][4], weights); + + // All other AFV weights. + for (int y = 0; y < 4; y++) + { + for (int x = 0; x < 4; x++) + { + if (x < 2 && y < 2) + { + continue; + } + + float interpolatedVal = Interpolate(AfvFrequencies[(y * 4) + x] - lo, hi, bands, 4); + + SetWeight(2 * x, 2 * y, interpolatedVal, weights); + } + } + + // Put 4x8 weights in odd rows, except (1, 0). + for (int y = 0; y < JxlFrameDimensions.BlockDimensions / 2; y++) + { + for (int x = 0; x < JxlFrameDimensions.BlockDimensions; x++) + { + if (x == 0 && y == 0) + { + continue; + } + + weights[(c * num) + (((2 * y) + 1) * JxlFrameDimensions.BlockDimensions) + x] = weights4x8[(c * 32) + (y * 8) + x]; + } + } + + // Put 4x4 weights in even rows / odd columns, except (0, 1). + for (int y = 0; y < JxlFrameDimensions.BlockDimensions / 2; y++) + { + for (int x = 0; x < JxlFrameDimensions.BlockDimensions / 2; x++) + { + if (x == 0 && y == 0) + { + continue; + } + + weights[(c * num) + ((2 * y) * JxlFrameDimensions.BlockDimensions) + (2 * x) + 1] = weights4x4[(c * 16) + (y * 4) + x]; + } + } + } + + break; + } + } + + int prevPos = pos; + + // Don't zero-init + Span invVal = stackalloc float[64]; + Span val = stackalloc float[64]; + + for (int i = 0; i < num * 3; i += 64) + { + weights.Slice(i, 64).CopyTo(invVal); + + // TODO: there's an unlikely check right here in + // reference: + // if (JXL_UNLIKELY(!AllFalse(d, Ge(inv_val, Set(d, 1.0f / kAlmostZero))) || + // !AllFalse(d, Lt(inv_val, Set(d, kAlmostZero))))) + // { + // throw new InvalidOperationException("Invalid quantization table"); + // } + // should we trade performance for an unlikely check? + val.Fill(1.0f); + TensorPrimitives.Divide(val, invVal, val); + + val.CopyTo(table.Slice(pos + i, 64)); + invVal.CopyTo(inverseTable.Slice(pos + i, 64)); + } + + pos += 3 * num; + + int xs = JxlDequantMatrices.RequiredSizeX[quantSizeTable]; + int ys = JxlDequantMatrices.RequiredSizeY[quantSizeTable]; + + JxlForwardCoefficientOrder.CoefficientLayout(ref ys, ref xs); + + for (int c = 0; c < 3; c++) + { + for (int y = 0; y < ys; y++) + { + for (int x = 0; x < xs; x++) + { + inverseTable[prevPos + (c * ys * xs * JxlFrameDimensions.DctBlockSize) + (y * JxlFrameDimensions.BlockDimensions * xs) + x] = 0; + } + } + } + + return true; + } + + public static bool DecodeDctParameters(JxlBitReader reader, JxlDctQuantWeightParameters parameters) + { + parameters.NumDistanceBands = (int)reader.ReadBits32(JxlQuantizerConstants.Log2MaxDistanceBands) + 1; + + for (int c = 0; c < 3; c++) + { + for (int i = 0; i < parameters.NumDistanceBands; i++) + { + if (!JxlF16Coder.Read(reader, ref parameters.DistanceBands[c][i])) + { + return false; + } + } + + if (parameters.DistanceBands[c][0] < AlmostZero) + { + throw new InvalidOperationException("Distance band seed is too small"); + } + + parameters.DistanceBands[c][0] *= 64f; + } + + return true; + } + + public static bool Decode(Configuration configuration, JxlBitReader br, JxlQuantizerEncoding encoding, int requiredSizeX, int requiredSizeY, int idx, JxlModularFrameDecoder modularFrameDecoder) + { + int requiredSize = requiredSizeX * requiredSizeY; + + requiredSizeX *= JxlFrameDimensions.BlockDimensions; + requiredSizeY *= JxlFrameDimensions.BlockDimensions; + + int mode = (int)br.ReadBits32(JxlQuantizerConstants.Log2NumQuantModes); + + switch ((JxlQuantMode)mode) + { + case JxlQuantMode.Library: + { + encoding.Predefined = (byte)br.ReadBits32(CeilLog2NumPredefinedTables); + + if (encoding.Predefined >= NumPredefinedTables) + { + throw new InvalidOperationException("Invalid predefined table"); + } + + break; + } + + case JxlQuantMode.Id: + { + if (requiredSize != 1) + { + throw new InvalidOperationException("Invalid mode"); + } + + for (int c = 0; c < 3; c++) + { + for (int i = 0; i < 3; i++) + { + if (!JxlF16Coder.Read(br, ref encoding.IdWeights![c][i])) + { + return false; + } + + if (Math.Abs(encoding.IdWeights[c][i]) < AlmostZero) + { + throw new InvalidOperationException("ID Quantizer is too small"); + } + + encoding.IdWeights[c][i] *= 64; + } + } + + break; + } + + case JxlQuantMode.Dct2: + { + if (requiredSize != 1) + { + throw new InvalidOperationException("Invalid mode"); + } + + for (int c = 0; c < 3; c++) + { + for (int i = 0; i < 6; i++) + { + if (!JxlF16Coder.Read(br, ref encoding.Dct2Weights![c][i])) + { + return false; + } + + if (Math.Abs(encoding.Dct2Weights[c][i]) < AlmostZero) + { + throw new InvalidOperationException("Quantizer is too small"); + } + + encoding.Dct2Weights[c][i] *= 64; + } + } + + break; + } + + case JxlQuantMode.Dct4x8: + { + if (requiredSize != 1) + { + throw new InvalidOperationException("Invalid mode"); + } + + for (int c = 0; c < 3; c++) + { + if (!JxlF16Coder.Read(br, ref encoding.Dct4x8Multipliers![c])) + { + return false; + } + + if (Math.Abs(encoding.Dct4x8Multipliers[c]) < AlmostZero) + { + throw new InvalidOperationException("DCT4X8 multiplier is too small"); + } + } + + if (!DecodeDctParameters(br, encoding.DctParameters!)) + { + return false; + } + + break; + } + + case JxlQuantMode.Dct4: + { + if (requiredSize != 1) + { + throw new InvalidOperationException("Invalid mode"); + } + + for (int c = 0; c < 3; c++) + { + for (int i = 0; i < 2; i++) + { + if (!JxlF16Coder.Read(br, ref encoding.Dct4Multipliers![c][i])) + { + return false; + } + + if (Math.Abs(encoding.Dct4Multipliers[c][i]) < AlmostZero) + { + throw new InvalidOperationException("DCT4 multiplier is too small"); + } + } + } + + if (!DecodeDctParameters(br, encoding.DctParameters!)) + { + return false; + } + + break; + } + + case JxlQuantMode.Afv: + { + if (requiredSize != 1) + { + throw new InvalidOperationException("Invalid mode"); + } + + for (int c = 0; c < 3; c++) + { + for (int i = 0; i < 9; i++) + { + if (!JxlF16Coder.Read(br, ref encoding.AfvWeights![c][i])) + { + return false; + } + } + + for (int i = 0; i < 6; i++) + { + encoding.AfvWeights![c][i] *= 64; + } + } + + if (!DecodeDctParameters(br, encoding.DctParameters!)) + { + return false; + } + + if (!DecodeDctParameters(br, encoding.DctParametersAfv4x4!)) + { + return false; + } + + break; + } + + case JxlQuantMode.Dct: + { + if (!DecodeDctParameters(br, encoding.DctParameters!)) + { + return false; + } + + break; + } + + case JxlQuantMode.Raw: + { + // Set mode early, to avoid mem-leak. + encoding.Mode = JxlQuantMode.Raw; + + if (!JxlModularFrameDecoder.DecodeQuantTable(configuration, requiredSizeX, requiredSizeY, br, encoding, idx, modularFrameDecoder)) + { + return false; + } + + break; + } + + default: + throw new InvalidOperationException("Invalid quant table encoding"); + } + + encoding.Mode = (JxlQuantMode)mode; + return true; + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerConstants.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerConstants.cs index ff6e9c3754..bc0042afaa 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerConstants.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerConstants.cs @@ -8,6 +8,14 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; /// internal static class JxlQuantizerConstants { + public const int Log2MaxDistanceBands = 4; + + public const int MaxDistanceBands = 1 + (1 << Log2MaxDistanceBands); + + public const int CeilLog2NumPredefinedTables = 0; + + public const int Log2NumQuantModes = 3; + /// /// Total number of quantization tables. /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineUtils.cs index 89b446e1f0..93f240da0d 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineUtils.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineUtils.cs @@ -13,15 +13,17 @@ internal static class JxlSplineUtils { public const float DesiredRenderingDistance = 1f; + private const float PiBy32 = MathF.PI / 32; + private static ReadOnlySpan ContinuousIDCTMultipliers => [ - MathF.PI / 32 * 0, MathF.PI / 32 * 1, MathF.PI / 32 * 2, MathF.PI / 32 * 3, MathF.PI / 32 * 4, - MathF.PI / 32 * 5, MathF.PI / 32 * 6, MathF.PI / 32 * 7, MathF.PI / 32 * 8, MathF.PI / 32 * 9, - MathF.PI / 32 * 10, MathF.PI / 32 * 11, MathF.PI / 32 * 12, MathF.PI / 32 * 13, MathF.PI / 32 * 14, - MathF.PI / 32 * 15, MathF.PI / 32 * 16, MathF.PI / 32 * 17, MathF.PI / 32 * 18, MathF.PI / 32 * 19, - MathF.PI / 32 * 20, MathF.PI / 32 * 21, MathF.PI / 32 * 22, MathF.PI / 32 * 23, MathF.PI / 32 * 24, - MathF.PI / 32 * 25, MathF.PI / 32 * 26, MathF.PI / 32 * 27, MathF.PI / 32 * 28, MathF.PI / 32 * 29, - MathF.PI / 32 * 30, MathF.PI / 32 * 31, + PiBy32 * 0, PiBy32 * 1, PiBy32 * 2, PiBy32 * 3, PiBy32 * 4, + PiBy32 * 5, PiBy32 * 6, PiBy32 * 7, PiBy32 * 8, PiBy32 * 9, + PiBy32 * 10, PiBy32 * 11, PiBy32 * 12, PiBy32 * 13, PiBy32 * 14, + PiBy32 * 15, PiBy32 * 16, PiBy32 * 17, PiBy32 * 18, PiBy32 * 19, + PiBy32 * 20, PiBy32 * 21, PiBy32 * 22, PiBy32 * 23, PiBy32 * 24, + PiBy32 * 25, PiBy32 * 26, PiBy32 * 27, PiBy32 * 28, PiBy32 * 29, + PiBy32 * 30, PiBy32 * 31, ]; public static float ContinuousInverseDCT(in Dct32 dct, float t) diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/BitsTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/BitsTests.cs new file mode 100644 index 0000000000..22f04cbe81 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/BitsTests.cs @@ -0,0 +1,89 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Processing; + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; + +public class BitsTests +{ + [Fact] + public void TestNumZeroBits() + { + Assert.Equal(32u, JxlMath.Num0BitsAboveMS1Bit(0u)); + Assert.Equal(64u, JxlMath.Num0BitsAboveMS1Bit(0uL)); + Assert.Equal(32u, JxlMath.Num0BitsBelowLS1Bit(0u)); + Assert.Equal(64u, JxlMath.Num0BitsBelowLS1Bit(0uL)); + + Assert.Equal(31u, JxlMath.Num0BitsAboveMS1Bit(1u)); + Assert.Equal(30u, JxlMath.Num0BitsAboveMS1Bit(2u)); + Assert.Equal(63u, JxlMath.Num0BitsAboveMS1Bit(1uL)); + Assert.Equal(62u, JxlMath.Num0BitsAboveMS1Bit(2uL)); + + Assert.Equal(0u, JxlMath.Num0BitsBelowLS1Bit(1u)); + Assert.Equal(0u, JxlMath.Num0BitsBelowLS1Bit(1uL)); + Assert.Equal(1u, JxlMath.Num0BitsBelowLS1Bit(2u)); + Assert.Equal(1u, JxlMath.Num0BitsBelowLS1Bit(2uL)); + + Assert.Equal(0u, JxlMath.Num0BitsAboveMS1Bit(0x80000000u)); + Assert.Equal(0u, JxlMath.Num0BitsAboveMS1Bit(0x8000000000000000uL)); + Assert.Equal(31u, JxlMath.Num0BitsBelowLS1Bit(0x80000000u)); + Assert.Equal(63u, JxlMath.Num0BitsBelowLS1Bit(0x8000000000000000uL)); + } + + [Fact] + public void TestFloorLog2() + { + Span expected = [0, 1, 1, 2, 2, 2, 2]; + + for (int i = 1; i <= 7; ++i) + { + Assert.Equal(expected[i - 1], JxlMath.FloorLog2Nonzero(i)); + Assert.Equal((ulong)expected[i - 1], JxlMath.FloorLog2Nonzero((ulong)i)); + } + + Assert.Equal(11u, JxlMath.FloorLog2Nonzero(0x00000fffu)); // 4095 + Assert.Equal(12u, JxlMath.FloorLog2Nonzero(0x00001000u)); // 4096 + Assert.Equal(12u, JxlMath.FloorLog2Nonzero(0x00001001u)); // 4097 + + Assert.Equal(31u, JxlMath.FloorLog2Nonzero(0x80000000u)); + Assert.Equal(31u, JxlMath.FloorLog2Nonzero(0x80000001u)); + Assert.Equal(31u, JxlMath.FloorLog2Nonzero(0xFFFFFFFFu)); + + Assert.Equal(31u, JxlMath.FloorLog2Nonzero(0x80000000uL)); + Assert.Equal(31u, JxlMath.FloorLog2Nonzero(0x80000001uL)); + Assert.Equal(31u, JxlMath.FloorLog2Nonzero(0xFFFFFFFFuL)); + + Assert.Equal(63u, JxlMath.FloorLog2Nonzero(0x8000000000000000uL)); + Assert.Equal(63u, JxlMath.FloorLog2Nonzero(0x8000000000000001uL)); + Assert.Equal(63u, JxlMath.FloorLog2Nonzero(0xFFFFFFFFFFFFFFFFuL)); + } + + [Fact] + public void TestCeilLog2() + { + Span expected = [0, 1, 2, 2, 3, 3, 3]; + + for (int i = 1; i <= 7; ++i) + { + Assert.Equal(expected[i - 1], JxlMath.CeilLog2Nonzero(i)); + Assert.Equal((ulong)expected[i - 1], JxlMath.CeilLog2Nonzero((ulong)i)); + } + + Assert.Equal(12u, JxlMath.CeilLog2Nonzero(0x00000fffu)); // 4095 + Assert.Equal(12u, JxlMath.CeilLog2Nonzero(0x00001000u)); // 4096 + Assert.Equal(13u, JxlMath.CeilLog2Nonzero(0x00001001u)); // 4097 + + Assert.Equal(31u, JxlMath.CeilLog2Nonzero(0x80000000u)); + Assert.Equal(32u, JxlMath.CeilLog2Nonzero(0x80000001u)); + Assert.Equal(32u, JxlMath.CeilLog2Nonzero(0xFFFFFFFFu)); + + Assert.Equal(31u, JxlMath.CeilLog2Nonzero(0x80000000uL)); + Assert.Equal(32u, JxlMath.CeilLog2Nonzero(0x80000001uL)); + Assert.Equal(32u, JxlMath.CeilLog2Nonzero(0xFFFFFFFFuL)); + + Assert.Equal(63u, JxlMath.CeilLog2Nonzero(0x8000000000000000uL)); + Assert.Equal(64u, JxlMath.CeilLog2Nonzero(0x8000000000000001uL)); + Assert.Equal(64u, JxlMath.CeilLog2Nonzero(0xFFFFFFFFFFFFFFFFuL)); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Jxl/README.md b/tests/ImageSharp.Tests/Formats/Jxl/README.md index 4eba637344..80fa3f63ab 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/README.md +++ b/tests/ImageSharp.Tests/Formats/Jxl/README.md @@ -1,5 +1,5 @@ # JPEG XL Tests ### Preprocessor directives -Enable a ALLOW_JPEGXL_SLOW_TESTS preprocessor directive to allow +Enable a `ALLOW_JPEGXL_SLOW_TESTS` preprocessor directive to allow slower tests that test the library more extensively. diff --git a/tests/ImageSharp.Tests/Formats/Jxl/TestUtils/ColorEncodingDescriptor.cs b/tests/ImageSharp.Tests/Formats/Jxl/TestUtils/ColorEncodingDescriptor.cs new file mode 100644 index 0000000000..581faf1a2b --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/TestUtils/ColorEncodingDescriptor.cs @@ -0,0 +1,19 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Cms; + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl.TestUtils; + +internal struct ColorEncodingDescriptor +{ + public JxlColorSpace ColorSpace { get; set; } + + public JxlWhitePoint WhitePoint { get; set; } + + public JxlPrimaries Primaries { get; set; } + + public JxlTransferFunction TransferFunction { get; set; } + + public JxlRenderingIntent RenderingIntent { get; set; } +} From 5653d39664b01b8c04ca730747406877e92008e9 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sun, 6 Sep 2026 20:57:41 +0400 Subject: [PATCH 144/328] Add MA encoder --- .../Jxl/Processing/Encoder/Ans/JxlToken.cs | 6 +- .../ContextPrediction/JxlContextPrediction.cs | 2 +- .../Modular/Encoding/JxlMaConstants.cs | 8 + .../Modular/Encoding/JxlMaEncoder.cs | 865 +++++++++++++++++ .../Modular/Encoding/JxlMaTreeContext.cs | 2 +- .../Modular/Encoding/JxlResidualToken.cs | 16 + .../Modular/Encoding/JxlTreeSamples.cs | 884 ++++++++++++++++++ .../Processing/Modular/Encoding/ListUtils.cs | 15 + .../Formats/Jxl/Processing/Modular/README.md | 15 + .../Formats/Jxl/Processing/Primitives/Rng.cs | 87 ++ 10 files changed, 1895 insertions(+), 5 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaEncoder.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlResidualToken.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlTreeSamples.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ListUtils.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/README.md create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Primitives/Rng.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlToken.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlToken.cs index 2b07fb14d9..feb293a98d 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlToken.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlToken.cs @@ -1,13 +1,13 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; -internal struct JxlToken(JxlSplineEntropyContext c, uint value) +internal struct JxlToken(JxlMaTreeContext c, uint value) { public bool IsLz77Length; - public JxlSplineEntropyContext Context = c; + public JxlMaTreeContext Context = c; public uint Value = value; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlContextPrediction.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlContextPrediction.cs index f856ef0e39..caeb0e68c6 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlContextPrediction.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlContextPrediction.cs @@ -12,7 +12,7 @@ internal static class JxlContextPrediction { private const int ExtraPropertiesPerChannel = 4; - private const int NumberOfProperties = 1; + public const int NumberOfProperties = 1; public static void SetPredictorMode(int i, JxlModularHeader header) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaConstants.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaConstants.cs index db4f1be9cb..6e375e3da7 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaConstants.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaConstants.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; + namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding; internal static class JxlMaConstants @@ -13,4 +15,10 @@ internal static class JxlMaConstants public const int MaxTreeSize = 1 << 22; public const int PropertyRangeFast = 512 << 4; + + public const int NumNonrefProperties = 2 + 13 + JxlContextPrediction.NumberOfProperties; + + public const int WpProp = NumNonrefProperties - JxlContextPrediction.NumberOfProperties; + + public const int GradientProp = 9; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaEncoder.cs new file mode 100644 index 0000000000..009cb49c6e --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaEncoder.cs @@ -0,0 +1,865 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Numerics; +using System.Numerics.Tensors; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using StaticPropertyRange = System.Runtime.CompilerServices.InlineArray2>; +using Tree = System.Collections.Generic.List; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding; + +internal static class JxlMaEncoder +{ + internal enum IntersectionType + { + None, + Partial, + Inside + } + + private static int Padded(int x) => JxlMath.RoundUpTo(x, Vector.Count); + + public static float EstimateBits(Span counts, int numSymbols) + { + int total = TensorPrimitives.Sum((ReadOnlySpan)counts.Slice(0, numSymbols)); + + Vector minprob = Vector.Create(1.0f / JxlAnsConstants.AnsTableSize); + Vector inverseTotal = Vector.Create(1.0f / total); + Vector bitsLanes = Vector.Zero; + + for (int i = 0; i < numSymbols; i += Vector.Count) + { + Vector countsIv = new(counts[i..]); + Vector countsFv = Vector.ConvertToSingle(countsIv); + Vector probs = countsFv * inverseTotal; + Vector mprobs = probs * minprob; + + Vector nbps = Vector.ConditionalSelect( + Vector.Equals(countsIv, Vector.Create(total)), + Vector.Zero, + Vector.Log2(mprobs)); + + bitsLanes -= countsFv * nbps; + } + + return Vector.Sum(bitsLanes); + } + + public static void MakeSplitNode(int pos, int property, int splitValue, JxlPredictor leftPredictor, int leftOffset, JxlPredictor rightPredictor, int rightOffset, Tree tree) + { + ref JxlPropertyDecisionNode treePos = ref CollectionsMarshal.AsSpan(tree)[pos]; + + treePos.LeftChild = tree.Count; + treePos.RightChild = tree.Count + 1; + treePos.SplitValue = splitValue; + treePos.Property = property; + + JxlPropertyDecisionNode newRightNode = new() + { + Property = -1, + Predictor = rightPredictor, + PredictorOffset = rightOffset, + Multiplier = 1 + }; + + JxlPropertyDecisionNode newLeftNode = new() + { + Property = -1, + Predictor = leftPredictor, + PredictorOffset = leftOffset, + Multiplier = 1 + }; + + tree.Add(newRightNode); + tree.Add(newLeftNode); + } + + public static IntersectionType BoxIntersects(StaticPropertyRange needle, StaticPropertyRange haystack, ref int partialAxis, ref int partialValue) + { + bool partial = false; + + for (int i = 0; i < JxlPredictorFacts.StaticProperties; i++) + { + if (haystack[i][0] >= needle[i][1]) + { + return IntersectionType.None; + } + + if (haystack[i][1] <= needle[i][0]) + { + return IntersectionType.None; + } + + if (haystack[i][0] <= needle[i][0] && haystack[i][1] >= needle[i][1]) + { + continue; + } + + partial = true; + partialAxis = i; + + int innerOffset = haystack[i][0] > needle[i][0] && haystack[i][0] < needle[i][1] ? 0 : 1; // yes, if true then 0 not 1 + partialValue = haystack[i][innerOffset] - 1; + } + + return partial ? IntersectionType.Partial : IntersectionType.Inside; + } + + public static void SplitTreeSamples(bool s, JxlTreeSamples samples, int begin, int pos, int end, int prop, int value) + { + int beginPos = begin; + int endPos = pos; + + do + { + while (beginPos < pos && samples.GetProperty(s, prop, beginPos) <= value) + { + beginPos++; + } + + while (endPos < end && samples.GetProperty(s, prop, endPos) > value) + { + endPos++; + } + + if (beginPos < pos && endPos < end) + { + samples.Swap(beginPos, endPos); + } + + beginPos++; + endPos++; + } + while (beginPos < pos && endPos < end); + } + + // Simple overload so we don't have to type out CollectionsMarshal.AsSpan all the time. + public static void CollectExtraBitsIncrease(bool s, JxlTreeSamples treeSamples, List residualTokens, List countIncrease, List extraBitsIncrease, int begin, int end, int propertyIndex, int maxSymbols) + => CollectExtraBitsIncrease( + s, + treeSamples, + residualTokens, + CollectionsMarshal.AsSpan(countIncrease), + CollectionsMarshal.AsSpan(extraBitsIncrease), + begin, + end, + propertyIndex, + maxSymbols); + + public static void CollectExtraBitsIncrease(bool s, JxlTreeSamples treeSamples, List residualTokens, Span countIncrease, Span extraBitsIncrease, int begin, int end, int propertyIndex, int maxSymbols) + { + for (int i2 = begin; i2 < end; i2++) + { + JxlResidualToken rt = residualTokens[i2]; + + int cnt = treeSamples.GetCount(i2); + int p = treeSamples.GetProperty(s, propertyIndex, i2); + int sym = rt.Token; + int ebi = rt.NumberOfBits * cnt; + + countIncrease[(p * maxSymbols) + sym] += cnt; + extraBitsIncrease[p] += ebi; + } + } + + public static unsafe void FindBestSplit(JxlTreeSamples treeSamples, float threshold, List mulInfo, StaticPropertyRange initialStaticPropertyRange, float fastDecodeMultiplier, Tree tree) + { + Stack nodes = []; + nodes.Push(new(0, 0, treeSamples.NumberOfDistinctSamples, initialStaticPropertyRange)); + + int numPred = treeSamples.NumberOfPredictors; + int numProp = treeSamples.NumberOfProperties; + + Span totalExtraBits = stackalloc int[numPred]; + + while (nodes.Count > 0) + { + NodeInfo last = nodes.Peek(); + + int pos = last.Pos; + int begin = last.Begin; + int end = last.End; + StaticPropertyRange staticPropertyRange = last.StaticPropertyRange; + + _ = nodes.Pop(); + + if (begin == end) + { + continue; + } + + SplitInfo bestSplitStaticConstant = default; + SplitInfo bestSplitStatic = default; + SplitInfo bestSplitNonStatic = default; + SplitInfo bestSplitNoWeightedPrediction = default; + + if (begin > end) + { + throw new InvalidOperationException("Begin should be <= end"); + } + + if (end > treeSamples.NumberOfDistinctSamples) + { + throw new InvalidOperationException("End index out of range"); + } + + int maxSymbols = 0; + + for (int pred = 0; pred < numPred; pred++) + { + for (int i = begin; i < end; i++) + { + int token = treeSamples.GetToken(pred, i); + maxSymbols = maxSymbols > token + 1 ? maxSymbols : token + 1; + } + } + + maxSymbols = Padded(maxSymbols); + + int[] counts = ArrayPool.Shared.Rent(maxSymbols * numPred); + + for (int pred = 0; pred < numPred; pred++) + { + int extraBits = 0; + List rtokens = treeSamples.GetResidualTokensForPrediction(pred); + + for (int i = begin; i < end; i++) + { + JxlResidualToken rt = rtokens[i]; + + int count = treeSamples.GetCount(i); + int eb = rt.NumberOfBits * count; + + counts[(pred * maxSymbols) + rt.Token] += count; + extraBits += eb; + } + + totalExtraBits[pred] = extraBits; + } + + float baseBits = 0; + { + int pred = treeSamples.FindPredictorIndex(tree[pos].Predictor); + baseBits = EstimateBits(counts.AsSpan(pred * maxSymbols), maxSymbols) + totalExtraBits[pred]; + } + + ref SplitInfo best = ref bestSplitNonStatic; + + SplitInfo forcedSplit = default; + + foreach (JxlModularMultiplierInfo mmi in mulInfo) + { + int axis = 0; + int val = 0; + IntersectionType t = BoxIntersects(staticPropertyRange, mmi.Range, ref axis, ref val); + + if (t == IntersectionType.None) + { + continue; + } + + if (t == IntersectionType.Inside) + { + CollectionsMarshal.AsSpan(tree)[pos].Multiplier = (int)mmi.Multiplier; + break; + } + + if (t == IntersectionType.Partial) + { + forcedSplit.Value = treeSamples.QuantizeStaticProperty(axis, val); + forcedSplit.Property = axis; + forcedSplit.LeftCost = forcedSplit.RightCost = (baseBits / 2) - threshold; + forcedSplit.LeftPredictor = forcedSplit.RightPredictor = CollectionsMarshal.AsSpan(tree)[pos].Predictor; + best = ref forcedSplit; + best.Position = begin; + + if (best.Property != treeSamples.PropertyFromIndex(best.Property)) + { + throw new InvalidOperationException("Invalid property"); + } + + if (best.Property < treeSamples.NumberOfStaticProperties) + { + for (int x = begin; x < end; x++) + { + if (treeSamples.GetProperty(true, best.Property, x) <= best.Value) + { + best.Position++; + } + } + } + else + { + int prop = best.Property - treeSamples.NumberOfStaticProperties; + + for (int x = begin; x < end; x++) + { + if (treeSamples.GetProperty(false, prop, x) <= best.Value) + { + best.Position++; + } + } + } + + break; + } + } + + if (!Unsafe.AreSame(ref best, ref forcedSplit)) + { + List countsIncrease = []; + List extraBitsIncrease = []; + + List leftCosts = []; + List rightCosts = []; + + int[] aboveCounts = ArrayPool.Shared.Rent(maxSymbols); + int[] belowCounts = ArrayPool.Shared.Rent(maxSymbols); + + float changePredictionPenalty = 800.0f / (100.0f + threshold); + + for (int prop = 0; prop < numProp && baseBits > threshold; prop++) + { + leftCosts.Clear(); + rightCosts.Clear(); + + int propertySize = treeSamples.CountPropertyValues(prop); + + if (extraBitsIncrease.Count < propertySize) + { + countsIncrease.Grow(0, propertySize * maxSymbols); + extraBitsIncrease.Grow(0, propertySize); + } + + int[] propertyValueUsedCount = ArrayPool.Shared.Rent(propertySize); + propertyValueUsedCount.AsSpan().Clear(); + + int firstUsed = propertySize; + int lastUsed = 0; + + if (prop < treeSamples.NumberOfStaticProperties) + { + for (int i = begin; i < end; i++) + { + int p = treeSamples.GetProperty(true, prop, i); + propertyValueUsedCount[p]++; + lastUsed = Math.Max(lastUsed, p); + firstUsed = Math.Max(firstUsed, p); + } + } + else + { + int prop_idx = prop - treeSamples.NumberOfStaticProperties; + + for (int i = begin; i < end; i++) + { + int p = treeSamples.GetProperty(false, prop_idx, i); + propertyValueUsedCount[p]++; + lastUsed = Math.Max(lastUsed, p); + firstUsed = Math.Max(firstUsed, p); + } + } + + leftCosts.Grow(default, lastUsed - firstUsed); + rightCosts.Grow(default, lastUsed - firstUsed); + + for (int pred = 0; pred < numPred; pred++) + { + List rtokens = treeSamples.GetResidualTokensForPrediction(pred); + + if (prop < treeSamples.NumberOfStaticProperties) + { + CollectExtraBitsIncrease(true, treeSamples, rtokens, countsIncrease, extraBitsIncrease, begin, end, prop, maxSymbols); + } + else + { + CollectExtraBitsIncrease(false, treeSamples, rtokens, countsIncrease, extraBitsIncrease, begin, end, prop - treeSamples.NumberOfStaticProperties, maxSymbols); + } + + counts.AsSpan().Slice(pred * maxSymbols, maxSymbols).CopyTo(aboveCounts); + belowCounts.AsSpan().Slice(0, maxSymbols).Clear(); + + int extraBitsBelow = 0; + + for (int i = firstUsed; i < lastUsed; i++) + { + if (propertyValueUsedCount[i] == 0) + { + continue; + } + + extraBitsBelow += extraBitsIncrease[i]; + extraBitsIncrease[i] = 0; + + for (int sym = 0; sym < maxSymbols; sym++) + { + aboveCounts[sym] -= countsIncrease[(i * maxSymbols) + sym]; + belowCounts[sym] += countsIncrease[(i * maxSymbols) + sym]; + countsIncrease[(i * maxSymbols) + sym] = 0; + } + + float rightCost = EstimateBits(aboveCounts.AsSpan(), maxSymbols) + totalExtraBits[pred] - extraBitsBelow; + float leftCost = EstimateBits(belowCounts.AsSpan(), maxSymbols) + extraBitsBelow; + + if (extraBitsBelow > totalExtraBits[pred]) + { + throw new InvalidOperationException("Too many extra bits"); + } + + float penalty = 0; + + if (treeSamples.PredictorFromIndex(pred) != tree[pos].Predictor && + tree[pos].Predictor != JxlPredictor.Weighted) + { + penalty = changePredictionPenalty; + } + + if (treeSamples.PredictorFromIndex(pred) == JxlPredictor.Weighted) + { + penalty += 1e-8f; + } + + if (treeSamples.PredictorFromIndex(pred) == JxlPredictor.Zero) + { + penalty -= 1e-8f; + } + + if (rightCost + penalty < rightCosts[i - firstUsed].TotalCost) + { + CostInfo cost = rightCosts[i - firstUsed]; + cost.Cost = rightCost; + cost.ExtraCost = penalty; + cost.Predictor = treeSamples.PredictorFromIndex(pred); + rightCosts[i - firstUsed] = cost; + } + + if (leftCost + penalty < leftCosts[i - firstUsed].TotalCost) + { + CostInfo cost = leftCosts[i - firstUsed]; + cost.Cost = leftCost; + cost.ExtraCost = penalty; + cost.Predictor = treeSamples.PredictorFromIndex(pred); + leftCosts[i - firstUsed] = cost; + } + } + } + + int split = begin; + + for (int i = firstUsed; i < lastUsed; i++) + { + if (propertyValueUsedCount[i] == 0) + { + continue; + } + + split += propertyValueUsedCount[i]; + + float rightCost = rightCosts[i - firstUsed].Cost; + float leftCost = leftCosts[i - firstUsed].Cost; + + bool usesWeightedPrediction = treeSamples.PropertyFromIndex(prop) == JxlMaConstants.WpProp || + leftCosts[i - firstUsed].Predictor == JxlPredictor.Weighted || + rightCosts[i - firstUsed].Predictor == JxlPredictor.Weighted; + + bool zeroEntropySide = rightCost == 0 || leftCost == 0; + + // Using pointer specifically for this variable instead of + // ref, as we can't assign a ref variable conditionally. + SplitInfo* referenceToBest = + treeSamples.PropertyFromIndex(prop) < JxlPredictorFacts.StaticProperties + ? (zeroEntropySide ? &bestSplitStaticConstant : &bestSplitStatic) + : (usesWeightedPrediction ? &bestSplitNonStatic : &bestSplitNoWeightedPrediction); + + if (leftCost + rightCost < referenceToBest->Cost) + { + referenceToBest->Property = prop; + referenceToBest->Value = i; + referenceToBest->Position = split; + referenceToBest->LeftCost = leftCost; + referenceToBest->LeftPredictor = leftCosts[i - firstUsed].Predictor; + referenceToBest->RightCost = rightCost; + referenceToBest->RightPredictor = rightCosts[i - firstUsed].Predictor; + } + } + + extraBitsIncrease[lastUsed] = 0; + + for (int sym = 0; sym < maxSymbols; sym++) + { + countsIncrease[(lastUsed * maxSymbols) + sym] = 0; + } + } + + if (bestSplitNoWeightedPrediction.Cost + threshold < baseBits && + bestSplitNoWeightedPrediction.Cost <= fastDecodeMultiplier * best.Cost) + { + best = ref bestSplitNoWeightedPrediction; + } + + if (bestSplitStatic.Cost + threshold < baseBits && + bestSplitStatic.Cost <= fastDecodeMultiplier * best.Cost) + { + best = ref bestSplitStatic; + } + + if (bestSplitStaticConstant.Cost + threshold < baseBits) + { + best = ref bestSplitStaticConstant; + } + + ArrayPool.Shared.Return(aboveCounts); + ArrayPool.Shared.Return(belowCounts); + } + + if (best.Cost + threshold < baseBits) + { + int p = treeSamples.PropertyFromIndex(best.Property); + int dequant = treeSamples.UnquantizeProperty(best.Property, best.Value); + + MakeSplitNode(pos, p, dequant, best.LeftPredictor, 0, best.RightPredictor, 0, tree); + + if (best.Property < treeSamples.NumberOfStaticProperties) + { + SplitTreeSamples(true, treeSamples, begin, best.Position, end, best.Property, best.Value); + } + else + { + SplitTreeSamples(false, treeSamples, begin, best.Position, end, best.Property - treeSamples.NumberOfStaticProperties, best.Value); + } + + StaticPropertyRange newStaticPropertyRange = staticPropertyRange; + + if (p < JxlPredictorFacts.StaticProperties) + { + if (dequant + 1 > newStaticPropertyRange[p][1]) + { + throw new InvalidOperationException("Dequantized coefficient is out of range"); + } + + newStaticPropertyRange[p][1] = dequant + 1; + + if (newStaticPropertyRange[p][0] >= newStaticPropertyRange[p][1]) + { + throw new InvalidOperationException("Static property is out of range"); + } + } + + nodes.Push(new(tree[pos].RightChild, begin, best.Position, newStaticPropertyRange)); + newStaticPropertyRange = staticPropertyRange; + + if (p < JxlPredictorFacts.StaticProperties) + { + if (newStaticPropertyRange[p][0] > dequant + 1) + { + throw new InvalidOperationException("Static property must be <= dequantized coefficient"); + } + + newStaticPropertyRange[p][0] = dequant + 1; + + if (newStaticPropertyRange[p][0] >= newStaticPropertyRange[p][1]) + { + throw new InvalidOperationException("Static property is out of range"); + } + } + + nodes.Push(new NodeInfo(tree[pos].LeftChild, best.Position, end, newStaticPropertyRange)); + } + + ArrayPool.Shared.Return(counts); + } + } + + public static void ComputeBestTree(JxlTreeSamples treeSamples, float threshold, List mulInfo, StaticPropertyRange staticPropertyRange, float fastDecodeMultiplier, Tree tree) + { + JxlPropertyDecisionNode node = new() + { + Property = -1, + Predictor = treeSamples.PredictorFromIndex(0), + PredictorOffset = 0, + Multiplier = 1 + }; + + tree.Add(node); + + if (treeSamples.NumberOfProperties >= 64) + { + throw new InvalidOperationException($"Too many properties: {treeSamples.NumberOfPredictors}"); + } + + FindBestSplit(treeSamples, threshold, mulInfo, staticPropertyRange, fastDecodeMultiplier, tree); + } + + public static List QuantizeHistogram(ReadOnlySpan histogram, int numChunks) + { + if (histogram.Length == 0 || numChunks == 0) + { + return []; + } + + int sum = TensorPrimitives.Sum(histogram); + + if (sum == 0) + { + return []; + } + + List thresholds = []; + + long cumulativeSum = 0; + long threshold = 1; + + for (int i = 0; i < histogram.Length; i++) + { + cumulativeSum += histogram[i]; + + if (cumulativeSum * numChunks >= threshold * sum) + { + thresholds.Add(i); + + while (cumulativeSum * numChunks >= threshold * sum) + { + threshold++; + } + } + } + + if (thresholds.Count > numChunks) + { + throw new InvalidOperationException("Too many thresholds"); + } + + thresholds.RemoveAt(thresholds.Count - 1); + + return thresholds; + } + + public static List QuantizeSamples(ReadOnlySpan samples, int numChunks) + { + const int range = 512; + + if (samples.Length == 0) + { + return []; + } + + int min = Math.Clamp(TensorPrimitives.Min(samples), -range, range); + + Span counts = stackalloc int[2048].Slice(0, (2 * range) + 1); + + for (int i = 0; i < samples.Length; i++) + { + int s = samples[i]; + int sampleOffset = Math.Clamp(s, -range, range) - min; + counts[sampleOffset]++; + } + + List thresholds = QuantizeHistogram(counts, numChunks); + + // For fast processing via TensorPrimitives + Span thresholdsSpan = CollectionsMarshal.AsSpan(thresholds); + TensorPrimitives.Add(thresholdsSpan, min, thresholdsSpan); + + return thresholds; + } + + public static void QuantMap(Span from, Span to, int numPegs, int bias) + { + int mapped = 0; + + for (int i = 0; i < numPegs; i++) + { + while (mapped < from.Length && i - bias > from[mapped]) + { + mapped++; + } + + to[i] = mapped; + } + } + + public static void CollectPixelSamples(Configuration configuration, JxlModularImage image, JxlModularOptions options, int groupId, Span groupPixelCount, Span channelPixelCount, List pixelSamples, List diffSamples) + { + if (options.NumberOfRepeats == 0) + { + throw new InvalidOperationException("No repeats"); + } + + // Power of 2 size is unknown. + Span alignedGroupPixelCount = stackalloc int[groupPixelCount.Length <= groupId ? groupId + 1 : groupPixelCount.Length]; + groupPixelCount.CopyTo(alignedGroupPixelCount); + + Span alignedChannelPixelCount = stackalloc int[channelPixelCount.Length < image.Channels.Count ? image.Channels.Count : channelPixelCount.Length]; + channelPixelCount.CopyTo(alignedGroupPixelCount); + + Rng rng = new((ulong)groupId); + + float fraction = MathF.Min(options.NumberOfRepeats * 0.1f, 0.99f); + Rng.GeometricDistribution dist = new(fraction); + + int totalPixels = 0; + List channelIds = []; + + int i; + for (i = 0; i < image.Channels.Count; i++) + { + JxlModularChannel channel = image.Channels[i]; + + if (i >= image.MetaChannels && (channel.Width > options.MaxChannelSize || channel.Height > options.MaxChannelSize)) + { + break; + } + + if (channel.Width <= 1 || channel.Height == 0) + { + continue; + } + + channelIds.Add(i); + + groupPixelCount[groupId] += channel.Width * channel.Height; + channelPixelCount[i] += channel.Width * channel.Height; + totalPixels += channel.Width * channel.Height; + } + + if (channelIds.Count == 0) + { + throw new InvalidOperationException("No channel IDs"); + } + + pixelSamples.Grow(0, (int)(pixelSamples.Count + (fraction * totalPixels))); + diffSamples.Grow(0, (int)(diffSamples.Count + (fraction * totalPixels))); + + i = 0; + int y = 0; + int x = 0; + + void Advance(uint amount) + { + x += (int)amount; + + while (x >= image.Channels[channelIds[i]].Width) + { + x -= image.Channels[channelIds[i]].Width; + y++; + + if (y == image.Channels[channelIds[i]].Height) + { + i++; + y = 0; + + if (i >= channelIds.Count) + { + return; + } + } + } + } + + Advance(rng.Geometric(in dist)); + + for (; i < channelIds.Count; Advance(rng.Geometric(in dist) + 1)) + { + Span row = image.Channels[channelIds[i]].GetRow(y); + pixelSamples.Add(row[x]); + + int xp = x == 0 ? 1 : x - 1; + diffSamples.Add(row[x] - row[xp]); + } + } + + public static void TokenizeTree(Tree tree, List tokens, Tree decoderTree) + { + if (tree.Count > JxlMaConstants.MaxTreeSize) + { + throw new InvalidOperationException("Too many tree nodes"); + } + + Queue q = []; + q.Enqueue(0); + + int leafId = 0; + decoderTree.Clear(); + + while (q.Count > 0) + { + int cur = q.Peek(); + _ = q.Dequeue(); + + if (tree[cur].Property < -1) + { + throw new InvalidOperationException("Property value is too small"); + } + + tokens.Add(new JxlToken(JxlMaTreeContext.Property, (uint)tree[cur].Property + 1)); + + if (tree[cur].Property == -1) + { + tokens.Add(new JxlToken(JxlMaTreeContext.Predictor, (uint)tree[cur].Predictor)); + tokens.Add(new JxlToken(JxlMaTreeContext.Offset, JxlPackSigned.PackUnsigned((int)tree[cur].PredictorOffset))); + + uint mulLog = JxlMath.Num0BitsBelowLS1Bit_Nonzero(tree[cur].Multiplier); + int mulBits = (tree[cur].Multiplier >> (int)mulLog) - 1; + + tokens.Add(new JxlToken(JxlMaTreeContext.MultiplierLog, mulLog)); + tokens.Add(new JxlToken(JxlMaTreeContext.MultiplierBits, (uint)mulBits)); + + if (tree[cur].Predictor >= JxlPredictor.Best) + { + throw new InvalidOperationException("Invalid predictor"); + } + + decoderTree.Add(new JxlPropertyDecisionNode(-1, 0, leafId, 0, tree[cur].Predictor, tree[cur].PredictorOffset, tree[cur].Multiplier)); + leafId++; + + continue; + } + + decoderTree.Add(new JxlPropertyDecisionNode(tree[cur].Property, tree[cur].SplitValue, decoderTree.Count + q.Count + 1, decoderTree.Count + q.Count + 2, JxlPredictor.Zero, 0, 1)); + + q.Enqueue(tree[cur].LeftChild); + q.Enqueue(tree[cur].RightChild); + + tokens.Add(new JxlToken(JxlMaTreeContext.SplitValue, JxlPackSigned.PackUnsigned(tree[cur].SplitValue))); + } + } + + private readonly record struct NodeInfo(int Pos, int Begin, int End, StaticPropertyRange StaticPropertyRange); + + private struct SplitInfo() + { + public int Property { get; set; } + + public int Value { get; set; } + + public int Position { get; set; } + + public float LeftCost { get; set; } = float.MaxValue; + + public float RightCost { get; set; } = float.MaxValue; + + public JxlPredictor LeftPredictor { get; set; } = JxlPredictor.Zero; + + public JxlPredictor RightPredictor { get; set; } = JxlPredictor.Zero; + + public readonly float Cost => this.LeftCost + this.RightCost; + } + + private struct CostInfo() + { + public float Cost { get; set; } = float.MaxValue; + + public float ExtraCost { get; set; } + + public JxlPredictor Predictor { get; set; } + + public readonly float TotalCost => this.Cost + this.ExtraCost; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaTreeContext.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaTreeContext.cs index 2c06e80ba7..8c04283516 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaTreeContext.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaTreeContext.cs @@ -3,7 +3,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding; -internal enum JxlMaTreeContext +internal enum JxlMaTreeContext : byte { SplitValue, diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlResidualToken.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlResidualToken.cs new file mode 100644 index 0000000000..8a8b387200 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlResidualToken.cs @@ -0,0 +1,16 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding; + +internal struct JxlResidualToken +{ + public int Token; + public int NumberOfBits; + + public JxlResidualToken(int token, int numberOfBits) + { + this.Token = token; + this.NumberOfBits = numberOfBits; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlTreeSamples.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlTreeSamples.cs new file mode 100644 index 0000000000..1d25122266 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlTreeSamples.cs @@ -0,0 +1,884 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics.Tensors; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using StaticPropertyRange = System.Runtime.CompilerServices.InlineArray2>; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding; + +/// +/// Contains all the necessary data needed to build a tree. +/// +internal sealed class JxlTreeSamples +{ + /// + /// This deduplication table entry marks an unused entry. + /// + private const int DeduplicationEntryUnused = -1; + + private const int PropertyRange = 511; + + /// + /// Residual information: token and number of extra bits per predictor. + /// + private List> residuals = []; + + /// + /// Number of occurrences of each sample. + /// + private readonly List sampleCounts = []; + + /// + /// Quantized static property values. + /// + private InlineArray2> staticProperties; + + /// + /// Property values, quantized to at most 256 distinct values. + /// + private readonly List> properties = []; + + /// + /// Decompactification info for . + /// + private readonly List> compactProperties = []; + + /// + /// List of properties to use. + /// + private List propertiesToUse = []; + + /// + /// List of predictors to use. + /// + private List predictors = []; + + /// + /// Mapping property value -> quantized property value. + /// + private InlineArray2> staticPropertyMapping; + + private readonly List> propertyMapping = []; + + /// + /// Table for deduplication. + /// + private List deduplicationTable = []; + + /// + /// Gets a value indicating whether there are any residual samples. + /// + public bool HasSamples => this.residuals.Count > 0 && this.residuals[0].Count > 0; + + /// + /// Gets the total number of distinct samples. + /// + public int NumberOfDistinctSamples => this.sampleCounts.Count; + + /// + /// Gets the total number of samples. + /// + public int NumberOfSamples { get; private set; } + + /// + /// Gets the number of quantized static property values. + /// + public int NumberOfStaticProperties { get; private set; } + + /// + /// Gets the total number of predictors. + /// + public int NumberOfPredictors => this.predictors.Count; + + /// + /// Gets the total number of properties. + /// + public int NumberOfProperties => this.propertiesToUse.Count; + + /// + /// Returns a List of residual tokens for the specified kind of prediction. + /// + /// The type of prediction. + /// Residual tokens corresponding to the prediction specified by the parameter . + public List GetResidualTokensForPrediction(int prediction) => this.residuals[prediction]; + + /// + /// Returns a reference to the residual token. + /// + /// Kind of prediction. + /// Index of the residual token within that prediction. + /// The residual token for prediction indexed . + public ref JxlResidualToken GetResidualToken(int pred, int i) => ref CollectionsMarshal.AsSpan(this.residuals[pred])[i]; + + /// + /// Returns a token of the residual for prediction index . + /// + /// The kind of prediction. + /// Index of the residual token within that prediction. + /// For residual token whose prediction is and index is , returns its token coefficient. + public int GetToken(int prediction, int index) => this.residuals[prediction][index].Token; + + /// + /// Returns the number of occurrences for sample . + /// + /// The index of the sample. + /// Number of times appears. + public int GetCount(int i) => this.sampleCounts[i]; + + /// + /// Finds the index of the predictor . + /// + /// The predictor to find the index for. + /// Index of the predictor in the predictors storage. + /// Thrown when the predictor can't be found. + public int FindPredictorIndex(JxlPredictor predictor) + { + ReadOnlySpan span = CollectionsMarshal.AsSpan(this.predictors); + + int index = span.IndexOf(predictor); + + if (index < 0) + { + // Should not happen. + throw new InvalidOperationException("Cannot find the index of the predictor"); + } + + return index; + } + + /// + /// Finds the index of the property . + /// + /// The property to find the index for. + /// Index of the property in the properties storage. + /// Thrown when the property isn't valid. + public int FindPropertyIndex(int property) + { + ReadOnlySpan span = CollectionsMarshal.AsSpan(this.propertiesToUse); + + int index = span.IndexOf(property); + + if (index != this.propertiesToUse[^1]) + { + // Should not happen. + throw new InvalidOperationException("Invalid property"); + } + + return index; + } + + /// + /// Returns the number of properties for a . + /// + /// Index of the property. + /// Number of property values for property with index . + public int CountPropertyValues(int propertyIndex) => this.compactProperties[propertyIndex].Count + 1; + + /// + /// Returns the value of a property. + /// + /// Prefer a static property? + /// The index of the properties table. + /// The index of the property. + /// + /// Property for index within table . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetProperty(bool useStaticProperty, int propertyIndex, int i) + { + if (useStaticProperty) + { + return this.staticProperties[propertyIndex][i]; + } + else + { + return this.properties[propertyIndex][i]; + } + } + + /// + /// Returns the dequantized property. + /// + /// Index of the target property. + /// Property quantizer. + /// The dequantized property. + /// Thrown when the quant is out of range. + public int UnquantizeProperty(int propertyIndex, int quant) + { + List compactProperties = this.compactProperties[propertyIndex]; + + if (quant >= compactProperties.Count) + { + throw new InvalidOperationException("Quant is out of range"); + } + + return compactProperties[quant]; + } + + /// + /// Returns a predictor for index . + /// + /// The index of the predictor. + /// Predictor at index . + public JxlPredictor PredictorFromIndex(int index) + { + DebugGuard.MustBeLessThan(index, this.predictors.Count, nameof(index)); + + return this.predictors[index]; + } + + /// + /// Returns a property for index . + /// + /// The index of the property. + /// Property at index . + public int PropertyFromIndex(int index) + { + DebugGuard.MustBeLessThan(index, this.propertiesToUse.Count, nameof(index)); + + return this.propertiesToUse[index]; + } + + /// + /// Invoked after processing samples completed. + /// + public void AllSamplesDone() => this.deduplicationTable = []; + + /// + /// Returns the quantized property value of . + /// + /// Index of the property. + /// Value to quantize. + /// Quantized value. + public int QuantizeProperty(int property, int v) + { + DebugGuard.MustBeGreaterThanOrEqualTo(property, this.NumberOfStaticProperties, nameof(property)); + + v = Math.Clamp(v, -PropertyRange, PropertyRange) + PropertyRange; + + return this.propertyMapping[property][v]; + } + + /// + /// Returns the quantized static property value of . + /// + /// Index of the static property. + /// Value to quantize. + /// Quantized value. + public int QuantizeStaticProperty(int property, int v) + { + DebugGuard.MustBeGreaterThanOrEqualTo(property, this.NumberOfStaticProperties, nameof(property)); + + v = Math.Clamp(v, -PropertyRange, PropertyRange) + PropertyRange; + + return this.staticPropertyMapping[property][v]; + } + + public void SetPredictor(JxlPredictor predictor, JxlTreeMode wpTreeMode) + { + if (wpTreeMode == JxlTreeMode.WpOnly) + { + this.predictors = [JxlPredictor.Weighted]; + + // equivalent of residuals.resize(1) + if (this.residuals.Count > 1) + { + this.residuals = [this.residuals[0]]; + } + else if (this.residuals.Count < 1) + { + this.residuals = [[]]; + } + + return; + } + + if (wpTreeMode == JxlTreeMode.NoWp && predictor == JxlPredictor.Weighted) + { + throw new InvalidOperationException("Invalid predictor settings"); + } + + if (predictor == JxlPredictor.Variable) + { + for (int i = 0; i < JxlPredictorFacts.ModularPredictors; i++) + { + this.predictors.Add((JxlPredictor)i); + } + + Span predictorsSpan = CollectionsMarshal.AsSpan(this.predictors); + RuntimeUtility.Swap(ref predictorsSpan[0], ref predictorsSpan[(int)JxlPredictor.Weighted]); + RuntimeUtility.Swap(ref predictorsSpan[1], ref predictorsSpan[(int)JxlPredictor.Gradient]); + } + else if (predictor == JxlPredictor.Best) + { + this.predictors = [JxlPredictor.Weighted, JxlPredictor.Gradient]; + } + else + { + this.predictors = [predictor]; + } + + if (wpTreeMode == JxlTreeMode.NoWp) + { + // delete all weighted predictors + _ = this.predictors.RemoveAll(p => p == JxlPredictor.Weighted); + } + + this.residuals.Grow([], this.predictors.Count); + } + + public void SetProperties(List properties, JxlTreeMode wpTreeMode) + { + this.propertiesToUse = properties; + + if (wpTreeMode == JxlTreeMode.WpOnly) + { + this.propertiesToUse = [JxlMaConstants.WpProp]; + } + else if (wpTreeMode == JxlTreeMode.GradientOnly) + { + this.propertiesToUse = [JxlMaConstants.GradientProp]; + } + else if (wpTreeMode == JxlTreeMode.NoWp) + { + // delete all weighted properties, tree mode + // says "no weighted predictors" + _ = this.propertiesToUse.RemoveAll(x => x == JxlMaConstants.WpProp); + } + + if (this.propertiesToUse.Count == 0) + { + // could happen in default tree mode or when properties parameter + // is empty + throw new InvalidOperationException("Invalid property set configuration"); + } + + this.NumberOfStaticProperties = 0; + + for (int i = 0; i < this.propertiesToUse.Count; ++i) + { + int prop = this.propertiesToUse[i]; + + if (prop < JxlPredictorFacts.StaticProperties) + { + if (i != prop) + { + throw new InvalidOperationException("Index is not equal to the property"); + } + + this.NumberOfStaticProperties++; + } + } + + this.properties.Resize(this.propertiesToUse.Count - this.NumberOfStaticProperties); + } + + public void InitializeTable(int logSize) + { + int size = 1 << logSize; + + if (this.deduplicationTable.Count == size) + { + return; + } + + this.deduplicationTable.Resize(size, DeduplicationEntryUnused); + + for (int i = 0; i < this.NumberOfDistinctSamples; i++) + { + if (this.sampleCounts[i] != ushort.MaxValue) + { + this.AddToTable(i); + } + } + } + + public void AddToTableAndMerge(int a) + { + int pos1 = Hash1(a); + int pos2 = Hash2(a); + + if (this.deduplicationTable[pos1] != DeduplicationEntryUnused && this.IsSameSample(a, this.deduplicationTable[pos1])) + { + if (this.sampleCounts[a] != 1) + { + throw new InvalidOperationException("Sample count must be 1"); + } + + this.sampleCounts[this.deduplicationTable[pos1]]++; + + if (this.sampleCounts[this.deduplicationTable[pos1]] == ushort.MaxValue) + { + this.deduplicationTable[pos1] = DeduplicationEntryUnused; + } + + return; + } + + if (this.deduplicationTable[pos2] != DeduplicationEntryUnused && this.IsSameSample(a, this.deduplicationTable[pos2])) + { + if (this.sampleCounts[a] != 1) + { + throw new InvalidOperationException("Sample count must be 1"); + } + + this.sampleCounts[this.deduplicationTable[pos2]]++; + + if (this.sampleCounts[this.deduplicationTable[pos2]] == ushort.MaxValue) + { + this.deduplicationTable[pos2] = DeduplicationEntryUnused; + } + + return; + } + + this.AddToTable(a); + } + + public void AddToTable(int a) + { + int pos1 = Hash1(a); + int pos2 = Hash2(a); + + if (this.deduplicationTable[pos1] == DeduplicationEntryUnused) + { + this.deduplicationTable[pos1] = a; + } + else if (this.deduplicationTable[pos2] == DeduplicationEntryUnused) + { + this.deduplicationTable[pos2] = a; + } + } + + public void PrepareForSamples(int extraNumSamples) + { + foreach (List residual in this.residuals) + { + residual.Grow(default, residual.Count + extraNumSamples); + } + + for (int i = 0; i < this.NumberOfStaticProperties; i++) + { + this.staticProperties[i].Grow(0, this.staticProperties[i].Count + extraNumSamples); + } + + foreach (List prop in this.properties) + { + prop.Grow((byte)0, prop.Count + extraNumSamples); + } + + int totalNumSamples = extraNumSamples + this.sampleCounts.Count; + int nextSize = JxlMath.CeilLog2Nonzero(totalNumSamples * 3 / 2); + + this.InitializeTable(nextSize); + } + + public int Hash1(int a) + { + const ulong constant = 0x1e35a7bd; + + ulong h = constant; + + foreach (List r in this.residuals) + { + h = (h * constant) + (ulong)r[a].Token; + h = (h * constant) + (ulong)r[a].NumberOfBits; + } + + for (int i = 0; i < this.NumberOfStaticProperties; i++) + { + h = (h * constant) + (ulong)this.staticProperties[i][a]; + } + + foreach (List property in this.properties) + { + h = (h * constant) + property[a]; + } + + return (int)((h >> 16) & (ulong)(this.deduplicationTable.Count - 1)); + } + + public int Hash2(int a) + { + const ulong constant = 0x1e35a7bd1e35a7bd; + + ulong h = constant; + + for (int i = 0; i < this.NumberOfStaticProperties; i++) + { + h = (h * constant) ^ (ulong)this.staticProperties[i][a]; + } + + foreach (List property in this.properties) + { + h = (h * constant) ^ property[a]; + } + + foreach (List r in this.residuals) + { + h = (h * constant) ^ (ulong)r[a].Token; + h = (h * constant) ^ (ulong)r[a].NumberOfBits; + } + + return (int)((h >> 16) & (ulong)(this.deduplicationTable.Count - 1)); + } + + public bool IsSameSample(int a, int b) + { + foreach (List r in this.residuals) + { + if (r[a].Token != r[b].Token) + { + return false; + } + + if (r[a].NumberOfBits != r[b].NumberOfBits) + { + return false; + } + } + + for (int i = 0; i < this.NumberOfStaticProperties; ++i) + { + if (this.staticProperties[i][a] != this.staticProperties[i][b]) + { + return false; + } + } + + foreach (List p in this.properties) + { + if (p[a] != p[b]) + { + return false; + } + } + + return false; + } + + public void AddSample(int pixel, Span properties, Span predictions) + { + for (int i = 0; i < this.predictors.Count; i++) + { + int v = pixel - predictions[(int)this.predictors[i]]; + new JxlAnsHybridUIntConfiguration(4, 1, 2).Encode(JxlPackSigned.PackUnsigned(v), out uint tok, out uint nbits, out uint bits); + + if (tok >= 256) + { + throw new InvalidOperationException("Token is too large"); + } + + if (nbits >= 256) + { + throw new InvalidOperationException("Number of bits is too large"); + } + + JxlResidualToken token = new((int)tok, (int)nbits); + this.residuals[i].Add(token); + } + + for (int i = 0; i < this.NumberOfStaticProperties; ++i) + { + this.staticProperties[i].Add(this.QuantizeStaticProperty(i, properties[i])); + } + + for (int i = this.NumberOfStaticProperties; i < this.propertiesToUse.Count; i++) + { + this.properties[i - this.NumberOfStaticProperties].Add(unchecked((byte)this.QuantizeProperty(i, properties[this.propertiesToUse[i]]))); + } + + this.sampleCounts.Add(1); + this.NumberOfSamples++; + + this.AddToTableAndMerge(this.sampleCounts.Count - 1); + + foreach (List residual in this.residuals) + { + // remove last item from List + residual.RemoveAt(residual.Count - 1); + } + + for (int i = 0; i < this.NumberOfStaticProperties; i++) + { + // ditto + this.staticProperties[i].RemoveAt(this.staticProperties[i].Count - 1); + } + + foreach (List property in this.properties) + { + // ditto + property.RemoveAt(property.Count - 1); + } + + // ditto + this.sampleCounts.RemoveAt(this.sampleCounts.Count - 1); + } + + public void Swap(int a, int b) + { + if (a == b) + { + return; + } + + foreach (List r in this.residuals) + { + // Get a Span for this List so we can get a ref to its items + // for use in RuntimeUtility.Swap (tuple-swap is slightly slower) + Span sp = CollectionsMarshal.AsSpan(r); + + RuntimeUtility.Swap(ref sp[a], ref sp[b]); + } + + for (int i = 0; i < this.NumberOfStaticProperties; i++) + { + // Ditto + Span sp = CollectionsMarshal.AsSpan(this.staticProperties[i]); + + RuntimeUtility.Swap(ref sp[a], ref sp[b]); + } + + foreach (List p in this.properties) + { + // Ditto + Span sp = CollectionsMarshal.AsSpan(p); + + RuntimeUtility.Swap(ref sp[a], ref sp[b]); + } + + // Ditto + Span sampleCounts = CollectionsMarshal.AsSpan(this.sampleCounts); + + RuntimeUtility.Swap(ref sampleCounts[a], ref sampleCounts[b]); + } + + public void PreQuantizeProperties( + Configuration configuration, + StaticPropertyRange range, + List multiplierInfo, + List groupPixelCount, + List channelPixelCount, + List pixelSamples, + List diffSamples, + int maxPropertyValues) + { + List groupMultiplierThresholds = []; + List channelMultiplierThresholds = []; + + foreach (JxlModularMultiplierInfo v in multiplierInfo) + { + if (v.Range[0][0] != range[0][0]) + { + channelMultiplierThresholds.Add(v.Range[0][0] - 1); + } + + if (v.Range[0][1] != range[0][1]) + { + channelMultiplierThresholds.Add(v.Range[0][1] - 1); + } + + if (v.Range[1][0] != range[1][0]) + { + groupMultiplierThresholds.Add(v.Range[1][0] - 1); + } + + if (v.Range[1][1] != range[1][1]) + { + groupMultiplierThresholds.Add(v.Range[1][1] - 1); + } + } + + channelMultiplierThresholds.Sort(); + channelMultiplierThresholds.Resize(0, channelMultiplierThresholds.Distinct().Count()); + groupMultiplierThresholds.Sort(); + groupMultiplierThresholds.Resize(0, groupMultiplierThresholds.Distinct().Count()); + + this.compactProperties.Resize([], this.propertiesToUse.Count); + + List QuantizeChannel() + { + if (channelMultiplierThresholds.Count > 0) + { + return channelMultiplierThresholds; + } + + return JxlMaEncoder.QuantizeHistogram( + CollectionsMarshal.AsSpan(groupPixelCount), + maxPropertyValues); + } + + List QuantizeGroupId() + { + if (groupMultiplierThresholds.Count > 0) + { + return groupMultiplierThresholds; + } + + return JxlMaEncoder.QuantizeHistogram( + CollectionsMarshal.AsSpan(groupPixelCount), + maxPropertyValues); + } + + List QuantizeCoordinate() + { + List quantized = new(maxPropertyValues - 1); + + for (int i = 0; i + 1 < maxPropertyValues; i++) + { + quantized[i] = ((i + 1) * 256 / maxPropertyValues) - 1; + } + + return quantized; + } + + List absPixelThresholds = []; + List pixelThresholds = []; + + List QuantizePixelProperty() + { + if (pixelThresholds.Count == 0) + { + pixelThresholds = JxlMaEncoder.QuantizeSamples( + CollectionsMarshal.AsSpan(pixelSamples), + maxPropertyValues); + } + + return pixelThresholds; + } + + List QuantizeAbsolutePixelProperty() + { + if (absPixelThresholds.Count == 0) + { + _ = QuantizePixelProperty(); // compute the non-abs thresholds + + Span pixelSamplesSpan = CollectionsMarshal.AsSpan(pixelSamples); + TensorPrimitives.Abs(pixelSamplesSpan, pixelSamplesSpan); + + absPixelThresholds = JxlMaEncoder.QuantizeSamples(pixelSamplesSpan, maxPropertyValues); + } + + return absPixelThresholds; + } + + List absoluteDiffThresholds = []; + List diffThresholds = []; + + List QuantizeDiffProperty() + { + if (diffThresholds.Count == 0) + { + diffThresholds = JxlMaEncoder.QuantizeSamples( + CollectionsMarshal.AsSpan(diffSamples), + maxPropertyValues); + } + + return diffThresholds; + } + + List QuantizeAbsoluteDiffProperty() + { + if (absoluteDiffThresholds.Count == 0) + { + _ = QuantizeDiffProperty(); + + Span diffSamplesSpan = CollectionsMarshal.AsSpan(diffSamples); + TensorPrimitives.Abs(diffSamplesSpan, diffSamplesSpan); + + absoluteDiffThresholds = JxlMaEncoder.QuantizeSamples(diffSamplesSpan, maxPropertyValues); + } + + return absoluteDiffThresholds; + } + + List QuantizeWeightedPrediction() + { + // TODO: static ReadOnlySpan ... => [...]? + if (maxPropertyValues < 32) + { + return [-127, -63, -31, -15, -7, -3, -1, 0, 1, + 3, 7, 15, 31, 63, 127]; + } + else if (maxPropertyValues < 64) + { + return [-255, -191, -127, -95, -63, -47, -31, -23, + -15, -11, -7, -5, -3, -1, 0, 1, + 3, 5, 7, 11, 15, 23, 31, 47, + 63, 95, 127, 191, 255]; + } + else + { + return [-255, -223, -191, -159, -127, -111, -95, -79, -63, -55, -47, + -39, -31, -27, -23, -19, -15, -13, -11, -9, -7, -6, + -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, + 6, 7, 9, 11, 13, 15, 19, 23, 27, 31, 39, + 47, 55, 63, 79, 95, 111, 127, 159, 191, 223, 255]; + } + } + + this.propertyMapping.Resize(0, this.propertiesToUse.Count - this.NumberOfStaticProperties); + + for (int i = 0; i < this.propertiesToUse.Count; i++) + { + if (this.propertiesToUse[i] == 0) + { + this.compactProperties[i] = QuantizeChannel(); + } + else if (this.propertiesToUse[i] == 1) + { + this.compactProperties[i] = QuantizeGroupId(); + } + else if (this.propertiesToUse[i] is 2 or 3) + { + this.compactProperties[i] = QuantizeCoordinate(); + } + else if (this.propertiesToUse[i] == 6 || this.propertiesToUse[i] == 7 || + this.propertiesToUse[i] == 8 || + (this.propertiesToUse[i] >= JxlMaConstants.NumNonrefProperties && (this.propertiesToUse[i] - JxlMaConstants.NumNonrefProperties) % 4 == 1)) + { + this.compactProperties[i] = QuantizePixelProperty(); + } + else if (this.propertiesToUse[i] == 4 || this.propertiesToUse[i] == 5 || + (this.propertiesToUse[i] >= JxlMaConstants.NumNonrefProperties && (this.propertiesToUse[i] - JxlMaConstants.NumNonrefProperties) % 4 == 0)) + { + this.compactProperties[i] = QuantizeAbsolutePixelProperty(); + } + else if (this.propertiesToUse[i] >= JxlMaConstants.NumNonrefProperties && (this.propertiesToUse[i] - JxlMaConstants.NumNonrefProperties) % 4 == 2) + { + this.compactProperties[i] = QuantizeAbsoluteDiffProperty(); + } + else if (this.propertiesToUse[i] == JxlMaConstants.WpProp) + { + this.compactProperties[i] = QuantizeWeightedPrediction(); + } + else + { + this.compactProperties[i] = QuantizeDiffProperty(); + } + + if (i < this.NumberOfStaticProperties) + { + JxlMaEncoder.QuantMap( + CollectionsMarshal.AsSpan(this.compactProperties[i]), + CollectionsMarshal.AsSpan(this.staticPropertyMapping[i]), + (PropertyRange * 2) + 1, + PropertyRange); + } + else + { + JxlMaEncoder.QuantMap( + CollectionsMarshal.AsSpan(this.compactProperties[i]), + CollectionsMarshal.AsSpan(this.propertyMapping[i - this.NumberOfStaticProperties]), + (PropertyRange * 2) + 1, + PropertyRange); + } + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ListUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ListUtils.cs new file mode 100644 index 0000000000..c203fc66a0 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ListUtils.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding; + +internal static class ListUtils +{ + public static void Grow(this List list, T item, int desiredSize) + { + while (list.Count < desiredSize) + { + list.Add(item); + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/README.md b/src/ImageSharp/Formats/Jxl/Processing/Modular/README.md new file mode 100644 index 0000000000..3339605a40 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/README.md @@ -0,0 +1,15 @@ +# Implementation of Modular coding mode for JPEG XL +JPEG XL files typically consist of two distinct coding modes: VarDCT +and Modular. VarDCT is lossy and transform-based, while Modular uses +the predictive coding approach and can achieve lossless or near-lossless +compression. + +This folder has the implementation of the Modular encoder/decoder. + +### Folder structure +In the Transforms folder, there are implementations for the Reversible +Color Transform (RCT), Squeeze Transform and Palette, both inverse +and forward. + +In the Encoding folder there are predictors using neighbors, averages, +weighted prediction and tree-based prediction. diff --git a/src/ImageSharp/Formats/Jxl/Processing/Primitives/Rng.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/Rng.cs new file mode 100644 index 0000000000..e11f243f8b --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/Rng.cs @@ -0,0 +1,87 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + +/// +/// Deterministic random number generator used for compatibility +/// with JPEG XL. +/// +internal struct Rng +{ + private ulong s0; + private ulong s1; + + public Rng(ulong seed) + { + this.s0 = 0x94D049BB133111EBUL; + this.s1 = 0xBF58476D1CE4E5B9UL + seed; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ulong Next() + { + ulong s1 = this.s0; + ulong s0 = this.s1; + ulong bits = s1 + s0; + this.s0 = s0; + + s1 ^= s1 << 23; + s1 ^= s0 ^ (s1 >> 18) ^ (s0 >> 5); + + this.s1 = s1; + + return bits; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public long UniformI(long begin, long end) => (long)(this.Next() % (ulong)(end - begin)) + begin; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ulong UniformU(ulong begin, ulong end) => (this.Next() % (end - begin)) + begin; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float UniformF(float begin, float end) + { + uint u = (uint)(this.Next() >> (64 - 23)) | 0x3F800000u; + float f = BitConverter.UInt32BitsToSingle(u); + + return ((end - begin) * (f - 1.0f)) + begin; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Bernoulli(float p) => this.UniformF(0, 1) < p; + + internal readonly struct GeometricDistribution + { + public readonly float Value { get; } + + public GeometricDistribution(float value) => this.Value = value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static GeometricDistribution Make(float p) => new(1.0f / MathF.Log(1.0f - p)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint Geometric(in GeometricDistribution dist) + { + float f = this.UniformF(0, 1); + float invLog1mp = dist.Value; + + float log = MathF.Log(1.0f - f) * invLog1mp; + + return (uint)log; + } + + public void Shuffle(Span span) + { + for (nuint i = 0; i + 1 < (nuint)span.Length; i++) + { + nuint a = (nuint)this.UniformU(i, (nuint)span.Length); + RuntimeUtility.Swap(ref span[(int)a], ref span[(int)i]); + } + } +} From d658dede5198d276ecfabacddb82e45f2134681a Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sun, 6 Sep 2026 23:38:22 +0400 Subject: [PATCH 145/328] Add modular encoder --- .../ContextPrediction/JxlContextPrediction.cs | 2 +- .../ContextPrediction/JxlModularHeader.cs | 5 - .../ContextPrediction/JxlPredictorFacts.cs | 5 + .../Modular/Encoding/JxlGroupHeader.cs | 4 +- .../Encoding/JxlModularEncodingEncoder.cs | 865 ++++++++++++++++++ 5 files changed, 872 insertions(+), 9 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncodingEncoder.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlContextPrediction.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlContextPrediction.cs index caeb0e68c6..b2add731ad 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlContextPrediction.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlContextPrediction.cs @@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPr /// internal static class JxlContextPrediction { - private const int ExtraPropertiesPerChannel = 4; + public const int ExtraPropertiesPerChannel = 4; public const int NumberOfProperties = 1; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularHeader.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularHeader.cs index 04a50fe270..bda4751e83 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularHeader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularHeader.cs @@ -23,11 +23,6 @@ internal sealed class JxlModularHeader : IJxlFields private int p3Ce; private InlineArray4 w; - /// - /// Initializes a new instance of the class. - /// - public JxlModularHeader() => JxlBundle.Init(this); - /// /// Gets or sets a value indicating whether all values are default. /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlPredictorFacts.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlPredictorFacts.cs index 5a360af13c..797c95d68c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlPredictorFacts.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlPredictorFacts.cs @@ -13,6 +13,11 @@ internal static class JxlPredictorFacts /// public const int ModularPredictors = (int)JxlPredictor.Average4 + 1; + /// + /// Nearest power of 2 of modular predictors. + /// + public const int ModularPredictorsAlignment = 16; + /// /// Number of modular encoder predictors. /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlGroupHeader.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlGroupHeader.cs index e702708258..e9d11b036f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlGroupHeader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlGroupHeader.cs @@ -12,8 +12,6 @@ internal sealed class JxlGroupHeader : IJxlFields { private bool useGlobalTree; - public JxlGroupHeader() => JxlBundle.Init(this); - public bool UseGlobalTree { get => this.useGlobalTree; @@ -22,7 +20,7 @@ public bool UseGlobalTree internal JxlModularHeader WeightedHeader { get; set; } = new(); - internal List Transforms { get; private set; } = []; + internal List Transforms { get; set; } = []; public bool Visit(JxlVisitor visitor) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncodingEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncodingEncoder.cs new file mode 100644 index 0000000000..6eeb246501 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncodingEncoder.cs @@ -0,0 +1,865 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using FlatTree = System.Collections.Generic.List; +using StaticPropertyRange = System.Runtime.CompilerServices.InlineArray2>; +using Tree = System.Collections.Generic.List; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding; + +/// +/// Modular-based coding encoder. +/// +internal static class JxlModularEncodingEncoder +{ + public static InlineArray3 PredictorColor(JxlPredictor p) + { + InlineArray3 result = default; + + switch (p) + { + case JxlPredictor.Zero: + break; // result is all 0 by default + + case JxlPredictor.Left: + result[0] = 255; // [255, 0, 0] + break; + + case JxlPredictor.Top: + result[1] = 255; // [0, 255, 0] + break; + + case JxlPredictor.Average0: + result[2] = 255; // [0, 0, 255] + break; + + case JxlPredictor.Average4: + // [192, 128, 128] + result[0] = 192; + result[1] = 128; + result[2] = 128; + break; + + case JxlPredictor.Select: + // [255, 255, 0] + result[0] = 255; + result[1] = 255; + break; + + case JxlPredictor.Gradient: + // [255, 0, 255] + result[1] = 255; + result[2] = 255; + break; + + case JxlPredictor.Weighted: + // [0, 255, 255] + result[1] = 255; + result[2] = 255; + break; + + default: + // [255, 255, 255] + result[0] = 255; + result[1] = 255; + result[2] = 255; + break; + } + + return result; + } + + public static Tree MakeFixedTree(int property, Span cutoffs, JxlPredictor pred, int numPixels, int bitDepth) + { + int logPx = JxlMath.CeilLog2Nonzero(numPixels); + int minGap = 0; + + if (logPx < 14) + { + minGap = 8 * (14 - logPx); + } + + int shift = bitDepth > 11 ? Math.Min(4, bitDepth - 11) : 0; + int mul = 1 << shift; + + Tree tree = []; + Queue q = []; + + tree.Add(JxlPropertyDecisionNode.Leaf(pred)); + q.Enqueue(new NodeInfo(0, cutoffs.Length, 0)); + + while (q.Count > 0) + { + NodeInfo info = q.Peek(); + _ = q.Dequeue(); + + if (info.Begin + minGap >= info.End) + { + continue; + } + + int split = (info.Begin + info.End) / 2; + int cutoff = cutoffs[split] * mul; + + tree[info.Pos] = JxlPropertyDecisionNode.Split(property, cutoff, tree.Count); + + q.Enqueue(new NodeInfo(split + 1, info.End, tree.Count)); + tree.Add(JxlPropertyDecisionNode.Leaf(pred)); + + q.Enqueue(new NodeInfo(info.Begin, split, tree.Count)); + tree.Add(JxlPropertyDecisionNode.Leaf(pred)); + } + + return tree; + } + + public static void GatherTreeData(Configuration configuration, JxlModularImage image, int channelIndex, int groupId, JxlModularHeader wpHeader, JxlModularOptions options, JxlTreeSamples treeSamples, ref int totalPixels) + { + JxlModularChannel channel = image.Channels[channelIndex]; + + InlineArray2 staticProperties = default; + staticProperties[0] = channelIndex; + staticProperties[1] = groupId; + + Span properties = stackalloc int[JxlMaConstants.NumNonrefProperties + (JxlContextPrediction.ExtraPropertiesPerChannel * options.MaxProperties)]; + + double pixelFraction = Math.Min(1.0f, options.NumberOfRepeats); + + if (pixelFraction > 0) + { + pixelFraction = Math.Max(pixelFraction, Math.Min(1.0, 1024.0 / (channel.Width * channel.Height))); + } + + ulong threshold = (ulong)((ulong.MaxValue >> 32) * pixelFraction); + + ulong s1 = 0x94D049BB133111EBuL; + ulong s2 = 0xBF58476D1CE4E5B9uL; + + bool UseSample() + { + ulong bits = s2 + s1; + s1 = s2; + s1 ^= s1 << 23; + s1 ^= s2 ^ (s1 >> 18) ^ (s2 >> 5); + s2 = s1; + return (bits >> 32) <= threshold; + } + + int pixelsPerRow = channel.Plane.PixelsPerRow; + using JxlModularChannel references = new(configuration, properties.Length - JxlMaConstants.NumNonrefProperties, channel.Width, 0, 0); + JxlModularState wpState = new(wpHeader, channel.Width); + + treeSamples.PrepareForSamples((int)(pixelFraction * channel.Height * channel.Width) + 64); + bool haveMultiplePredictors = treeSamples.NumberOfPredictors != 1; + + void ComputeSample(Span p, int x, int y, Span properties, ref int totalPixels) + { + Span pred = stackalloc int[JxlPredictorFacts.ModularPredictorsAlignment].Slice(0, JxlPredictorFacts.ModularPredictors); + + if (haveMultiplePredictors) + { + _ = JxlContextPrediction.PredictLearnAll(properties, channel.Width, ref p[x], pixelsPerRow, x, y, references, wpState, pred); + } + else + { + pred[(int)treeSamples.PredictorFromIndex(0)] = JxlContextPrediction.PredictLearn(properties, channel.Width, ref p[x], pixelsPerRow, x, y, treeSamples.PredictorFromIndex(0), references, wpState).Guess; + } + + totalPixels++; + + if (UseSample()) + { + treeSamples.AddSample(p[x], properties, pred); + } + + wpState.UpdatePredictionErrors(p[x], x, y, channel.Width); + } + + Span pred = stackalloc int[JxlPredictorFacts.ModularPredictorsAlignment].Slice(0, JxlPredictorFacts.ModularPredictors); + + for (int y = 0; y < channel.Height; y++) + { + Span p = channel.GetRow(y); + JxlContextPrediction.PrecomputeReferences(channel, y, image, channelIndex, references); + JxlContextPrediction.InitializePropertiesForRow(properties, staticProperties, y); + + if (y > 1 && channel.Width > 8 && references.Width == 0) + { + for (int x = 0; x < 2; x++) + { + ComputeSample(p, x, y, properties, ref totalPixels); + } + + for (int x = 2; x < channel.Width - 2; x++) + { + if (haveMultiplePredictors) + { + _ = JxlContextPrediction.PredictLearnAllNoEdgeCases(properties, channel.Width, ref p[x], pixelsPerRow, x, y, references, wpState, pred); + } + else + { + pred[(int)treeSamples.PredictorFromIndex(0)] = JxlContextPrediction.PredictLearnNoEdgeCases(properties, channel.Width, ref p[x], pixelsPerRow, x, y, treeSamples.PredictorFromIndex(0), references, wpState).Guess; + } + + totalPixels++; + + if (UseSample()) + { + treeSamples.AddSample(p[x], properties, pred); + } + + wpState.UpdatePredictionErrors(p[x], x, y, channel.Width); + } + + for (int x = channel.Width - 2; x < channel.Width; x++) + { + ComputeSample(p, x, y, properties, ref totalPixels); + } + } + else + { + for (int x = 0; x < channel.Width; x++) + { + ComputeSample(p, x, y, properties, ref totalPixels); + } + } + } + } + + public static Tree LearnTree(JxlTreeSamples treeSamples, int totalPixels, JxlModularOptions options, List? info = null, StaticPropertyRange staticPropertyRange = default) + { + Tree tree = []; + + for (int i = 0; i < 2; i++) + { + if (staticPropertyRange[i][1] == 0) + { + staticPropertyRange[i][1] = int.MaxValue; + } + } + + if (!treeSamples.HasSamples) + { + JxlPropertyDecisionNode node = new() + { + Predictor = treeSamples.PredictorFromIndex(0), + Property = -1, + PredictorOffset = 0, + Multiplier = 1 + }; + + tree.Add(node); + return tree; + } + + float pixelFraction = treeSamples.NumberOfSamples * 1.0f / totalPixels; + float requiredCost = (pixelFraction * 0.9f) + 0.1f; + + treeSamples.AllSamplesDone(); + + JxlMaEncoder.ComputeBestTree(treeSamples, options.SplittingHeuristicsModeThreshold * requiredCost, info, staticPropertyRange, options.FastDecodeMultiplier, tree); + return tree; + } + + public static void EncodeModularChannelMAANS(JxlModularImage image, int channelIndex, JxlModularHeader wpHeader, Tree globalTree, Span tokens, int groupId, bool skipEncoderFastPath) + { + JxlModularChannel channel = image.Channels[channelIndex]; + + if (channel.Width == 0 || channel.Height == 0) + { + throw new InvalidOperationException("Width or height is 0"); + } + + InlineArray2 staticProperties = default; + staticProperties[0] = channelIndex; + staticProperties[1] = groupId; + + bool useWp = false; + bool isWpOnly = false; + bool isGradientOnly = false; + int numProps = 0; + + FlatTree tree = JxlModularEncoding.FilterTree(globalTree, staticProperties, ref numProps, ref useWp, ref isWpOnly, ref isGradientOnly); + JxlMaTreeLookup treeLookup = new(tree); + + JxlTreeLut treeLut = new(false, false); + int tokenp = 0; // Pointer to next token + + if (isWpOnly) + { + isWpOnly = JxlModularEncoding.TreeToLookupTable(tree, treeLut); + } + + if (isGradientOnly) + { + isGradientOnly = JxlModularEncoding.TreeToLookupTable(tree, treeLut); + } + + int onerow = channel.Plane.PixelsPerRow; + int treeCount = tree.Count; + + if (isWpOnly && !skipEncoderFastPath) + { + JxlModularState wpState = new(wpHeader, channel.Width); + Span properties = [0]; + bool unhealthy = false; + + for (int y = 0; y < channel.Height; y++) + { + Span r = channel.GetRow(y); + + for (int x = 0; x < channel.Width; x++) + { + int offset = 0; + + // Neighbors + int left = x > 0 ? channel.GetRowPlus(y, x - 1)[0] : y > 0 ? channel.GetRowPlus(y, x - onerow)[0] : 0; + int top = y > 0 ? channel.GetRowPlus(y, x - onerow)[0] : left; + int topleft = x > 0 && y > 0 ? channel.GetRowPlus(y, x - 1 - onerow)[0] : left; + int topright = x + 1 < channel.Width && y > 0 ? channel.GetRowPlus(y, x + 1 - onerow)[0] : top; + int toptop = y > 1 ? channel.GetRowPlus(y, x - onerow - onerow)[0] : top; + + int guess = (int)wpState.Predict(true, x, y, channel.Width, top, left, topright, topleft, toptop, properties, offset); + int pos = JxlMaConstants.PropertyRangeFast + Math.Clamp(properties[0], -JxlMaConstants.PropertyRangeFast, JxlMaConstants.PropertyRangeFast - 1); + + int ctxId = treeLut.ContextLookup[pos]; + unhealthy |= JxlMath.SubOverflow(r[x], guess, out int residual); + + tokens[tokenp++] = new JxlToken((JxlMaTreeContext)ctxId, JxlPackSigned.PackUnsigned(residual)); + wpState.UpdatePredictionErrors(r[x], x, y, channel.Width); + } + } + + ThrowIfResidualUnderflow(unhealthy); + } + else if (treeCount == 1 && tree[0].Predictor == JxlPredictor.Gradient && tree[0].Multiplier == 1 && tree[0].PredictorOffset == 0 && !skipEncoderFastPath) + { + bool unhealthy = false; + + for (int y = 0; y < channel.Height; y++) + { + Span r = channel.GetRow(y); + + for (int x = 0; x < channel.Width; x++) + { + int left = x > 0 ? channel.GetRowPlus(y, x - 1)[0] : y > 0 ? channel.GetRowPlus(y, x - onerow)[0] : 0; + int top = y > 0 ? channel.GetRowPlus(y, x - onerow)[0] : left; + int topleft = x > 0 && y > 0 ? channel.GetRowPlus(y, x - 1 - onerow)[0] : left; + + int guess = JxlContextPrediction.ClampedGradient(top, left, topleft); + unhealthy |= JxlMath.SubOverflow(r[x], guess, out int residual); + + tokens[tokenp++] = new JxlToken((JxlMaTreeContext)tree[0].ChildID, JxlPackSigned.PackUnsigned(residual)); + } + } + + ThrowIfResidualUnderflow(unhealthy); + } + else if (isGradientOnly && !skipEncoderFastPath) + { + bool unhealthy = false; + + for (int y = 0; y < channel.Height; y++) + { + Span r = channel.GetRow(y); + + for (int x = 0; x < channel.Width; x++) + { + int left = x > 0 ? channel.GetRowPlus(y, x - 1)[0] : y > 0 ? channel.GetRowPlus(y, x - onerow)[0] : 0; + int top = y > 0 ? channel.GetRowPlus(y, x - onerow)[0] : left; + int topleft = x > 0 && y > 0 ? channel.GetRowPlus(y, x - 1 - onerow)[0] : left; + + int guess = JxlContextPrediction.ClampedGradient(top, left, topleft); + int pos = JxlMaConstants.PropertyRangeFast + Math.Min( + Math.Max(-JxlMaConstants.PropertyRangeFast, top + left - topleft), + JxlMaConstants.PropertyRangeFast - 1); + + uint ctxId = treeLut.ContextLookup[pos]; + unhealthy |= JxlMath.SubOverflow(r[x], guess, out int residual); + + tokens[tokenp++] = new JxlToken((JxlMaTreeContext)ctxId, JxlPackSigned.PackUnsigned(residual)); + } + } + + ThrowIfResidualUnderflow(unhealthy); + } + else if (treeCount == 1 && tree[0].Predictor == JxlPredictor.Zero && tree[0].Multiplier == 1 && tree[0].PredictorOffset == 0 && !skipEncoderFastPath) + { + for (int y = 0; y < channel.Height; y++) + { + Span p = channel.GetRow(y); + + for (int x = 0; x < channel.Width; x++) + { + tokens[tokenp++] = new JxlToken((JxlMaTreeContext)tree[0].ChildID, JxlPackSigned.PackUnsigned(p[x])); + } + } + } + else if (treeCount == 1 && tree[0].Predictor != JxlPredictor.Weighted && (tree[0].Multiplier & (tree[0].Multiplier - 1)) == 0 && tree[0].PredictorOffset == 0 && !skipEncoderFastPath) + { + uint mulShift = JxlMath.FloorLog2Nonzero((uint)tree[0].Multiplier); + + for (int y = 0; y < channel.Height; y++) + { + Span r = channel.GetRow(y); + + for (int x = 0; x < channel.Width; x++) + { + JxlPredictionResult pred = JxlContextPrediction.PredictNoTreeNoWeightedPrediction(channel.Width, ref r[x], onerow, x, y, tree[0].Predictor); + int residual = r[x] - pred.Guess; + + if ((residual >> (int)mulShift) * tree[0].Multiplier != residual) + { + throw new InvalidOperationException("Residual coefficient is not valid"); + } + + tokens[tokenp++] = new JxlToken((JxlMaTreeContext)tree[0].ChildID, JxlPackSigned.PackUnsigned(residual >> (int)mulShift)); + } + } + } + else if (!useWp && !skipEncoderFastPath) + { + Span properties = stackalloc int[numProps]; + using JxlModularChannel references = new(configuration, properties.Length - JxlMaConstants.NumNonrefProperties, channel.Width, 0, 0); + + for (int y = 0; y < channel.Height; y++) + { + Span p = channel.GetRow(y); + + JxlContextPrediction.PrecomputeReferences(channel, y, image, channelIndex, references); + JxlContextPrediction.InitializePropertiesForRow(properties, staticProperties, y); + + for (int x = 0; x < channel.Width; x++) + { + JxlPredictionResult res = JxlContextPrediction.PredictTreeNoWeightedPrediction(properties, channel.Width, ref p[x], onerow, x, y, treeLookup, references); + int residual = p[x] - res.Guess; + + if ((residual % res.Multiplier) != 0) + { + throw new InvalidOperationException("Residual coefficient is invalid"); + } + + tokens[tokenp++] = new JxlToken((JxlMaTreeContext)res.Context, JxlPackSigned.PackUnsigned(residual / res.Multiplier)); + } + } + } + else + { + Span properties = stackalloc int[numProps]; + using JxlModularChannel references = new(configuration, properties.Length - JxlMaConstants.NumNonrefProperties, channel.Width, 0, 0); + + JxlModularState wpState = new(wpHeader, channel.Width); + + for (int y = 0; y < channel.Height; y++) + { + Span p = channel.GetRow(y); + JxlContextPrediction.PrecomputeReferences(channel, y, image, channelIndex, references); + JxlContextPrediction.InitializePropertiesForRow(properties, staticProperties, y); + + for (int x = 0; x < channel.Width; x++) + { + JxlPredictionResult res = JxlContextPrediction.PredictTreeWeightedPrediction(properties, channel.Width, ref p[x], onerow, x, y, treeLookup, references, wpState); + + int residual = p[x] - res.Guess; + + if ((residual % res.Multiplier) != 0) + { + throw new InvalidOperationException("Residual coefficient is invalid"); + } + + tokens[tokenp++] = new JxlToken((JxlMaTreeContext)res.Context, JxlPackSigned.PackUnsigned(residual / res.Multiplier)); + wpState.UpdatePredictionErrors(p[x], x, y, channel.Width); + } + } + } + } + + public static Tree PredefinedTree(JxlTreeKind treeKind, int totalPixels, int bitDepth, int prevProp) + { + switch (treeKind) + { + case JxlTreeKind.JpegTranscodeAcMeta: + return [JxlPropertyDecisionNode.Leaf(JxlPredictor.Zero)]; + + case JxlTreeKind.TrivialTreeNoPredictor: + return [JxlPropertyDecisionNode.Leaf(JxlPredictor.Zero)]; + + case JxlTreeKind.FalconAcMeta: + return [JxlPropertyDecisionNode.Leaf(JxlPredictor.Left)]; + + case JxlTreeKind.AcMeta: + { + if (totalPixels < 1024) + { + return [JxlPropertyDecisionNode.Leaf(JxlPredictor.Left)]; + } + + Tree tree = []; + + tree.Add(JxlPropertyDecisionNode.Split(0, 1, 1)); + tree.Add(JxlPropertyDecisionNode.Split(0, 2, 3)); + tree.Add(JxlPropertyDecisionNode.Split(0, 0, 5)); + tree.Add(JxlPropertyDecisionNode.Split(6, 3, 21)); + tree.Add(JxlPropertyDecisionNode.Split(2, 0, 7)); + tree.Add(JxlPropertyDecisionNode.Leaf(JxlPredictor.Gradient)); + tree.Add(JxlPropertyDecisionNode.Leaf(JxlPredictor.Gradient)); + tree.Add(JxlPropertyDecisionNode.Split(7, 5, 9)); + tree.Add(JxlPropertyDecisionNode.Split(7, 5, 15)); + tree.Add(JxlPropertyDecisionNode.Split(7, 11, 11)); + tree.Add(JxlPropertyDecisionNode.Split(7, 3, 13)); + tree.Add(JxlPropertyDecisionNode.Leaf(JxlPredictor.Left)); + tree.Add(JxlPropertyDecisionNode.Leaf(JxlPredictor.Left)); + tree.Add(JxlPropertyDecisionNode.Leaf(JxlPredictor.Left)); + tree.Add(JxlPropertyDecisionNode.Leaf(JxlPredictor.Left)); + tree.Add(JxlPropertyDecisionNode.Split(7, 11, 17)); + tree.Add(JxlPropertyDecisionNode.Split(7, 3, 19)); + tree.Add(JxlPropertyDecisionNode.Leaf(JxlPredictor.Zero)); + tree.Add(JxlPropertyDecisionNode.Leaf(JxlPredictor.Zero)); + tree.Add(JxlPropertyDecisionNode.Leaf(JxlPredictor.Zero)); + tree.Add(JxlPropertyDecisionNode.Leaf(JxlPredictor.Zero)); + tree.Add(JxlPropertyDecisionNode.Split(7, 3, 23)); + tree.Add(JxlPropertyDecisionNode.Split(7, 3, 25)); + tree.Add(JxlPropertyDecisionNode.Leaf(JxlPredictor.Zero)); + tree.Add(JxlPropertyDecisionNode.Leaf(JxlPredictor.Zero)); + tree.Add(JxlPropertyDecisionNode.Leaf(JxlPredictor.Zero)); + tree.Add(JxlPropertyDecisionNode.Leaf(JxlPredictor.Zero)); + + return tree; + } + + case JxlTreeKind.WpFixedDc: + { + Span cutoffs = [ + -500, -392, -255, -191, -127, -95, -63, -47, -31, -23, -15, + -11, -7, -4, -3, -1, 0, 1, 3, 5, 7, 11, + 15, 23, 31, 47, 63, 95, 127, 191, 255, 392, 500]; + + return MakeFixedTree(JxlMaConstants.WpProp, cutoffs, JxlPredictor.Weighted, totalPixels, bitDepth); + } + + case JxlTreeKind.GradientFixedDc: + { + Span cutoffs = [ + -500, -392, -255, -191, -127, -95, -63, -47, -31, -23, -15, + -11, -7, -4, -3, -1, 0, 1, 3, 5, 7, 11, + 15, 23, 31, 47, 63, 95, 127, 191, 255, 392, 500]; + + return MakeFixedTree(prevProp > 0 ? JxlMaConstants.NumNonrefProperties + 2 : JxlMaConstants.GradientProp, cutoffs, JxlPredictor.Gradient, totalPixels, bitDepth); + } + + case JxlTreeKind.Learn: + { + throw new InvalidOperationException($"{nameof(JxlTreeKind.Learn)} is not a predefined tree"); + } + } + + throw new InvalidOperationException("Invalid tree type: " + treeKind); + } + + public static Tree LearnTree(JxlModularImage[] images, Span options, int start, int stop, List? multiplierInfo = null) + { + multiplierInfo ??= []; + + JxlTreeSamples samples = new(); + samples.SetPredictor(options[start].Predictor, options[start].WpTreeMode); + samples.SetProperties(options[start].SplittingHeuristicsProperties, options[start].WpTreeMode); + + int maxC = 0; + + List pixelSamples = []; + List diffSamples = []; + List groupPixelCount = []; + List channelPixelCount = []; + + for (int i = start; i < stop; i++) + { + maxC = Math.Max(images[i].Channels.Count, maxC); + JxlMaEncoder.CollectPixelSamples(images[i], options[i], i, groupPixelCount, channelPixelCount, pixelSamples, diffSamples); + } + + // StaticPropRange range; + // range[0] = { { 0, max_c } }; + // range[1] = { { start, stop } }; + StaticPropertyRange range = default; + InlineArray2 currRange = default; + currRange[0] = 0; + currRange[1] = maxC; + range[0] = currRange; + currRange[0] = start; + currRange[1] = stop; + range[1] = currRange; + + samples.PreQuantizeProperties(range, multiplierInfo, groupPixelCount, channelPixelCount, pixelSamples, diffSamples, options[start].MaxPropertyValues); + + int totalPixels = 0; + + for (int i = 0; i < images[start].Channels.Count; i++) + { + if (i >= images[start].MetaChannels && + (images[start].Channels[i].Width > options[start].MaxChannelSize || + images[start].Channels[i].Height > options[start].MaxChannelSize)) + { + break; + } + + totalPixels += images[start].Channels[i].Width * images[start].Channels[i].Height; + } + + totalPixels = Math.Max(totalPixels, 1); + + JxlModularHeader wpHeader = new(); + + for (int i = start; i < stop; i++) + { + int nb_channels = images[i].Channels.Count; + + if (images[i].Width == 0 || images[i].Height == 0 || nb_channels < 1) + { + continue; + } + + if (images[i].IsInvalid) + { + throw new InvalidOperationException("Invalid image"); + } + + if (options[i].TreeKind != JxlTreeKind.Learn) + { + throw new InvalidOperationException("Tree type must be Learn"); + } + + JxlBundle.Init(wpHeader); + + if (JxlContextPrediction.IsWeightedPredictor(options[i].Predictor)) + { + JxlContextPrediction.SetPredictorMode(options[i].WpMode, wpHeader); + } + + for (int c = 0; c < nb_channels; c++) + { + if (c >= images[i].MetaChannels && + (images[i].Channels[c].Width > options[i].MaxChannelSize || + images[i].Channels[c].Height > options[i].MaxChannelSize)) + { + break; + } + + if (images[i].Channels[c].Width == 0 || images[i].Channels[c].Height == 0) + { + continue; // skip empty channels + } + + GatherTreeData(images[i], c, i, wpHeader, options[i], samples, totalPixels); + } + } + + Tree tree = LearnTree(samples, totalPixels, options[start], multiplierInfo, range); + + return tree; + } + + public static void ModularCompress(JxlModularImage image, JxlModularOptions options, int groupId, Tree tree, JxlGroupHeader header, List tokens, ref int width) + { + int numChannels = image.Channels.Count; + + if (image.Width == 0 || image.Height == 0 || numChannels < 1) + { + return; + } + + if (image.IsInvalid) + { + throw new InvalidOperationException("Invalid image"); + } + + JxlBundle.Init(header); + + if (JxlContextPrediction.IsWeightedPredictor(options.Predictor)) + { + JxlContextPrediction.SetPredictorMode(options.WpMode, header.WeightedHeader); + } + + header.Transforms = image.Transforms; + header.UseGlobalTree = true; + + int imageWidth = 0; + int totalTokens = 0; + + for (int i = 0; i < numChannels; i++) + { + if (i >= image.MetaChannels && + (image.Channels[i].Width > options.MaxChannelSize || + image.Channels[i].Height > options.MaxChannelSize)) + { + break; + } + + if (image.Channels[i].Width > imageWidth) + { + imageWidth = image.Channels[i].Width; + } + + totalTokens += image.Channels[i].Width * image.Channels[i].Height; + } + + if (options.ZeroTokens) + { + tokens.Grow(default, tokens.Count + totalTokens); + } + else + { + int pos = tokens.Count; + tokens.Grow(default, pos + totalTokens); + + int tokenp = pos; + + for (int i = 0; i < numChannels; i++) + { + if (i >= image.MetaChannels && + (image.Channels[i].Width > options.MaxChannelSize || + image.Channels[i].Height > options.v)) + { + break; + } + + if (image.Channels[i].Width == 0 || image.Channels[i].Height == 0) + { + continue; + } + + EncodeModularChannelMAANS(image, i, header.WeightedHeader, tree, CollectionsMarshal.AsSpan(tokens)[tokenp..], groupId, options.SkipEncoderFastPath); + } + + if (tokenp != tokens.Count) + { + throw new InvalidOperationException("Tokens were not written"); + } + } + + width = imageWidth; + } + + public static void ModularGenericCompress(JxlModularImage image, JxlModularOptions options, JxlBitWriter writer, JxlAuxiliaryOutput auxOut, JxlLayerType layerType, int groupId) + { + int numChannels = image.Channels.Count; + + if (image.Width == 0 || image.Height == 0 || numChannels < 1) + { + return; + } + + if (image.IsInvalid) + { + throw new InvalidOperationException("Invalid image"); + } + + JxlModularOptions modularOptions = options.DeepClone(); // Make a copy to modify it + + if (modularOptions.Predictor == JxlPredictor.Undefined) + { + modularOptions.Predictor = JxlPredictor.Gradient; + } + + long bits = writer.BitsWritten; + + JxlGroupHeader groupHeader = new(); + JxlBundle.Init(groupHeader); + + if (JxlContextPrediction.IsWeightedPredictor(modularOptions.Predictor)) + { + JxlContextPrediction.SetPredictorMode(modularOptions.WpMode, groupHeader.WeightedHeader); + } + + groupHeader.Transforms = image.Transforms; + + JxlBundle.Write(groupHeader, writer, layerType, auxOut); + + Tree tree = []; + + if (modularOptions.TreeKind == JxlTreeKind.Learn) + { + tree = LearnTree(image, options, 0, 1); + } + else + { + // It's a predefined tree. + int totalPixels = 0; + + for (int i = 0; i < numChannels; i++) + { + if (i >= image.MetaChannels && + (image.Channels[i].Width > options.MaxChannelSize + || image.Channels[i].Height > options.MaxChannelSize)) + { + break; + } + + totalPixels += image.Channels[i].Width * image.Channels[i].Height; + } + + totalPixels = Math.Max(totalPixels, 1); + + tree = PredefinedTree(options.TreeKind, totalPixels, image.BitDepth, options.MaxProperties); + } + + Tree decodedTree = []; + List> treeTokens = [[]]; + + JxlMaEncoder.TokenizeTree(tree, treeTokens[0], decodedTree); + + if (tree.Count != decodedTree.Count) + { + throw new InvalidOperationException("Tree lengths mismatch"); + } + + tree = decodedTree; + + // TODO: missing types here + JxlEntropyEncodingData code = default; + int cost = BuildAndEncodeHistograms(configuration, options.HistogramParameters, JxlMaConstants.NumTreeContexts, treeTokens, code, writer, JxlLayerType.ModularTree, auxOut); + WriteTokens(treeTokens[0], code, 0, writer, JxlLayerType.ModularTree, auxOut); + + int imageWidth = 0; + List> tokens = [[]]; + ModularCompress(image, options, groupId, tree, groupHeader, tokens[0], ref imageWidth); + + code = default; + JxlHistogramParameters histoParameters = options.HistogramParameters; + histoParameters.ImageWidths.Add(imageWidth); + _ = BuildAndEncodeHistograms(configuration, histoParameters, (tree.Count + 1) / 2, tokens, ref code, writer, layerType, auxOut); + + WriteTokens(tokens[0], code, 0, writer, layerType, auxOut); + bits = writer.BitsWritten - bits; + } + + private static void ThrowIfResidualUnderflow(bool isUnderflow) + { + if (isUnderflow) + { + throw new InvalidOperationException("Residual coefficient underflow"); + } + } + + private struct NodeInfo + { + public NodeInfo(int begin, int end, int pos) + { + this.Begin = begin; + this.End = end; + this.Pos = pos; + } + + public int Begin { get; set; } + + public int End { get; set; } + + public int Pos { get; set; } + } +} From 148764c1d3f519c0f5d1428b9fd66e0acf8e2fc3 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 7 Sep 2026 00:05:44 +0400 Subject: [PATCH 146/328] Add MA decoder --- .../Modular/Encoding/JxlMaDecoder.cs | 196 ++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaDecoder.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaDecoder.cs new file mode 100644 index 0000000000..bec5061305 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaDecoder.cs @@ -0,0 +1,196 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using Tree = System.Collections.Generic.List; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding; + +internal static class JxlMaDecoder +{ + private enum NextAction + { + CheckAndGoLeft, + GoRight, + Pop + } + + public static void ValidateTree(Tree tree) + { + const int heightLimit = 2048; + + if (tree.Count == 0) + { + return; + } + + int numProperties = tree.Max(x => x.Property + 1); + Span<(int First, int Second)> propertyRanges = stackalloc (int First, int Second)[numProperties]; + + for (int i = 0; i < numProperties; i++) + { + propertyRanges[i].First = int.MinValue; + propertyRanges[i].Second = int.MaxValue; + } + + Stack stack = []; + stack.Push(new WorkItem(0, 0, 0, NextAction.CheckAndGoLeft)); + + while (stack.Count > 0) + { + if (stack.Count > heightLimit) + { + throw new InvalidOperationException("Tree too tall"); + } + + WorkItem item = stack.Peek(); + JxlPropertyDecisionNode node = tree[item.NodeIndex]; + + switch (item.Action) + { + case NextAction.CheckAndGoLeft: + { + int p = node.Property; + if (p == -1) + { + _ = stack.Pop(); + continue; + } + + int v = node.SplitValue; + int l = propertyRanges[p].First; + int u = propertyRanges[p].Second; + + if (l > v || u <= v) + { + throw new InvalidOperationException("Invalid tree"); + } + + item.OriginalL = l; + item.OriginalU = u; + item.Action = NextAction.GoRight; + propertyRanges[node.Property].First = node.SplitValue + 1; + + stack.Push(new WorkItem(node.LeftChild, 0, 0, NextAction.CheckAndGoLeft)); + continue; + } + + case NextAction.GoRight: + item.Action = NextAction.Pop; + propertyRanges[node.Property].First = item.OriginalL; + propertyRanges[node.Property].Second = node.SplitValue; + stack.Push(new WorkItem(node.LeftChild, 0, 0, NextAction.CheckAndGoLeft)); + continue; + + case NextAction.Pop: + propertyRanges[node.Property].Second = item.OriginalU; + _ = stack.Pop(); + continue; + } + } + } + + public static void DecodeTree(JxlBitReader br, JxlAnsSymbolReader reader, List contextMap, Tree tree, int treeSizeLimit) + { + int leafId = 0; + int toDecode = 1; + tree.Clear(); + + while (toDecode > 0) + { + if (tree.Count > treeSizeLimit) + { + throw new InvalidOperationException("Tree is too large"); + } + + toDecode--; + int prop1 = reader.ReadHybridUint(JxlMaTreeContext.Property, br, contextMap); + if (prop1 > 256) + { + throw new InvalidOperationException("Invalid tree property value"); + } + + int property = prop1 - 1; + if (property == -1) + { + int predictor = reader.ReadHybridUint(JxlMaTreeContext.Predictor, br, contextMap); + + if (predictor >= JxlPredictorFacts.ModularPredictors) + { + throw new InvalidOperationException("Invalid predictor"); + } + + int predictor_offset = JxlPackSigned.UnpackSigned(reader.ReadHybridUint(JxlMaTreeContext.Offset, br, contextMap)); + int mul_log = reader.ReadHybridUint(JxlMaTreeContext.MultiplierLog, br, contextMap); + + if (mul_log >= 31) + { + throw new InvalidOperationException("Invalid multiplier logarithm"); + } + + int mul_bits = reader.ReadHybridUint(JxlMaTreeContext.MultiplierBits, br, contextMap); + + if (mul_bits >= (1 << (31 - mul_log)) - 1) + { + throw new InvalidOperationException("Invalid multiplier"); + } + + int multiplier = (mul_bits + 1) << mul_log; + JxlPredictor p = (JxlPredictor)predictor; + + tree.Add(new JxlPropertyDecisionNode(-1, 0, leafId, 0, p, predictor_offset, multiplier)); + leafId++; + continue; + } + + int splitval = JxlPackSigned.UnpackSigned(reader.ReadHybridUint(JxlMaTreeContext.SplitValue, br, contextMap)); + tree.Add( + new JxlPropertyDecisionNode( + property, + splitval, + tree.Count + toDecode + 1, + tree.Count + toDecode + 2, + JxlPredictor.Zero, + 0, + 1)); + + toDecode += 2; + } + + ValidateTree(tree); + } + + public static void DecodeTree(Configuration configuration, JxlBitReader reader, Tree tree, int treeSizeLimit) + { + List treeContextMap = []; + JxlAnsCode code = new(); + + JxlAnsReader.DecodeHistograms(configuration, reader, JxlMaConstants.NumTreeContexts, code, treeContextMap); + + if (code.DegenerateSymbols[treeContextMap[(int)JxlMaTreeContext.Property]] > 0) + { + throw new InvalidOperationException("Infinite tree"); + } + + JxlAnsSymbolReader symbolReader = JxlAnsSymbolReader.Create(code, reader); + DecodeTree(reader, symbolReader, treeContextMap, tree, Math.Min(treeSizeLimit, JxlMaConstants.MaxTreeSize)); + + if (!symbolReader.CheckAnsFinalState()) + { + throw new InvalidOperationException("ANS decode final state failed"); + } + } + + private struct WorkItem(int nodeIndex, int origL, int origU, NextAction action) + { + public int NodeIndex { get; set; } = nodeIndex; + + public int OriginalL { get; set; } = origL; + + public int OriginalU { get; set; } = origU; + + public NextAction Action { get; set; } = action; + } +} From 412693e488e1cd414149df1969a53d6cc10fe931 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 7 Sep 2026 00:33:35 +0400 Subject: [PATCH 147/328] Complete core modular functions --- .../Modular/Encoding/JxlModularEncoding.cs | 18 ++--- .../Encoding/JxlModularEncodingEncoder.cs | 26 +++--- .../Processing/Modular/JxlModularChannel.cs | 4 +- .../Jxl/Processing/Modular/JxlModularImage.cs | 73 ++++++++++++++++- .../Processing/Modular/JxlModularOptions.cs | 64 +++++++++++++++ .../Modular/Transforms/JxlTransform.cs | 79 ++++++++++++++++++- 6 files changed, 239 insertions(+), 25 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularOptions.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncoding.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncoding.cs index df0afa4da2..e1dabc6534 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncoding.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncoding.cs @@ -33,7 +33,7 @@ public static bool TreeToLookupTable(FlatTree tree, JxlTreeLut lut) return false; } - JxlMaNode node = tree[cur.Pos]; + JxlFlatDecisionNode node = tree[cur.Pos]; // Leaf. if (node.Property0 == -1) @@ -76,7 +76,7 @@ public static bool TreeToLookupTable(FlatTree tree, JxlTreeLut lut) continue; } - if (node.Properties[0] >= NumStaticProperties) + if (node.Properties[0] >= JxlPredictorFacts.StaticProperties) { ranges.Push(new(node.SplitValues[0], cur.End, node.ChildID)); ranges.Push(new(node.SplitValue0, node.SplitValues[0], node.ChildID + 1)); @@ -87,7 +87,7 @@ public static bool TreeToLookupTable(FlatTree tree, JxlTreeLut lut) } // <= side - if (node.Properties[1] >= NumStaticProperties) + if (node.Properties[1] >= JxlPredictorFacts.StaticProperties) { ranges.Push( new(node.SplitValues[1], node.SplitValue0, node.ChildID + 2)); @@ -116,12 +116,12 @@ void MarkProperty(int p, ref bool gradientOnly) { hasWeightedPrediction = true; } - else if (p >= NumStaticProperties) + else if (p >= JxlPredictorFacts.StaticProperties) { hasNonWeighted = true; } - if (p >= NumStaticProperties && p != GradientProp) + if (p >= JxlPredictorFacts.StaticProperties && p != GradientProp) { gradientOnly = false; } @@ -136,7 +136,7 @@ void MarkProperty(int p, ref bool gradientOnly) int cur = nodes.Peek(); _ = nodes.Dequeue(); - while (globalTree[cur].Property < NumStaticProperties && globalTree[cur].Property != -1) + while (globalTree[cur].Property is < JxlPredictorFacts.StaticProperties and not -1) { if (staticProps[globalTree[cur].Property] > globalTree[cur].SplitValue) { @@ -176,7 +176,7 @@ void MarkProperty(int p, ref bool gradientOnly) { int currentChild = i == 0 ? globalTree[cur].LeftChild : globalTree[cur].RightChild; - while (globalTree[currentChild].Property < kNumStaticProperties && globalTree[currentChild].Property != -1) + while (globalTree[currentChild].Property < JxlPredictorFacts.StaticProperties && globalTree[currentChild].Property != -1) { if (staticProps[globalTree[currentChild].Property] > globalTree[currentChild].SplitValue) @@ -220,7 +220,7 @@ void MarkProperty(int p, ref bool gradientOnly) if (numProps > JxlMaConstants.NumTreeContexts) { - numProps = (JxlMath.DivCeil(numProps - JxlMaConstants.NumTreeContexts, kExtraPropsPerChannel) * kExtraPropsPerChannel) + JxlMaConstants.NumTreeContexts; + numProps = (JxlMath.DivCeil(numProps - JxlMaConstants.NumTreeContexts, JxlContextPrediction.ExtraPropertiesPerChannel) * JxlContextPrediction.ExtraPropertiesPerChannel) + JxlMaConstants.NumTreeContexts; } else { @@ -281,7 +281,7 @@ bool GlobalTreeIsAllGradientNoOp() return false; } } - else if (n.Property >= NumStaticProperties) + else if (n.Property >= JxlPredictorFacts.StaticProperties) { return false; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncodingEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncodingEncoder.cs index 6eeb246501..c69e2addf2 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncodingEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncodingEncoder.cs @@ -264,11 +264,11 @@ public static Tree LearnTree(JxlTreeSamples treeSamples, int totalPixels, JxlMod treeSamples.AllSamplesDone(); - JxlMaEncoder.ComputeBestTree(treeSamples, options.SplittingHeuristicsModeThreshold * requiredCost, info, staticPropertyRange, options.FastDecodeMultiplier, tree); + JxlMaEncoder.ComputeBestTree(treeSamples, options.SplittingHeuristicsNodeThreshold * requiredCost, info, staticPropertyRange, options.FastDecodeMultiplier, tree); return tree; } - public static void EncodeModularChannelMAANS(JxlModularImage image, int channelIndex, JxlModularHeader wpHeader, Tree globalTree, Span tokens, int groupId, bool skipEncoderFastPath) + public static void EncodeModularChannelMAANS(Configuration configuration, JxlModularImage image, int channelIndex, JxlModularHeader wpHeader, Tree globalTree, Span tokens, int groupId, bool skipEncoderFastPath) { JxlModularChannel channel = image.Channels[channelIndex]; @@ -563,7 +563,7 @@ public static Tree PredefinedTree(JxlTreeKind treeKind, int totalPixels, int bit throw new InvalidOperationException("Invalid tree type: " + treeKind); } - public static Tree LearnTree(JxlModularImage[] images, Span options, int start, int stop, List? multiplierInfo = null) + public static Tree LearnTree(Configuration configuration, JxlModularImage[] images, Span options, int start, int stop, List? multiplierInfo = null) { multiplierInfo ??= []; @@ -581,7 +581,7 @@ public static Tree LearnTree(JxlModularImage[] images, Span o for (int i = start; i < stop; i++) { maxC = Math.Max(images[i].Channels.Count, maxC); - JxlMaEncoder.CollectPixelSamples(images[i], options[i], i, groupPixelCount, channelPixelCount, pixelSamples, diffSamples); + JxlMaEncoder.CollectPixelSamples(configuration, images[i], options[i], i, groupPixelCount, channelPixelCount, pixelSamples, diffSamples); } // StaticPropRange range; @@ -596,7 +596,7 @@ public static Tree LearnTree(JxlModularImage[] images, Span o currRange[1] = stop; range[1] = currRange; - samples.PreQuantizeProperties(range, multiplierInfo, groupPixelCount, channelPixelCount, pixelSamples, diffSamples, options[start].MaxPropertyValues); + samples.PreQuantizeProperties(configuration, range, multiplierInfo, groupPixelCount, channelPixelCount, pixelSamples, diffSamples, options[start].MaxPropertyValues); int totalPixels = 0; @@ -618,9 +618,9 @@ public static Tree LearnTree(JxlModularImage[] images, Span o for (int i = start; i < stop; i++) { - int nb_channels = images[i].Channels.Count; + int numChannels = images[i].Channels.Count; - if (images[i].Width == 0 || images[i].Height == 0 || nb_channels < 1) + if (images[i].Width == 0 || images[i].Height == 0 || numChannels < 1) { continue; } @@ -642,7 +642,7 @@ public static Tree LearnTree(JxlModularImage[] images, Span o JxlContextPrediction.SetPredictorMode(options[i].WpMode, wpHeader); } - for (int c = 0; c < nb_channels; c++) + for (int c = 0; c < numChannels; c++) { if (c >= images[i].MetaChannels && (images[i].Channels[c].Width > options[i].MaxChannelSize || @@ -656,7 +656,7 @@ public static Tree LearnTree(JxlModularImage[] images, Span o continue; // skip empty channels } - GatherTreeData(images[i], c, i, wpHeader, options[i], samples, totalPixels); + GatherTreeData(configuration, images[i], c, i, wpHeader, options[i], samples, totalPixels); } } @@ -724,7 +724,7 @@ public static void ModularCompress(JxlModularImage image, JxlModularOptions opti { if (i >= image.MetaChannels && (image.Channels[i].Width > options.MaxChannelSize || - image.Channels[i].Height > options.v)) + image.Channels[i].Height > options.MaxChannelSize)) { break; } @@ -734,7 +734,7 @@ public static void ModularCompress(JxlModularImage image, JxlModularOptions opti continue; } - EncodeModularChannelMAANS(image, i, header.WeightedHeader, tree, CollectionsMarshal.AsSpan(tokens)[tokenp..], groupId, options.SkipEncoderFastPath); + EncodeModularChannelMAANS(configuration, image, i, header.WeightedHeader, tree, CollectionsMarshal.AsSpan(tokens)[tokenp..], groupId, options.SkipEncoderFastPath); } if (tokenp != tokens.Count) @@ -746,7 +746,7 @@ public static void ModularCompress(JxlModularImage image, JxlModularOptions opti width = imageWidth; } - public static void ModularGenericCompress(JxlModularImage image, JxlModularOptions options, JxlBitWriter writer, JxlAuxiliaryOutput auxOut, JxlLayerType layerType, int groupId) + public static void ModularGenericCompress(Configuration configuration, JxlModularImage image, JxlModularOptions options, JxlBitWriter writer, JxlAuxiliaryOutput auxOut, JxlLayerType layerType, int groupId) { int numChannels = image.Channels.Count; @@ -785,7 +785,7 @@ public static void ModularGenericCompress(JxlModularImage image, JxlModularOptio if (modularOptions.TreeKind == JxlTreeKind.Learn) { - tree = LearnTree(image, options, 0, 1); + tree = LearnTree(configuration, image, options, 0, 1); } else { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularChannel.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularChannel.cs index 02581f15fe..4d227853bb 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularChannel.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularChannel.cs @@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular; /// /// A wrapper over for modular operations. /// -internal sealed class JxlModularChannel +internal sealed class JxlModularChannel : IDisposable { public JxlModularChannel(Configuration configuration, int width, int height, int horizShift, int vertShift) { @@ -78,4 +78,6 @@ public void Shrink(Configuration configuration, int newWidth, int newHeight) public Span GetRowMinus(int y, int minus) => this.Plane.GetRowMinus(y, minus); public Span GetRowPlus(int y, int plus) => this.Plane.GetRowPlus(y, plus); + + public void Dispose() => this.Plane.Dispose(); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularImage.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularImage.cs index ebcdcdcdc8..9b3ce7e02f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularImage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularImage.cs @@ -1,12 +1,31 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; + namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular; -internal sealed class JxlModularImage +internal sealed class JxlModularImage : IDisposable { + public JxlModularImage(int width, int height, int bitDepth) + { + this.Width = width; + this.Height = height; + this.BitDepth = bitDepth; + this.MetaChannels = 0; + this.IsInvalid = false; + } + + public JxlModularImage() + : this(0, 0, 8) + { + } + public List Channels { get; set; } = []; + public List Transforms { get; set; } = []; + /// /// Gets or sets the total number of metachannels in this image. /// @@ -16,4 +35,56 @@ internal sealed class JxlModularImage /// Gets or sets the bit depth used in this image. /// public int BitDepth { get; set; } + + /// + /// Gets or sets modular image width. + /// + public int Width { get; set; } + + /// + /// Gets or sets modular image height. + /// + public int Height { get; set; } + + /// + /// Gets or sets a value indicating whether the modular image has an error. + /// + public bool IsInvalid { get; set; } + + public bool IsEmpty => this.Channels.Any(x => x.Width > 0 && x.Height > 0); + + public void UndoTransforms(Configuration configuration, JxlModularHeader wpHeader) + { + while (this.Transforms.Count > 0) + { + JxlTransform transform = this.Transforms.First(); + transform.Inverse(configuration, this, wpHeader); + this.Transforms.RemoveAt(0); + } + } + + public static JxlModularImage Create(Configuration configuration, int width, int height, int bitDepth, int channels) + { + JxlModularImage result = new(width, height, bitDepth); + + for (int i = 0; i < channels; i++) + { + JxlModularChannel c = new(configuration, width, height, 0, 0); + + result.Channels.Add(c); + result.Channels[^1].Component = i; + } + + return result; + } + + public void Dispose() + { + foreach (JxlModularChannel channel in this.Channels) + { + channel.Dispose(); + } + + this.Channels = []; + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularOptions.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularOptions.cs new file mode 100644 index 0000000000..5346b6a3d0 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularOptions.cs @@ -0,0 +1,64 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular; + +internal sealed class JxlModularOptions +{ + /// + /// Gets or sets value indicating when to stop decoding/encoding + /// when reaching a non-meta channel that has a dimension bigger + /// than this value. + /// + public int MaxChannelSize { get; set; } = 0xFFFFFF; + + /// + /// Gets or sets value used during decoding for validation + /// of transforms (squeezing) scheme. + /// + public int GroupDimension { get; set; } = 0x1FFFFFFF; + + /// + /// Gets or sets fraction of pixels to look at to learn a MA tree. + /// + public float NumberOfRepeats { get; set; } = 0.5f; + + /// + /// Gets or sets maximum number of previous channel properties + /// to use in the MA trees. + /// + public int MaxProperties { get; set; } + + /// + /// Gets or sets properties that default to channel, group, weighted, + /// gradient residual, W-NW, NW-N, N-NE, N-NN. + /// + public List SplittingHeuristicsProperties { get; set; } = [0, 1, 15, 9, 10, 11, 12, 13]; + + public float SplittingHeuristicsNodeThreshold { get; set; } = 96f; + + public int MaxPropertyValues { get; set; } = 32; + + public JxlPredictor Predictor { get; set; } = JxlPredictor.Undefined; + + public int WpMode { get; set; } + + public float FastDecodeMultiplier { get; set; } = 1.01f; + + public JxlTreeMode WpTreeMode { get; set; } = JxlTreeMode.Default; + + public bool SkipEncoderFastPath { get; set; } + + public JxlTreeKind TreeKind { get; set; } = JxlTreeKind.Learn; + + public JxlHistogramParameters HistogramParameters { get; set; } = new(); + + /// + /// Gets or sets a value indicating whether to ignore the image and just + /// pretend all tokens are zeroes. + /// + public bool ZeroTokens { get; set; } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs index cf3c2156f7..6d8c73f8f2 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs @@ -2,12 +2,47 @@ // Licensed under the Six Labors Split License. using System.Numerics.Tensors; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; internal sealed class JxlTransform : IJxlFields { + public JxlTransform() + : this(JxlTransformType.Invalid) + { + } + + public JxlTransform(JxlTransformType id) + { + JxlBundle.Init(this); + this.TransformType = id; + } + + public JxlTransformType TransformType { get; set; } + + public int BeginC { get; set; } + + public int RctType { get; set; } + + public int NumC { get; set; } + + public int Colors { get; set; } + + public int Deltas { get; set; } + + public List Squeezes { get; set; } = []; + + public int MaxDeltaError { get; set; } + + public JxlPredictor Predictor { get; set; } + + public bool OrderedPalette { get; set; } = true; + + public bool LossyPalette { get; set; } + public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); public static void CheckEqualChannels(JxlModularImage image, int c1, int c2) @@ -19,7 +54,7 @@ public static void CheckEqualChannels(JxlModularImage image, int c1, int c2) throw new InvalidOperationException($"Invalid channel range: {c1}..{c2} (there are only {channelsCount} channels)"); } - if (c1 < image.NbMetaChannels && c2 >= image.NbMetaChannels) + if (c1 < image.MetaChannels && c2 >= image.MetaChannels) { throw new InvalidOperationException("Invalid: transforming mix of meta and nonmeta"); } @@ -56,4 +91,46 @@ public static void ComputeMinMax(JxlModularChannel channel, out int min, out int max = Math.Max(maxRow, max); } } + + public void Inverse(Configuration configuration, JxlModularImage input, JxlModularHeader wpHeader) + { + switch (this.TransformType) + { + case JxlTransformType.Rct: + JxlRct.InverseRct(configuration, input, this.BeginC, this.RctType); + break; + + case JxlTransformType.Squeeze: + JxlSqueeze.InverseSqueeze(configuration, input, CollectionsMarshal.AsSpan(this.Squeezes)); + break; + + case JxlTransformType.Palette: + JxlPalette.InversePalette(configuration, input, this.BeginC, this.Colors, this.Deltas, this.Predictor, wpHeader); + break; + + default: + throw new InvalidOperationException($"Unknown transform: {this.TransformType}"); + } + } + + public void MetaApply(Configuration configuration, JxlModularImage image) + { + switch (this.TransformType) + { + case JxlTransformType.Rct: + CheckEqualChannels(image, this.BeginC, this.BeginC + 2); + break; + + case JxlTransformType.Squeeze: + JxlSqueeze.MetaSqueeze(configuration, image, this.Squeezes); + break; + + case JxlTransformType.Palette: + JxlPalette.MetaPalette(configuration, image, this.BeginC, this.BeginC + this.NumC - 1, this.Colors, this.Deltas); + break; + + default: + throw new InvalidOperationException($"Unknown transform: {this.TransformType}"); + } + } } From 649034b023b685d084800b057715287d0eab8581 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 7 Sep 2026 00:50:51 +0400 Subject: [PATCH 148/328] Add PQ transfer function --- ...ceptualQuantizationTransferFunctionBase.cs | 54 ------- .../JxlPqTransferFunction.cs | 142 ++++++++++++++++++ 2 files changed, 142 insertions(+), 54 deletions(-) delete mode 100644 src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlPerceptualQuantizationTransferFunctionBase.cs create mode 100644 src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlPqTransferFunction.cs diff --git a/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlPerceptualQuantizationTransferFunctionBase.cs b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlPerceptualQuantizationTransferFunctionBase.cs deleted file mode 100644 index eea7b06560..0000000000 --- a/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlPerceptualQuantizationTransferFunctionBase.cs +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Jxl.Cms.TransferFunctions; - -/// -/// Base class for PQ transfer function. -/// -internal abstract class JxlPerceptualQuantizationTransferFunctionBase -{ - private const double M1 = 2610.0 / 16384; - private const double M2 = (2523.0 / 4096) * 128; - private const double C1 = 3424.0 / 4096; - private const double C2 = (2413.0 / 4096) * 32; - private const double C3 = (2392.0 / 4096) * 32; - - protected static double DisplayFromEncoded(float displayIntensityTarget, double e) - { - if (e == 0.0) - { - return 0.0; - } - - double originalSign = e; - - e = Math.Abs(e); - - double xp = Math.Pow(e, 1.0 / M2); - double num = Math.Max(xp - C1, 0.0); - double den = C2 - (C3 * xp); - double d = Math.Pow(num / den, 1.0 / M1); - - return Math.CopySign(d * (10000.0 / displayIntensityTarget), originalSign); - } - - protected static double EncodedFromDisplay(float displayIntensityTarget, double d) - { - if (d == 0.0) - { - return 0.0; - } - - double originalSign = d; - - d = Math.Abs(d); - - double xp = Math.Pow(d * (displayIntensityTarget * (1 / 10000)), M1); - double num = C1 + (xp * C2); - double den = 1.0 + (xp * C3); - double e = Math.Pow(num / den, M2); - - return Math.CopySign(e, originalSign); - } -} diff --git a/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlPqTransferFunction.cs b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlPqTransferFunction.cs new file mode 100644 index 0000000000..654d05429b --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlPqTransferFunction.cs @@ -0,0 +1,142 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms.TransferFunctions; + +/// +/// Perceptual Quantization transfer function. +/// +internal struct JxlPqTransferFunction(float displayIntensityTarget = DefaultIntensityTarget) +{ + private const double M1 = 2610.0 / 16384; + private const double M2 = (2523.0 / 4096) * 128; + private const double C1 = 3424.0 / 4096; + private const double C2 = (2413.0 / 4096) * 32; + private const double C3 = (2392.0 / 4096) * 32; + + private readonly float scaleFactorTo10000Nits = displayIntensityTarget * (1.0f / 10000.0f); + private readonly float scaleFactorFrom10000Nits = 10000.0f / displayIntensityTarget; + + private static ReadOnlySpan DisplayP => + [ + 2.62975656e-04f, 2.62975656e-04f, 2.62975656e-04f, 2.62975656e-04f, + -6.23553089e-03f, -6.23553089e-03f, -6.23553089e-03f, -6.23553089e-03f, + 7.38602301e-01f, 7.38602301e-01f, 7.38602301e-01f, 7.38602301e-01f, + 2.64553172e+00f, 2.64553172e+00f, 2.64553172e+00f, 2.64553172e+00f, + 5.50034862e-01f, 5.50034862e-01f, 5.50034862e-01f, 5.50034862e-01f + ]; + + private static ReadOnlySpan DisplayQ => + [ + 4.21350107e+02f, 4.21350107e+02f, 4.21350107e+02f, 4.21350107e+02f, + -4.28736818e+02f, -4.28736818e+02f, -4.28736818e+02f, -4.28736818e+02f, + 1.74364667e+02f, 1.74364667e+02f, 1.74364667e+02f, 1.74364667e+02f, + -3.39078883e+01f, -3.39078883e+01f, -3.39078883e+01f, -3.39078883e+01f, + 2.67718770e+00f, 2.67718770e+00f, 2.67718770e+00f, 2.67718770e+00f + ]; + + private static ReadOnlySpan EncodedP => + [ + 1.351392e-02f, 1.351392e-02f, 1.351392e-02f, 1.351392e-02f, + -1.095778e+00f, -1.095778e+00f, -1.095778e+00f, -1.095778e+00f, + 5.522776e+01f, 5.522776e+01f, 5.522776e+01f, 5.522776e+01f, + 1.492516e+02f, 1.492516e+02f, 1.492516e+02f, 1.492516e+02f, + 4.838434e+01f, 4.838434e+01f, 4.838434e+01f, 4.838434e+01f + ]; + + private static ReadOnlySpan EncodedQ => + [ + 1.012416e+00f, 1.012416e+00f, 1.012416e+00f, 1.012416e+00f, + 2.016708e+01f, 2.016708e+01f, 2.016708e+01f, 2.016708e+01f, + 9.263710e+01f, 9.263710e+01f, 9.263710e+01f, 9.263710e+01f, + 1.120607e+02f, 1.120607e+02f, 1.120607e+02f, 1.120607e+02f, + 2.590418e+01f, 2.590418e+01f, 2.590418e+01f, 2.590418e+01f + ]; + + private static ReadOnlySpan EncodedPLo => + [ + 9.863406e-06f, 9.863406e-06f, 9.863406e-06f, 9.863406e-06f, + 3.881234e-01f, 3.881234e-01f, 3.881234e-01f, 3.881234e-01f, + 1.352821e+02f, 1.352821e+02f, 1.352821e+02f, 1.352821e+02f, + 6.889862e+04f, 6.889862e+04f, 6.889862e+04f, 6.889862e+04f, + -2.864824e+05f, -2.864824e+05f, -2.864824e+05f, -2.864824e+05f + ]; + + private static ReadOnlySpan EncodedQLo => + [ + 3.371868e+01f, 3.371868e+01f, 3.371868e+01f, 3.371868e+01f, + 1.477719e+03f, 1.477719e+03f, 1.477719e+03f, 1.477719e+03f, + 1.608477e+04f, 1.608477e+04f, 1.608477e+04f, 1.608477e+04f, + -4.389884e+04f, -4.389884e+04f, -4.389884e+04f, -4.389884e+04f, + -2.072546e+05f, -2.072546e+05f, -2.072546e+05f, -2.072546e+05f + ]; + + public static double DisplayFromEncoded(float displayIntensityTarget, double e) + { + if (e == 0.0) + { + return 0.0; + } + + double originalSign = e; + + e = Math.Abs(e); + + double xp = Math.Pow(e, 1.0 / M2); + double num = Math.Max(xp - C1, 0.0); + double den = C2 - (C3 * xp); + double d = Math.Pow(num / den, 1.0 / M1); + + return Math.CopySign(d * (10000.0 / displayIntensityTarget), originalSign); + } + + public static double EncodedFromDisplay(float displayIntensityTarget, double d) + { + if (d == 0.0) + { + return 0.0; + } + + double originalSign = d; + + d = Math.Abs(d); + + double xp = Math.Pow(d * (displayIntensityTarget * (1 / 10000)), M1); + double num = C1 + (xp * C2); + double den = 1.0 + (xp * C3); + double e = Math.Pow(num / den, M2); + + return Math.CopySign(e, originalSign); + } + + public Vector DisplayFromEncoded(Vector x) + { + Vector sign = Vector.Create(0x80000000u).As(); + Vector originalSign = x & sign; + x = Vector.AndNot(sign, x); + + Vector xpxx = (x * x) + x; + Vector magnitude = EvaluateRationalPolynomial(xpxx, DisplayP, DisplayQ); + + return Vector.AndNot(sign, magnitude * Vector.Create(this.scaleFactorFrom10000Nits)) | originalSign; + } + + public Vector EncodedFromDisplay(Vector x) + { + Vector sign = Vector.Create(0x80000000u).As(); + Vector originalSign = x & sign; + x = Vector.AndNot(sign, x); + + + Vector xto025 = Vector.SquareRoot(Vector.SquareRoot(x * Vector.Create(this.scaleFactorTo10000Nits))); + + Vector magnitude = Vector.ConditionalSelect( + Vector.LessThan(x, Vector.Create(1e-4f)), + EvaluateRationalPolynomial(xto025, EncodedPLo, EncodedQLo), + EvaluateRationalPolynomial(xto025, EncodedP, EncodedQ)); + + return Vector.AndNot(sign, magnitude) | originalSign; + } +} From f2ccbacb4368ffdfba55b0bd765883ab19a36eba Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 7 Sep 2026 00:51:19 +0400 Subject: [PATCH 149/328] Remove double emptyline --- .../Formats/Jxl/Cms/TransferFunctions/JxlPqTransferFunction.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlPqTransferFunction.cs b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlPqTransferFunction.cs index 654d05429b..f0df5c00c7 100644 --- a/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlPqTransferFunction.cs +++ b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlPqTransferFunction.cs @@ -129,7 +129,6 @@ public Vector EncodedFromDisplay(Vector x) Vector originalSign = x & sign; x = Vector.AndNot(sign, x); - Vector xto025 = Vector.SquareRoot(Vector.SquareRoot(x * Vector.Create(this.scaleFactorTo10000Nits))); Vector magnitude = Vector.ConditionalSelect( From e315e04bc503c3c7726433d8e20328e8f121d943 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 7 Sep 2026 00:53:26 +0400 Subject: [PATCH 150/328] Merge HLG base/derived class into one; abbreviate transfer function class name --- ...ctionBase.cs => JxlHlgTransferFunction.cs} | 71 +++++++++++++++---- .../JxlHybridLogGammaTransferFunction.cs | 53 -------------- 2 files changed, 58 insertions(+), 66 deletions(-) rename src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/{JxlHybridLogGammaTransferFunctionBase.cs => JxlHlgTransferFunction.cs} (52%) delete mode 100644 src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlHybridLogGammaTransferFunction.cs diff --git a/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlHybridLogGammaTransferFunctionBase.cs b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlHlgTransferFunction.cs similarity index 52% rename from src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlHybridLogGammaTransferFunctionBase.cs rename to src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlHlgTransferFunction.cs index 795db106c3..053a2a5cfb 100644 --- a/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlHybridLogGammaTransferFunctionBase.cs +++ b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlHlgTransferFunction.cs @@ -1,33 +1,78 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; +using SixLabors.ImageSharp.Formats.Jxl.Processing; + namespace SixLabors.ImageSharp.Formats.Jxl.Cms.TransferFunctions; /// -/// Base class for HLG transfer function. +/// Hybrid Log Gamma transfer function. /// -internal abstract class JxlHybridLogGammaTransferFunctionBase +internal sealed class JxlHlgTransferFunction { // Shared constants used by transfer functions - protected const float A = 0.17883277f; - protected const float RA = 1.0f / A; - protected const float B = 1 - (4 * A); - protected const float C = 0.5599107295f; - protected const float Inverse12 = 1.0f / 12.0f; + private const float A = 0.17883277f; + private const float RA = 1.0f / A; + private const float B = 1 - (4 * A); + private const float C = 0.5599107295f; + private const float Inverse12 = 1.0f / 12.0f; + private const float HiAdd = B * Inverse12; + private const float HiMul = 0.003639807079052639f; // MathF.Exp(-C * RA) * Inverse12 + private const float HiPow = 8.067285659607931f; // RA * JxlMath.InverseLog2E + + /// + /// Initializes a new instance of the class. + /// + /// + /// Use static methods. Don't instantiate this class. + /// + private JxlHlgTransferFunction() + { + } + + public static Vector EncodedFromDisplay(Vector x) + { + Vector sign = Vector.Create(0x80000000u).As(); + Vector originalSign = x & sign; + x = Vector.AndNot(sign, x); + Vector belowInverse12 = Vector.LessThan(x, Vector.Create(Inverse12)); + + Vector lo = Vector.SquareRoot(Vector.Create(3.0f) * x); + Vector hi = (Vector.Create(A * JxlMath.InverseLog2E) * Vector.Log2((Vector.Create(12f) * x) + Vector.Create(-B))) + Vector.Create(C); + Vector magnitude = Vector.ConditionalSelect(belowInverse12, lo, hi); + return Vector.AndNot(sign, magnitude) | originalSign; + } + + public static Vector DisplayFromEncoded(Vector x) + { + Vector sign = Vector.Create(0x80000000u).As(); + Vector originalSign = x & sign; + x = Vector.AndNot(sign, x); + Vector below05 = Vector.LessThan(x, Vector.Create(0.5f)); + + Vector lo = x * (x * Vector.Create(1f / 3f)); + Vector hi = (Pow2(x * Vector.Create(HiPow)) * Vector.Create(HiMul)) + Vector.Create(HiAdd); + Vector magnitude = Vector.ConditionalSelect(below05, lo, hi); + + return Vector.AndNot(sign, magnitude) | originalSign; + } + + private static Vector Pow2(Vector x) => JxlSimdUtils.FastPow2f(x); /// /// Converts encoded signal to display signal. /// /// The encoded signal /// The display signal - protected static double DisplayFromEncoded(double encoded) => Ootf(InverseOotf(encoded)); + public static double DisplayFromEncoded(double encoded) => Ootf(InverseOotf(encoded)); /// /// Converts display signal to encoded signal. /// /// The display signal /// The encoded signal - protected static double EncodedFromDisplay(double display) => Oetf(InverseOetf(display)); + public static double EncodedFromDisplay(double display) => Oetf(InverseOetf(display)); /// /// Opto-Electronic Transfer Function - converts @@ -39,7 +84,7 @@ internal abstract class JxlHybridLogGammaTransferFunctionBase /// /// Scene light /// Digital video signal - private static double Oetf(double s) + public static double Oetf(double s) { if (s == 0) { @@ -70,7 +115,7 @@ private static double Oetf(double s) /// /// Digital video signal /// Scene light - private static double InverseOetf(double e) + public static double InverseOetf(double e) { if (e == 0) { @@ -100,7 +145,7 @@ private static double InverseOetf(double e) /// /// Input signal /// Digital video signal - private static double Ootf(double s) => s; + public static double Ootf(double s) => s; /// /// Inverse Opto-Optical Transfer Function - as-is. @@ -110,5 +155,5 @@ private static double InverseOetf(double e) /// /// Digital video signal /// Scene light - private static double InverseOotf(double s) => s; + public static double InverseOotf(double s) => s; } diff --git a/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlHybridLogGammaTransferFunction.cs b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlHybridLogGammaTransferFunction.cs deleted file mode 100644 index 3957c3ac7b..0000000000 --- a/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlHybridLogGammaTransferFunction.cs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using SixLabors.ImageSharp.Formats.Jxl.Processing; - -namespace SixLabors.ImageSharp.Formats.Jxl.Cms.TransferFunctions; - -internal sealed class JxlHybridLogGammaTransferFunction : JxlHybridLogGammaTransferFunctionBase -{ - private const float HiAdd = B * Inverse12; - private const float HiMul = 0.003639807079052639f; // MathF.Exp(-C * RA) * Inverse12 - private const float HiPow = 8.067285659607931f; // RA * JxlMath.InverseLog2E - - /// - /// Initializes a new instance of the class. - /// - /// - /// Use static methods. Don't instantiate this class. - /// - private JxlHybridLogGammaTransferFunction() - { - } - - public static Vector EncodedFromDisplay(Vector x) - { - Vector sign = Vector.Create(0x80000000u).As(); - Vector originalSign = x & sign; - x = Vector.AndNot(sign, x); - Vector belowInverse12 = Vector.LessThan(x, Vector.Create(Inverse12)); - - Vector lo = Vector.SquareRoot(Vector.Create(3.0f) * x); - Vector hi = (Vector.Create(A * JxlMath.InverseLog2E) * Vector.Log2((Vector.Create(12f) * x) + Vector.Create(-B))) + Vector.Create(C); - Vector magnitude = Vector.ConditionalSelect(belowInverse12, lo, hi); - return Vector.AndNot(sign, magnitude) | originalSign; - } - - public static Vector DisplayFromEncoded(Vector x) - { - Vector sign = Vector.Create(0x80000000u).As(); - Vector originalSign = x & sign; - x = Vector.AndNot(sign, x); - Vector below05 = Vector.LessThan(x, Vector.Create(0.5f)); - - Vector lo = x * (x * Vector.Create(1f / 3f)); - Vector hi = (Pow2(x * Vector.Create(HiPow)) * Vector.Create(HiMul)) + Vector.Create(HiAdd); - Vector magnitude = Vector.ConditionalSelect(below05, lo, hi); - - return Vector.AndNot(sign, magnitude) | originalSign; - } - - private static Vector Pow2(Vector x) => x * x; -} From cebed59b8529cdb50e44cb17af6738d187dfc3dd Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 7 Sep 2026 15:18:00 +0400 Subject: [PATCH 151/328] Implement all transfer functions, add unpremultiply alpha to write to output stage, specify enum type as byte --- .../JxlBt709TransferFunction.cs | 41 +++++++- .../JxlSRgbTransferFunction.cs | 82 +++++++++++++++ .../Cms/TransferFunctions/JxlTransferUtils.cs | 71 +++++++++++++ .../Formats/Jxl/Processing/JxlUnsafe.cs | 17 ++++ .../Modular/Encoding/JxlMaEncoder.cs | 2 +- .../RenderPipeline/WriteToOutputStage.cs | 99 ++++++++++++++++++- 6 files changed, 308 insertions(+), 4 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlSRgbTransferFunction.cs create mode 100644 src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlTransferUtils.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlUnsafe.cs diff --git a/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlBt709TransferFunction.cs b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlBt709TransferFunction.cs index 99b6548b60..dfc9de66ea 100644 --- a/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlBt709TransferFunction.cs +++ b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlBt709TransferFunction.cs @@ -1,6 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; +using SixLabors.ImageSharp.Formats.Jxl.Processing; + namespace SixLabors.ImageSharp.Formats.Jxl.Cms.TransferFunctions; /// @@ -8,13 +11,47 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Cms.TransferFunctions; /// internal static class JxlBt709TransferFunction { - public static double EncodedFromDisplay(double d) + // Encoded From Display constants + private const float Threshold = 0.018f; + private const float MulLow = 4.5f; + private const float MulHi = 1.099f; + private const float PowHi = 0.45f; + private const float Sub = -0.099f; + + // Display From Encoded constants + private const float InverseThreshold = 0.081f; + private const float InverseMulLow = 1f / 4.5f; + private const float InverseMulHi = 1f / 1.099f; + private const float InversePowHi = 1f / 0.45f; + private const float InverseAdd = 0.099f * InverseMulHi; + + public static float EncodedFromDisplay(float d) { if (d < Threshold) { return MulLow * d; } - return (MulHi * Math.Pow(d, PowHi)) + Sub; + return (MulHi * MathF.Pow(d, PowHi)) + Sub; + } + + public static Vector EncodedFromDisplay(Vector x) + { + Vector low = Vector.Create(MulLow) * x; + Vector high = (Vector.Create(MulHi) * JxlSimdUtils.FastPowf(x, Vector.Create(PowHi))) + Vector.Create(Sub); + return Vector.ConditionalSelect( + Vector.LessThanOrEqual(x, Vector.Create(Threshold)), + low, + high); + } + + public static Vector DisplayFromEncoded(Vector x) + { + Vector low = Vector.Create(InverseMulLow) * x; + Vector high = JxlSimdUtils.FastPowf((x * Vector.Create(InverseMulHi)) + Vector.Create(InverseAdd), Vector.Create(InversePowHi)); + return Vector.ConditionalSelect( + Vector.LessThan(x, Vector.Create(InverseThreshold)), + low, + high); } } diff --git a/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlSRgbTransferFunction.cs b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlSRgbTransferFunction.cs new file mode 100644 index 0000000000..adc817ff78 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlSRgbTransferFunction.cs @@ -0,0 +1,82 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms.TransferFunctions; + +internal static class JxlSRgbTransferFunction +{ + private const float ThresholdSRGBToLinear = 0.04045f; + private const float ThresholdLinearToSRGB = 0.0031308f; + private const float LowDiv = 12.92f; + private const float LowDivInverse = 1.0f / LowDiv; + + private static ReadOnlySpan DisplayP => + [ + 2.200248328e-04f, 2.200248328e-04f, 2.200248328e-04f, 2.200248328e-04f, + 1.043637593e-02f, 1.043637593e-02f, 1.043637593e-02f, 1.043637593e-02f, + 1.624820318e-01f, 1.624820318e-01f, 1.624820318e-01f, 1.624820318e-01f, + 7.961564959e-01f, 7.961564959e-01f, 7.961564959e-01f, 7.961564959e-01f, + 8.210152774e-01f, 8.210152774e-01f, 8.210152774e-01f, 8.210152774e-01f + ]; + + private static ReadOnlySpan DisplayQ => + [ + 2.631846970e-01f, 2.631846970e-01f, 2.631846970e-01f, 2.631846970e-01f, + 1.076976492e+00f, 1.076976492e+00f, 1.076976492e+00f, 1.076976492e+00f, + 4.987528350e-01f, 4.987528350e-01f, 4.987528350e-01f, 4.987528350e-01f, + -5.512498495e-02f, -5.512498495e-02f, -5.512498495e-02f, -5.512498495e-02f, + 6.521209011e-03f, 6.521209011e-03f, 6.521209011e-03f, 6.521209011e-03f + ]; + + private static ReadOnlySpan EncodedP => + [ + -5.135152395e-04f, -5.135152395e-04f, -5.135152395e-04f, -5.135152395e-04f, + 5.287254571e-03f, 5.287254571e-03f, 5.287254571e-03f, 5.287254571e-03f, + 3.903842876e-01f, 3.903842876e-01f, 3.903842876e-01f, 3.903842876e-01f, + 1.474205315e+00f, 1.474205315e+00f, 1.474205315e+00f, 1.474205315e+00f, + 7.352629620e-01f, 7.352629620e-01f, 7.352629620e-01f, 7.352629620e-01f + ]; + + private static ReadOnlySpan EncodedQ => + [ + 1.004519624e-02f, 1.004519624e-02f, 1.004519624e-02f, 1.004519624e-02f, + 3.036675394e-01f, 3.036675394e-01f, 3.036675394e-01f, 3.036675394e-01f, + 1.340816930e+00f, 1.340816930e+00f, 1.340816930e+00f, 1.340816930e+00f, + 9.258482155e-01f, 9.258482155e-01f, 9.258482155e-01f, 9.258482155e-01f, + 2.424867759e-02f, 2.424867759e-02f, 2.424867759e-02f, 2.424867759e-02f + ]; + + public static Vector DisplayFromEncoded(Vector x) + { + Vector sign = Vector.Create(0x80000000u).As(); + Vector originalSign = x & sign; + x = Vector.AndNot(sign, x); + + Vector linear = x * Vector.Create(LowDivInverse); + Vector poly = EvaluateRationalPolynomial(x, DisplayP, DisplayQ); + Vector magnitude = Vector.ConditionalSelect( + Vector.GreaterThan(x, Vector.Create(ThresholdSRGBToLinear)), + poly, + linear); + + return Vector.AndNot(sign, magnitude) | originalSign; + } + + public static Vector EncodedFromDisplay(Vector x) + { + Vector sign = Vector.Create(0x80000000u).As(); + Vector originalSign = x & sign; + x = Vector.AndNot(sign, x); + + Vector linear = x * Vector.Create(LowDiv); + Vector poly = EvaluateRationalPolynomial(Vector.SquareRoot(x), DisplayP, DisplayQ); + Vector magnitude = Vector.ConditionalSelect( + Vector.GreaterThan(x, Vector.Create(ThresholdLinearToSRGB)), + poly, + linear); + + return Vector.AndNot(sign, magnitude) | originalSign; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlTransferUtils.cs b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlTransferUtils.cs new file mode 100644 index 0000000000..8b9760c73d --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlTransferUtils.cs @@ -0,0 +1,71 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms.TransferFunctions; + +internal static class JxlTransferUtils +{ + public static Vector FastLinearToSrgb(Vector v) + { + Vector v025_05 = ((v.As() & new Vector(0x3effffff)) | new Vector(0x3e800000)).As(); + + Vector d1 = (v025_05 * Vector.Create(0.059914046f)) - Vector.Create(0.108894556f); + Vector d2 = (d1 * v025_05) + Vector.Create(0.107963754f); + Vector pow = (d2 * v025_05) + Vector.Create(0.018092343f); + + const uint baseBits = 0x40000000; + + ReadOnlySpan powers25to18 = + [ + 0x00, 0x0a, 0x19, 0x26, + 0x32, 0x41, 0x4d, 0x5c, + 0x68, 0x75, 0x83, 0x8f, + 0xa0, 0xaa, 0xb9, 0xc6 + ]; + + ReadOnlySpan powers17to10 = + [ + 0x00, 0xb7, 0x04, 0x0d, + 0xcb, 0xe7, 0x41, 0x68, + 0x51, 0xd1, 0xeb, 0xf2, + 0x00, 0xb7, 0x04, 0x0d + ]; + + Vector bits = Vector.AsVectorInt32(v); + Vector exp = (bits >> 23) - Vector.Create(118); + + exp &= new Vector(0xf); + + Vector p25to18 = GatherByteTable(exp, powers25to18); + Vector p17to10 = GatherByteTable(exp, powers17to10); + + Vector mulBits = + (p25to18 << 18) | + (p17to10 << 10) | + new Vector((int)baseBits); + + Vector mul = Vector.AsVectorSingle(mulBits); + + Vector cutoff = new(0.0031308f); + + return Vector.ConditionalSelect( + Vector.LessThan(v, cutoff), + v * new Vector(12.92f), + (pow * mul) - new Vector(0.055f)); + } + + private static Vector GatherByteTable(Vector indices, ReadOnlySpan table) + { + int count = Vector.Count; + Span result = stackalloc int[count]; + + for (int i = 0; i < count; i++) + { + result[i] = table[indices[i]]; + } + + return new Vector(result); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlUnsafe.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlUnsafe.cs new file mode 100644 index 0000000000..5060a08c66 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlUnsafe.cs @@ -0,0 +1,17 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal static unsafe class JxlUnsafe +{ + public static bool IsSimdAligned(T* ptr) + where T : unmanaged + => IsAligned(ptr, Vector.Count * sizeof(T)); + + public static bool IsAligned(T* ptr, int alignment) + where T : unmanaged + => ((long)ptr % alignment) == 0; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaEncoder.cs index 009cb49c6e..115136b1bb 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaEncoder.cs @@ -17,7 +17,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding; internal static class JxlMaEncoder { - internal enum IntersectionType + internal enum IntersectionType : byte { None, Partial, diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs index 2d1b989c9a..62ae1a00fa 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs @@ -4,7 +4,6 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; @@ -14,6 +13,23 @@ internal sealed class WriteToOutputStage { private const int ChunkSize = 1024; + private int width; + private int height; + private IJxlImageOutput main; + private int numColors; + private bool wantAlpha; + private bool hasAlpha; + private bool unpremultiplyAlpha; + private int alphaC; + private bool flipX; + private bool flipY; + private bool transpose; + private readonly List extraChannels = []; + private readonly List opaqueAlpha = []; + private readonly Configuration configuration; + private List> tempIn; + private List> tempOut; + /// /// Gets the 32x32 blue noise dithering pattern lookup /// table (from ), @@ -234,4 +250,85 @@ orientation is JxlExifOrientation.Transpose or JxlExifOrientation.Rotate90 or JxlExifOrientation.Rotate270 or JxlExifOrientation.AntiTranspose; + + private unsafe void UnpremultiplyAlpha(int threadId, int len, float** lineBuffers) + { + // Highly unsafe code! ⚠️ + float** tempIn = stackalloc float*[4]; + Vector one = Vector.One; + + for (int c = 0; c < this.main.PixelFormat.Channels; ++c) + { + // size_t tix = thread_id * main_.num_channels_ + c; + // temp_in[c] = temp_in_[tix].address(); + // memcpy(temp_in[c], line_buffers[c], sizeof(float) * len); + int tix = (threadId * this.main.PixelFormat.Channels) + c; + + tempIn[c] = (float*)Unsafe.AsPointer(ref MemoryMarshal.Cast(this.tempIn[tix].Span)[0]); + + MemoryMarshal.CreateSpan(ref Unsafe.AsRef(lineBuffers[c]), len) + .CopyTo(MemoryMarshal.CreateSpan(ref Unsafe.AsRef(tempIn[c]), len)); + } + + Vector smallAlpha = Vector.Create(SmallAlpha); + + for (int ix = 0; ix < len; ix += Vector.Count) + { + float* ptr = tempIn[this.numColors + ix]; + + // Using an aligned and unaligned branch + // REVIEW: does the branch outweigh alignment? we will have to benchmark this + // when the codec can build + if (JxlUnsafe.IsSimdAligned(ptr)) + { + Vector alpha = Vector.LoadAlignedNonTemporal(tempIn[this.numColors] + ix); + Vector mul = one / Vector.Max(smallAlpha, alpha); + + for (int c = 0; c < this.numColors; ++c) + { + float* currPtr = tempIn[c] + ix; + + if (JxlUnsafe.IsSimdAligned(currPtr)) + { + Vector val = Vector.LoadAlignedNonTemporal(currPtr); + Vector.StoreAlignedNonTemporal(val * mul, currPtr); + } + else + { + Vector val = Vector.Load(currPtr); + Vector.Store(val * mul, currPtr); + } + } + } + else + { + Vector alpha = Vector.Load(tempIn[this.numColors] + ix); + Vector mul = one / Vector.Max(smallAlpha, alpha); + + for (int c = 0; c < this.numColors; ++c) + { + float* currPtr = tempIn[c] + ix; + + if (JxlUnsafe.IsSimdAligned(currPtr)) + { + Vector val = Vector.LoadAlignedNonTemporal(currPtr); + Vector.StoreAlignedNonTemporal(val * mul, currPtr); + } + else + { + Vector val = Vector.Load(currPtr); + Vector.Store(val * mul, currPtr); + } + } + } + } + + for (int c = 0; c < this.main.PixelFormat.Channels; c++) + { + fixed (byte* ptr = this.tempIn[c].Span) + { + lineBuffers[c] = (float*)ptr; + } + } + } } From ab43e7b4aec2b301b009ed0750837e7af13c7316 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 7 Sep 2026 21:28:09 +0400 Subject: [PATCH 152/328] Add tests, field writes, update folder structure, add group border assigner, complete noise decoder, XYB encoder, reference DCT --- .../Formats/Jxl/Cms/JxlOpsinConstants.cs | 2 + .../Formats/Jxl/Fields/JxlBitsCoder.cs | 10 + .../Formats/Jxl/Fields/JxlBundle.cs | 84 ++++ .../Formats/Jxl/Fields/JxlF16Coder.cs | 66 +++ .../Formats/Jxl/Fields/JxlU32Coder.cs | 31 +- .../Formats/Jxl/Fields/JxlU64Coder.cs | 53 ++ .../Jxl/Processing/Blending/JxlBlending.cs | 2 +- .../Decoder/{ => Ans}/JxlAnsCode.cs | 2 +- .../Decoder/{ => Ans}/JxlAnsReader.cs | 2 +- .../Decoder/{ => Ans}/JxlAnsSymbolReader.cs | 2 +- .../Jxl/Processing/Decoder/JxlBitReader.cs | 16 +- .../Decoder/JxlGroupBorderAssigner.cs | 330 +++++++++++++ .../Jxl/Processing/Decoder/JxlNoiseDecoder.cs | 86 ++++ .../Decoder/JxlPassesDecoderState.cs | 1 + .../Decoder/Modular/JxlModularFrameDecoder.cs | 25 + .../Decoder/{ => Patch}/JxlPatchBlendMode.cs | 2 +- .../Decoder/{ => Patch}/JxlPatchBlending.cs | 2 +- .../Decoder/{ => Patch}/JxlPatchDictionary.cs | 3 +- .../Decoder/{ => Patch}/JxlPatchPosition.cs | 2 +- .../{ => Patch}/JxlPatchReferencePosition.cs | 2 +- .../Processing/Encoder/JxlLinearAlgebra.cs | 2 +- .../Jxl/Processing/Encoder/JxlXybEncoder.cs | 466 ++++++++++++++++++ .../Jxl/Processing/Image/JxlImageFeatures.cs | 2 +- .../Processing/Image/JxlImageOperations.cs | 14 + .../Jxl/Processing/JxlCoefficientOrder.cs | 1 + .../Formats/Jxl/Processing/JxlConvolve.cs | 4 +- .../Modular/Encoding/JxlMaDecoder.cs | 1 + .../Modular/Encoding/JxlModularEncoding.cs | 1 + .../Jxl/Processing/Primitives/JxlXorShift.cs | 2 +- .../Processing/Primitives/RectangleUtils.cs | 22 +- .../RenderPipeline/PatchDictionaryStage.cs | 2 +- .../Processing/Splines/JxlQuantizedSpline.cs | 1 + .../Jxl/Processing/Splines/JxlSplines.cs | 1 + .../Jxl/Processing/AlphaBlendingTests.cs | 332 +++++++++++++ .../Processing/Encoder/LinearAlgebraTests.cs | 169 +++++++ .../Jxl/Processing/EntropyCoderTests.cs | 105 ++++ .../Formats/Jxl/Processing/GaborishTests.cs | 113 +++++ .../Jxl/Processing/ImageOperationsTests.cs | 146 ++++++ .../Formats/Jxl/TestUtils/TestDCT.cs | 108 ++++ 39 files changed, 2186 insertions(+), 29 deletions(-) rename src/ImageSharp/Formats/Jxl/Processing/Decoder/{ => Ans}/JxlAnsCode.cs (96%) rename src/ImageSharp/Formats/Jxl/Processing/Decoder/{ => Ans}/JxlAnsReader.cs (99%) rename src/ImageSharp/Formats/Jxl/Processing/Decoder/{ => Ans}/JxlAnsSymbolReader.cs (92%) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlGroupBorderAssigner.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/Modular/JxlModularFrameDecoder.cs rename src/ImageSharp/Formats/Jxl/Processing/Decoder/{ => Patch}/JxlPatchBlendMode.cs (78%) rename src/ImageSharp/Formats/Jxl/Processing/Decoder/{ => Patch}/JxlPatchBlending.cs (74%) rename src/ImageSharp/Formats/Jxl/Processing/Decoder/{ => Patch}/JxlPatchDictionary.cs (98%) rename src/ImageSharp/Formats/Jxl/Processing/Decoder/{ => Patch}/JxlPatchPosition.cs (73%) rename src/ImageSharp/Formats/Jxl/Processing/Decoder/{ => Patch}/JxlPatchReferencePosition.cs (76%) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlXybEncoder.cs create mode 100644 tests/ImageSharp.Tests/Formats/Jxl/Processing/AlphaBlendingTests.cs create mode 100644 tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/LinearAlgebraTests.cs create mode 100644 tests/ImageSharp.Tests/Formats/Jxl/Processing/EntropyCoderTests.cs create mode 100644 tests/ImageSharp.Tests/Formats/Jxl/Processing/GaborishTests.cs create mode 100644 tests/ImageSharp.Tests/Formats/Jxl/Processing/ImageOperationsTests.cs create mode 100644 tests/ImageSharp.Tests/Formats/Jxl/TestUtils/TestDCT.cs diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlOpsinConstants.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlOpsinConstants.cs index a0029c0c27..2b304885d1 100644 --- a/src/ImageSharp/Formats/Jxl/Cms/JxlOpsinConstants.cs +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlOpsinConstants.cs @@ -21,4 +21,6 @@ internal static class JxlOpsinConstants public const float OpsinAbsorbanceBias0 = 0.0037930732552754493f; public const float OpsinAbsorbanceBias1 = OpsinAbsorbanceBias0; public const float OpsinAbsorbanceBias2 = OpsinAbsorbanceBias0; + + public static ReadOnlySpan OpsinAbsorbanceBias => [OpsinAbsorbanceBias0, OpsinAbsorbanceBias1, OpsinAbsorbanceBias2]; } diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs index 0b2640467b..3e4a2b6676 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs @@ -31,6 +31,16 @@ public static bool CanEncode(int bits, uint value, ref int encodedBits) return true; } + public static void Write(int bits, uint value, JxlBitWriter writer) + { + if (value >= (1u << bits)) + { + throw new InvalidOperationException("Value is too large to encode in " + bits + " bits"); + } + + writer.Write(bits, value); + } + // NOTE: BitsCoder::Read (fields.cc:418) returns a uint32_t, // suggesting the input bit size does not exceed 32 bits. public static uint Read(uint bits, JxlBitReader reader) => reader.ReadBits32(bits); diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs index 7202932539..94e5aa627c 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs @@ -1,7 +1,12 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; +using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Processing; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; namespace SixLabors.ImageSharp.Formats.Jxl.Fields; @@ -74,6 +79,23 @@ public static bool Read(JxlBitReader reader, IJxlFields fields) return visitor.OK; } + public static bool CanEncode(IJxlFields fields, ref int extensionBits, ref long totalBits) + { + JxlCanEncodeVisitor canEncodeVisitor = new(); + + if (!canEncodeVisitor.Visit(fields)) + { + return false; + } + + if (!canEncodeVisitor.GetSizes(ref extensionBits, ref totalBits)) + { + return false; + } + + return true; + } + /// /// Tries to read the fields from a bit-reader. /// @@ -86,4 +108,66 @@ public static bool CanRead(JxlBitReader reader, IJxlFields fields) _ = visitor.Visit(fields); return visitor.OK; } + + public static bool Write(IJxlFields fields, JxlBitWriter writer, JxlLayerType layer, JxlAuxiliaryOutput auxOutput) + { + int extensionBits = 0; + long totalBits = 0; + if (!CanEncode(fields, ref extensionBits, ref totalBits)) + { + return false; + } + + return writer.WithMaxBits(totalBits, layer, auxOutput, () => + { + JxlWriteVisitor visitor = new(extensionBits, writer); + + if (!visitor.Visit(fields)) + { + return false; + } + + return visitor.OK(); + }); + } + + public static bool WriteCodestreamHeaders(JxlCodecMetadata metadata, JxlBitWriter writer, JxlAuxiliaryOutput auxOut) + { + // Marker/signature + if (!writer.WithMaxBits(16, JxlLayerType.Header, auxOut, () => + { + writer.Write(8, 0xFF); + writer.Write(8, JxlShared.CodestreamMarker); + return true; + })) + { + return false; + } + + if (!WriteSizeHeader(metadata.Size!, writer, JxlLayerType.Header, auxOut)) + { + return false; + } + + if (!WriteImageMetadata(metadata.ImageMetadata!, writer, JxlLayerType.Header, auxOut)) + { + return false; + } + + metadata.CustomTransformData!.NonserializedXybEncoded = metadata.ImageMetadata!.XybEncoded; + + return Write(metadata.CustomTransformData!, writer, JxlLayerType.Header, auxOut); + } + + public static bool WriteFrameHeader(JxlFrameHeader frame, JxlBitWriter writer, JxlAuxiliaryOutput auxOut) + => Write(frame, writer, JxlLayerType.Header, auxOut); + + public static bool WriteImageMetadata(JxlImageMetadata metadata, JxlBitWriter writer, JxlLayerType layer, JxlAuxiliaryOutput auxOut) + => Write(metadata, writer, layer, auxOut); + + public static bool WriteQuantizerParameters(JxlQuantizerParameters metadata, JxlBitWriter writer, JxlLayerType layer, JxlAuxiliaryOutput auxOut) + => Write(metadata, writer, layer, auxOut); + + public static bool WriteSizeHeader(JxlSizeHeader header, JxlBitWriter writer, JxlLayerType layer, JxlAuxiliaryOutput auxOut) + => Write(header, writer, layer, auxOut); } diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs index b930e5dc49..3da369a18e 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs @@ -66,4 +66,70 @@ public static bool Read(JxlBitReader reader, ref float value) return true; } + + public static bool Write(float value, JxlBitWriter writer) + { + uint bits32 = BitConverter.SingleToUInt32Bits(value); + + uint sign = bits32 >> 31; + uint biasedExp32 = (bits32 >> 23) & 0xFF; + uint mantissa32 = bits32 & 0x7FFFFF; + + int exp = (int)biasedExp32 - 127; + if (exp > 15) + { + throw new InvalidOperationException("Too big to encode, CanEncode should return false"); + } + + // Tiny or zero => zero. + if (exp < -24) + { + writer.Write(16, 0); + return true; + } + + uint biasedExp16 = 0; + uint mantissa16 = 0; + + if (exp < -14) + { + biasedExp16 = 0; + + uint subExp = unchecked((uint)(-14 - exp)); + + if (subExp is not (>= 1 and < 11)) + { + return false; + } + + mantissa16 = (1u << (int)(10 - subExp)) + (mantissa32 >> (int)(13 + subExp)); + } + else + { + // exp = [-14, 15] + biasedExp16 = unchecked((uint)(exp + 15)); + + if (biasedExp16 is not (>= 1 and < 31)) + { + return false; + } + + mantissa16 = mantissa32 >> 13; + } + + if (mantissa16 >= 1024) + { + return false; + } + + uint bits16 = (sign << 15) | (biasedExp16 << 10) | mantissa16; + + if (bits16 >= 0x10000) + { + return false; + } + + writer.Write(16, bits16); + return true; + } } diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs index bdfb456673..660b403887 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs @@ -73,7 +73,7 @@ public static uint Read(in JxlU32Enc enc, JxlBitReader reader) /// public static bool ChooseSelector(in JxlU32Enc enc, uint value, ref uint selector, ref int totalBits) { - int bitsRequired = 32 - JxlMath.Num0BitsAboveMS1Bit(value); + uint bitsRequired = 32 - JxlMath.Num0BitsAboveMS1Bit(value); if (bitsRequired > 32) { @@ -116,11 +116,38 @@ public static bool ChooseSelector(in JxlU32Enc enc, uint value, ref uint selecto if (totalBits == 64) { - DebugGuard.IsTrue(false, "No matching selector"); + throw new InvalidOperationException("No matching selector"); + } + return true; + } + + public static bool Write(in JxlU32Enc enc, uint value, JxlBitWriter writer) + { + uint selector = 0; + int totalBits = 0; + + if (!ChooseSelector(in enc, value, ref selector, ref totalBits)) + { return false; } + writer.Write(2, selector); + + JxlU32Distribution d = enc.GetDistribution((int)selector); + + if (!d.IsDirect) + { + uint offset = d.Offset; + + if (value < offset) + { + return false; + } + + writer.WriteBits(totalBits - 2, value - offset); + } + return true; } } diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs index 659b6f994d..8726c9a27b 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs @@ -99,6 +99,59 @@ public static bool CanEncode(ulong value, ref int encodedBits) return true; } + public static bool Write(ulong value, JxlBitWriter writer) + { + if (value == 0) + { + // Selector: use 0 bits, value 0 + writer.Write(2, 0); + } + else if (value <= 16) + { + // Selector: use 4 bits, value 1..16 + writer.Write(2, 1); + writer.Write(4, value - 1); + } + else if (value <= 272) + { + // Selector: use 8 bits, value 17..272 + writer.Write(2, 2); + writer.Write(8, value - 17); + } + else + { + // Selector: varint, first a 12-bit group, after that per 8-bit group. + writer.Write(2, 3); + writer.Write(12, value & 4095); + value >>= 12; + int shift = 12; + while (value > 0 && shift < 60) + { + // Indicate varint not done + writer.Write(1, 1); + writer.Write(8, value & 255); + value >>= 8; + shift += 8; + } + + if (value > 0) + { + // This only could happen if shift == N - 4. + writer.Write(1, 1); + writer.Write(4, value & 15); + + // Implicitly closed sequence, no extra stop bit is required. + } + else + { + // Indicate end of varint + writer.Write(1, 0); + } + } + + return true; + } + /// /// Always returns 73. /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlending.cs b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlending.cs index 7ddad4714e..a595406f36 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlending.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlending.cs @@ -4,7 +4,7 @@ using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; -using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Patch; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Blending; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsCode.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsCode.cs similarity index 96% rename from src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsCode.cs rename to src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsCode.cs index 56dbab4ed3..e5cd1f783b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsCode.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsCode.cs @@ -4,7 +4,7 @@ using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; internal sealed class JxlAnsCode { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsReader.cs similarity index 99% rename from src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs rename to src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsReader.cs index 180dbd3c1c..683ee27835 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsReader.cs @@ -5,7 +5,7 @@ using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; internal static class JxlAnsReader { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsSymbolReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsSymbolReader.cs similarity index 92% rename from src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsSymbolReader.cs rename to src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsSymbolReader.cs index 8c4a83ca2f..115b4fad68 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsSymbolReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsSymbolReader.cs @@ -3,7 +3,7 @@ using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; internal sealed class JxlAnsSymbolReader { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs index 33b7307d30..0f199f27cc 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs @@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; /// /// Represents a bitstream reader. /// -internal sealed class JxlBitReader(Stream stream) +internal class JxlBitReader(Stream stream) { private ulong buffer; private uint bufferRemainingBits; @@ -154,17 +154,17 @@ private uint ReadBits32Core(uint n, bool peek = false) } } - public uint ReadBits32(uint bits) => this.ReadBits32Core(bits, peek: false); + public virtual uint ReadBits32(uint bits) => this.ReadBits32Core(bits, peek: false); - public uint PeekBits32(uint bits) => this.ReadBits32Core(bits, peek: true); + public virtual uint PeekBits32(uint bits) => this.ReadBits32Core(bits, peek: true); - public void SkipBits32(uint bits) => _ = this.ReadBits32(bits); + public virtual void SkipBits32(uint bits) => _ = this.ReadBits32(bits); - public ulong ReadBits64(ulong bits) => this.ReadBits64Core((uint)bits, peek: false); + public virtual ulong ReadBits64(ulong bits) => this.ReadBits64Core((uint)bits, peek: false); - public ulong PeekBits64(ulong bits) => this.ReadBits64Core((uint)bits, peek: true); + public virtual ulong PeekBits64(ulong bits) => this.ReadBits64Core((uint)bits, peek: true); - public void SkipBits64(ulong bits) => _ = this.ReadBits64(bits); + public virtual void SkipBits64(ulong bits) => _ = this.ReadBits64(bits); - public bool ReadBoolean() => this.ReadBits32Core(1, peek: false) == 1; + public virtual bool ReadBoolean() => this.ReadBits32Core(1, peek: false) == 1; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlGroupBorderAssigner.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlGroupBorderAssigner.cs new file mode 100644 index 0000000000..3833c5ede2 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlGroupBorderAssigner.cs @@ -0,0 +1,330 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +internal sealed class JxlGroupBorderAssigner +{ + public const int MaxToFinalize = 3; + + private readonly JxlFrameDimensions frameDim; + private readonly Buffer2D counters; + + private const byte TopLeft = 0x01; + private const byte TopRight = 0x02; + private const byte BottomRight = 0x04; + private const byte BottomLeft = 0x08; + + public JxlGroupBorderAssigner(Configuration configuration, JxlFrameDimensions frameDim) + { + this.frameDim = frameDim; + + int xSize = frameDim.XSizeGroups; + int ySize = frameDim.YSizeGroups; + + int counterCount = (xSize + 1 + 7) / 8; + this.counters = configuration.MemoryAllocator.Allocate2D(ySize + 1, counterCount); + + void Set(int x, int y, uint corners) + { + int shift = 4 * (x & 7); + int bits = (int)(corners << shift); + this.counters[y, x / 8] |= bits; + } + + for (int x = 0; x < xSize + 1; x++) + { + Set(x, 0, TopLeft | TopRight); + Set(x, ySize, BottomLeft | BottomRight); + } + + for (int y = 0; y < ySize + 1; y++) + { + Set(0, y, TopLeft | BottomLeft); + Set(xSize, y, TopRight | BottomRight); + } + } + + public void ClearDone(int groupId) + { + void Clear(int x, int y, uint corners) + { + int shift = 4 * (x & 7); + int mask = ~(int)(corners << shift); + + ref int counter = ref this.counters[y, x / 8]; + _ = Interlocked.And(ref counter, mask); + } + + (int x, int y) = Math.DivRem(groupId, this.frameDim.XSizeGroups); + + Clear(x, y, BottomRight); + Clear(x + 1, y, BottomLeft); + Clear(x, y + 1, TopRight); + Clear(x + 1, y + 1, TopLeft); + } + + public void GroupDone(int groupId, int padx, int pady, Span rectsToFinalize, out int numToFinalize) + { + (int x, int y) = Math.DivRem(groupId, this.frameDim.XSizeGroups); + + Rectangle blockRect = RectangleUtils.CreateRectangle( + x * this.frameDim.GroupDimension / JxlFrameDimensions.BlockDimensions, + y * this.frameDim.GroupDimension / JxlFrameDimensions.BlockDimensions, + this.frameDim.GroupDimension / JxlFrameDimensions.BlockDimensions, + this.frameDim.GroupDimension / JxlFrameDimensions.BlockDimensions, + this.frameDim.XSizeBlocks, + this.frameDim.YSizeBlocks); + + int FetchStatus(int cx, int cy, uint bit) + { + int shift = 4 * (cx & 7); + int bits = (int)(bit << shift); + + ref int counter = ref this.counters[cy, cx / 8]; + + int status = Interlocked.Or(ref counter, bits); + status >>= shift; + + return (int)((bit | (uint)status) & 0xF); + } + + int topLeftStatus = FetchStatus(x, y, BottomRight); + int topRightStatus = FetchStatus(x + 1, y, BottomLeft); + int bottomRightStatus = FetchStatus(x + 1, y + 1, TopLeft); + int bottomLeftStatus = FetchStatus(x, y + 1, TopRight); + + int x1 = blockRect.X + blockRect.Width; + int y1 = blockRect.Y + blockRect.Height; + + bool isLastGroupX = this.frameDim.XSizeGroups == x + 1; + bool isLastGroupY = this.frameDim.YSizeGroups == y + 1; + + Span xpos = + [ + blockRect.X == 0 + ? 0 + : (blockRect.X * JxlFrameDimensions.BlockDimensions) - padx, + + blockRect.X == 0 + ? 0 + : Math.Min( + this.frameDim.XSize, + (blockRect.X * JxlFrameDimensions.BlockDimensions) + padx), + + isLastGroupX + ? this.frameDim.XSize + : (x1 * JxlFrameDimensions.BlockDimensions) - padx, + + Math.Min( + this.frameDim.XSize, + (x1 * JxlFrameDimensions.BlockDimensions) + padx) + ]; + + Span ypos = + [ + blockRect.Y == 0 + ? 0 + : (blockRect.Y * JxlFrameDimensions.BlockDimensions) - pady, + + blockRect.Y == 0 + ? 0 + : Math.Min( + this.frameDim.YSize, + (blockRect.Y * JxlFrameDimensions.BlockDimensions) + pady), + + isLastGroupY + ? this.frameDim.YSize + : (y1 * JxlFrameDimensions.BlockDimensions) - pady, + + Math.Min( + this.frameDim.YSize, + (y1 * JxlFrameDimensions.BlockDimensions) + pady) + ]; + + numToFinalize = 0; + + void AppendRect(int x0, int x1, int y0, int y1, ref int numToFinalize, Span rectsToFinalize, Span xpos, Span ypos) + { + Rectangle rect = new( + xpos[x0], + ypos[y0], + xpos[x1] - xpos[x0], + ypos[y1] - ypos[y0]); + + if (rect.Width == 0 || rect.Height == 0) + { + return; + } + + rectsToFinalize[numToFinalize++] = rect; + } + + bool[,] availablePartsMask = new bool[3, 3]; + + // Center + availablePartsMask[1, 1] = true; + + // Corners + if (topLeftStatus == 0xF) + { + availablePartsMask[0, 0] = true; + } + + if (topRightStatus == 0xF) + { + availablePartsMask[2, 0] = true; + } + + if (bottomRightStatus == 0xF) + { + availablePartsMask[2, 2] = true; + } + + if (bottomLeftStatus == 0xF) + { + availablePartsMask[0, 2] = true; + } + + // Other borders + if ((topLeftStatus & TopRight) != 0) + { + availablePartsMask[1, 0] = true; + } + + if ((topLeftStatus & BottomLeft) != 0) + { + availablePartsMask[0, 1] = true; + } + + if ((topRightStatus & BottomRight) != 0) + { + availablePartsMask[2, 1] = true; + } + + if ((bottomLeftStatus & BottomRight) != 0) + { + availablePartsMask[1, 2] = true; + } + + const int noSegment = 3; + + Span<(int First, int Last)> horizontalSegments = + [ + (noSegment, noSegment), + (noSegment, noSegment), + (noSegment, noSegment) + ]; + + for (int py = 0; py < 3; py++) + { + for (int px = 0; px < 3; px++) + { + if (!availablePartsMask[px, py]) + { + continue; + } + + if (horizontalSegments[py].First == noSegment) + { + horizontalSegments[py] = (px, horizontalSegments[py].Last); + } + + horizontalSegments[py] = (horizontalSegments[py].First, px + 1); + } + } + + if (horizontalSegments[0] == horizontalSegments[1] && + horizontalSegments[0] == horizontalSegments[2]) + { + AppendRect( + horizontalSegments[0].First, + horizontalSegments[0].Last, + 0, + 3, + ref numToFinalize, + rectsToFinalize, + xpos, + ypos); + } + else if (horizontalSegments[0] == horizontalSegments[1]) + { + AppendRect( + horizontalSegments[0].First, + horizontalSegments[0].Last, + 0, + 2, + ref numToFinalize, + rectsToFinalize, + xpos, + ypos); + + AppendRect( + horizontalSegments[2].First, + horizontalSegments[2].Last, + 2, + 3, + ref numToFinalize, + rectsToFinalize, + xpos, + ypos); + } + else if (horizontalSegments[1] == horizontalSegments[2]) + { + AppendRect( + horizontalSegments[0].First, + horizontalSegments[0].Last, + 0, + 1, + ref numToFinalize, + rectsToFinalize, + xpos, + ypos); + + AppendRect( + horizontalSegments[1].First, + horizontalSegments[1].Last, + 1, + 3, + ref numToFinalize, + rectsToFinalize, + xpos, + ypos); + } + else + { + AppendRect( + horizontalSegments[0].First, + horizontalSegments[0].Last, + 0, + 1, + ref numToFinalize, + rectsToFinalize, + xpos, + ypos); + + AppendRect( + horizontalSegments[1].First, + horizontalSegments[1].Last, + 1, + 2, + ref numToFinalize, + rectsToFinalize, + xpos, + ypos); + + AppendRect( + horizontalSegments[2].First, + horizontalSegments[2].Last, + 2, + 3, + ref numToFinalize, + rectsToFinalize, + xpos, + ypos); + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs index 1ddb429cba..47e685bdfc 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs @@ -1,11 +1,16 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Net.NetworkInformation; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Noise; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using static System.Net.Mime.MediaTypeNames; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; @@ -61,4 +66,85 @@ public static void GenerateRandomImage(JxlXorShift rng, Rectangle rectangle, Jxl } } } + + public static void Random3Planes( + int visibleFrameIndex, + int nonVisibleFrameIndex, + int x0, + int y0, + (JxlImageF Image, Rectangle Bounds) plane0, + (JxlImageF Image, Rectangle Bounds) plane1, + (JxlImageF Image, Rectangle Bounds) plane2) + { + JxlXorShift rng = new((uint)visibleFrameIndex, (uint)nonVisibleFrameIndex, (uint)x0, (uint)y0); + + GenerateRandomImage(rng, plane0.Bounds, plane0.Image); + GenerateRandomImage(rng, plane1.Bounds, plane1.Image); + GenerateRandomImage(rng, plane2.Bounds, plane2.Image); + } + + private static void PrepareNoiseInput( + JxlPassesDecoderState decState, + JxlFrameDimensions frameDim, + JxlFrameHeader frameHeader, + int groupIndex, + int thread) + { + int groupDim = frameDim.GroupDimension; + + int gx = groupIndex % frameDim.XSizeGroups; + int gy = groupIndex / frameDim.XSizeGroups; + + JxlRenderPipelineInput input = decState.RenderPipeline.GetInputBuffers(groupIndex, thread); + int noiseCStart = 3 + frameHeader.Metadata!.ImageMetadata!.ExtraChannelCount; + + InlineArray3<(JxlImageF Image, Rectangle Rect)> rects = default; + + for (int iy = 0; iy < frameHeader.Upsampling; iy++) + { + for (int ix = 0; ix < frameHeader.Upsampling; ix++) + { + for (int c = 0; c < 3; c++) + { + var r = input.GetBuffer(noiseCStart + c); + + int x1 = r.Rect.X0 + r.Rect.XSize; + int y1 = r.Rect.Y0 + r.Rect.YSize; + + rects[c] = ( + r.Image, + RectangleUtils.CreateRectangle( + r.Rect.X0 + (ix * groupDim), + r.Rect.Y0 + (iy * groupDim), + groupDim, + groupDim, + x1, + y1)); + } + + Random3Planes( + decState.VisibleFrameIndex, + decState.NonvisibleFrameIndex, + (int)((gx * frameHeader.Upsampling) + ix) * groupDim, + (int)((gy * frameHeader.Upsampling) + iy) * groupDim, + rects[0], + rects[1], + rects[2]); + } + } + } + + public static float DecodeFloatParam(float precision, JxlBitReader br) + { + int absValQuant = (int)br.ReadBits32(10); + return absValQuant / precision; + } + + public static void DecodeNoise(JxlBitReader br, JxlNoiseParameters noiseParams) + { + for (int i = 0; i < noiseParams.Lookup.Length; i++) + { + noiseParams.Lookup[i] = DecodeFloatParam(JxlNoiseConstants.Precision, br); + } + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs index 3e32e94a48..adc70f36c3 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs @@ -4,6 +4,7 @@ using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Modular/JxlModularFrameDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Modular/JxlModularFrameDecoder.cs new file mode 100644 index 0000000000..416a14b49a --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Modular/JxlModularFrameDecoder.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Modular; + +internal sealed class JxlModularFrameDecoder +{ + private readonly Configuration configuration; + private readonly JxlModularImage fullImage; + private readonly List globalTransform; + private JxlFrameDimensions frameDimensions; + private bool doColor; + private bool haveSomething; + private bool useFullImage = true; + private bool allSameShift; + private JxlAnsCode code; + private readonly List contextMap; + private JxlGroupHeader groupHeader; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchBlendMode.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchBlendMode.cs similarity index 78% rename from src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchBlendMode.cs rename to src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchBlendMode.cs index 755cfe4eb9..89ddd2caae 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchBlendMode.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchBlendMode.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Patch; internal enum JxlPatchBlendMode : byte { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchBlending.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchBlending.cs similarity index 74% rename from src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchBlending.cs rename to src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchBlending.cs index 7cedfc3053..ff7bda0d7b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchBlending.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchBlending.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Patch; internal struct JxlPatchBlending { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchDictionary.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchDictionary.cs similarity index 98% rename from src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchDictionary.cs rename to src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchDictionary.cs index d91c41e38e..096c0d3de2 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchDictionary.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchDictionary.cs @@ -1,10 +1,11 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Patch; internal sealed class JxlPatchDictionary { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchPosition.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchPosition.cs similarity index 73% rename from src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchPosition.cs rename to src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchPosition.cs index 264d694e85..6cb90a3ab0 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchPosition.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchPosition.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Patch; internal struct JxlPatchPosition { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchReferencePosition.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchReferencePosition.cs similarity index 76% rename from src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchReferencePosition.cs rename to src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchReferencePosition.cs index 8e11eb55ad..1d604ffe90 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchReferencePosition.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchReferencePosition.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Patch; internal struct JxlPatchReferencePosition { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlLinearAlgebra.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlLinearAlgebra.cs index 9b8e2f9fa6..be1f4a75b6 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlLinearAlgebra.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlLinearAlgebra.cs @@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; /// internal static class JxlLinearAlgebra { - public static void ConvertToDiagonal(Matrix2x2 a, Vector2 diag, Matrix2x2 u) + public static void ConvertToDiagonal(Matrix2x2 a, ref Vector2 diag, ref Matrix2x2 u) { DebugGuard.MustBeLessThan(Math.Abs(a[0][1] - a[1][0]), 1e-15, nameof(a)); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlXybEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlXybEncoder.cs new file mode 100644 index 0000000000..037eb46c1f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlXybEncoder.cs @@ -0,0 +1,466 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Numerics.Tensors; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats.Jxl.Cms; +using SixLabors.ImageSharp.Formats.Jxl.Cms.TransferFunctions; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; + +internal static class JxlXybEncoder +{ + private static void OpsinAbsorbance( + Vector r, + Vector g, + Vector b, + ReadOnlySpan premulAbsorb, + out Vector mixed0, + out Vector mixed1, + out Vector mixed2) + { + ReadOnlySpan bias = JxlOpsinConstants.OpsinAbsorbanceBias; + + mixed0 = Vector.FusedMultiplyAdd( + Vector.Create(premulAbsorb), + r, + Vector.FusedMultiplyAdd( + Vector.Create(premulAbsorb[8..]), + g, + Vector.FusedMultiplyAdd( + Vector.Create(premulAbsorb[16..]), + b, + Vector.Create(bias[0])))); + + mixed1 = Vector.FusedMultiplyAdd( + Vector.Create(premulAbsorb[24..]), + r, + Vector.FusedMultiplyAdd( + Vector.Create(premulAbsorb[32..]), + g, + Vector.FusedMultiplyAdd( + Vector.Create(premulAbsorb[40..]), + b, + Vector.Create(bias[1])))); + + mixed2 = Vector.FusedMultiplyAdd( + Vector.Create(premulAbsorb[48..]), + r, + Vector.FusedMultiplyAdd( + Vector.Create(premulAbsorb[56..]), + g, + Vector.FusedMultiplyAdd( + Vector.Create(premulAbsorb[64..]), + b, + Vector.Create(bias[2])))); + } + + private static void StoreXYB( + Vector r, + Vector g, + Vector b, + Span valX, + Span valY, + Span valZ) + { + Vector half = Vector.Create(0.5f); + + (half * (r - g)).CopyTo(valX); + (half * (r + g)).CopyTo(valY); + b.CopyTo(valZ); + } + + private static void LinearRgbToXyb( + Vector r, + Vector g, + Vector b, + ReadOnlySpan premulAbsorb, + Span valX, + Span valY, + Span valZ) + { + OpsinAbsorbance( + r, + g, + b, + premulAbsorb, + out Vector mixed0, + out Vector mixed1, + out Vector mixed2); + + mixed0 = ZeroIfNegative(mixed0); + mixed1 = ZeroIfNegative(mixed1); + mixed2 = ZeroIfNegative(mixed2); + + int n = Vector.Count; + + mixed0 = CubeRootAndAdd( + mixed0, + Vector.Create(premulAbsorb[(9 * n)..])); + + mixed1 = CubeRootAndAdd( + mixed1, + Vector.Create(premulAbsorb[(10 * n)..])); + + mixed2 = CubeRootAndAdd( + mixed2, + Vector.Create(premulAbsorb[(11 * n)..])); + + StoreXYB(mixed0, mixed1, mixed2, valX, valY, valZ); + } + + public static void LinearRgbRowToXyb( + Span row0, + Span row1, + Span row2, + ReadOnlySpan premulAbsorb, + int xSize) + { + int n = Vector.Count; + + for (int x = 0; x < xSize; x += n) + { + Vector r = Vector.Create(row0[x..]); + Vector g = Vector.Create(row1[x..]); + Vector b = Vector.Create(row2[x..]); + + LinearRgbToXyb( + r, + g, + b, + premulAbsorb, + row0[x..], + row1[x..], + row2[x..]); + } + } + + private static Vector CubeRootAndAdd(Vector v, Vector add) + { + // HACK: Vector doesn't support cuberoot + Span span = stackalloc float[Vector.Count]; + v.StoreUnsafe(ref MemoryMarshal.GetReference(span)); + TensorPrimitives.Cbrt(span, span); + return Vector.LoadUnsafe(ref MemoryMarshal.GetReference(span)) + add; + } + + private static Vector ZeroIfNegative(Vector v) => + Vector.ConditionalSelect( + Vector.LessThan(v, Vector.Zero), + Vector.Zero, + v); + + private static Vector LinearFromSRgb(Vector encoded) + => JxlSRgbTransferFunction.DisplayFromEncoded(encoded); + + private static void LinearSrgbToXyb( + Configuration configuration, + Memory premulAbsorb, + JxlImage3F image) + { + int xSize = image.Width; + + void ProcessRow(int y, ReadOnlySpan premulAbsorb) + { + Span row0 = image.PlaneRow(0, y); + Span row1 = image.PlaneRow(1, y); + Span row2 = image.PlaneRow(2, y); + + int lanes = Vector.Count; + + for (int x = 0; x < xSize; x += lanes) + { + Vector r = new(row0[x..]); + Vector g = new(row1[x..]); + Vector b = new(row2[x..]); + + LinearRgbToXyb( + r, + g, + b, + premulAbsorb, + row0[x..], + row1[x..], + row2[x..]); + } + } + + _ = Parallel.For(0, image.Height, configuration.GetParallelOptions(), x => + { + ProcessRow(x, premulAbsorb.Span); + }); + } + + private static void SRgbToXyb( + Configuration configuration, + Memory premulAbsorb, + JxlImage3F image) + { + int xSize = image.Width; + + void ProcessRow(int y, ReadOnlySpan premulAbsorb) + { + Span row0 = image.PlaneRow(0, y); + Span row1 = image.PlaneRow(1, y); + Span row2 = image.PlaneRow(2, y); + + int lanes = Vector.Count; + + for (int x = 0; x < xSize; x += lanes) + { + Vector r = LinearFromSRgb(new Vector(row0[x..])); + Vector g = LinearFromSRgb(new Vector(row1[x..])); + Vector b = LinearFromSRgb(new Vector(row2[x..])); + + LinearRgbToXyb( + r, + g, + b, + premulAbsorb, + row0[x..], + row1[x..], + row2[x..]); + } + } + + _ = Parallel.For(0, image.Height, configuration.GetParallelOptions(), x => + { + ProcessRow(x, premulAbsorb.Span); + }); + } + + private static void SrgbToXybAndLinear( + Configuration configuration, + Memory premulAbsorb, + JxlImage3F image, + JxlImage3F linear) + { + int xSize = image.Width; + + void ProcessRow(int y, ReadOnlySpan premulAbsorb) + { + Span rowImage0 = image.PlaneRow(0, y); + Span rowImage1 = image.PlaneRow(1, y); + Span rowImage2 = image.PlaneRow(2, y); + + Span rowLinear0 = linear.PlaneRow(0, y); + Span rowLinear1 = linear.PlaneRow(1, y); + Span rowLinear2 = linear.PlaneRow(2, y); + + int lanes = Vector.Count; + + for (int x = 0; x < xSize; x += lanes) + { + Vector r = LinearFromSRgb(new Vector(rowImage0[x..])); + Vector g = LinearFromSRgb(new Vector(rowImage1[x..])); + Vector b = LinearFromSRgb(new Vector(rowImage2[x..])); + + r.CopyTo(rowLinear0[x..]); + g.CopyTo(rowLinear1[x..]); + b.CopyTo(rowLinear2[x..]); + + LinearRgbToXyb( + r, + g, + b, + premulAbsorb, + rowImage0[x..], + rowImage1[x..], + rowImage2[x..]); + } + } + + _ = Parallel.For(0, image.Height, configuration.GetParallelOptions(), x => + { + ProcessRow(x, premulAbsorb.Span); + }); + } + + private static void ComputePremulAbsorb(float intensityTarget, Span premulAbsorb) + { + float mul = intensityTarget / 255.0f; + int lanes = Vector.Count; + + for (int j = 0; j < 3; j++) + { + for (int i = 0; i < 3; i++) + { + float absorb = JxlOpsinAbsorbanceMatrix[j][i] * mul; + + premulAbsorb + .Slice(((j * 3) + i) * lanes, lanes) + .Fill(absorb); + } + } + + for (int i = 0; i < 3; i++) + { + float negBiasCbrt = -MathF.Cbrt(JxlOpsinAbsorbanceBias[i]); + + premulAbsorb + .Slice((9 + i) * lanes, lanes) + .Fill(negBiasCbrt); + } + } + + public static bool ToXyb( + Configuration configuration, + JxlColorEncoding currentEncoding, + float intensityTarget, + JxlImageF? black, + JxlImage3F image, + JxlCmsInterface cms, + JxlImage3F? linear) + { + if (black is not null) + { + if (image.GetRectangle() != black.GetRectangle()) + { + return false; + } + } + + if (linear is not null) + { + if (image.GetRectangle() != linear.GetRectangle()) + { + return false; + } + } + + int lanes = Vector.Count; + + Memory premulAbsorb = new float[lanes * 12]; + ComputePremulAbsorb(intensityTarget, premulAbsorb.Span); + + bool wantLinear = linear is not null; + + JxlColorEncoding linearSrgb = JxlColorEncoding.LinearSRgb(currentEncoding.IsGray); + + if (linearSrgb.SameColorEncoding(currentEncoding)) + { + if (wantLinear) + { + JxlImageOperations.CopyImageTo(image, linear!); + } + + LinearSrgbToXyb(configuration, premulAbsorb, image); + return true; + } + + if (currentEncoding.IsSRgb) + { + if (wantLinear) + { + SrgbToXybAndLinear(configuration, premulAbsorb, image, linear!); + return true; + } + + SRgbToXyb(configuration, premulAbsorb, image); + return true; + } + + // TODO + ApplyColorTransform( + configuration, + currentEncoding, + intensityTarget, + image, + black, + image.GetRectangle(), + linearSrgb, + cms, + wantLinear ? linear! : image); + + if (wantLinear) + { + JxlImageOperations.CopyImageTo(linear!, image); + } + + LinearSrgbToXyb(configuration, premulAbsorb, image); + return true; + } + + public static void RgbToYcbcr( + Configuration configuration, + JxlImageF rPlane, + JxlImageF gPlane, + JxlImageF bPlane, + JxlImageF yPlane, + JxlImageF cbPlane, + JxlImageF crPlane) + { + int xSize = rPlane.Width; + int ySize = rPlane.Height; + + if (xSize == 0 || ySize == 0) + { + return; + } + + Vector k128 = new(128.0f / 255); + Vector kR = new(0.299f); + Vector kG = new(0.587f); + Vector kB = new(0.114f); + Vector kAmpR = new(0.701f); + Vector kAmpB = new(0.886f); + + Vector kDiffR = kAmpR + kR; + Vector kDiffB = kAmpB + kB; + + Vector kNormR = Vector.One / (kAmpR + kG + kB); + Vector kNormB = Vector.One / (kR + kG + kAmpB); + + int step = Vector.Count; + + int groupArea = JxlFrameDimensions.GroupDimensions * JxlFrameDimensions.GroupDimensions; + int linesPerGroup = JxlMath.DivCeil(groupArea, xSize); + int numStripes = JxlMath.DivCeil(ySize, linesPerGroup); + + void Transform(int index) + { + int y0 = index * linesPerGroup; + int y1 = Math.Min(y0 + linesPerGroup, ySize); + + for (int y = y0; y < y1; y++) + { + ReadOnlySpan rRow = rPlane.GetRow(y); + ReadOnlySpan gRow = gPlane.GetRow(y); + ReadOnlySpan bRow = bPlane.GetRow(y); + + Span yRow = yPlane.GetRow(y); + Span cbRow = cbPlane.GetRow(y); + Span crRow = crPlane.GetRow(y); + + for (int x = 0; x < xSize; x += step) + { + Vector r = new(rRow[x..]); + Vector g = new(gRow[x..]); + Vector b = new(bRow[x..]); + + Vector rBase = r * kR; + Vector rDiff = r * kDiffR; + Vector gBase = g * kG; + Vector bBase = b * kB; + Vector bDiff = b * kDiffB; + + Vector yBase = rBase + gBase + bBase; + Vector yVec = yBase - k128; + Vector cbVec = (bDiff - yBase) * kNormB; + Vector crVec = (rDiff - yBase) * kNormR; + + yVec.CopyTo(yRow[x..]); + cbVec.CopyTo(cbRow[x..]); + crVec.CopyTo(crRow[x..]); + } + } + } + + _ = Parallel.For(0, numStripes, configuration.GetParallelOptions(), Transform); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageFeatures.cs b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageFeatures.cs index eb0e4581da..2bbfeb2fdf 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageFeatures.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageFeatures.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Patch; using SixLabors.ImageSharp.Formats.Jxl.Processing.Noise; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Image; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs index af665db380..89badfa498 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs @@ -364,6 +364,20 @@ public static void FillImage(T value, JxlPlane image) } } + /// + /// Fills every value in every plane of the image with . + /// + /// The type of the image. + /// The value to fill every plane with. + /// The image to fill. + public static void FillImage(T value, JxlImage3 image) + where T : unmanaged + { + FillImage(value, image.Plane(0)); + FillImage(value, image.Plane(1)); + FillImage(value, image.Plane(2)); + } + /// /// Sets every value in the plane to 0. See also . /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs index 00c162be9a..c240746a3a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs @@ -4,6 +4,7 @@ using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs index 2baad82d6d..233a98549d 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs @@ -263,10 +263,10 @@ public static void SlowSymmetric3Row( } public static void SlowSymmetric3( - JxlImageF input, + JxlPlane input, Rectangle rect, JxlWeightsSymmetric3 weights, - JxlImageF output) + JxlPlane output) { int width = rect.Width; int height = rect.Height; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaDecoder.cs index bec5061305..cfe5be459e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaDecoder.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using Tree = System.Collections.Generic.List; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncoding.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncoding.cs index e1dabc6534..a63960f038 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncoding.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncoding.cs @@ -5,6 +5,7 @@ using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlXorShift.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlXorShift.cs index 8f04197ee5..e5b45f7344 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlXorShift.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlXorShift.cs @@ -23,7 +23,7 @@ public JxlXorShift(ulong seed) } } - public void XorShift128Plus(uint seed1, uint seed2, uint seed3, uint seed4) + public JxlXorShift(uint seed1, uint seed2, uint seed3, uint seed4) { this.s0[0] = SplitMix64((((ulong)seed1 << 32) + seed2) + 0x9E3779B97F4A7C15uL); this.s1[0] = SplitMix64((((ulong)seed3 << 32) + seed4) + 0x9E3779B97F4A7C15uL); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Primitives/RectangleUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/RectangleUtils.cs index 072b51983f..5226e7561a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Primitives/RectangleUtils.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/RectangleUtils.cs @@ -5,13 +5,13 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; internal static class RectangleUtils { - public static int X0(in Rectangle rect) => rect.X; + public static int X0(this in Rectangle rect) => rect.X; - public static int Y0(in Rectangle rect) => rect.Y; + public static int Y0(this in Rectangle rect) => rect.Y; - public static int X1(in Rectangle rect) => rect.X + rect.Width; + public static int X1(this in Rectangle rect) => rect.X + rect.Width; - public static int Y1(in Rectangle rect) => rect.Y + rect.Height; + public static int Y1(this in Rectangle rect) => rect.Y + rect.Height; public static Rectangle Extend(Rectangle curr, int border, Rectangle parent) { @@ -22,4 +22,18 @@ public static Rectangle Extend(Rectangle curr, int border, Rectangle parent) return new(newX0, newY0, newX1 - newX0, newY1 - newY0); } + + public static bool IsInside(this Rectangle a, Rectangle b) => + a.X0() >= b.X0() && + a.X1() <= b.X1() && + a.Y0() >= b.Y0() && + a.Y1() <= b.Y1(); + + public static Rectangle CreateRectangle(int xbegin, int ybegin, int xSizeMax, int ySizeMax, int xEnd, int yEnd) + => new(xbegin, ybegin, ClampedSize(xbegin, xSizeMax, xEnd), ClampedSize(ybegin, ySizeMax, yEnd)); + + private static int ClampedSize(int begin, int sizeMax, int end) + => begin + sizeMax <= end + ? sizeMax + : (end > begin ? end - begin : 0); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/PatchDictionaryStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/PatchDictionaryStage.cs index f2f2f0054f..18660007fc 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/PatchDictionaryStage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/PatchDictionaryStage.cs @@ -2,7 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; -using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Patch; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs index 0a06b60da7..e87f08a09a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs @@ -5,6 +5,7 @@ using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplines.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplines.cs index 52e83b4bb4..c40462b8c5 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplines.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplines.cs @@ -5,6 +5,7 @@ using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/AlphaBlendingTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/AlphaBlendingTests.cs new file mode 100644 index 0000000000..57e3a8a629 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/AlphaBlendingTests.cs @@ -0,0 +1,332 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Blending; + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; + +public class AlphaBlendingTests +{ + private static float[] RgbToArray(Rgb rgb) => [rgb.R, rgb.G, rgb.B]; + + private static readonly float[] Expected = [77.2f, 83.0f, 90.6f]; + + [Fact] + public void BlendingWithNonPremultiplied() + { + Rgb bgRgb = new(100, 110, 120); + float bgA = 180f / 255; + + Rgb fgRgb = new(25, 21, 23); + float fgA = 15420f / 65535; + float fgA2 = 2.0f; + + float[] outR = [0]; + float[] outG = [0]; + float[] outB = [0]; + float[] outAlpha = [0]; + + JxlAlphaBlendingInputLayer inputBg = new() + { + R = [bgRgb.R], + G = [bgRgb.G], + B = [bgRgb.B], + A = [bgA], + }; + + JxlAlphaBlendingInputLayer inputFg = new() + { + R = [fgRgb.R], + G = [fgRgb.G], + B = [fgRgb.B], + A = [fgA], + }; + + JxlAlphaBlendingOutput output = new() + { + R = outR, + G = outG, + B = outB, + A = outAlpha, + }; + + JxlAlphaHelper.PerformAlphaBlending( + inputBg, + inputFg, + output, + 1, + alphaIsPremultiplied: false, + clamp: false); + + Assert.Equal( + Expected, + [outR[0], outG[0], outB[0]], + new ApproximateFloatComparer(0.05f)); + + Assert.True(MathF.Abs(outAlpha[0] - (3174f / 4095)) <= 1e-5f); + + inputFg = new() + { + R = [fgRgb.R], + G = [fgRgb.G], + B = [fgRgb.B], + A = [fgA2], + }; + + JxlAlphaHelper.PerformAlphaBlending( + inputBg, + inputFg, + output, + 1, + alphaIsPremultiplied: false, + clamp: true); + + Assert.Equal( + RgbToArray(fgRgb), + [outR[0], outG[0], outB[0]], + new ApproximateFloatComparer(0.05f)); + + Assert.True(MathF.Abs(outAlpha[0] - 1.0f) <= 1e-5f); + } + + [Fact] + public void BlendingWithPremultiplied() + { + Rgb bgRgb = new(100, 110, 120); + float bgA = 180f / 255; + + Rgb fgRgb = new(25, 21, 23); + float fgA = 15420f / 65535; + float fgA2 = 2.0f; + + float[] outR = [0]; + float[] outG = [0]; + float[] outB = [0]; + float[] outAlpha = [0]; + + JxlAlphaBlendingInputLayer inputBg = new() + { + R = [bgRgb.R], + G = [bgRgb.G], + B = [bgRgb.B], + A = [bgA], + }; + + JxlAlphaBlendingInputLayer inputFg = new() + { + R = [fgRgb.R], + G = [fgRgb.G], + B = [fgRgb.B], + A = [fgA], + }; + + JxlAlphaBlendingOutput output = new() + { + R = outR, + G = outG, + B = outB, + A = outAlpha, + }; + + JxlAlphaHelper.PerformAlphaBlending( + inputBg, + inputFg, + output, + 1, + alphaIsPremultiplied: true, + clamp: false); + + Assert.Equal( + new float[] { 101.5f, 105.1f, 114.8f }, + [outR[0], outG[0], outB[0]], + new ApproximateFloatComparer(0.05f)); + + Assert.True(MathF.Abs(outAlpha[0] - (3174f / 4095)) <= 1e-5f); + + inputFg = new() + { + R = [fgRgb.R], + G = [fgRgb.G], + B = [fgRgb.B], + A = [fgA2], + }; + + JxlAlphaHelper.PerformAlphaBlending( + inputBg, + inputFg, + output, + 1, + alphaIsPremultiplied: true, + clamp: true); + + Assert.Equal( + RgbToArray(fgRgb), + [outR[0], outG[0], outB[0]], + new ApproximateFloatComparer(0.05f)); + + Assert.True(MathF.Abs(outAlpha[0] - 1.0f) <= 1e-5f); + } + + [Fact] + public void Mul() + { + Span bg = [100]; + Span fg = [25]; + Span output = [0]; + + JxlAlphaHelper.PerformMultiplyBlending( + bg, + fg, + output, + 1, + clamp: false); + + Assert.True(MathF.Abs(output[0] - (fg[0] * bg[0])) <= 0.05f); + + JxlAlphaHelper.PerformMultiplyBlending( + bg, + fg, + output, + 1, + clamp: true); + + Assert.True(MathF.Abs(output[0] - bg[0]) <= 0.05f); + } + + [Fact] + public void PremultiplyAndUnpremultiply() + { + float[] alpha = + { + 0f, + 63f / 255, + 127f / 255, + 1f + }; + + float[] r = { 120, 130, 140, 150 }; + float[] g = { 124, 134, 144, 154 }; + float[] b = { 127, 137, 147, 157 }; + + JxlAlphaHelper.PremultiplyAlpha(r, g, b, alpha, alpha.Length); + + Assert.Equal( + new[] + { + 0f, + 130 * 63f / 255, + 140 * 127f / 255, + 150f + }, + r, + new ApproximateFloatComparer(1e-5f)); + + Assert.Equal( + new[] + { + 0f, + 134 * 63f / 255, + 144 * 127f / 255, + 154f + }, + g, + new ApproximateFloatComparer(1e-5f)); + + Assert.Equal( + new[] + { + 0f, + 137 * 63f / 255, + 147 * 127f / 255, + 157f + }, + b, + new ApproximateFloatComparer(1e-5f)); + + JxlAlphaHelper.UnpremultiplyAlpha(r, g, b, alpha, alpha.Length); + + Assert.Equal( + new[] { 120f, 130f, 140f, 150f }, + r, + new ApproximateFloatComparer(1e-4f)); + + Assert.Equal( + new[] { 124f, 134f, 144f, 154f }, + g, + new ApproximateFloatComparer(1e-4f)); + + Assert.Equal( + new[] { 127f, 137f, 147f, 157f }, + b, + new ApproximateFloatComparer(1e-4f)); + } + + [Fact] + public void UnpremultiplyAndPremultiply() + { + float[] alpha = + { + 0f, + 63f / 255, + 127f / 255, + 1f + }; + + float[] r = { 50, 60, 70, 80 }; + float[] g = { 54, 64, 74, 84 }; + float[] b = { 57, 67, 77, 87 }; + + JxlAlphaHelper.UnpremultiplyAlpha(r, g, b, alpha, alpha.Length); + + Assert.Equal( + new[] + { + 50f * (1 << 26), + 60 * 255f / 63, + 70 * 255f / 127, + 80f + }, + r, + new ApproximateFloatComparer(1e-4f)); + + Assert.Equal( + new[] + { + 54f * (1 << 26), + 64 * 255f / 63, + 74 * 255f / 127, + 84f + }, + g, + new ApproximateFloatComparer(1e-4f)); + + Assert.Equal( + new[] + { + 57f * (1 << 26), + 67 * 255f / 63, + 77 * 255f / 127, + 87f + }, + b, + new ApproximateFloatComparer(1e-4f)); + + JxlAlphaHelper.PremultiplyAlpha(r, g, b, alpha, alpha.Length); + + Assert.Equal( + new[] { 50f, 60f, 70f, 80f }, + r, + new ApproximateFloatComparer(1e-4f)); + + Assert.Equal( + new[] { 54f, 64f, 74f, 84f }, + g, + new ApproximateFloatComparer(1e-4f)); + + Assert.Equal( + new[] { 57f, 67f, 77f, 87f }, + b, + new ApproximateFloatComparer(1e-4f)); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/LinearAlgebraTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/LinearAlgebraTests.cs new file mode 100644 index 0000000000..e131952743 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/LinearAlgebraTests.cs @@ -0,0 +1,169 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; +using Matrix2x2 = System.Runtime.CompilerServices.InlineArray2>; +using Vector2 = System.Runtime.CompilerServices.InlineArray2; + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing.Encoder; + +public class LinearAlgebraTests +{ + private static Vector2 CreateVector2(double a, double b) + { + Vector2 v2 = default; + + v2[0] = a; + v2[1] = b; + + return v2; + } + + private static Matrix2x2 Diagonal(Vector2 d) + { + Matrix2x2 result = default; + + result[0][0] = d[0]; + result[0][1] = 0; + result[1][0] = 0; + result[1][1] = d[1]; + + return result; + } + + private static Matrix2x2 Identity() => Diagonal(CreateVector2(1.0f, 1.0f)); + + private static Matrix2x2 MatrixMultiply(Matrix2x2 a, Matrix2x2 b) + { + Matrix2x2 result = default; + + for (int y = 0; y < 2; y++) + { + for (int x = 0; x < 2; x++) + { + result[y][x] = (a[0][x] * b[y][0]) + (a[1][x] * b[y][1]); + } + } + + return result; + } + + private static Matrix2x2 Transpose(Matrix2x2 a) + { + Matrix2x2 result = default; + + result[0] = CreateVector2(a[0][0], a[1][0]); + result[1] = CreateVector2(a[0][1], a[1][1]); + + return result; + } + + private static Matrix2x2 RandomSymmetricMatrix( + Rng rng, + float vmin, + float vmax) + { + Matrix2x2 result = default; + + result[0][0] = rng.UniformF(vmin, vmax); + result[0][1] = rng.UniformF(vmin, vmax); + result[1][0] = result[0][1]; + result[1][1] = rng.UniformF(vmin, vmax); + + return result; + } + + private static void VerifyMatrixEqual(Matrix2x2 a, Matrix2x2 b, float epsilon) + { + TolerantMath comparer = new(epsilon); + + for (int y = 0; y < 2; y++) + { + for (int x = 0; x < 2; x++) + { + Assert.True( + comparer.AreEqual(a[y][x], b[y][x]), + $"Matrices differ at [{y}][{x}]: {a[y][x]} != {b[y][x]}"); + } + } + } + + private static void VerifyOrthogonal(Matrix2x2 a, float epsilon) => VerifyMatrixEqual( + Identity(), + MatrixMultiply(Transpose(a), a), + epsilon); + + [Fact] + public void ConvertToDiagonal() + { + { + Matrix2x2 i = Identity(); + Matrix2x2 u = default; + Vector2 d = default; + + JxlLinearAlgebra.ConvertToDiagonal(i, ref d, ref u); + + VerifyMatrixEqual(i, u, 1e-15f); + + for (int k = 0; k < 2; k++) + { + Assert.True( + Math.Abs(d[k] - 1.0f) <= 1e-15f, + $"d[{k}] = {d[k]}"); + } + } + + { + Matrix2x2 a = Identity(); + a[0][1] = 2.0f; + a[1][0] = 2.0f; + + Matrix2x2 u = default; + Vector2 d = default; + + JxlLinearAlgebra.ConvertToDiagonal(a, ref d, ref u); + + VerifyOrthogonal(u, 1e-12f); + VerifyMatrixEqual( + a, + MatrixMultiply(u, MatrixMultiply(Diagonal(d), Transpose(u))), + 1e-12f); + } + + { + Matrix2x2 a = default; + a[0] = CreateVector2(0.000208649f, 1.13687e-12f); + a[1] = CreateVector2(1.13687e-12f, 0.000208649f); + + Matrix2x2 u = default; + Vector2 d = default; + + JxlLinearAlgebra.ConvertToDiagonal(a, ref d, ref u); + + VerifyOrthogonal(u, 1e-12f); + VerifyMatrixEqual( + a, + MatrixMultiply(u, MatrixMultiply(Diagonal(d), Transpose(u))), + 1e-11f); + } + + { + Rng rng = new(0); + + for (int i = 0; i < 1_000_000; i++) + { + Matrix2x2 a = RandomSymmetricMatrix(rng, -1.0f, 1.0f); + Matrix2x2 u = default; + Vector2 d = default; + + JxlLinearAlgebra.ConvertToDiagonal(a, ref d, ref u); + + VerifyOrthogonal(u, 1e-12f); + VerifyMatrixEqual( + a, + MatrixMultiply(u, MatrixMultiply(Diagonal(d), Transpose(u))), + 5e-10f); + } + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/EntropyCoderTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/EntropyCoderTests.cs new file mode 100644 index 0000000000..d3dce2c713 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/EntropyCoderTests.cs @@ -0,0 +1,105 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; + +public class EntropyCoderTests +{ + [Fact] + public void PackUnpack() + { + for (int i = -31; i < 32; i++) + { + uint packed = JxlPackSigned.PackUnsigned(i); + + Assert.True(packed < 63u); + + int unpacked = JxlPackSigned.UnpackSigned(packed); + + Assert.Equal(i, unpacked); + } + } + + private sealed class MockBitReader : JxlBitReader + { + public ulong NBits { get; set; } + + public ulong Bits { get; set; } + + public MockBitReader(Stream stream) + : base(stream) + { + } + + public override uint PeekBits32(uint n) + { + Assert.Equal(this.NBits, n); + return (uint)this.Bits; + } + + public override uint ReadBits32(uint n) + { + Assert.Equal(this.NBits, n); + return (uint)this.Bits; + } + } + + private static void HybridUintRoundtrip( + JxlAnsHybridUIntConfiguration config, + uint limit = 1u << 24) + { + Rng rng = new(0); + + const int numIntegers = 1 << 20; + + uint[] integers = new uint[numIntegers]; + uint[] token = new uint[numIntegers]; + uint[] nbits = new uint[numIntegers]; + uint[] bits = new uint[numIntegers]; + + for (int i = 0; i < numIntegers; i++) + { + integers[i] = (uint)rng.UniformU(0, limit + 1); + config.Encode( + integers[i], + out token[i], + out nbits[i], + out bits[i]); + } + + for (int i = 0; i < numIntegers; i++) + { + MockBitReader br = new(Stream.Null) + { + NBits = nbits[i], + Bits = bits[i] + }; + + Assert.Equal( + integers[i], + JxlAnsSymbolReader.ReadHybridUintConfig( + config, + token[i], + ref br)); + } + } + + [Fact] + public void Test000() => HybridUintRoundtrip(new JxlAnsHybridUIntConfiguration(0, 0, 0)); + + [Fact] + public void Test411() => HybridUintRoundtrip(new JxlAnsHybridUIntConfiguration(4, 1, 1)); + + [Fact] + public void Test420() => HybridUintRoundtrip(new JxlAnsHybridUIntConfiguration(4, 2, 0)); + + [Fact] + public void Test421() => HybridUintRoundtrip( + new JxlAnsHybridUIntConfiguration(4, 2, 1), + 256); +} diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/GaborishTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/GaborishTests.cs new file mode 100644 index 0000000000..67461ddc89 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/GaborishTests.cs @@ -0,0 +1,113 @@ +// 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; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; + +public class GaborishTests +{ + private static JxlWeightsSymmetric3 GaborishKernel(float weight1, float weight2) + { + const float weight0 = 1.0f; + + // Normalize. + float mul = 1.0f / (weight0 + (4.0f * (weight1 + weight2))); + float w0 = weight0 * mul; + float w1 = weight1 * mul; + float w2 = weight2 * mul; + + JxlWeightsSymmetric3 wgt = new(); + wgt.SetC(Vector128.Create(w0)); + wgt.SetD(Vector128.Create(w1)); + wgt.SetR(Vector128.Create(w2)); + + return wgt; + } + + private static void ConvolveGaborish( + JxlPlane input, + float weight1, + float weight2, + JxlPlane output) + { + Assert.True(JxlImageOperations.SameSize(input, output)); + + // The test will fail if this throws. + JxlConvolve.SlowSymmetric3( + input, + input.GetRectangle(), + GaborishKernel(weight1, weight2), + output); + } + + private static void TestRoundTrip( + JxlImage3F input, + float maxL1) + { + JxlImage3F fwd = new(TestEnvironment.Configuration, input.XSize, input.YSize); + + ConvolveGaborish( + input.Plane(0), + 0, + 0, + fwd.Plane(0)); + + ConvolveGaborish( + input.Plane(1), + 0, + 0, + fwd.Plane(1)); + + ConvolveGaborish( + input.Plane(2), + 0, + 0, + fwd.Plane(2)); + + const float w = 0.92718927264540152f; + + InlineArray3 weights = default; + weights[0] = w; + weights[1] = w; + weights[2] = w; + + // The test will fail if this throws. + JxlGaborish.InverseGaborish( + TestEnvironment.Configuration, + fwd, + fwd.GetRectangle(), + weights); + + // TODO: VerifyRelativeError + Assert.True( + VerifyRelativeError( + input, + fwd, + maxL1, + 1e-4f)); + } + + [Fact] + public void TestZero() + { + JxlImage3F input = new(TestEnvironment.Configuration, 20, 20); + JxlImageOperations.ZeroFillImage(input); + TestRoundTrip(input, 0.0f); + } + + [Fact] + public void TestFlat() + { + JxlImage3F input = new(TestEnvironment.Configuration, 20, 20); + JxlImageOperations.FillImage(1.0f, input); + TestRoundTrip(input, 1e-5f); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/ImageOperationsTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/ImageOperationsTests.cs new file mode 100644 index 0000000000..a7751b2c49 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/ImageOperationsTests.cs @@ -0,0 +1,146 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Memory; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; + +public class ImageOperationsTests +{ + private static void TestFillImpl(JxlImage3 image, string layout) + where T : unmanaged + { + JxlImageOperations.FillImage((T)Convert.ChangeType(1, typeof(T), null), image); + + for (int y = 0; y < image.YSize; y++) + { + for (int c = 0; c < 3; c++) + { + Span row = image.PlaneRow(c, y); + + for (int x = 0; x < image.XSize; x++) + { + if (!EqualityComparer.Default.Equals( + row[x], + (T)Convert.ChangeType(1, typeof(T), null))) + { + Assert.Fail( + $"Not 1 at c={c} {x}, {y} " + + $"({image.XSize} x {image.YSize}) ({layout})"); + } + + row[x] = (T)Convert.ChangeType(2, typeof(T), null); + } + } + } + + JxlImageOperations.ZeroFillImage(image); + + for (int c = 0; c < 3; c++) + { + for (int y = 0; y < image.YSize; y++) + { + Span row = image.PlaneRow(c, y); + + for (int x = 0; x < image.XSize; x++) + { + if (!EqualityComparer.Default.Equals( + row[x], + default)) + { + Assert.Fail( + $"Not 0 at c={c} {x}, {y} " + + $"({image.XSize} x {image.YSize}) ({layout})"); + } + + row[x] = (T)Convert.ChangeType(3, typeof(T), null); + } + } + } + } + + private static void TestFillT() + where T : unmanaged + { + foreach (uint xSize in new uint[] { 0, 1, 15, 16, 31, 32 }) + { + foreach (uint ySize in new uint[] { 0, 1, 15, 16, 31, 32 }) + { + JxlImage3 image = JxlImage3.Create(TestEnvironment.Configuration, (int)xSize, (int)ySize); + + TestFillImpl(image, "size ctor"); + } + } + } + + [Fact] + public void TestFill() + { + TestFillT(); + TestFillT(); + TestFillT(); + } + + [Fact] + public void CopyImageToWithPaddingTest() + { + JxlPlane src = JxlPlane.Create(TestEnvironment.Configuration, 100, 61); + + for (int y = 0; y < src.YSize; y++) + { + Span row = src.GetRow(y); + + for (int x = 0; x < src.XSize; x++) + { + row[x] = (uint)((x * 1000) + y); + } + } + + Rectangle srcRect = new(10, 20, 30, 40); + Assert.True(srcRect.IsInside(src.GetRectangle())); + + JxlPlane dst = JxlPlane.Create(TestEnvironment.Configuration, 60, 50); + + JxlImageOperations.FillImage(0u, dst); + + Rectangle dstRect = new(20, 5, 30, 40); + Assert.True(dstRect.IsInside(dst.GetRectangle())); + + Assert.True( + JxlImageOperations.CopyImageToWithPadding( + srcRect, + src, + padding: 2, + dstRect, + dst)); + + Rectangle paddedDstRect = new( + 20 - 2, + 5 - 2, + 30 + 4, + 40 + 3); + + for (int y = 0; y < dst.YSize; y++) + { + Span row = dst.GetRow(y); + + for (int x = 0; x < dst.XSize; x++) + { + if (new Rectangle(x, y, 1, 1).IsInside(paddedDstRect)) + { + Assert.Equal( + (uint)( + ((x - dstRect.X0() + srcRect.X0()) * 1000) + + (y - dstRect.Y0() + srcRect.Y0())), + row[x]); + } + else + { + Assert.Equal(0u, row[x]); + } + } + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Jxl/TestUtils/TestDCT.cs b/tests/ImageSharp.Tests/Formats/Jxl/TestUtils/TestDCT.cs new file mode 100644 index 0000000000..244b4c9076 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/TestUtils/TestDCT.cs @@ -0,0 +1,108 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl.TestUtils; + +/// +/// This is a naive Discrete Cosine Transform, without any optimization. +/// It is a reference to test against the optimized DCT. +/// +internal static class TestDCT +{ + private static double Alpha(int u) => u == 0 ? 0.7071067811865475 : 1.0; + + public static void Dct1D(ReadOnlySpan block, Span output, int n, int m) + { + Span matrix = stackalloc double[n * n]; + double scale = Math.Sqrt(2.0) / n; + + for (int y = 0; y < n; y++) + { + for (int u = 0; u < n; u++) + { + matrix[(n * u) + y] = + Alpha(u) * + Math.Cos((y + 0.5) * u * Math.PI / n) * + scale; + } + } + + for (int x = 0; x < m; x++) + { + for (int u = 0; u < n; u++) + { + output[(m * u) + x] = 0; + + for (int y = 0; y < n; y++) + { + output[(m * u) + x] += matrix[(n * u) + y] * block[(m * y) + x]; + } + } + } + } + + public static void Idct1D(ReadOnlySpan block, Span output, int n, int m) + { + Span matrix = stackalloc double[n * n]; + double scale = Math.Sqrt(2.0); + + for (int y = 0; y < n; y++) + { + for (int u = 0; u < n; u++) + { + matrix[(n * y) + u] = + Alpha(u) * + Math.Cos((y + 0.5) * u * Math.PI / n) * + scale; + } + } + + for (int x = 0; x < m; x++) + { + for (int u = 0; u < n; u++) + { + output[(m * u) + x] = 0; + + for (int y = 0; y < n; y++) + { + output[(m * u) + x] += matrix[(n * u) + y] * block[(m * y) + x]; + } + } + } + } + + public static void TransposeBlock(ReadOnlySpan input, Span output, int n, int m) + { + for (int x = 0; x < n; x++) + { + for (int y = 0; y < m; y++) + { + output[(y * n) + x] = input[(x * m) + y]; + } + } + } + + public static void DctSlow(Span block, int n) + { + int blockSize = n * n; + Span g = stackalloc double[blockSize]; + + Dct1D(block, g, n, n); + TransposeBlock(g, block, n, n); + + Dct1D(block, g, n, n); + TransposeBlock(g, block, n, n); + } + + public static void IdctSlow(Span block, int n) + { + int blockSize = n * n; + Span g = stackalloc double[blockSize]; + + Idct1D(block, g, n, n); + TransposeBlock(g, block, n, n); + + Idct1D(block, g, n, n); + TransposeBlock(g, block, n, n); + } +} From 836a0554e1bef0d78492ad310e64337dab7ad7ea Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 7 Sep 2026 21:41:44 +0400 Subject: [PATCH 153/328] Add bit writer --- .../Formats/Jxl/Fields/JxlBitsCoder.cs | 1 + .../Formats/Jxl/Fields/JxlBundle.cs | 3 +- .../Formats/Jxl/Fields/JxlCanEncodeVisitor.cs | 2 +- .../Formats/Jxl/Fields/JxlF16Coder.cs | 1 + .../Formats/Jxl/Fields/JxlU32Coder.cs | 1 + .../Formats/Jxl/Fields/JxlU64Coder.cs | 1 + .../Jxl/Processing/Encoder/JxlBitWriter.cs | 88 +++++++++++++++++++ 7 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlBitWriter.cs diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs index 3e4a2b6676..787d16d088 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; namespace SixLabors.ImageSharp.Formats.Jxl.Fields; diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs index 94e5aa627c..866e00f317 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs @@ -5,6 +5,7 @@ using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Processing; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; using SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; @@ -118,7 +119,7 @@ public static bool Write(IJxlFields fields, JxlBitWriter writer, JxlLayerType la return false; } - return writer.WithMaxBits(totalBits, layer, auxOutput, () => + return writer.WithMaxBits(totalBits, () => { JxlWriteVisitor visitor = new(extensionBits, writer); diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlCanEncodeVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlCanEncodeVisitor.cs index aae11aa120..87192bfefc 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlCanEncodeVisitor.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlCanEncodeVisitor.cs @@ -102,7 +102,7 @@ public bool GetSizes(ref int extensionBits, ref long totalBits) extensionBits = (int)this.encodedBits - (int)this.posAfterExt; int encodedBits = 0; - this.OK &= JxlU64Coder.CanEncode(extensionBits, ref encodedBits); + this.OK &= JxlU64Coder.CanEncode((ulong)extensionBits, ref encodedBits); totalBits += encodedBits; for (int i = 1; i < BitOperations.PopCount(this.extensions); i++) diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs index 3da369a18e..d5b0d30fd2 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; namespace SixLabors.ImageSharp.Formats.Jxl.Fields; diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs index 660b403887..3fa3a3ab79 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs @@ -3,6 +3,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Processing; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; namespace SixLabors.ImageSharp.Formats.Jxl.Fields; diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs index 8726c9a27b..3336ecf0f2 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; namespace SixLabors.ImageSharp.Formats.Jxl.Fields; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlBitWriter.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlBitWriter.cs new file mode 100644 index 0000000000..e497da0a34 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlBitWriter.cs @@ -0,0 +1,88 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; + +internal sealed class JxlBitWriter(Stream stream) +{ + private const int BitsPerByte = 8; + + private bool isLimited; + private ulong bitsLimit; + + private byte currentByte; + private int bitsInCurrentByte; + + public long BitsWritten { get; private set; } + + public void Write(int nBits, int bits) => this.Write(nBits, (ulong)bits); + + public void Write(int nBits, ulong bits) + { + DebugGuard.MustBeGreaterThanOrEqualTo(nBits, 0, nameof(nBits)); + DebugGuard.MustBeLessThanOrEqualTo(nBits, 64, nameof(nBits)); + + if (this.isLimited) + { + if ((ulong)nBits > this.bitsLimit) + { + throw new InvalidOperationException("Too many bits were written"); + } + + this.bitsLimit -= (ulong)nBits; + } + + while (nBits > 0) + { + int bitsAvailable = BitsPerByte - this.bitsInCurrentByte; + int count = Math.Min(nBits, bitsAvailable); + + ulong mask = count == 64 + ? ulong.MaxValue + : (1UL << count) - 1; + + this.currentByte |= (byte)((bits & mask) << this.bitsInCurrentByte); + + bits >>= count; + nBits -= count; + this.bitsInCurrentByte += count; + this.BitsWritten += count; + + if (this.bitsInCurrentByte == BitsPerByte) + { + stream.WriteByte(this.currentByte); + this.currentByte = 0; + this.bitsInCurrentByte = 0; + } + } + } + + public bool WithMaxBits(ulong maxBits, Func func) + { + bool previousIsLimited = this.isLimited; + ulong previousLimit = this.bitsLimit; + + this.isLimited = true; + this.bitsLimit = maxBits; + + try + { + return func(); + } + finally + { + this.isLimited = previousIsLimited; + this.bitsLimit = previousLimit; + } + } + + public void Flush() + { + if (this.bitsInCurrentByte != 0) + { + stream.WriteByte(this.currentByte); + this.currentByte = 0; + this.bitsInCurrentByte = 0; + } + } +} From 22d1d3710cd5bb3837802c163a17998d852a0539 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 7 Sep 2026 21:51:32 +0400 Subject: [PATCH 154/328] Add visit to custom transform data --- .../Jxl/IO/Metadata/JxlCustomTransformData.cs | 163 +++++++++++++++++- .../Jxl/IO/Metadata/JxlImageMetadata.cs | 10 +- 2 files changed, 162 insertions(+), 11 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCustomTransformData.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCustomTransformData.cs index 7cc5b9fe56..882615d73d 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCustomTransformData.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCustomTransformData.cs @@ -8,19 +8,170 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; internal sealed class JxlCustomTransformData : IJxlFields { + private InlineArray15 upsampling2Weights; + private InlineArray55 upsampling4Weights; + private InlineArray210 upsampling8Weights; + private uint customWeightsMask; + private bool allDefault; + + public JxlCustomTransformData() => JxlBundle.Init(this); + + private static ReadOnlySpan Weights2 => + [ + -0.01716200f, -0.03452303f, -0.04022174f, -0.02921014f, -0.00624645f, + 0.14111091f, 0.28896755f, 0.00278718f, -0.01610267f, 0.56661550f, + 0.03777607f, -0.01986694f, -0.03144731f, -0.01185068f, -0.00213539f + ]; + + private static ReadOnlySpan Weights4 => + [ + -0.02419067f, -0.03491987f, -0.03693351f, -0.03094285f, -0.00529785f, + -0.01663432f, -0.03556863f, -0.03888905f, -0.03516850f, -0.00989469f, + 0.23651958f, 0.33392945f, -0.01073543f, -0.01313181f, -0.03556694f, + 0.13048175f, 0.40103025f, 0.03951150f, -0.02077584f, 0.46914198f, + -0.00209270f, -0.01484589f, -0.04064806f, 0.18942530f, 0.56279892f, + 0.06674400f, -0.02335494f, -0.03551682f, -0.00754830f, -0.02267919f, + -0.02363578f, 0.00315804f, -0.03399098f, -0.01359519f, -0.00091653f, + -0.00335467f, -0.01163294f, -0.01610294f, -0.00974088f, -0.00191622f, + -0.01095446f, -0.03198464f, -0.04455121f, -0.02799790f, -0.00645912f, + 0.06390599f, 0.22963888f, 0.00630981f, -0.01897349f, 0.67537268f, + 0.08483369f, -0.02534994f, -0.02205197f, -0.01667999f, -0.00384443f + ]; + + private static ReadOnlySpan Weights8 => + [ + -0.02928613f, -0.03706353f, -0.03783812f, -0.03324558f, -0.00447632f, + -0.02519406f, -0.03752601f, -0.03901508f, -0.03663285f, -0.00646649f, + -0.02066407f, -0.03838633f, -0.04002101f, -0.03900035f, -0.00901973f, + -0.01626393f, -0.03954148f, -0.04046620f, -0.03979621f, -0.01224485f, + 0.29895328f, 0.35757708f, -0.02447552f, -0.01081748f, -0.04314594f, + 0.23903219f, 0.41119301f, -0.00573046f, -0.01450239f, -0.04246845f, + 0.17567618f, 0.45220643f, 0.02287757f, -0.01936783f, -0.03583255f, + 0.11572472f, 0.47416733f, 0.06284440f, -0.02685066f, 0.42720050f, + -0.02248939f, -0.01155273f, -0.04562755f, 0.28689496f, 0.49093869f, + -0.00007891f, -0.01545926f, -0.04562659f, 0.21238920f, 0.53980934f, + 0.03369474f, -0.02070211f, -0.03866988f, 0.14229550f, 0.56593398f, + 0.08045181f, -0.02888298f, -0.03680918f, -0.00542229f, -0.02920477f, + -0.02788574f, -0.02118180f, -0.03942402f, -0.00775547f, -0.02433614f, + -0.03193943f, -0.02030828f, -0.04044014f, -0.01074016f, -0.01930822f, + -0.03620399f, -0.01974125f, -0.03919545f, -0.01456093f, -0.00045072f, + -0.00360110f, -0.01020207f, -0.01231907f, -0.00638988f, -0.00071592f, + -0.00279122f, -0.00957115f, -0.01288327f, -0.00730937f, -0.00107783f, + -0.00210156f, -0.00890705f, -0.01317668f, -0.00813895f, -0.00153491f, + -0.02128481f, -0.04173044f, -0.04831487f, -0.03293190f, -0.00525260f, + -0.01720322f, -0.04052736f, -0.05045706f, -0.03607317f, -0.00738030f, + -0.01341764f, -0.03965629f, -0.05151616f, -0.03814886f, -0.01005819f, + 0.18968273f, 0.33063684f, -0.01300105f, -0.01372950f, -0.04017465f, + 0.13727832f, 0.36402234f, 0.01027890f, -0.01832107f, -0.03365072f, + 0.08734506f, 0.38194295f, 0.04338228f, -0.02525993f, 0.56408126f, + 0.00458352f, -0.01648227f, -0.04887868f, 0.24585519f, 0.62026135f, + 0.04314807f, -0.02213737f, -0.04158014f, 0.16637289f, 0.65027023f, + 0.09621636f, -0.03101388f, -0.04082742f, -0.00904519f, -0.02790922f, + -0.02117818f, 0.00798662f, -0.03995711f, -0.01243427f, -0.02231705f, + -0.02946266f, 0.00992055f, -0.03600283f, -0.01684920f, -0.00111684f, + -0.00411204f, -0.01297130f, -0.01723725f, -0.01022545f, -0.00165306f, + -0.00313110f, -0.01218016f, -0.01763266f, -0.01125620f, -0.00231663f, + -0.01374149f, -0.03797620f, -0.05142937f, -0.03117307f, -0.00581914f, + -0.01064003f, -0.03608089f, -0.05272168f, -0.03375670f, -0.00795586f, + 0.09628104f, 0.27129991f, -0.00353779f, -0.01734151f, -0.03153981f, + 0.05686230f, 0.28500998f, 0.02230594f, -0.02374955f, 0.68214326f, + 0.05018048f, -0.02320852f, -0.04383616f, 0.18459474f, 0.71517975f, + 0.10805613f, -0.03263677f, -0.03637639f, -0.01394373f, -0.02511203f, + -0.01728636f, 0.05407331f, -0.02867568f, -0.01893131f, -0.00240854f, + -0.00446511f, -0.01636187f, -0.02377053f, -0.01522848f, -0.00333334f, + -0.00819975f, -0.02964169f, -0.04499287f, -0.02745350f, -0.00612408f, + 0.02727416f, 0.19446600f, 0.00159832f, -0.02232473f, 0.74982506f, + 0.11452620f, -0.03348048f, -0.01605681f, -0.02070339f, -0.00458223f + ]; + public bool NonserializedXybEncoded { get; set; } - public bool AllDefault { get; set; } + public bool AllDefault + { + get => this.allDefault; + set => this.allDefault = value; + } public JxlOpsinInverseMatrix? OpsinInverseMatrix { get; set; } - public int CustomWeightsMask { get; set; } + public uint CustomWeightsMask + { + get => this.customWeightsMask; + set => this.customWeightsMask = value; + } + + public InlineArray15 Upsampling2Weights + { + get => this.upsampling2Weights; + set => this.upsampling2Weights = value; + } + + public InlineArray55 Upsampling4Weights + { + get => this.upsampling4Weights; + set => this.upsampling4Weights = value; + } + + public InlineArray210 Upsampling8Weights + { + get => this.upsampling8Weights; + set => this.upsampling8Weights = value; + } + + public bool Visit(JxlVisitor visitor) + { + if (visitor.AllDefault(this, ref this.allDefault)) + { + visitor.SetDefault(this); + return true; + } + + if (visitor.Conditional(this.NonserializedXybEncoded)) + { + if (!visitor.VisitNested(this.OpsinInverseMatrix!)) + { + return false; + } + } + + if (!visitor.Bits(3, 0, ref this.customWeightsMask)) + { + return false; + } - public InlineArray15 Upsampling2Weights { get; set; } + if (visitor.Conditional((this.CustomWeightsMask & 1) != 0)) + { + for (int i = 0; i < 15; i++) + { + if (!visitor.F16(Weights2[i], ref this.upsampling2Weights[i])) + { + return false; + } + } + } - public InlineArray55 Upsampling4Weights { get; set; } + if (visitor.Conditional((this.CustomWeightsMask & 2) != 0)) + { + for (int i = 0; i < 55; i++) + { + if (!visitor.F16(Weights4[i], ref this.upsampling4Weights[i])) + { + return false; + } + } + } - public InlineArray210 Upsampling8Weights { get; set; } + if (visitor.Conditional((this.CustomWeightsMask & 4) != 0)) + { + for (int i = 0; i < 210; i++) + { + if (!visitor.F16(Weights8[i], ref this.upsampling8Weights[i])) + { + return false; + } + } + } - public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); + return true; + } } diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs index e05bb09512..9b8d6e220e 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs @@ -26,7 +26,7 @@ internal sealed class JxlImageMetadata : IJxlFields public bool HaveIntrinsicSize { get; set; } - public JxlSizeHeader IntrinsicSize { get; set; } + public JxlSizeHeader IntrinsicSize { get; set; } = new(); public JxlToneMapping? ToneMapping { get; set; } @@ -34,9 +34,9 @@ internal sealed class JxlImageMetadata : IJxlFields public List ExtraChannels { get; set; } = []; - public JxlPreviewHeader PreviewSize { get; set; } + public JxlPreviewHeader PreviewSize { get; set; } = new(); - public JxlAnimationHeader Animation { get; set; } + public JxlAnimationHeader Animation { get; set; } = new(); public long Extensions { get; set; } @@ -76,7 +76,7 @@ public int AlphaBits return 0; } - return ec.BitDepth?.BitsPerSample ?? 0; + return (int?)ec.BitDepth?.BitsPerSample ?? 0; } set @@ -119,7 +119,7 @@ public void SetUIntSamples(int bits) { if (this.BitDepth != null) { - this.BitDepth.BitsPerSample = bits; + this.BitDepth.BitsPerSample = (uint)bits; this.BitDepth.ExponentBitsPerSample = 0; this.BitDepth.FloatingPointSample = false; } From 133116741eb6c6a052b8a61c3ef13490d6a2ef02 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 8 Sep 2026 05:02:12 +0400 Subject: [PATCH 155/328] Add JPEG parser --- .../Formats/Jxl/IO/Jpeg/JpegDebug.cs | 14 + .../Formats/Jxl/IO/Jpeg/JpegHuffmanDecoder.cs | 4 +- .../Formats/Jxl/IO/Jpeg/JpegReadMode.cs | 13 + .../Formats/Jxl/IO/Jpeg/JpegReader.cs | 1497 +++++++++++++++++ .../Jxl/IO/Metadata/JxlExtraChannelInfo.cs | 14 +- 5 files changed, 1539 insertions(+), 3 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegDebug.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegReadMode.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegReader.cs diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegDebug.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegDebug.cs new file mode 100644 index 0000000000..21361b694c --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegDebug.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg; + +internal static class JpegDebug +{ + [Conditional("DEBUG")] + public static void LogWarning(string message) => + // TODO: do we allow Debug.WriteLine in this codebase? + Debug.WriteLine($"⚠️ JPEG XL [JPEG parser]: {message}"); +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegHuffmanDecoder.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegHuffmanDecoder.cs index 5cff485be9..a41af0bf35 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegHuffmanDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegHuffmanDecoder.cs @@ -8,8 +8,8 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg; /// internal static class JpegHuffmanDecoder { - private const int RootTableBits = 8; - private const int LookupSize = 8; + public const int RootTableBits = 8; + public const int LookupSize = 8; private static int NextTableBitSize(Span count, int length) { diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegReadMode.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegReadMode.cs new file mode 100644 index 0000000000..be4de6a5ed --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegReadMode.cs @@ -0,0 +1,13 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg; + +internal enum JpegReadMode : byte +{ + ReadHeader, + + ReadTables, + + ReadEverything +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegReader.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegReader.cs new file mode 100644 index 0000000000..395d283e83 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegReader.cs @@ -0,0 +1,1497 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; +using SixLabors.ImageSharp.Formats.Jxl.Processing; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using HuffmanTableEntry = SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.JpegHuffmanDecoder.HuffmanTableEntry; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg; + +/// +/// JPEG file parser. The codec uses this parser as part of its +/// JPEG to JPEG XL lossless compression feature. +/// +internal static class JpegReader +{ + private const int BrunsliMaxSampling = 15; + + /// + /// Gets a lookup used by + /// + private static ReadOnlySpan IsValidMarkerLookup => + [ + 1, 1, 1, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, + 0, 1, 1, 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 1, 0, + ]; + + private static int ReadUint8(ReadOnlySpan data, ref int pos) => data[pos++]; + + private static int ReadUint16(ReadOnlySpan data, ref int pos) + { + int value = (data[pos] << 8) | data[pos + 1]; + pos += 2; + return value; + } + + private static void VerifyLength(int pos, int length, int needed) + { + if ((uint)needed > (uint)(length - pos)) + { + throw new InvalidDataException($"Unexpected end of input: pos={pos} need={needed} len={length}"); + } + } + + private static void VerifyInput(int value, int low, int high, string name) + { + if (value < low || value > high) + { + throw new InvalidDataException($"Invalid {name}: {value}"); + } + } + + private static void VerifyMarkerEnd(int startPos, int markerLength, int pos) + { + if (startPos + markerLength != pos) + { + throw new InvalidDataException($"Invalid marker length: declared={markerLength} actual={pos - startPos}"); + } + } + + private static void ExpectMarker(ReadOnlySpan data, int pos) + { + if (pos > data.Length - 2 || data[pos] != 0xff) + { + int value = pos < data.Length ? data[pos] : 0; + + throw new InvalidDataException($"Marker byte (0xff) expected, found: 0x{value:x2} pos={pos} len={data.Length}"); + } + } + + private static void ProcessMarkerSOF(ReadOnlySpan data, int len, JpegReadMode mode, ref int pos, JpegData jpg) + { + if (jpg.Width != 0) + { + throw new InvalidDataException("Duplicate SOF marker"); + } + + int startPos = pos; + VerifyLength(pos, len, 8); + + int markerLength = ReadUint16(data, ref pos); + int precision = ReadUint8(data, ref pos); + int height = ReadUint16(data, ref pos); + int width = ReadUint16(data, ref pos); + int numComponents = ReadUint8(data, ref pos); + + VerifyInput(precision, 8, 8, nameof(precision)); + VerifyInput(height, 1, JpegDataConstants.MaxDimPixels, nameof(height)); + VerifyInput(width, 1, JpegDataConstants.MaxDimPixels, nameof(width)); + VerifyInput(numComponents, 1, JpegDataConstants.MaxComponents, nameof(numComponents)); + + VerifyLength(pos, len, 3 * numComponents); + + jpg.Height = height; + jpg.Width = width; + jpg.Components = new(numComponents); + + Span idsSeen = stackalloc bool[256]; + + int maxHSampFactor = 1; + int maxVSampFactor = 1; + + foreach (JpegComponent component in jpg.Components) + { + int id = ReadUint8(data, ref pos); + + if (idsSeen[id]) + { + throw new InvalidDataException($"Duplicate ID {id} in SOF."); + } + + idsSeen[id] = true; + component.Id = id; + + int factor = ReadUint8(data, ref pos); + int hSampFactor = factor >> 4; + int vSampFactor = factor & 0xf; + + VerifyInput(hSampFactor, 1, BrunsliMaxSampling, nameof(hSampFactor)); + VerifyInput(vSampFactor, 1, BrunsliMaxSampling, nameof(vSampFactor)); + + component.HorizontalSampleFactor = hSampFactor; + component.VerticalSampleFactor = vSampFactor; + component.QuantIndex = ReadUint8(data, ref pos); + + maxHSampFactor = Math.Max(maxHSampFactor, hSampFactor); + maxVSampFactor = Math.Max(maxVSampFactor, vSampFactor); + } + + int mcuRows = JxlMath.DivCeil(height, maxVSampFactor * 8); + int mcuCols = JxlMath.DivCeil(width, maxHSampFactor * 8); + + foreach (JpegComponent component in jpg.Components) + { + if (maxHSampFactor % component.HorizontalSampleFactor != 0 || + maxVSampFactor % component.VerticalSampleFactor != 0) + { + throw new InvalidDataException("Non-integral subsampling ratios"); + } + + component.WidthInBlocks = mcuCols * component.HorizontalSampleFactor; + component.HeightInBlocks = mcuRows * component.VerticalSampleFactor; + } + + VerifyMarkerEnd(startPos, markerLength, pos); + } + + private static void ProcessMarkerSOS(ReadOnlySpan data, int len, ref int pos, JpegData jpg) + { + int startPos = pos; + + VerifyLength(pos, len, 3); + + int markerLength = ReadUint16(data, ref pos); + int componentsInScan = ReadUint8(data, ref pos); + + VerifyInput( + componentsInScan, + 1, + jpg.Components.Count, + nameof(componentsInScan)); + + JpegScanInfo scanInfo = new() + { + NumComponents = componentsInScan + }; + + VerifyLength(pos, len, 2 * componentsInScan); + + Span idsSeen = stackalloc bool[256]; + + for (int i = 0; i < componentsInScan; i++) + { + int id = ReadUint8(data, ref pos); + + if (idsSeen[id]) + { + throw new InvalidDataException($"Duplicate ID {id} in SOS."); + } + + idsSeen[id] = true; + + int componentIndex = -1; + + for (int j = 0; j < jpg.Components.Count; j++) + { + if (jpg.Components[j].Id == id) + { + componentIndex = j; + break; + } + } + + if (componentIndex < 0) + { + throw new InvalidDataException($"SOS marker: Could not find component with id {id}"); + } + + int c = ReadUint8(data, ref pos); + + int dcTableIndex = c >> 4; + int acTableIndex = c & 0xf; + + VerifyInput(dcTableIndex, 0, 3, nameof(dcTableIndex)); + VerifyInput(acTableIndex, 0, 3, nameof(acTableIndex)); + + scanInfo.Components[i].ComponentIndex = componentIndex; + scanInfo.Components[i].DcTableIndex = dcTableIndex; + scanInfo.Components[i].AcTableIndex = acTableIndex; + } + + VerifyLength(pos, len, 3); + + scanInfo.Ss = ReadUint8(data, ref pos); + scanInfo.Se = ReadUint8(data, ref pos); + + VerifyInput(scanInfo.Ss, 0, 63, nameof(scanInfo.Ss)); + VerifyInput( + scanInfo.Se, + scanInfo.Ss, + 63, + nameof(scanInfo.Se)); + + int c2 = ReadUint8(data, ref pos); + + scanInfo.Ah = c2 >> 4; + scanInfo.Al = c2 & 0xf; + + if (scanInfo.Ah != 0 && scanInfo.Al != scanInfo.Ah - 1) + { + JpegDebug.LogWarning($"Invalid progressive parameters: Al={scanInfo.Al}, Ah={scanInfo.Ah}"); + } + + // Check that all Huffman tables needed for this scan are defined. + for (int i = 0; i < componentsInScan; i++) + { + bool foundDcTable = false; + bool foundAcTable = false; + + int dcTableIndex = scanInfo.Components[i].DcTableIndex; + int acTableIndex = scanInfo.Components[i].AcTableIndex; + + foreach (JpegHuffmanCode code in jpg.HuffmanCodes) + { + int slotId = code.SlotId; + + if (slotId == dcTableIndex) + { + foundDcTable = true; + } + else if (slotId == acTableIndex + 16) + { + foundAcTable = true; + } + } + + if (scanInfo.Ss == 0 && !foundDcTable) + { + throw new InvalidDataException( + $"SOS marker: Could not find DC Huffman table " + + $"with index {dcTableIndex}"); + } + + if (scanInfo.Se > 0 && !foundAcTable) + { + throw new InvalidDataException( + $"SOS marker: Could not find AC Huffman table " + + $"with index {acTableIndex}"); + } + } + + jpg.ScanInfos.Add(scanInfo); + VerifyMarkerEnd(startPos, markerLength, pos); + } + + private static void ProcessMarkerDHT(ReadOnlySpan data, int len, JpegReadMode mode, Span dcHuffLut, Span acHuffLut, ref int pos, JpegData jpg) + { + int startPos = pos; + + VerifyLength(pos, len, 2); + + int markerLength = ReadUint16(data, ref pos); + + if (markerLength == 2) + { + // Empty DHT marker. Useless, but does occur in the wild. + // Represent it with a dummy all-zeroes Huffman table. + JpegHuffmanCode huff = new() + { + IsLast = true + }; + + jpg.HuffmanCodes.Add(huff); + return; + } + + Span valuesSeen = stackalloc bool[256]; + + while (pos < startPos + markerLength) + { + VerifyLength(pos, len, 1 + JpegDataConstants.JpegHuffmanMaxBitLength); + + JpegHuffmanCode huff = new() + { + SlotId = ReadUint8(data, ref pos) + }; + + int huffmanIndex = huff.SlotId; + bool isAcTable = (huff.SlotId & 0x10) != 0; + + Span huffLut; + + if (isAcTable) + { + huffmanIndex -= 0x10; + VerifyInput(huffmanIndex, 0, 3, nameof(huffmanIndex)); + + huffLut = acHuffLut.Slice( + huffmanIndex * JpegHuffmanDecoder.LookupSize, + JpegHuffmanDecoder.LookupSize); + } + else + { + VerifyInput(huffmanIndex, 0, 3, nameof(huffmanIndex)); + + huffLut = dcHuffLut.Slice( + huffmanIndex * JpegHuffmanDecoder.LookupSize, + JpegHuffmanDecoder.LookupSize); + } + + huff.Counts[0] = 0; + + int totalCount = 0; + int space = 1 << JpegDataConstants.JpegHuffmanMaxBitLength; + int maxDepth = 1; + + for (int i = 1; i <= JpegDataConstants.JpegHuffmanMaxBitLength; i++) + { + int count = ReadUint8(data, ref pos); + + if (count != 0) + { + maxDepth = i; + } + + huff.Counts[i] = count; + totalCount += count; + + space -= count * (1 << (JpegDataConstants.JpegHuffmanMaxBitLength - i)); + } + + VerifyInput( + totalCount, + 0, + isAcTable ? JpegDataConstants.JpegHuffmanAlphabetSize : JpegDataConstants.JpegDcAlphabetSize, + nameof(totalCount)); + + VerifyLength(pos, len, totalCount); + + valuesSeen.Clear(); // set everything to false + + for (int i = 0; i < totalCount; i++) + { + int value = ReadUint8(data, ref pos); + + if (!isAcTable) + { + VerifyInput( + value, + 0, + JpegDataConstants.JpegDcAlphabetSize - 1, + nameof(value)); + } + + if (valuesSeen[value]) + { + throw new InvalidDataException($"Duplicate Huffman code value {value}"); + } + + valuesSeen[value] = true; + huff.Values[i] = value; + } + + huff.Counts[maxDepth]++; + huff.Values[totalCount] = JpegDataConstants.JpegHuffmanAlphabetSize; + + space -= 1 << (JpegDataConstants.JpegHuffmanMaxBitLength - maxDepth); + + if (space < 0) + { + throw new InvalidDataException("Invalid Huffman code lengths"); + } + + if (space > 0 && huffLut[0].Value != 0xffff) + { + for (int i = 0; i < JpegHuffmanDecoder.LookupSize; i++) + { + huffLut[i].Bits = 0; + huffLut[i].Value = 0xffff; + } + } + + huff.IsLast = pos == startPos + markerLength; + + if (mode == JpegReadMode.ReadEverything) + { + JpegHuffmanDecoder.BuildJpegHuffmanTable( + huff.Counts, + huff.Values, + huffLut); + } + + jpg.HuffmanCodes.Add(huff); + } + + VerifyMarkerEnd(startPos, markerLength, pos); + } + + private static void ProcessMarkerDQT(ReadOnlySpan data, int len, ref int pos, JpegData jpg) + { + int startPos = pos; + VerifyLength(pos, len, 2); + int markerLength = ReadUint16(data, ref pos); + + if (markerLength == 2) + { + throw new InvalidDataException("DQT marker: no quantization table found"); + } + + while (pos < startPos + markerLength && jpg.Quant.Count < JpegDataConstants.MaximumQuantizationTables) + { + VerifyLength(pos, len, 1); + + int quantTableIndex = ReadUint8(data, ref pos); + int quantTablePrecision = quantTableIndex >> 4; + + VerifyInput( + quantTablePrecision, + 0, + 1, + nameof(quantTablePrecision)); + + quantTableIndex &= 0xf; + + VerifyInput( + quantTableIndex, + 0, + 3, + nameof(quantTableIndex)); + + VerifyLength( + pos, + len, + (quantTablePrecision + 1) * JxlFrameDimensions.DctBlockSize); + + JpegQuantizationTable table = new() + { + Index = quantTableIndex, + Precision = quantTablePrecision + }; + + for (int i = 0; i < JxlFrameDimensions.DctBlockSize; i++) + { + int quantValue = quantTablePrecision != 0 + ? ReadUint16(data, ref pos) + : ReadUint8(data, ref pos); + + VerifyInput( + quantValue, + 1, + 65535, + nameof(quantValue)); + + table.Values[JpegDataConstants.JpegNaturalOrder[i]] = quantValue; + } + + table.IsLast = pos == startPos + markerLength; + + jpg.Quant.Add(table); + } + + VerifyMarkerEnd(startPos, markerLength, pos); + } + + private static void ProcessDRI(ReadOnlySpan data, int len, ref int pos, ref bool foundDri, JpegData jpg) + { + if (foundDri) + { + throw new InvalidDataException("Duplicate DRI marker."); + } + + foundDri = true; + int startPos = pos; + + VerifyLength(pos, len, 4); + + int markerLength = ReadUint16(data, ref pos); + int restartInterval = ReadUint16(data, ref pos); + + jpg.RestartInterval = restartInterval; + + VerifyMarkerEnd(startPos, markerLength, pos); + } + + private static void ProcessAPP(ReadOnlySpan data, int len, ref int pos, JpegData jpg) + { + VerifyLength(pos, len, 2); + + int markerLength = ReadUint16(data, ref pos); + + VerifyInput(markerLength, 2, 65535, nameof(markerLength)); + VerifyLength(pos, len, markerLength - 2); + + if (pos < 3) + { + throw new InvalidDataException("Invalid APP marker position"); + } + + ReadOnlySpan appData = data.Slice(pos - 3, markerLength + 1); + jpg.AppData.Add([.. appData]); + + pos += markerLength - 2; + } + + private static void ProcessMarkerCOM(ReadOnlySpan data, int len, ref int pos, JpegData jpg) + { + VerifyLength(pos, len, 2); + + int markerLength = ReadUint16(data, ref pos); + + VerifyInput(markerLength, 2, 65535, nameof(markerLength)); + VerifyLength(pos, len, markerLength - 2); + + if (pos < 3) + { + throw new InvalidDataException("Invalid COM marker position"); + } + + ReadOnlySpan comData = data.Slice(pos - 3, markerLength + 1); + jpg.ComData.Add([.. comData]); + + pos += markerLength - 2; + } + + private static int ReadSymbol(ReadOnlySpan table, ref BitReaderState br) + { + br.FillBitWindow(); + + int value = (int)((br.Value >> (br.BitsLeft - 8)) & 0xff); + HuffmanTableEntry entry = table[value]; + + int bitCount = entry.Bits - 8; + + if (bitCount > 0) + { + br.BitsLeft -= 8; + + entry = table[entry.Value + (int)( + (br.Value >> (br.BitsLeft - bitCount)) & + ((1UL << bitCount) - 1))]; + } + + br.BitsLeft -= entry.Bits; + return entry.Value; + } + + private static int HuffExtend(int x, int s) + { + DebugGuard.MustBeGreaterThanOrEqualTo(s, 1, nameof(s)); + + int half = 1 << (s - 1); + + if (x >= half) + { + if (x < (1 << s)) + { + throw new InvalidOperationException("x is invalid"); + } + + return x; + } + + return x - (1 << s) + 1; + } + + private static bool DecodeDCTBlock( + ReadOnlySpan dcHuff, + ReadOnlySpan acHuff, + int ss, + int se, + int al, + ref int eobRun, + ref bool resetState, + ref int numZeroRuns, + ref BitReaderState br, + ref int lastDcCoeff, + Span coeffs) + { + int am = 1 << al; + bool eobRunAllowed = ss > 0; + + if (ss == 0) + { + int s = ReadSymbol(dcHuff, ref br); + + if (s >= JpegDataConstants.JpegDcAlphabetSize) + { + return false; + } + + int diff = 0; + + if (s > 0) + { + int bits = br.ReadBits(s); + diff = HuffExtend(bits, s); + } + + int coeff = diff + lastDcCoeff; + int dcCoeff = coeff * am; + + coeffs[0] = dcCoeff; + + if (dcCoeff != coeffs[0]) + { + return false; + } + + lastDcCoeff = coeff; + ss++; + } + + if (ss > se) + { + return true; + } + + if (eobRun > 0) + { + eobRun--; + return true; + } + + numZeroRuns = 0; + + for (int k = ss; k <= se; k++) + { + int sr = ReadSymbol(acHuff, ref br); + + if (sr >= JpegDataConstants.JpegHuffmanAlphabetSize) + { + return false; + } + + int r = sr >> 4; + int s = sr & 15; + + if (s > 0) + { + k += r; + + if (k > se) + { + return false; + } + + if (s + al >= JpegDataConstants.JpegDcAlphabetSize) + { + return false; + } + + int bits = br.ReadBits(s); + int coeff = HuffExtend(bits, s); + + coeffs[JpegDataConstants.JpegNaturalOrder[k]] = coeff * am; + numZeroRuns = 0; + } + else if (r == 15) + { + k += 15; + numZeroRuns++; + } + else + { + if (eobRunAllowed && k == ss && eobRun == 0) + { + // Two EOB runs immediately after each other signal that + // the JPEG encoder should force a state reset here. + resetState = true; + } + + eobRun = 1 << r; + + if (r > 0) + { + if (!eobRunAllowed) + { + return false; + } + + eobRun += br.ReadBits(r); + } + + break; + } + } + + eobRun--; + return true; + } + + private static bool RefineDCTBlock( + ReadOnlySpan acHuff, + int ss, + int se, + int al, + ref int eobRun, + ref bool resetState, + ref BitReaderState br, + Span coeffs) + { + int am = 1 << al; + bool eobRunAllowed = ss > 0; + + int s; + if (ss == 0) + { + s = br.ReadBits(1); + int dcCoeff = coeffs[0]; + + dcCoeff |= s * am; + coeffs[0] = dcCoeff; + + ++ss; + } + + if (ss > se) + { + return true; + } + + int p1 = am; + int m1 = -am; + int k = ss; + bool inZeroRun = false; + + if (eobRun <= 0) + { + for (; k <= se; k++) + { + int symbol = ReadSymbol(acHuff, ref br); + + if (symbol >= JpegDataConstants.JpegHuffmanAlphabetSize) + { + return false; + } + + int r = symbol >> 4; + s = symbol & 15; + + if (s != 0) + { + if (s != 1) + { + return false; + } + + s = br.ReadBits(1) != 0 ? p1 : m1; + inZeroRun = false; + } + else + { + if (r != 15) + { + if (eobRunAllowed && k == ss && eobRun == 0) + { + resetState = true; + } + + eobRun = 1 << r; + + if (r > 0) + { + if (!eobRunAllowed) + { + return false; + } + + eobRun += br.ReadBits(r); + } + + break; + } + + inZeroRun = true; + } + + do + { + int index = JpegDataConstants.JpegNaturalOrder[k]; + int coefficient = coeffs[index]; + + if (coefficient != 0) + { + if (br.ReadBits(1) != 0) + { + if ((coefficient & p1) == 0) + { + coefficient += coefficient >= 0 + ? p1 + : m1; + } + } + + coeffs[index] = coefficient; + } + else + { + if (--r < 0) + { + break; + } + } + + k++; + } + while (k <= se); + + if (s != 0) + { + if (k > se) + { + return false; + } + + coeffs[JpegDataConstants.JpegNaturalOrder[k]] = s; + } + } + } + + if (inZeroRun) + { + return false; + } + + if (eobRun > 0) + { + for (; k <= se; k++) + { + int index = JpegDataConstants.JpegNaturalOrder[k]; + int coefficient = coeffs[index]; + + if (coefficient != 0) + { + if (br.ReadBits(1) != 0) + { + if ((coefficient & p1) == 0) + { + coefficient += coefficient >= 0 + ? p1 + : m1; + } + } + + coeffs[index] = coefficient; + } + } + } + + --eobRun; + return true; + } + + private static bool ProcessRestart(ReadOnlySpan data, ref int nextRestartMarker, ref BitReaderState br, JpegData jpg) + { + int pos = 0; + + if (!br.FinishStream(jpg, ref pos)) + { + return false; + } + + int expectedMarker = 0xd0 + nextRestartMarker; + ExpectMarker(data, pos); + + int marker = data[pos + 1]; + + if (marker != expectedMarker) + { + return false; + } + + br.Reset(pos + 2); + + nextRestartMarker = (nextRestartMarker + 1) & 0x7; + + return true; + } + + private static bool ProcessScan(ReadOnlySpan data, int len, ReadOnlySpan dcHuffLut, ReadOnlySpan acHuffLut, Span scanProgression, bool isProgressive, ref int pos, JpegData jpg) + { + ProcessMarkerSOS(data, len, ref pos, jpg); + + JpegScanInfo scanInfo = jpg.ScanInfos[^1]; + + bool isInterleaved = scanInfo.NumComponents > 1; + + int maxHSampFactor = 1; + int maxVSampFactor = 1; + + foreach (JpegComponent component in jpg.Components) + { + maxHSampFactor = Math.Max(maxHSampFactor, component.HorizontalSampleFactor); + maxVSampFactor = Math.Max(maxVSampFactor, component.VerticalSampleFactor); + } + + int mcuRows = JxlMath.DivCeil(jpg.Height, maxVSampFactor * 8); + int mcusPerRow = JxlMath.DivCeil(jpg.Width, maxHSampFactor * 8); + + if (!isInterleaved) + { + JpegComponent c = jpg.Components[scanInfo.Components[0].ComponentIndex]; + + mcusPerRow = JxlMath.DivCeil( + jpg.Width * c.HorizontalSampleFactor, + 8 * maxHSampFactor); + + mcuRows = JxlMath.DivCeil( + jpg.Height * c.VerticalSampleFactor, + 8 * maxVSampFactor); + } + + Span lastDcCoeff = stackalloc int[JpegDataConstants.MaxComponents]; + lastDcCoeff.Clear(); + + BitReaderState br = new(data, pos); + + int restartsToGo = jpg.RestartInterval; + int nextRestartMarker = 0; + int eobRun = -1; + int blockScanIndex = 0; + + int al = isProgressive ? scanInfo.Al : 0; + int ah = isProgressive ? scanInfo.Ah : 0; + int ss = isProgressive ? scanInfo.Ss : 0; + int se = isProgressive ? scanInfo.Se : 63; + + ushort scanBitmask = ah == 0 ? (ushort)(0xffff << al) : (ushort)(1u << al); + ushort refinementBitmask = (ushort)((1 << al) - 1); + + for (int i = 0; i < scanInfo.NumComponents; ++i) + { + int compIdx = scanInfo.Components[i].ComponentIndex; + + for (int k = ss; k <= se; ++k) + { + int progressionIndex = (compIdx * JxlFrameDimensions.DctBlockSize) + k; + ushort previousMask = scanProgression[progressionIndex]; + + if ((previousMask & scanBitmask) != 0) + { + return false; + } + + if ((previousMask & refinementBitmask) != 0) + { + return false; + } + + scanProgression[progressionIndex] |= scanBitmask; + } + } + + if (al > 10) + { + return false; + } + + foreach (JpegComponent c in jpg.Components) + { + if (c.Coefficients.Count == 0) + { + int numBlocks = c.WidthInBlocks * c.HeightInBlocks; + c.Coefficients = new List(numBlocks * JxlFrameDimensions.DctBlockSize); + } + } + + for (int mcuY = 0; mcuY < mcuRows; ++mcuY) + { + for (int mcuX = 0; mcuX < mcusPerRow; ++mcuX) + { + if (jpg.RestartInterval > 0) + { + if (restartsToGo == 0) + { + if (!ProcessRestart(data, ref nextRestartMarker, ref br, jpg)) + { + return false; + } + + restartsToGo = jpg.RestartInterval; + lastDcCoeff.Clear(); + + if (eobRun > 0) + { + return false; + } + + eobRun = -1; + } + + --restartsToGo; + } + + for (int i = 0; i < scanInfo.NumComponents; ++i) + { + JpegComponentScanInfo si = scanInfo.Components[i]; + JpegComponent c = jpg.Components[si.ComponentIndex]; + + ReadOnlySpan dcLut = + dcHuffLut.Slice( + si.DcTableIndex * JpegHuffmanDecoder.LookupSize, + JpegHuffmanDecoder.LookupSize); + + ReadOnlySpan acLut = + acHuffLut.Slice( + si.AcTableIndex * JpegHuffmanDecoder.LookupSize, + JpegHuffmanDecoder.LookupSize); + + int nblocksY = isInterleaved ? c.VerticalSampleFactor : 1; + int nblocksX = isInterleaved ? c.HorizontalSampleFactor : 1; + + for (int iy = 0; iy < nblocksY; ++iy) + { + for (int ix = 0; ix < nblocksX; ++ix) + { + int blockY = (mcuY * nblocksY) + iy; + int blockX = (mcuX * nblocksX) + ix; + int blockIdx = (blockY * c.WidthInBlocks) + blockX; + + bool resetState = false; + int numZeroRuns = 0; + + Span coeffs = CollectionsMarshal.AsSpan(c.Coefficients).Slice(blockIdx * JxlFrameDimensions.DctBlockSize, JxlFrameDimensions.DctBlockSize); + + bool success; + + if (ah == 0) + { + success = DecodeDCTBlock( + dcLut, + acLut, + ss, + se, + al, + ref eobRun, + ref resetState, + ref numZeroRuns, + ref br, + ref lastDcCoeff[si.ComponentIndex], + coeffs); + } + else + { + success = RefineDCTBlock( + acLut, + ss, + se, + al, + ref eobRun, + ref resetState, + ref br, + coeffs); + } + + if (!success) + { + return false; + } + + if (resetState) + { + scanInfo.ResetPoints.Add(blockScanIndex); + } + + if (numZeroRuns > 0) + { + scanInfo.ExtraZeroRuns.Add( + new JpegExtraZeroRunInfo + { + BlockIndex = blockScanIndex, + NumExtraZeroRuns = numZeroRuns + }); + } + + blockScanIndex++; + } + } + } + } + } + + if (eobRun > 0) + { + return false; + } + + if (!br.FinishStream(jpg, ref pos)) + { + return false; + } + + if (pos > len) + { + return false; + } + + return true; + } + + private static int FindNextMarker(ReadOnlySpan data, int pos) + { + int numSkipped = 0; + + while (pos + 1 < data.Length && + (data[pos] != 0xff || + data[pos + 1] < 0xc0 || + IsValidMarkerLookup[data[pos + 1] - 0xc0] == 0)) + { + pos++; + numSkipped++; + } + + return numSkipped; + } + + private static bool FixupIndexes(JpegData jpg) + { + for (int i = 0; i < jpg.Components.Count; ++i) + { + JpegComponent c = jpg.Components[i]; + bool foundIndex = false; + + for (int j = 0; j < jpg.Quant.Count; ++j) + { + if (jpg.Quant[j].Index == c.QuantIndex) + { + c.QuantIndex = j; + foundIndex = true; + break; + } + } + + if (!foundIndex) + { + return false; + } + } + + return true; + } + + public static bool ReadJpeg(ReadOnlySpan data, JpegReadMode mode, JpegData jpg) + { + int len = data.Length; + int pos = 0; + + ExpectMarker(data, pos); + + int marker = data[pos + 1]; + pos += 2; + + if (marker != 0xd8) + { + return false; + } + + int lutSize = JpegDataConstants.MaxHuffmanTables * JpegHuffmanDecoder.LookupSize; + + Span dcHuffLut = stackalloc HuffmanTableEntry[lutSize]; + Span acHuffLut = stackalloc HuffmanTableEntry[lutSize]; + + bool foundSof = false; + bool foundSos = false; + bool foundDri = false; + + Span scanProgression = stackalloc ushort[JpegDataConstants.MaxComponents * JxlFrameDimensions.DctBlockSize]; + + scanProgression.Clear(); + + jpg.PaddingBits.Clear(); + + bool isProgressive = false; + + do + { + int numSkipped = FindNextMarker(data, pos); + + if (numSkipped > 0) + { + // Add a fake marker to indicate arbitrary + // in-between-markers data. + jpg.MarkerOrder.Add(0xff); + jpg.InterMarkerData.Add([.. data.Slice(pos, numSkipped)]); + + pos += numSkipped; + } + + ExpectMarker(data, pos); + + marker = data[pos + 1]; + pos += 2; + + switch (marker) + { + case 0xc0: + case 0xc1: + case 0xc2: + isProgressive = marker == 0xc2; + + ProcessMarkerSOF( + data, + len, + mode, + ref pos, + jpg); + + foundSof = true; + break; + + case 0xc4: + ProcessMarkerDHT( + data, + len, + mode, + dcHuffLut, + acHuffLut, + ref pos, + jpg); + + break; + + case 0xd0: + case 0xd1: + case 0xd2: + case 0xd3: + case 0xd4: + case 0xd5: + case 0xd6: + case 0xd7: + // RST markers do not have any data. + break; + + case 0xd9: + // Found end marker. + break; + + case 0xda: + if (mode == JpegReadMode.ReadEverything) + { + if (!ProcessScan( + data, + len, + dcHuffLut, + acHuffLut, + scanProgression, + isProgressive, + ref pos, + jpg)) + { + return false; + } + } + + foundSos = true; + break; + + case 0xdb: + ProcessMarkerDQT(data, len, ref pos, jpg); + break; + + case 0xdd: + ProcessDRI(data, len, ref pos, ref foundDri, jpg); + break; + + case >= 0xe0 and <= 0xef: + if (mode != JpegReadMode.ReadTables) + { + ProcessAPP(data, len, ref pos, jpg); + } + + break; + + case 0xfe: + if (mode != JpegReadMode.ReadTables) + { + ProcessMarkerCOM(data, len, ref pos, jpg); + } + + break; + + default: + return false; + } + + jpg.MarkerOrder.Add((byte)marker); + + if (mode == JpegReadMode.ReadHeader && foundSof) + { + break; + } + } + while (marker != 0xd9); + + if (!foundSof || !foundSos) + { + return false; + } + + if (mode == JpegReadMode.ReadEverything) + { + if (pos < len) + { + jpg.TailData = [.. data[pos..]]; + } + + if (!FixupIndexes(jpg)) + { + return false; + } + + if (jpg.HuffmanCodes.Count == 0) + { + return false; + } + + if (jpg.HuffmanCodes.Count >= JpegDataConstants.MaxDhtMarkers) + { + return false; + } + } + + return true; + } + + private ref struct BitReaderState + { + private readonly ReadOnlySpan data; + private readonly int length; + + private int pos; + private ulong value; + private int bitsLeft; + private int nextMarkerPos; + + public BitReaderState(ReadOnlySpan data, int pos) + { + this.data = data; + this.length = data.Length; + + this.pos = pos; + this.value = 0; + this.bitsLeft = 0; + this.nextMarkerPos = this.length - 2; + + this.FillBitWindow(); + } + + public int BitsLeft + { + readonly get => this.bitsLeft; + set => this.bitsLeft = value; + } + + public ulong Value + { + readonly get => this.value; + set => this.value = value; + } + + public void Reset(int pos) + { + this.pos = pos; + this.value = 0; + this.bitsLeft = 0; + this.nextMarkerPos = this.length - 2; + + this.FillBitWindow(); + } + + private byte GetNextByte() + { + if (this.pos >= this.nextMarkerPos) + { + ++this.pos; + return 0; + } + + byte c = this.data[this.pos++]; + + if (c == 0xff) + { + byte escape = this.data[this.pos]; + + if (escape == 0) + { + ++this.pos; + } + else + { + // 0xff followed by a non-zero byte means that we found + // the start of the next marker segment. + this.nextMarkerPos = this.pos - 1; + } + } + + return c; + } + + public void FillBitWindow() + { + if (this.bitsLeft <= 16) + { + while (this.bitsLeft <= 56) + { + this.value <<= 8; + this.value |= this.GetNextByte(); + this.bitsLeft += 8; + } + } + } + + public int ReadBits(int bitCount) + { + this.FillBitWindow(); + + ulong result = + (this.value >> (this.bitsLeft - bitCount)) & + ((1UL << bitCount) - 1); + + this.bitsLeft -= bitCount; + return (int)result; + } + + public bool FinishStream(JpegData jpg, ref int pos) + { + int paddingBitCount = this.bitsLeft & 7; + + if (paddingBitCount > 0) + { + ulong paddingMask = (1UL << paddingBitCount) - 1; + ulong paddingBits = + (this.value >> (this.bitsLeft - paddingBitCount)) & + paddingMask; + + if (paddingBits != paddingMask) + { + jpg.HasZeroPaddingBit = true; + } + + for (int i = paddingBitCount - 1; i >= 0; --i) + { + jpg.PaddingBits.Add((byte)((paddingBits >> i) & 1)); + } + } + + int unusedBytesLeft = this.bitsLeft >> 3; + + while (unusedBytesLeft-- > 0) + { + --this.pos; + + if (this.pos < this.nextMarkerPos && + this.data[this.pos] == 0 && + this.data[this.pos - 1] == 0xff) + { + --this.pos; + } + } + + if (this.pos > this.nextMarkerPos) + { + return false; + } + + pos = this.pos; + return true; + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs index 16e149d1d4..1ad671969e 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs @@ -23,5 +23,17 @@ internal sealed class JxlExtraChannelInfo : IJxlFields public int CfaChannel { get; set; } - public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); + public bool Visit(JxlVisitor visitor) + { + if (visitor.AllDefault(this, ref this.allDefault)) + { + visitor.SetDefault(this); + return true; + } + + if (!visitor.Enum(JxlExtraChannel.Alpha, ref this.Type)) + { + return false; + } + } } From aaf4d1ad1dbcc025392bf066a88ddb04eb5a6c8f Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 8 Sep 2026 06:04:29 +0400 Subject: [PATCH 156/328] Add JPEG writer (incomplete) --- .../Formats/Jxl/IO/Jpeg/JpegBitWriter.cs | 190 +++ .../Formats/Jxl/IO/Jpeg/JpegDctCodingState.cs | 17 + .../Jxl/IO/Jpeg/JpegHuffmanCodeTable.cs | 22 + .../Formats/Jxl/IO/Jpeg/JpegHuffmanDecoder.cs | 2 +- .../Jxl/IO/Jpeg/JpegSerializationStage.cs | 12 + .../Jxl/IO/Jpeg/JpegSerializationState.cs | 40 + .../Jxl/IO/Jpeg/JpegSerializationStatus.cs | 12 + .../Formats/Jxl/IO/Jpeg/JpegWriter.cs | 1304 +++++++++++++++++ 8 files changed, 1598 insertions(+), 1 deletion(-) create mode 100644 src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegBitWriter.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegDctCodingState.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegHuffmanCodeTable.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegSerializationStage.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegSerializationState.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegSerializationStatus.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegWriter.cs diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegBitWriter.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegBitWriter.cs new file mode 100644 index 0000000000..bb97c941b3 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegBitWriter.cs @@ -0,0 +1,190 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg; + +internal sealed class JpegBitWriter +{ + private const int ChunkSize = 16 * 1024; + + private readonly Stream stream; + private readonly byte[] buffer; + + private int pos; + private ulong putBuffer; + private int putBits; + + public JpegBitWriter(Stream stream) + { + this.stream = stream; + this.buffer = new byte[ChunkSize]; + this.putBits = 64; + } + + public bool Healthy { get; private set; } = true; + + private void SwapBuffer() + { + if (this.pos == 0) + { + return; + } + + this.stream.Write(this.buffer, 0, this.pos); + this.pos = 0; + } + + public void Reserve(int nBytes) + { + if (this.pos + nBytes > this.buffer.Length) + { + this.SwapBuffer(); + } + } + + private void EmitByte(int value) + { + this.buffer[this.pos++] = (byte)value; + + if (value == 0xff) + { + this.buffer[this.pos++] = 0; + } + } + + public void EmitMarker(int marker) + { + this.Reserve(2); + + this.buffer[this.pos++] = 0xff; + this.buffer[this.pos++] = (byte)marker; + } + + public void StoreBe64(ulong value) + { + this.buffer[this.pos++] = (byte)(value >> 56); + this.buffer[this.pos++] = (byte)(value >> 48); + this.buffer[this.pos++] = (byte)(value >> 40); + this.buffer[this.pos++] = (byte)(value >> 32); + this.buffer[this.pos++] = (byte)(value >> 24); + this.buffer[this.pos++] = (byte)(value >> 16); + this.buffer[this.pos++] = (byte)(value >> 8); + this.buffer[this.pos++] = (byte)value; + } + + public void DischargeBitBuffer(int nBits, ulong bits) + { + this.putBuffer |= bits >> -this.putBits; + + if (HasZeroByte(~this.putBuffer)) + { + this.Reserve(16); + + this.EmitByte((int)(this.putBuffer >> 56)); + this.EmitByte((int)(this.putBuffer >> 48)); + this.EmitByte((int)(this.putBuffer >> 40)); + this.EmitByte((int)(this.putBuffer >> 32)); + this.EmitByte((int)(this.putBuffer >> 24)); + this.EmitByte((int)(this.putBuffer >> 16)); + this.EmitByte((int)(this.putBuffer >> 8)); + this.EmitByte((int)this.putBuffer); + } + else + { + this.Reserve(8); + this.StoreBe64(this.putBuffer); + } + + this.putBits += 64; + this.putBuffer = bits << this.putBits; + } + + public void WriteBits(int nBits, ulong bits) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(nBits); + + this.putBits -= nBits; + + if (this.putBits < 0) + { + if (nBits > 64) + { + this.putBits += nBits; + this.Healthy = false; + } + else + { + this.DischargeBitBuffer(nBits, bits); + } + } + else + { + this.putBuffer |= bits << this.putBits; + } + } + + public bool JumpToByteBoundary(ref ReadOnlySpan padBits) + { + int nBits = this.putBits & 7; + byte padPattern; + + if (padBits.IsEmpty) + { + padPattern = (byte)((1 << nBits) - 1); + } + else + { + padPattern = 0; + byte danglingBits = 0; + + for (int i = 0; i < nBits; ++i) + { + if (padBits.IsEmpty) + { + return false; + } + + byte bit = padBits[0]; + padBits = padBits[1..]; + + danglingBits |= bit; + + padPattern <<= 1; + padPattern |= bit; + } + + if ((danglingBits & ~1) != 0) + { + return false; + } + } + + this.Reserve(16); + + while (this.putBits <= 56) + { + int c = (int)(this.putBuffer >> 56); + + this.EmitByte(c); + + this.putBuffer <<= 8; + this.putBits += 8; + } + + if (this.putBits < 64) + { + int padMask = 0xff >> (64 - this.putBits); + + int c = + ((int)(this.putBuffer >> 56) & ~padMask) | + padPattern; + + this.EmitByte(c); + } + + this.putBuffer = 0; + this.putBits = 64; + + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegDctCodingState.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegDctCodingState.cs new file mode 100644 index 0000000000..14fc0f9a3a --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegDctCodingState.cs @@ -0,0 +1,17 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg; + +internal sealed class JpegDctCodingState +{ + public int EobRun { get; set; } + + public JpegHuffmanCodeTable? CurAcHuff { get; set; } + + public List RefinementBits { get; } = new(64); + + public int RefinementBitsCount { get; set; } + + public void Flush(JpegBitWriter writer) => JpegWriter.Flush(this, writer); +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegHuffmanCodeTable.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegHuffmanCodeTable.cs new file mode 100644 index 0000000000..847485e2e5 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegHuffmanCodeTable.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg; + +// InlineArray values cannot be assigned as a property +#pragma warning disable SA1401 // Fields should be private + +internal sealed class JpegHuffmanCodeTable +{ + public InlineArray256 Depth; + + public InlineArray256 Code; + + public bool Initialized { get; set; } + + public void InitializeDepths(byte value = 0) + { + Span depths = this.Depth; + depths.Fill(value); + } +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegHuffmanDecoder.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegHuffmanDecoder.cs index a41af0bf35..d0e11df072 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegHuffmanDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegHuffmanDecoder.cs @@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg; internal static class JpegHuffmanDecoder { public const int RootTableBits = 8; - public const int LookupSize = 8; + public const int LookupSize = 1024; private static int NextTableBitSize(Span count, int length) { diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegSerializationStage.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegSerializationStage.cs new file mode 100644 index 0000000000..47d1b2d3ad --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegSerializationStage.cs @@ -0,0 +1,12 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg; + +internal enum JpegSerializationStage +{ + Initialize, + SerializeSection, + Done, + Error +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegSerializationState.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegSerializationState.cs new file mode 100644 index 0000000000..0636bf28c9 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegSerializationState.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg; + +internal sealed class JpegSerializationState +{ + public JpegSerializationStage Stage { get; set; } = JpegSerializationStage.Initialize; + + public int SectionIndex { get; set; } + + public int DhtIndex { get; set; } + + public int DqtIndex { get; set; } + + public int AppIndex { get; set; } + + public int ComIndex { get; set; } + + public int DataIndex { get; set; } + + public int ScanIndex { get; set; } + + public JpegHuffmanCodeTable[] DcHuffTable { get; set; } = []; + + public JpegHuffmanCodeTable[] AcHuffTable { get; set; } = []; + + // Should be field to get ref to it + public ReadOnlyMemory PadBits; + + public int PadBitsPos { get; set; } + + public bool SeenDriMarker { get; set; } + + public bool IsProgressive { get; set; } + + public List> OutputQueue { get; set; } = []; + + public EncodeScanState ScanState { get; set; } +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegSerializationStatus.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegSerializationStatus.cs new file mode 100644 index 0000000000..8b7bf5e0e3 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegSerializationStatus.cs @@ -0,0 +1,12 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg; + +internal enum JpegSerializationStatus : byte +{ + NeedsMoreInput, + NeedsMoreOutput, + Error, + Done +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegWriter.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegWriter.cs new file mode 100644 index 0000000000..f36a5c244c --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegWriter.cs @@ -0,0 +1,1304 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; +using SixLabors.ImageSharp.Formats.Jxl.Processing; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg; + +internal class JpegWriter +{ + private static void DCTCodingStateInit(JpegDctCodingState state) + { + state.EobRun = 0; + state.CurAcHuff = null; + state.RefinementBits.Clear(); + state.RefinementBitsCount = 0; + } + + internal static void Flush(JpegDctCodingState state, JpegBitWriter bw) + { + if (state.EobRun > 0) + { + bw.Reserve(16); + + int nBits = JxlMath.FloorLog2Nonzero(state.EobRun); + int symbol = nBits << 4; + + WriteSymbol(symbol, state.CurAcHuff!, bw); + + if (nBits > 0) + { + bw.WriteBits( + nBits, + (ulong)(state.EobRun & ((1 << nBits) - 1))); + } + + state.EobRun = 0; + } + + const int stride = 124; + + int numWords = state.RefinementBitsCount >> 4; + int i = 0; + + while (i < numWords) + { + int limit = Math.Min(i + stride, numWords); + + bw.Reserve(512); + + for (; i < limit; ++i) + { + bw.WriteBits(16, state.RefinementBits[i]); + } + } + + bw.Reserve(16); + + int tail = state.RefinementBitsCount & 0xf; + + if (tail != 0) + { + bw.WriteBits( + tail, + state.RefinementBits[^1]); + } + + state.RefinementBits.Clear(); + state.RefinementBitsCount = 0; + } + + private static void WriteSymbol(int symbol, JpegHuffmanCodeTable table, JpegBitWriter bw) => bw.WriteBits( + table.Depth[symbol], + (ulong)table.Code[symbol]); + + private static void WriteSymbolBits(int symbol, JpegHuffmanCodeTable table, JpegBitWriter bw, int nBits, ulong bits) => bw.WriteBits( + nBits + table.Depth[symbol], + bits | ((ulong)table.Code[symbol] << nBits)); + + private static void BufferEndOfBand(JpegDctCodingState state, JpegHuffmanCodeTable acHuff, ReadOnlySpan newBitsArray, JpegBitWriter bw) + { + if (state.EobRun == 0) + { + state.CurAcHuff = acHuff; + } + + ++state.EobRun; + + if (!newBitsArray.IsEmpty) + { + ulong newBits = 0; + + for (int i = 0; i < newBitsArray.Length; ++i) + { + newBits = (newBits << 1) | (uint)newBitsArray[i]; + } + + int tail = state.RefinementBitsCount & 0xf; + + if (tail != 0) + { + int stuffBitsCount = Math.Min(16 - tail, newBitsArray.Length); + ushort stuffBits = (ushort)(newBits >> (newBitsArray.Length - stuffBitsCount)); + + stuffBits &= (ushort)((1 << stuffBitsCount) - 1); + + int last = state.RefinementBits.Count - 1; + + state.RefinementBits[last] = (ushort)((state.RefinementBits[last] << stuffBitsCount) | stuffBits); + + state.RefinementBitsCount += stuffBitsCount; + + newBitsArray = newBitsArray[stuffBitsCount..]; + } + + int newBitsCount = newBitsArray.Length; + + while (newBitsCount >= 16) + { + state.RefinementBits.Add((ushort)(newBits >> (newBitsCount - 16))); + + newBitsCount -= 16; + state.RefinementBitsCount += 16; + } + + if (newBitsCount != 0) + { + state.RefinementBits.Add((ushort)(newBits & ((1u << newBitsCount) - 1))); + + state.RefinementBitsCount += newBitsCount; + } + } + + if (state.EobRun == 0x7fff) + { + Flush(state, bw); + } + } + + private static bool BuildJpegHuffmanCodeTable(JpegHuffmanCode huff, JpegHuffmanCodeTable table) + { + Span huffCode = stackalloc int[JpegDataConstants.JpegHuffmanAlphabetSize]; + Span huffSize = stackalloc uint[JpegDataConstants.JpegHuffmanAlphabetSize + 1]; + + int p = 0; + + for (int l = 1; l <= JpegDataConstants.JpegHuffmanMaxBitLength; ++l) + { + int count = huff.Counts[l]; + + if (p + count > JpegDataConstants.JpegHuffmanAlphabetSize + 1) + { + return false; + } + + while (count-- > 0) + { + huffSize[p++] = (uint)l; + } + } + + if (p == 0) + { + return true; + } + + int lastP = p - 1; + huffSize[lastP] = 0; + + int code = 0; + uint si = huffSize[0]; + p = 0; + + while (huffSize[p] != 0) + { + while (huffSize[p] == si) + { + huffCode[p++] = code++; + } + + code <<= 1; + ++si; + } + + for (p = 0; p < lastP; ++p) + { + int value = huff.Values[p]; + table.Depth[value] = (byte)huffSize[p]; + table.Code[value] = (short)huffCode[p]; + } + + return true; + } + + private static bool EncodeSOI(JpegSerializationState state) + { + state.OutputQueue.Add([0xff, 0xd8]); + return true; + } + + private static bool EncodeEOI(JpegData jpg, JpegSerializationState state) + { + state.OutputQueue.Add([0xff, 0xd9]); + state.OutputQueue.Add(jpg.TailData); + return true; + } + + private static bool EncodeRestart(byte marker, JpegSerializationState state) + { + state.OutputQueue.Add([0xff, marker]); + return true; + } + + private static bool EncodeSOF(JpegData jpg, byte marker, JpegSerializationState state) + { + if (marker <= 0xc2) + { + state.IsProgressive = marker == 0xc2; + } + + int componentCount = jpg.Components.Count; + int markerLength = 8 + (3 * componentCount); + + state.OutputQueue.Add([(byte)(markerLength + 2)]); + + Span data = CollectionsMarshal.AsSpan(state.OutputQueue[^1]); + int pos = 0; + + data[pos++] = 0xff; + data[pos++] = marker; + data[pos++] = (byte)(markerLength >> 8); + data[pos++] = (byte)markerLength; + data[pos++] = JpegPrecision; + data[pos++] = (byte)(jpg.Height >> 8); + data[pos++] = (byte)jpg.Height; + data[pos++] = (byte)(jpg.Width >> 8); + data[pos++] = (byte)jpg.Width; + data[pos++] = (byte)componentCount; + + for (int i = 0; i < componentCount; ++i) + { + JpegComponent component = jpg.Components[i]; + + data[pos++] = (byte)component.Id; + data[pos++] = (byte)((component.HorizontalSampleFactor << 4) | component.VerticalSampleFactor); + + int quantIndex = component.QuantIndex; + + if ((uint)quantIndex >= (uint)jpg.Quant.Count) + { + return false; + } + + data[pos++] = (byte)jpg.Quant[quantIndex].Index; + } + + return true; + } + + private static bool EncodeSOS(JpegData jpg, JpegScanInfo scanInfo, JpegSerializationState state) + { + int scanCount = scanInfo.NumComponents; + int markerLength = 6 + (2 * scanCount); + + state.OutputQueue.Add([(byte)(markerLength + 2)]); + + Span data = CollectionsMarshal.AsSpan(state.OutputQueue[^1]); + int pos = 0; + + data[pos++] = 0xff; + data[pos++] = 0xda; + data[pos++] = (byte)(markerLength >> 8); + data[pos++] = (byte)markerLength; + data[pos++] = (byte)scanCount; + + for (int i = 0; i < scanCount; ++i) + { + JpegComponentScanInfo si = scanInfo.Components[i]; + + if ((uint)si.ComponentIndex >= (uint)jpg.Components.Count) + { + return false; + } + + data[pos++] = (byte)jpg.Components[si.ComponentIndex].Id; + data[pos++] = (byte)((si.DcTableIndex << 4) + si.AcTableIndex); + } + + data[pos++] = (byte)scanInfo.Ss; + data[pos++] = (byte)scanInfo.Se; + data[pos++] = (byte)((scanInfo.Ah << 4) | scanInfo.Al); + + return true; + } + + private static bool EncodeDHT(JpegData jpg, JpegSerializationState state) + { + List huffmanCode = jpg.HuffmanCodes; + + int markerLength = 2; + + for (int i = state.DhtIndex; i < huffmanCode.Count; ++i) + { + JpegHuffmanCode huff = huffmanCode[i]; + + for (int j = 0; j < 17; ++j) + { + markerLength += huff.Counts[j]; + } + + // Empty DHT marker. + if (markerLength == 2) + { + break; + } + + markerLength += JpegDataConstants.JpegHuffmanMaxBitLength; + + if (huff.IsLast) + { + break; + } + } + + state.OutputQueue.Add([(byte)(markerLength + 2)]); + + Span data = CollectionsMarshal.AsSpan(state.OutputQueue[^1]); + int pos = 0; + + data[pos++] = 0xff; + data[pos++] = 0xc4; + data[pos++] = (byte)(markerLength >> 8); + data[pos++] = (byte)markerLength; + + while (true) + { + int huffmanCodeIndex = state.DhtIndex++; + + if ((uint)huffmanCodeIndex >= (uint)huffmanCode.Count) + { + return false; + } + + JpegHuffmanCode huff = huffmanCode[huffmanCodeIndex]; + + int index = huff.SlotId; + JpegHuffmanCodeTable huffTable = new(); + + int totalCount = 0; + int maxLength = 0; + + for (int i = 0; i < 17; ++i) + { + if (huff.Counts[i] != 0) + { + maxLength = i; + } + + totalCount += huff.Counts[i]; + } + + // Empty DHT marker. + if (totalCount == 0) + { + break; + } + + if ((index & 0x10) != 0) + { + index -= 0x10; + huffTable = state.AcHuffTable[index]; + } + else + { + huffTable = state.DcHuffTable[index]; + } + + huffTable.InitDepths(127); + + if (!BuildJpegHuffmanCodeTable(huff, huffTable)) + { + return false; + } + + huffTable.Initialized = true; + + --totalCount; + + data[pos++] = (byte)huff.SlotId; + + for (int i = 1; i <= JpegDataConstants.JpegHuffmanMaxBitLength; ++i) + { + data[pos++] = (byte)(i == maxLength ? huff.Counts[i] - 1 : huff.Counts[i]); + } + + for (int i = 0; i < totalCount; ++i) + { + data[pos++] = (byte)huff.Values[i]; + } + + if (huff.IsLast) + { + break; + } + } + + return true; + } + + private static bool EncodeDQT(JpegData jpg, JpegSerializationState state) + { + int markerLength = 2; + + for (int i = state.DqtIndex; i < jpg.Quant.Count; ++i) + { + JpegQuantizationTable table = jpg.Quant[i]; + + markerLength += 1 + ((table.Precision != 0 ? 2 : 1) * JxlFrameDimensions.DctBlockSize); + + if (table.IsLast) + { + break; + } + } + + state.OutputQueue.Add([(byte)(markerLength + 2)]); + + Span data = CollectionsMarshal.AsSpan(state.OutputQueue[^1]); + int pos = 0; + + data[pos++] = 0xff; + data[pos++] = 0xdb; + data[pos++] = (byte)(markerLength >> 8); + data[pos++] = (byte)markerLength; + + while (true) + { + int index = state.DqtIndex++; + + if ((uint)index >= (uint)jpg.Quant.Count) + { + return false; + } + + JpegQuantizationTable table = jpg.Quant[index]; + + data[pos++] = (byte)((table.Precision << 4) + table.Index); + + for (int i = 0; i < JxlFrameDimensions.DctBlockSize; ++i) + { + int valueIndex = JpegDataConstants.JpegNaturalOrder[i]; + int value = table.Values[valueIndex]; + + if (table.Precision != 0) + { + data[pos++] = (byte)(value >> 8); + } + + data[pos++] = (byte)value; + } + + if (table.IsLast) + { + break; + } + } + + return true; + } + + private static bool EncodeDRI(JpegData jpg, JpegSerializationState state) + { + state.SeenDriMarker = true; + + List driMarker = + [ + 0xff, + 0xdd, + 0, + 4, + (byte)(jpg.RestartInterval >> 8), + (byte)jpg.RestartInterval + ]; + + state.OutputQueue.Add(driMarker); + return true; + } + + private static bool EncodeAPP(JpegData jpg, byte marker, JpegSerializationState state) + { + // TODO: Check that marker corresponds to payload. + _ = marker; + + int appIndex = state.AppIndex++; + + if ((uint)appIndex >= (uint)jpg.AppData.Count) + { + return false; + } + + state.OutputQueue.Add([0xff]); + state.OutputQueue.Add(jpg.AppData[appIndex]); + + return true; + } + + private static bool EncodeCOM(JpegData jpg, JpegSerializationState state) + { + int comIndex = state.ComIndex++; + + if ((uint)comIndex >= (uint)jpg.ComData.Count) + { + return false; + } + + state.OutputQueue.Add([0xff]); + state.OutputQueue.Add(jpg.ComData[comIndex]); + + return true; + } + + private static bool EncodeInterMarkerData(JpegData jpg, JpegSerializationState state) + { + int index = state.DataIndex++; + + if ((uint)index >= (uint)jpg.InterMarkerData.Count) + { + return false; + } + + state.OutputQueue.Add(jpg.InterMarkerData[index]); + + return true; + } + + private static bool EncodeDCTBlockSequential(ReadOnlySpan coeffs, JpegHuffmanCodeTable dcHuff, JpegHuffmanCodeTable acHuff, int numZeroRuns, ref int lastDcCoeff, JpegBitWriter bw) + { + int temp2; + int temp; + int litmus = 0; + + temp2 = coeffs[0]; + temp = temp2 - lastDcCoeff; + lastDcCoeff = temp2; + + temp2 = temp >> ((8 * sizeof(int)) - 1); + temp += temp2; + temp2 ^= temp; + + int dcNBits = temp2 == 0 ? 0 : JxlMath.FloorLog2Nonzero(temp2) + 1; + + WriteSymbol(dcNBits, dcHuff, bw); + + if (dcNBits != 0) + { + bw.WriteBits(dcNBits, (ulong)(temp & ((1 << dcNBits) - 1))); + } + + int r = 0; + + for (int i = 1; i < JxlFrameDimensions.DctBlockSize; ++i) + { + temp = coeffs[JpegDataConstants.JpegNaturalOrder[i]]; + + if (temp == 0) + { + ++r; + } + else + { + temp2 = temp >> ((8 * sizeof(int)) - 1); + temp += temp2; + temp2 ^= temp; + + if (r > 15) + { + WriteSymbol(0xf0, acHuff, bw); + r -= 16; + + if (r > 15) + { + WriteSymbol(0xf0, acHuff, bw); + r -= 16; + } + + if (r > 15) + { + WriteSymbol(0xf0, acHuff, bw); + r -= 16; + } + } + + litmus |= temp2; + + int acNBits = (int)JxlMath.FloorLog2Nonzero((uint)(ushort)temp2) + 1; + int symbol = (r << 4) + acNBits; + + WriteSymbolBits( + symbol, + acHuff, + bw, + acNBits, + (ulong)(temp & ((1 << acNBits) - 1))); + + r = 0; + } + } + + for (int i = 0; i < numZeroRuns; ++i) + { + WriteSymbol(0xf0, acHuff, bw); + r -= 16; + } + + if (r > 0) + { + WriteSymbol(0, acHuff, bw); + } + + return litmus >= 0; + } + + private static bool EncodeDCTBlockProgressive( + ReadOnlySpan coeffs, + JpegHuffmanCodeTable dcHuff, + JpegHuffmanCodeTable acHuff, + int ss, + int se, + int al, + int numZeroRuns, + JpegDctCodingState codingState, + ref int lastDcCoeff, + JpegBitWriter bw) + { + bool eobRunAllowed = ss > 0; + + int temp2; + int temp; + + if (ss == 0) + { + temp2 = coeffs[0] >> al; + temp = temp2 - lastDcCoeff; + lastDcCoeff = temp2; + + temp2 = temp; + + if (temp < 0) + { + temp = -temp; + + if (temp < 0) + { + return false; + } + + --temp2; + } + + int nBits = temp == 0 + ? 0 + : JxlMath.FloorLog2Nonzero(temp) + 1; + + WriteSymbol(nBits, dcHuff, bw); + + if (nBits != 0) + { + bw.WriteBits(nBits, (ulong)(temp2 & ((1 << nBits) - 1))); + } + + ++ss; + } + + if (ss > se) + { + return true; + } + + int r = 0; + + for (int k = ss; k <= se; ++k) + { + temp = coeffs[JpegDataConstants.JpegNaturalOrder[k]]; + + if (temp == 0) + { + ++r; + continue; + } + + if (temp < 0) + { + temp = -temp; + + if (temp < 0) + { + return false; + } + + temp >>= al; + temp2 = ~temp; + } + else + { + temp >>= al; + temp2 = temp; + } + + if (temp == 0) + { + ++r; + continue; + } + + codingState.Flush(bw); + + while (r > 15) + { + WriteSymbol(0xf0, acHuff, bw); + r -= 16; + } + + int nBits = JxlMath.FloorLog2Nonzero(temp) + 1; + int symbol = (r << 4) + nBits; + + WriteSymbol(symbol, acHuff, bw); + bw.WriteBits(nBits, (ulong)(temp2 & ((1 << nBits) - 1))); + + r = 0; + } + + if (numZeroRuns > 0) + { + codingState.Flush(bw); + + for (int i = 0; i < numZeroRuns; ++i) + { + WriteSymbol(0xf0, acHuff, bw); + r -= 16; + } + } + + if (r > 0) + { + BufferEndOfBand( + codingState, + acHuff, + [], + bw); + + if (!eobRunAllowed) + { + codingState.Flush(bw); + } + } + + return true; + } + + private static bool EncodeRefinementBits( + ReadOnlySpan coeffs, + JpegHuffmanCodeTable acHuff, + int ss, + int se, + int al, + JpegDctCodingState codingState, + JpegBitWriter bw) + { + bool eobRunAllowed = ss > 0; + + if (ss == 0) + { + // Emit next bit of DC component. + bw.WriteBits(1, (ulong)((coeffs[0] >> al) & 1)); + + ++ss; + } + + if (ss > se) + { + return true; + } + + Span absValues = stackalloc int[JxlFrameDimensions.DctBlockSize]; + + int eob = 0; + + for (int k = ss; k <= se; ++k) + { + int absValue = Numerics.Abs(coeffs[JpegDataConstants.JpegNaturalOrder[k]]); + + absValues[k] = absValue >> al; + + if (absValues[k] == 1) + { + eob = k; + } + } + + int r = 0; + + Span refinementBits = stackalloc int[JxlFrameDimensions.DctBlockSize]; + + int refinementBitsCount = 0; + + for (int k = ss; k <= se; ++k) + { + if (absValues[k] == 0) + { + ++r; + continue; + } + + while (r > 15 && k <= eob) + { + codingState.Flush(bw); + + WriteSymbol(0xf0, acHuff, bw); + r -= 16; + + for (int i = 0; i < refinementBitsCount; ++i) + { + bw.WriteBits(1, (ulong)refinementBits[i]); + } + + refinementBitsCount = 0; + } + + if (absValues[k] > 1) + { + refinementBits[refinementBitsCount++] = + absValues[k] & 1; + + continue; + } + + codingState.Flush(bw); + + int symbol = (r << 4) + 1; + + int newNonZeroBit = coeffs[JpegDataConstants.JpegNaturalOrder[k]] < 0 ? 0 : 1; + + WriteSymbol(symbol, acHuff, bw); + bw.WriteBits(1, (ulong)newNonZeroBit); + + for (int i = 0; i < refinementBitsCount; ++i) + { + bw.WriteBits(1, (ulong)refinementBits[i]); + } + + refinementBitsCount = 0; + r = 0; + } + + if (r > 0 || refinementBitsCount != 0) + { + BufferEndOfBand( + codingState, + acHuff, + refinementBits, + bw); + + if (!eobRunAllowed) + { + codingState.Flush(bw); + } + } + + return true; + } + + private static JpegSerializationStatus DoEncodeScan( + JpegData jpg, + JpegSerializationState state, + int mode) + { + JpegScanInfo scanInfo = jpg.ScanInfos[state.ScanIndex]; + EncodeScanState ss = state.ScanState; + + int restartInterval = state.SeenDriMarker + ? jpg.RestartInterval + : 0; + + int GetNextExtraZeroRunIndex() + { + if (ss.ExtraZeroRunsPos < scanInfo.ExtraZeroRuns.Count) + { + return scanInfo.ExtraZeroRuns[ss.ExtraZeroRunsPos].BlockIdx; + } + + return -1; + } + + int GetNextResetPoint() + { + if (ss.NextResetPointPos < scanInfo.ResetPoints.Count) + { + return scanInfo.ResetPoints[ss.NextResetPointPos++]; + } + + return -1; + } + + if (ss.Stage == EncodeScanState.StageHead) + { + if (!EncodeSOS(jpg, scanInfo, state)) + { + return JpegSerializationStatus.Error; + } + + ss.Bw = new JpegBitWriter(); + ss.CodingState = new JpegDctCodingState(); + ss.RestartsToGo = restartInterval; + ss.NextRestartMarker = 0; + ss.BlockScanIndex = 0; + ss.ExtraZeroRunsPos = 0; + ss.NextExtraZeroRunIndex = GetNextExtraZeroRunIndex(); + ss.NextResetPointPos = 0; + ss.NextResetPoint = GetNextResetPoint(); + ss.McuY = 0; + ss.LastDcCoeff.AsSpan().Clear(); + ss.Stage = EncodeScanState.StageBody; + } + + JpegBitWriter bw = ss.Bw; + JpegDctCodingState codingState = ss.CodingState; + + if (ss.Stage != EncodeScanState.StageBody) + { + return JpegSerializationStatus.Error; + } + + bool isInterleaved = scanInfo.NumComponents > 1; + + jpg.CalculateMcuSize(scanInfo, out int mcusPerRow, out int mcuRows); + + bool isProgressive = state.IsProgressive; + int al = isProgressive ? scanInfo.Al : 0; + int ssValue = isProgressive ? scanInfo.Ss : 0; + int se = isProgressive ? scanInfo.Se : 63; + + bool wantAc = ssValue != 0 || se != 0; + bool wantDc = ssValue == 0; + + bool completeAc = true; + bool hasAc = true; + + if (wantAc && !hasAc) + { + return JpegSerializationStatus.NeedsMoreInput; + } + + bool completeDc = hasAc; + bool complete = wantAc ? completeAc : completeDc; + + _ = complete; + + int lastMcuY = complete ? mcuRows : 0; + + for (; ss.McuY < lastMcuY; ++ss.McuY) + { + for (int mcuX = 0; mcuX < mcusPerRow; ++mcuX) + { + if (restartInterval > 0 && ss.RestartsToGo == 0) + { + codingState.Flush(bw); + + if (!bw.JumpToByteBoundary(ref state.PadBits)) + { + return JpegSerializationStatus.Error; + } + + bw.EmitMarker((byte)(0xd0 + ss.NextRestartMarker)); + + ss.NextRestartMarker++; + ss.NextRestartMarker &= 0x7; + ss.RestartsToGo = restartInterval; + ss.LastDcCoeff.AsSpan().Clear(); + } + + // Encode one MCU. + for (int i = 0; i < scanInfo.NumComponents; ++i) + { + JpegComponentScanInfo si = scanInfo.Components[i]; + JpegComponent c = jpg.Components[si.ComponentIndex]; + + int dcTblIdx = si.DcTableIndex; + int acTblIdx = si.AcTableIndex; + + JpegHuffmanCodeTable dcHuff = state.DcHuffTable[dcTblIdx]; + JpegHuffmanCodeTable acHuff = state.AcHuffTable[acTblIdx]; + + if (wantDc && !dcHuff.Initialized) + { + return JpegSerializationStatus.Error; + } + + if (wantAc && !acHuff.Initialized) + { + return JpegSerializationStatus.Error; + } + + int nBlocksY = isInterleaved ? c.VerticalSampleFactor : 1; + int nBlocksX = isInterleaved ? c.HorizontalSampleFactor : 1; + + for (int iy = 0; iy < nBlocksY; ++iy) + { + for (int ix = 0; ix < nBlocksX; ++ix) + { + int blockY = (ss.McuY * nBlocksY) + iy; + int blockX = (mcuX * nBlocksX) + ix; + int blockIdx = (blockY * c.WidthInBlocks) + blockX; + + if (ss.BlockScanIndex == ss.NextResetPoint) + { + codingState.Flush(bw); + ss.NextResetPoint = GetNextResetPoint(); + } + + int numZeroRuns = 0; + + if (ss.BlockScanIndex == ss.NextExtraZeroRunIndex) + { + numZeroRuns = scanInfo.ExtraZeroRuns[ss.ExtraZeroRunsPos].NumExtraZeroRuns; + ++ss.ExtraZeroRunsPos; + ss.NextExtraZeroRunIndex = GetNextExtraZeroRunIndex(); + } + + ReadOnlySpan coeffs = CollectionsMarshal.AsSpan(c.Coefficients).Slice(blockIdx << 6, JxlFrameDimensions.DctBlockSize); + + bool ok; + + bw.Reserve(512); + + if (mode == 0) + { + ok = EncodeDCTBlockSequential( + coeffs, + dcHuff, + acHuff, + numZeroRuns, + ref ss.LastDcCoeff[si.ComponentIndex], + bw); + } + else if (mode == 1) + { + ok = EncodeDCTBlockProgressive( + coeffs, + dcHuff, + acHuff, + ssValue, + se, + al, + numZeroRuns, + codingState, + ref ss.LastDcCoeff[si.ComponentIndex], + bw); + } + else + { + ok = EncodeRefinementBits( + coeffs, + acHuff, + ssValue, + se, + al, + codingState, + bw); + } + + if (!ok) + { + return JpegSerializationStatus.Error; + } + + ++ss.BlockScanIndex; + } + } + } + + --ss.RestartsToGo; + } + } + + if (ss.McuY < mcuRows) + { + if (!bw.Healthy) + { + return JpegSerializationStatus.Error; + } + + return JpegSerializationStatus.NeedsMoreInput; + } + + codingState.Flush(bw); + + if (!bw.JumpToByteBoundary( + ref state.PadBits, + state.PadBitsEnd)) + { + return JpegSerializationStatus.Error; + } + + bw.Finish(); + + ss.Stage = EncodeScanState.StageHead; + ++state.ScanIndex; + + if (!bw.Healthy) + { + return JpegSerializationStatus.Error; + } + + return JpegSerializationStatus.Done; + } + + private static JpegSerializationStatus EncodeScan(JpegData jpg, JpegSerializationState state) + { + JpegScanInfo scanInfo = jpg.ScanInfos[state.ScanIndex]; + + bool isProgressive = state.IsProgressive; + + int al = isProgressive ? scanInfo.Al : 0; + int ah = isProgressive ? scanInfo.Ah : 0; + int ss = isProgressive ? scanInfo.Ss : 0; + int se = isProgressive ? scanInfo.Se : 63; + + bool needSequential = + !isProgressive || + (ah == 0 && + al == 0 && + ss == 0 && + se == 63); + + if (needSequential) + { + return DoEncodeScan(jpg, state, 0); + } + + if (ah == 0) + { + return DoEncodeScan(jpg, state, 1); + } + + return DoEncodeScan(jpg, state, 2); + } + + private static JpegSerializationStatus SerializeSection(byte marker, JpegSerializationState state, JpegData jpg) + { + static JpegSerializationStatus ToStatus(bool result) => + result + ? JpegSerializationStatus.Done + : JpegSerializationStatus.Error; + + return marker switch + { + 0xc0 or 0xc1 or 0xc2 or 0xc9 or 0xca => ToStatus(EncodeSOF(jpg, marker, state)), + 0xc4 => ToStatus(EncodeDHT(jpg, state)), + 0xd0 or 0xd1 or 0xd2 or 0xd3 or 0xd4 or 0xd5 or 0xd6 or 0xd7 => ToStatus(EncodeRestart(marker, state)), + 0xd9 => ToStatus(EncodeEOI(jpg, state)), + 0xda => EncodeScan(jpg, state), + 0xdb => ToStatus(EncodeDQT(jpg, state)), + 0xdd => ToStatus(EncodeDRI(jpg, state)), + 0xe0 or 0xe1 or 0xe2 or 0xe3 or 0xe4 or 0xe5 or 0xe6 or 0xe7 or 0xe8 or 0xe9 or 0xea or 0xeb or 0xec or 0xed or 0xee or 0xef => ToStatus(EncodeAPP(jpg, marker, state)), + 0xfe => ToStatus(EncodeCOM(jpg, state)), + 0xff => ToStatus(EncodeInterMarkerData(jpg, state)), + _ => JpegSerializationStatus.Error, + }; + } + + private static bool WriteJpegInternal(JpegData jpg, JpegOutput output, JpegSerializationState state) + { + bool MaybePushOutput() + { + if (state.Stage != JpegSerializationStage.Error) + { + while (state.OutputQueue.Count > 0) + { + List chunk = state.OutputQueue[0]; + + int numWritten = output.Write( + chunk.Next, + chunk.Count); + + if (numWritten == 0 && chunk.Count > 0) + { + return false; + } + + if (chunk.Count - numWritten == 0) + { + state.OutputQueue.RemoveAt(0); + } + } + } + + return true; + } + + while (true) + { + switch (state.Stage) + { + case JpegSerializationStage.Initialize: + { + // Valid Brunsli requires at least the 0xD9 marker. + // This might happen on a corrupted stream or unconditioned JPEGData. + if (jpg.MarkerOrder.Count == 0) + { + state.Stage = JpegSerializationStage.Error; + break; + } + + state.DcHuffTable = new JpegHuffmanCodeTable[JpegDataConstants.MaxHuffmanTables]; + state.AcHuffTable = new JpegHuffmanCodeTable[JpegDataConstants.MaxHuffmanTables]; + + if (jpg.HasZeroPaddingBit) + { + state.PadBits = jpg.PaddingBits; + } + + _ = EncodeSOI(state); + + if (!MaybePushOutput()) + { + return false; + } + + state.Stage = JpegSerializationStage.SerializeSection; + + break; + } + + case JpegSerializationStage.SerializeSection: + { + if (state.SectionIndex >= jpg.MarkerOrder.Count) + { + state.Stage = JpegSerializationStage.Done; + break; + } + + byte marker = jpg.MarkerOrder[state.SectionIndex]; + + JpegSerializationStatus status = + SerializeSection(marker, state, jpg); + + if (status == JpegSerializationStatus.Error) + { + state.Stage = JpegSerializationStage.Error; + break; + } + + if (!MaybePushOutput()) + { + return false; + } + + if (status == JpegSerializationStatus.NeedsMoreInput) + { + return false; + } + + if (status != JpegSerializationStatus.Done) + { + state.Stage = JpegSerializationStage.Error; + return false; + } + + ++state.SectionIndex; + break; + } + + case JpegSerializationStage.Done: + { + if (state.OutputQueue.Count != 0) + { + return false; + } + + if (state.PadBits.Length != 0) + { + return false; + } + + return true; + } + + case JpegSerializationStage.Error: + return false; + + default: + return false; + } + } + } + + public static bool WriteJpeg(JpegData jpg, JpegOutput output) + { + JpegSerializationState state = new(); + return WriteJpegInternal(jpg, output, state); + } +} From fdba3ee0426c379db8f8ef75cb9a56b27e11da82 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 8 Sep 2026 20:28:36 +0400 Subject: [PATCH 157/328] Add transforms decoder, some ANS encoder work --- .../Formats/Jxl/IO/Jpeg/JpegWriter.cs | 2 +- .../Jxl/Processing/Dct/JxlDctScales.cs | 57 + .../Decoder/JxlTransformsDecoder.cs | 1215 +++++++++++++++++ .../Encoder/JxlAcStrategyEncoder.cs | 256 ++++ 4 files changed, 1529 insertions(+), 1 deletion(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlTransformsDecoder.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAcStrategyEncoder.cs diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegWriter.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegWriter.cs index f36a5c244c..62ea5230e5 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegWriter.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegWriter.cs @@ -377,7 +377,7 @@ private static bool EncodeDHT(JpegData jpg, JpegSerializationState state) huffTable = state.DcHuffTable[index]; } - huffTable.InitDepths(127); + huffTable.InitializeDepths(127); if (!BuildJpegHuffmanCodeTable(huff, huffTable)) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctScales.cs b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctScales.cs index 0785afe128..e6f4ecfa7e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctScales.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctScales.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Runtime.CompilerServices; + namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; /// @@ -385,4 +387,59 @@ internal static class JxlDctScales 256 => Multipliers256, _ => [] }; + + [MethodImpl(InliningOptions.HotPath | InliningOptions.AlwaysInline)] + public static ReadOnlySpan GetResampleScales(int x, int y) + { + DebugGuard.IsTrue(x is 1 or 2 or 4 or 8 or 16 or 32 or 64 or 128 or 256, "x is invalid"); + if (x == 1) + { + DebugGuard.IsTrue(y == 8, "Only y=8 resample scale for x=1 exists"); + return ResampleScales1_8; + } + else if (x == 2) + { + DebugGuard.IsTrue(y == 16, "Only y=16 resample scale for x=2 exists"); + return ResampleScales2_16; + } + else if (x == 4) + { + DebugGuard.IsTrue(y == 32, "Only y=32 resample scale for x=4 exists"); + return ResampleScales4_32; + } + else if (x == 8) + { + DebugGuard.IsTrue(y is 1 or 64, "Only y=1,64 resample scale for x=8 exists"); + return y == 1 ? ResampleScales8_1 : ResampleScales8_64; + } + else if (x == 16) + { + DebugGuard.IsTrue(y is 2 or 128, "Only y=2,128 resample scale for x=16 exists"); + return y == 2 ? ResampleScales16_2 : ResampleScales16_128; + } + else if (x == 32) + { + DebugGuard.IsTrue(y is 4 or 256, "Only y=4,256 resample scale for x=32 exists"); + return y == 4 ? ResampleScales32_4 : ResampleScales32_256; + } + else if (x == 64) + { + DebugGuard.IsTrue(y == 8, "Only y=8 resample scale for x=64 exists"); + return ResampleScales64_8; + } + else if (x == 128) + { + DebugGuard.IsTrue(y == 16, "Only y=16 resample scale for x=128 exists"); + return ResampleScales128_16; + } + else if (x == 256) + { + DebugGuard.IsTrue(y == 32, "Only y=32 resample scale for x=256 exists"); + return ResampleScales256_32; + } + else + { + return []; + } + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlTransformsDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlTransformsDecoder.cs new file mode 100644 index 0000000000..42304d1894 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlTransformsDecoder.cs @@ -0,0 +1,1215 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +internal static class JxlTransformsDecoder +{ + /// + /// Lookup for the inverse AFV transform. + /// + private static readonly float[][] AfvBasis4x4 = + [ + [ + 0.25f, + 0.25f, + 0.25f, + 0.25f, + 0.25f, + 0.25f, + 0.25f, + 0.25f, + 0.25f, + 0.25f, + 0.25f, + 0.25f, + 0.25f, + 0.25f, + 0.25f, + 0.25f, + ], + [ + 0.876902929799142f, + 0.2206518106944235f, + -0.10140050393753763f, + -0.1014005039375375f, + 0.2206518106944236f, + -0.10140050393753777f, + -0.10140050393753772f, + -0.10140050393753763f, + -0.10140050393753758f, + -0.10140050393753769f, + -0.1014005039375375f, + -0.10140050393753768f, + -0.10140050393753768f, + -0.10140050393753759f, + -0.10140050393753763f, + -0.10140050393753741f, + ], + [ + 0.0f, + 0.0f, + 0.40670075830260755f, + 0.44444816619734445f, + 0.0f, + 0.0f, + 0.19574399372042936f, + 0.2929100136981264f, + -0.40670075830260716f, + -0.19574399372042872f, + 0.0f, + 0.11379074460448091f, + -0.44444816619734384f, + -0.29291001369812636f, + -0.1137907446044814f, + 0.0f, + ], + [ + 0.0f, + 0.0f, + -0.21255748058288748f, + 0.3085497062849767f, + 0.0f, + 0.4706702258572536f, + -0.1621205195722993f, + 0.0f, + -0.21255748058287047f, + -0.16212051957228327f, + -0.47067022585725277f, + -0.1464291867126764f, + 0.3085497062849487f, + 0.0f, + -0.14642918671266536f, + 0.4251149611657548f, + ], + [ + 0.0f, + -0.7071067811865474f, + 0.0f, + 0.0f, + 0.7071067811865476f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + ], + [ + -0.4105377591765233f, + 0.6235485373547691f, + -0.06435071657946274f, + -0.06435071657946266f, + 0.6235485373547694f, + -0.06435071657946284f, + -0.0643507165794628f, + -0.06435071657946274f, + -0.06435071657946272f, + -0.06435071657946279f, + -0.06435071657946266f, + -0.06435071657946277f, + -0.06435071657946277f, + -0.06435071657946273f, + -0.06435071657946274f, + -0.0643507165794626f, + ], + [ + 0.0f, + 0.0f, + -0.4517556589999482f, + 0.15854503551840063f, + 0.0f, + -0.04038515160822202f, + 0.0074182263792423875f, + 0.39351034269210167f, + -0.45175565899994635f, + 0.007418226379244351f, + 0.1107416575309343f, + 0.08298163094882051f, + 0.15854503551839705f, + 0.3935103426921022f, + 0.0829816309488214f, + -0.45175565899994796f, + ], + [ + 0.0f, + 0.0f, + -0.304684750724869f, + 0.5112616136591823f, + 0.0f, + 0.0f, + -0.290480129728998f, + -0.06578701549142804f, + 0.304684750724884f, + 0.2904801297290076f, + 0.0f, + -0.23889773523344604f, + -0.5112616136592012f, + 0.06578701549142545f, + 0.23889773523345467f, + 0.0f, + ], + [ + 0.0f, + 0.0f, + 0.3017929516615495f, + 0.25792362796341184f, + 0.0f, + 0.16272340142866204f, + 0.09520022653475037f, + 0.0f, + 0.3017929516615503f, + 0.09520022653475055f, + -0.16272340142866173f, + -0.35312385449816297f, + 0.25792362796341295f, + 0.0f, + -0.3531238544981624f, + -0.6035859033230976f, + ], + [ + 0.0f, + 0.0f, + 0.40824829046386274f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + -0.4082482904638628f, + -0.4082482904638635f, + 0.0f, + 0.0f, + -0.40824829046386296f, + 0.0f, + 0.4082482904638634f, + 0.408248290463863f, + 0.0f, + ], + [ + 0.0f, + 0.0f, + 0.1747866975480809f, + 0.0812611176717539f, + 0.0f, + 0.0f, + -0.3675398009862027f, + -0.307882213957909f, + -0.17478669754808135f, + 0.3675398009862011f, + 0.0f, + 0.4826689115059883f, + -0.08126111767175039f, + 0.30788221395790305f, + -0.48266891150598584f, + 0.0f, + ], + [ + 0.0f, + 0.0f, + -0.21105601049335784f, + 0.18567180916109802f, + 0.0f, + 0.0f, + 0.49215859013738733f, + -0.38525013709251915f, + 0.21105601049335806f, + -0.49215859013738905f, + 0.0f, + 0.17419412659916217f, + -0.18567180916109904f, + 0.3852501370925211f, + -0.1741941265991621f, + 0.0f, + ], + [ + 0.0f, + 0.0f, + -0.14266084808807264f, + -0.3416446842253372f, + 0.0f, + 0.7367497537172237f, + 0.24627107722075148f, + -0.08574019035519306f, + -0.14266084808807344f, + 0.24627107722075137f, + 0.14883399227113567f, + -0.04768680350229251f, + -0.3416446842253373f, + -0.08574019035519267f, + -0.047686803502292804f, + -0.14266084808807242f, + ], + [ + 0.0f, + 0.0f, + -0.13813540350758585f, + 0.3302282550303788f, + 0.0f, + 0.08755115000587084f, + -0.07946706605909573f, + -0.4613374887461511f, + -0.13813540350758294f, + -0.07946706605910261f, + 0.49724647109535086f, + 0.12538059448563663f, + 0.3302282550303805f, + -0.4613374887461554f, + 0.12538059448564315f, + -0.13813540350758452f, + ], + [ + 0.0f, + 0.0f, + -0.17437602599651067f, + 0.0702790691196284f, + 0.0f, + -0.2921026642334881f, + 0.3623817333531167f, + 0.0f, + -0.1743760259965108f, + 0.36238173335311646f, + 0.29210266423348785f, + -0.4326608024727445f, + 0.07027906911962818f, + 0.0f, + -0.4326608024727457f, + 0.34875205199302267f, + ], + [ + 0.0f, + 0.0f, + 0.11354987314994337f, + -0.07417504595810355f, + 0.0f, + 0.19402893032594343f, + -0.435190496523228f, + 0.21918684838857466f, + 0.11354987314994257f, + -0.4351904965232251f, + 0.5550443808910661f, + -0.25468277124066463f, + -0.07417504595810233f, + 0.2191868483885728f, + -0.25468277124066413f, + 0.1135498731499429f, + ], + ]; + + public static void ReinterpretingDct( + int dctRows, + int dctCols, + int lfRows, + int lfCols, + int rows, + int cols, + Span input, + int inputStride, + Span output, + int outputStride, + Span block, + Span scratchSpace) + { + JxlDct.ComputeScaledDct(rows, cols, new JxlDctSource(input, inputStride), block, scratchSpace); + + if (rows < cols) + { + ReadOnlySpan sp1 = JxlDctScales.GetResampleScales(rows, dctRows); + ReadOnlySpan sp2 = JxlDctScales.GetResampleScales(cols, dctCols); + + for (int y = 0; y < lfRows; y++) + { + for (int x = 0; x < lfCols; x++) + { + output[(y * outputStride) + x] = block[(y * cols) + x] * sp1[y] * sp2[x]; + } + } + } + else + { + ReadOnlySpan sp2 = JxlDctScales.GetResampleScales(rows, dctRows); + ReadOnlySpan sp1 = JxlDctScales.GetResampleScales(cols, dctCols); + + for (int y = 0; y < lfCols; y++) + { + for (int x = 0; x < lfRows; x++) + { + output[(y * outputStride) + x] = block[(y * rows) + x] * sp1[y] * sp2[y]; + } + } + } + } + + public static void Idct2TopBlock(int s, Span block, int strideOut, Span output) + { + Span temp = stackalloc float[JxlFrameDimensions.DctBlockSize]; + int num2x2 = s >> 1; + + for (int y = 0; y < num2x2; y++) + { + int y2 = y << 1; + int y2Plus1 = y2 | 1; // y2 is guaranteed to be even due to left shift + + for (int x = 0; x < num2x2; x++) + { + int x2 = x << 1; + int x2Plus1 = x2 | 1; // x2 is guaranteed to be even due to left shift + int n2x2PlusX = num2x2 + x; + + float c00 = block[(y * JxlFrameDimensions.BlockDimensions) + x]; + float c01 = block[(y * JxlFrameDimensions.BlockDimensions) + n2x2PlusX]; + float c10 = block[((y + num2x2) * JxlFrameDimensions.BlockDimensions) + x]; + float c11 = block[((y + num2x2) * JxlFrameDimensions.BlockDimensions) + n2x2PlusX]; + + float r00 = c00 + c01 + c10 + c11; + float r01 = c00 + c01 - c10 - c11; + float r10 = c00 - c01 + c10 - c11; + float r11 = c00 - c01 - c10 + c11; + + temp[(y2 * JxlFrameDimensions.BlockDimensions) + x2] = r00; + temp[(y2 * JxlFrameDimensions.BlockDimensions) + x2Plus1] = r01; + temp[(y2Plus1 * JxlFrameDimensions.BlockDimensions) + x2] = r10; + temp[(y2Plus1 * JxlFrameDimensions.BlockDimensions) + x2Plus1] = r11; + } + } + + for (int y = 0; y < s; y++) + { + for (int x = 0; x < s; x++) + { + output[(y * strideOut) + x] = temp[(y * JxlFrameDimensions.BlockDimensions) + x]; + } + } + } + + public static void AfvIdct4x4(Span coeffs, Span pixels) + { + if (!Vector.IsHardwareAccelerated || Vector.Count > 16) + { + // Vector.Count can be > 16 on some CPUs, + // e.g. some ARM SVE2 processors can reach 2048 bits + for (int i = 0; i < 16; i++) + { + float pixel = 0f; + + for (int j = 0; j < 16; j++) + { + float cf = coeffs[j]; + float basis = AfvBasis4x4[j][i]; + pixel = (cf * basis) + pixel; + } + + pixels[i] = pixel; + } + } + else + { + for (int i = 0; i < 16; i += Vector.Count) + { + Vector pixel = Vector.Zero; + + for (int j = 0; j < 16; j++) + { + Vector cf = Vector.Create(coeffs[j]); + Vector basis = Vector.Create(AfvBasis4x4[j].AsSpan(i)); + pixel = (cf * basis) + pixel; + } + + pixel.CopyTo(pixels[i..]); + } + } + } + + public static void AfvTransformToPixels(int afvKind, Span coefficients, Span pixels, int pixelsStride) + { + Span scratchSpace = stackalloc float[4 * 8 * 4]; // 128 floats, 512 bytes + + // This is an equivalent of Math.DivRem(afvKind, 2) + int afvX = afvKind & 1; + int afvY = afvKind >> 1; + + InlineArray3 dcs = default; + float block00 = coefficients[0]; + float block01 = coefficients[1]; + float block10 = coefficients[8]; + + dcs[0] = (block00 + block10 + block01) * 4.0f; + dcs[1] = block00 + block10 - block01; + + dcs[2] = block00 - block10; + Span coeff = stackalloc float[4 * 4]; // No need to zero-initialize + coeff[0] = dcs[0]; + + // TODO: unrolling this would lead to big performance benefits + for (int iy = 0; iy < 4; iy++) + { + // Variables so we don't repeat multiplication over and over again + int iy4 = iy * 4; // iy multiplied by 4 + int iy28 = iy * 2 * 8; // iy multiplied by 2 multiplied by 8 + + for (int ix = 0; ix < 4; ix++) + { + if (ix == 0 && iy == 0) + { + continue; + } + + coeff[iy4 + ix] = coefficients[iy28 + (ix * 2)]; + } + } + + Span block = stackalloc float[4 * 8]; // Don't zero-init + AfvIdct4x4(coeff, block); + + int afvX4 = afvX * 4; + + for (int iy = 0; iy < 4; iy++) + { + // Variables so we don't do repeated arithmetic + int iyAfvY4 = iy + (afvY * 4); + int iyAfvY4Stride = iyAfvY4 * pixelsStride; + int yOffset = afvY == 1 ? 3 - iy : iy; + int yOffsetMul4 = yOffset * 4; + + for (int ix = 0; ix < 4; ix++) + { + pixels[iyAfvY4Stride + afvX4 + ix] = block[yOffsetMul4 + (afvX == 1 ? 3 - ix : ix)]; + } + } + + // IDCT4x4 in (odd, even) positions. + block[0] = dcs[1]; + for (int iy = 0; iy < 4; iy++) + { + int iy4 = iy * 4; + int iy28 = iy * 2 * 8; + + for (int ix = 0; ix < 4; ix++) + { + if (ix == 0 && iy == 0) + { + continue; + } + + block[iy4 + ix] = coefficients[iy28 + (ix * 2) + 1]; + } + } + + JxlDct.ComputeScaledInverseDct( + 4, + 4, + block, + new JxlDctOutput(pixels[((afvY * 4 * pixelsStride) + (afvX == 1 ? 0 : 4))..], pixelsStride), + scratchSpace); + + block[0] = dcs[2]; + + for (int iy = 0; iy < 4; iy++) + { + int iy8 = iy * 8; + + for (int ix = 0; ix < 8; ix++) + { + if (ix == 0 && iy == 0) + { + continue; + } + + block[iy8 + ix] = coefficients[((1 + (iy * 2)) * 8) + ix]; + } + } + + JxlDct.ComputeScaledInverseDct( + 4, + 8, + block, + new JxlDctOutput(pixels[((afvY == 1 ? 0 : 4) * pixelsStride)..], pixelsStride), + scratchSpace); + } + + public static void TransformToPixels(JxlAcStrategyType strategy, Span coefficients, Span pixels, int pixelsStride, Span scratchSpace) + { + switch (strategy) + { + case JxlAcStrategyType.IDENTITY: + { + // Identity matrix-based transform + InlineArray4 dcs = default; + + float block00 = coefficients[0]; + float block01 = coefficients[1]; + float block10 = coefficients[8]; + float block11 = coefficients[9]; + + dcs[0] = block00 + block01 + block10 + block11; + dcs[1] = block00 + block01 - block10 - block11; + dcs[2] = block00 - block01 + block10 - block11; + dcs[3] = block00 - block01 - block10 + block11; + + for (int y = 0; y < 2; y++) + { + for (int x = 0; x < 2; x++) + { + float blockDc = dcs[(y * 2) + x]; + float residualSum = 0; + + for (int iy = 0; iy < 4; iy++) + { + for (int ix = 0; ix < 4; ix++) + { + if (ix == 0 && iy == 0) + { + continue; + } + + residualSum += coefficients[((y + (iy * 2)) * 8) + x + (ix * 2)]; + } + } + + pixels[(((4 * y) + 1) * pixelsStride) + (4 * x) + 1] = blockDc - (residualSum * (1.0f / 16)); + + for (int iy = 0; iy < 4; iy++) + { + for (int ix = 0; ix < 4; ix++) + { + if (ix == 1 && iy == 1) + { + continue; + } + + pixels[(((y * 4) + iy) * pixelsStride) + (x * 4) + ix] = + coefficients[((y + (iy * 2)) * 8) + x + (ix * 2)] + + pixels[(((4 * y) + 1) * pixelsStride) + (4 * x) + 1]; + } + } + + pixels[(y * 4 * pixelsStride) + (x * 4)] = + coefficients[((y + 2) * 8) + x + 2] + + pixels[(((4 * y) + 1) * pixelsStride) + (4 * x) + 1]; + } + } + + break; + } + + case JxlAcStrategyType.DCT8X4: + { + // Discrete Cosine Transform on an 8x4 block + InlineArray2 dcs = default; + + float block0 = coefficients[0]; + float block1 = coefficients[8]; + + dcs[0] = block0 + block1; + dcs[1] = block0 - block1; + + Span block = stackalloc float[4 * 8]; + + for (int x = 0; x < 2; x++) + { + block[0] = dcs[x]; + + for (int iy = 0; iy < 4; iy++) + { + for (int ix = 0; ix < 8; ix++) + { + if (ix == 0 && iy == 0) + { + continue; + } + + block[(iy * 8) + ix] = coefficients[((x + (iy * 2)) * 8) + ix]; + } + } + + JxlDct.ComputeScaledInverseDct( + 8, + 4, + block, + new JxlDctOutput(pixels[(x * 4)..], pixelsStride), + scratchSpace); + } + + break; + } + + case JxlAcStrategyType.DCT4X8: + { + // Discrete Cosine Transform on an 4x8 block + InlineArray2 dcs = default; + + float block0 = coefficients[0]; + float block1 = coefficients[8]; + + dcs[0] = block0 + block1; + dcs[1] = block0 - block1; + + Span block = stackalloc float[4 * 8]; + + for (int y = 0; y < 2; y++) + { + block[0] = dcs[y]; + + for (int iy = 0; iy < 4; iy++) + { + for (int ix = 0; ix < 8; ix++) + { + if (ix == 0 && iy == 0) + { + continue; + } + + block[(iy * 8) + ix] = coefficients[((y + (iy * 2)) * 8) + ix]; + } + } + + JxlDct.ComputeScaledInverseDct( + 4, + 8, + block, + new JxlDctOutput(pixels[(y * 4 * pixelsStride)..], pixelsStride), + scratchSpace); + } + + break; + } + + case JxlAcStrategyType.DCT4X4: + { + InlineArray4 dcs = default; + + float block00 = coefficients[0]; + float block01 = coefficients[1]; + float block10 = coefficients[8]; + float block11 = coefficients[9]; + + dcs[0] = block00 + block01 + block10 + block11; + dcs[1] = block00 + block01 - block10 - block11; + dcs[2] = block00 - block01 + block10 - block11; + dcs[3] = block00 - block01 - block10 + block11; + + Span block = stackalloc float[4 * 4]; + + for (int y = 0; y < 2; y++) + { + for (int x = 0; x < 2; x++) + { + block[0] = dcs[(y * 2) + x]; + + for (int iy = 0; iy < 4; iy++) + { + for (int ix = 0; ix < 4; ix++) + { + if (ix == 0 && iy == 0) + { + continue; + } + + block[(iy * 4) + ix] = coefficients[((y + (iy * 2)) * 8) + x + (ix * 2)]; + } + } + + JxlDct.ComputeScaledInverseDct( + 4, + 4, + block, + new JxlDctOutput(pixels[((y * 4 * pixelsStride) + (x * 4))..], pixelsStride), + scratchSpace); + } + } + + break; + } + + case JxlAcStrategyType.DCT2X2: + { + Span coeffs = stackalloc float[JxlFrameDimensions.DctBlockSize]; + coefficients[..JxlFrameDimensions.DctBlockSize].CopyTo(coeffs); + + Idct2TopBlock(2, coeffs, JxlFrameDimensions.BlockDimensions, coeffs); + Idct2TopBlock(4, coeffs, JxlFrameDimensions.BlockDimensions, coeffs); + Idct2TopBlock(8, coeffs, JxlFrameDimensions.BlockDimensions, coeffs); + + for (int y = 0; y < JxlFrameDimensions.BlockDimensions; y++) + { + for (int x = 0; x < JxlFrameDimensions.BlockDimensions; x++) + { + pixels[(y * pixelsStride) + x] = coeffs[(y * JxlFrameDimensions.BlockDimensions) + x]; + } + } + + break; + } + + case JxlAcStrategyType.DCT16X16: + { + JxlDct.ComputeScaledInverseDct(16, 16, coefficients, new JxlDctOutput(pixels, pixelsStride), scratchSpace); + break; + } + + case JxlAcStrategyType.DCT16X8: + { + JxlDct.ComputeScaledInverseDct(16, 8, coefficients, new JxlDctOutput(pixels, pixelsStride), scratchSpace); + break; + } + + case JxlAcStrategyType.DCT8X16: + { + JxlDct.ComputeScaledInverseDct(8, 16, coefficients, new JxlDctOutput(pixels, pixelsStride), scratchSpace); + break; + } + + case JxlAcStrategyType.DCT32X8: + { + JxlDct.ComputeScaledInverseDct(32, 8, coefficients, new JxlDctOutput(pixels, pixelsStride), scratchSpace); + break; + } + + case JxlAcStrategyType.DCT8X32: + { + JxlDct.ComputeScaledInverseDct(8, 32, coefficients, new JxlDctOutput(pixels, pixelsStride), scratchSpace); + break; + } + + case JxlAcStrategyType.DCT32X16: + { + JxlDct.ComputeScaledInverseDct(32, 16, coefficients, new JxlDctOutput(pixels, pixelsStride), scratchSpace); + break; + } + + case JxlAcStrategyType.DCT16X32: + { + JxlDct.ComputeScaledInverseDct(16, 32, coefficients, new JxlDctOutput(pixels, pixelsStride), scratchSpace); + break; + } + + case JxlAcStrategyType.DCT32X32: + { + JxlDct.ComputeScaledInverseDct(32, 32, coefficients, new JxlDctOutput(pixels, pixelsStride), scratchSpace); + break; + } + + case JxlAcStrategyType.DCT: + { + JxlDct.ComputeScaledInverseDct(8, 8, coefficients, new JxlDctOutput(pixels, pixelsStride), scratchSpace); + break; + } + + case JxlAcStrategyType.AFV0: + { + AfvTransformToPixels(0, coefficients, pixels, pixelsStride); + break; + } + + case JxlAcStrategyType.AFV1: + { + AfvTransformToPixels(1, coefficients, pixels, pixelsStride); + break; + } + + case JxlAcStrategyType.AFV2: + { + AfvTransformToPixels(2, coefficients, pixels, pixelsStride); + break; + } + + case JxlAcStrategyType.AFV3: + { + AfvTransformToPixels(3, coefficients, pixels, pixelsStride); + break; + } + + case JxlAcStrategyType.DCT64X32: + { + JxlDct.ComputeScaledInverseDct(64, 32, coefficients, new JxlDctOutput(pixels, pixelsStride), scratchSpace); + break; + } + + case JxlAcStrategyType.DCT32X64: + { + JxlDct.ComputeScaledInverseDct(32, 64, coefficients, new JxlDctOutput(pixels, pixelsStride), scratchSpace); + break; + } + + case JxlAcStrategyType.DCT64X64: + { + JxlDct.ComputeScaledInverseDct(64, 64, coefficients, new JxlDctOutput(pixels, pixelsStride), scratchSpace); + break; + } + + case JxlAcStrategyType.DCT128X64: + { + JxlDct.ComputeScaledInverseDct(128, 64, coefficients, new JxlDctOutput(pixels, pixelsStride), scratchSpace); + break; + } + + case JxlAcStrategyType.DCT64X128: + { + JxlDct.ComputeScaledInverseDct(64, 128, coefficients, new JxlDctOutput(pixels, pixelsStride), scratchSpace); + break; + } + + case JxlAcStrategyType.DCT128X128: + { + JxlDct.ComputeScaledInverseDct(128, 128, coefficients, new JxlDctOutput(pixels, pixelsStride), scratchSpace); + break; + } + + case JxlAcStrategyType.DCT256X128: + { + JxlDct.ComputeScaledInverseDct(256, 128, coefficients, new JxlDctOutput(pixels, pixelsStride), scratchSpace); + break; + } + + case JxlAcStrategyType.DCT128X256: + { + JxlDct.ComputeScaledInverseDct(128, 256, coefficients, new JxlDctOutput(pixels, pixelsStride), scratchSpace); + break; + } + + case JxlAcStrategyType.DCT256X256: + { + JxlDct.ComputeScaledInverseDct(256, 256, coefficients, new JxlDctOutput(pixels, pixelsStride), scratchSpace); + break; + } + } + } + + public static void LowestFrequenciesFromDc(JxlAcStrategyType strategy, Span dc, int dcStride, Span llf, Span scratch) + { + Span warmBlock = stackalloc float[4 * 4]; + Span warmScratchSpace = stackalloc float[4 * 4 * 4]; + + switch (strategy) + { + case JxlAcStrategyType.DCT16X8: + { + ReinterpretingDct( + 2 * JxlFrameDimensions.BlockDimensions, + JxlFrameDimensions.BlockDimensions, + 2, + 1, + 2, + 1, + dc, + dcStride, + llf, + 2 * JxlFrameDimensions.BlockDimensions, + warmBlock, + warmScratchSpace); + break; + } + + case JxlAcStrategyType.DCT8X16: + { + ReinterpretingDct( + JxlFrameDimensions.BlockDimensions, + 2 * JxlFrameDimensions.BlockDimensions, + 1, + 2, + 1, + 2, + dc, + dcStride, + llf, + 2 * JxlFrameDimensions.BlockDimensions, + warmBlock, + warmScratchSpace); + break; + } + + case JxlAcStrategyType.DCT16X16: + { + ReinterpretingDct( + 2 * JxlFrameDimensions.BlockDimensions, + 2 * JxlFrameDimensions.BlockDimensions, + 2, + 2, + 2, + 2, + dc, + dcStride, + llf, + 2 * JxlFrameDimensions.BlockDimensions, + warmBlock, + warmScratchSpace); + break; + } + + case JxlAcStrategyType.DCT32X8: + { + ReinterpretingDct( + 4 * JxlFrameDimensions.BlockDimensions, + JxlFrameDimensions.BlockDimensions, + 4, + 1, + 4, + 1, + dc, + dcStride, + llf, + 4 * JxlFrameDimensions.BlockDimensions, + warmBlock, + warmScratchSpace); + break; + } + + case JxlAcStrategyType.DCT8X32: + { + ReinterpretingDct( + JxlFrameDimensions.BlockDimensions, + 4 * JxlFrameDimensions.BlockDimensions, + 1, + 4, + 1, + 4, + dc, + dcStride, + llf, + 4 * JxlFrameDimensions.BlockDimensions, + warmBlock, + warmScratchSpace); + break; + } + + case JxlAcStrategyType.DCT32X16: + { + ReinterpretingDct( + 4 * JxlFrameDimensions.BlockDimensions, + 2 * JxlFrameDimensions.BlockDimensions, + 4, + 2, + 4, + 2, + dc, + dcStride, + llf, + 4 * JxlFrameDimensions.BlockDimensions, + warmBlock, + warmScratchSpace); + break; + } + + case JxlAcStrategyType.DCT16X32: + { + ReinterpretingDct( + 2 * JxlFrameDimensions.BlockDimensions, + 4 * JxlFrameDimensions.BlockDimensions, + 2, + 4, + 2, + 4, + dc, + dcStride, + llf, + 4 * JxlFrameDimensions.BlockDimensions, + warmBlock, + warmScratchSpace); + break; + } + + case JxlAcStrategyType.DCT32X32: + { + ReinterpretingDct( + 4 * JxlFrameDimensions.BlockDimensions, + 4 * JxlFrameDimensions.BlockDimensions, + 4, + 4, + 4, + 4, + dc, + dcStride, + llf, + 4 * JxlFrameDimensions.BlockDimensions, + warmBlock, + warmScratchSpace); + break; + } + + case JxlAcStrategyType.DCT64X32: + { + ReinterpretingDct( + 8 * JxlFrameDimensions.BlockDimensions, + 4 * JxlFrameDimensions.BlockDimensions, + 8, + 4, + 8, + 4, + dc, + dcStride, + llf, + 8 * JxlFrameDimensions.BlockDimensions, + scratch, + scratch[(8 * 4)..]); + break; + } + + case JxlAcStrategyType.DCT32X64: + { + ReinterpretingDct( + 4 * JxlFrameDimensions.BlockDimensions, + 8 * JxlFrameDimensions.BlockDimensions, + 4, + 8, + 4, + 8, + dc, + dcStride, + llf, + 8 * JxlFrameDimensions.BlockDimensions, + scratch, + scratch[(4 * 8)..]); + break; + } + + case JxlAcStrategyType.DCT64X64: + { + ReinterpretingDct( + 8 * JxlFrameDimensions.BlockDimensions, + 8 * JxlFrameDimensions.BlockDimensions, + 8, + 8, + 8, + 8, + dc, + dcStride, + llf, + 8 * JxlFrameDimensions.BlockDimensions, + scratch, + scratch[(8 * 8)..]); + break; + } + + case JxlAcStrategyType.DCT128X64: + { + ReinterpretingDct( + 16 * JxlFrameDimensions.BlockDimensions, + 8 * JxlFrameDimensions.BlockDimensions, + 16, + 8, + 16, + 8, + dc, + dcStride, + llf, + 16 * JxlFrameDimensions.BlockDimensions, + scratch, + scratch[(16 * 8)..]); + break; + } + + case JxlAcStrategyType.DCT64X128: + { + ReinterpretingDct( + 8 * JxlFrameDimensions.BlockDimensions, + 16 * JxlFrameDimensions.BlockDimensions, + 8, + 16, + 8, + 16, + dc, + dcStride, + llf, + 16 * JxlFrameDimensions.BlockDimensions, + scratch, + scratch[(8 * 16)..]); + break; + } + + case JxlAcStrategyType.DCT128X128: + { + ReinterpretingDct( + 16 * JxlFrameDimensions.BlockDimensions, + 16 * JxlFrameDimensions.BlockDimensions, + 16, + 16, + 16, + 16, + dc, + dcStride, + llf, + 16 * JxlFrameDimensions.BlockDimensions, + scratch, + scratch[(16 * 16)..]); + break; + } + + case JxlAcStrategyType.DCT256X128: + { + ReinterpretingDct( + 32 * JxlFrameDimensions.BlockDimensions, + 16 * JxlFrameDimensions.BlockDimensions, + 32, + 16, + 32, + 16, + dc, + dcStride, + llf, + 32 * JxlFrameDimensions.BlockDimensions, + scratch, + scratch[(32 * 16)..]); + break; + } + + case JxlAcStrategyType.DCT128X256: + { + ReinterpretingDct( + 16 * JxlFrameDimensions.BlockDimensions, + 32 * JxlFrameDimensions.BlockDimensions, + 16, + 32, + 16, + 32, + dc, + dcStride, + llf, + 32 * JxlFrameDimensions.BlockDimensions, + scratch, + scratch[(16 * 32)..]); + break; + } + + case JxlAcStrategyType.DCT256X256: + { + ReinterpretingDct( + 32 * JxlFrameDimensions.BlockDimensions, + 32 * JxlFrameDimensions.BlockDimensions, + 32, + 32, + 32, + 32, + dc, + dcStride, + llf, + 32 * JxlFrameDimensions.BlockDimensions, + scratch, + scratch[(32 * 32)..]); + break; + } + + case JxlAcStrategyType.DCT: + case JxlAcStrategyType.DCT2X2: + case JxlAcStrategyType.DCT4X4: + case JxlAcStrategyType.DCT4X8: + case JxlAcStrategyType.DCT8X4: + case JxlAcStrategyType.AFV0: + case JxlAcStrategyType.AFV1: + case JxlAcStrategyType.AFV2: + case JxlAcStrategyType.AFV3: + case JxlAcStrategyType.IDENTITY: + llf[0] = dc[0]; + break; + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAcStrategyEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAcStrategyEncoder.cs new file mode 100644 index 0000000000..0cdce9b6a8 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAcStrategyEncoder.cs @@ -0,0 +1,256 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; + +internal static class JxlAcStrategyEncoder +{ + private static readonly byte[][] TypeColors = + [ + [0xFF, 0xFF, 0x00], // DCT8 | yellow + [0xFF, 0x80, 0x80], // HORNUSS | vivid tangerine + [0xFF, 0x80, 0x80], // DCT2x2 | vivid tangerine + [0xFF, 0x80, 0x80], // DCT4x4 | vivid tangerine + [0x80, 0xFF, 0x00], // DCT16x16 | chartreuse + [0x00, 0xC0, 0x00], // DCT32x32 | waystone green + [0xC0, 0xFF, 0x00], // DCT16x8 | lime + [0xC0, 0xFF, 0x00], // DCT8x16 | lime + [0x00, 0xFF, 0x00], // DCT32x8 | green + [0x00, 0xFF, 0x00], // DCT8x32 | green + [0x00, 0xFF, 0x00], // DCT32x16 | green + [0x00, 0xFF, 0x00], // DCT16x32 | green + [0xFF, 0x80, 0x00], // DCT4x8 | orange juice + [0xFF, 0x80, 0x00], // DCT8x4 | orange juice + [0xFF, 0xFF, 0x80], // AFV0 | butter + [0xFF, 0xFF, 0x80], // AFV1 | butter + [0xFF, 0xFF, 0x80], // AFV2 | butter + [0xFF, 0xFF, 0x80], // AFV3 | butter + [0x00, 0xC0, 0xFF], // DCT64x64 | capri + [0x00, 0xFF, 0xFF], // DCT64x32 | aqua + [0x00, 0xFF, 0xFF], // DCT32x64 | aqua + [0x00, 0x40, 0xFF], // DCT128x128 | rare blue + [0x00, 0x80, 0xFF], // DCT128x64 | magic ink + [0x00, 0x80, 0xFF], // DCT64x128 | magic ink + [0x00, 0x00, 0xC0], // DCT256x256 | keese blue + [0x00, 0x00, 0xFF], // DCT256x128 | blue + [0x00, 0x00, 0xFF], // DCT128x256 | blue + [0x00, 0x00, 0x00] // invalid | black + ]; + +#pragma warning disable // Indentation warnings only + + private static readonly byte[][] Mask = + [ + [ + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + ], // DCT8 + [ + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 1, 0, 0, 1, 0, 0, // + 0, 0, 1, 0, 0, 1, 0, 0, // + 0, 0, 1, 1, 1, 1, 0, 0, // + 0, 0, 1, 0, 0, 1, 0, 0, // + 0, 0, 1, 0, 0, 1, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + ], // HORNUSS + [ + 1, 1, 1, 1, 1, 1, 1, 1, // + 1, 0, 1, 0, 1, 0, 1, 0, // + 1, 1, 1, 1, 1, 1, 1, 1, // + 1, 0, 1, 0, 1, 0, 1, 0, // + 1, 1, 1, 1, 1, 1, 1, 1, // + 1, 0, 1, 0, 1, 0, 1, 0, // + 1, 1, 1, 1, 1, 1, 1, 1, // + 1, 0, 1, 0, 1, 0, 1, 0, // + ], // 2x2 + [ + 0, 0, 0, 0, 1, 0, 0, 0, // + 0, 0, 0, 0, 1, 0, 0, 0, // + 0, 0, 0, 0, 1, 0, 0, 0, // + 0, 0, 0, 0, 1, 0, 0, 0, // + 1, 1, 1, 1, 1, 1, 1, 1, // + 0, 0, 0, 0, 1, 0, 0, 0, // + 0, 0, 0, 0, 1, 0, 0, 0, // + 0, 0, 0, 0, 1, 0, 0, 0, // + ], // 4x4 + [ ], // DCT16x16 (unused) + [ ], // DCT32x32 (unused) + [ ], // DCT16x8 (unused) + [ ], // DCT8x16 (unused) + [ ], // DCT32x8 (unused) + [ ], // DCT8x32 (unused) + [ ], // DCT32x16 (unused) + [ ], // DCT16x32 (unused) + [ + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 1, 1, 1, 1, 1, 1, 1, 1, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + ], // DCT4x8 + [ + 0, 0, 0, 0, 1, 0, 0, 0, // + 0, 0, 0, 0, 1, 0, 0, 0, // + 0, 0, 0, 0, 1, 0, 0, 0, // + 0, 0, 0, 0, 1, 0, 0, 0, // + 0, 0, 0, 0, 1, 0, 0, 0, // + 0, 0, 0, 0, 1, 0, 0, 0, // + 0, 0, 0, 0, 1, 0, 0, 0, // + 0, 0, 0, 0, 1, 0, 0, 0, // + ], // DCT8x4 + [ + 1, 1, 1, 1, 1, 0, 0, 0, // + 1, 1, 1, 1, 0, 0, 0, 0, // + 1, 1, 1, 0, 0, 0, 0, 0, // + 1, 1, 0, 0, 0, 0, 0, 0, // + 1, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + ], // AFV0 + [ + 0, 0, 0, 0, 1, 1, 1, 1, // + 0, 0, 0, 0, 0, 1, 1, 1, // + 0, 0, 0, 0, 0, 0, 1, 1, // + 0, 0, 0, 0, 0, 0, 0, 1, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + ], // AFV1 + [ + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 1, 0, 0, 0, 0, 0, 0, 0, // + 1, 1, 0, 0, 0, 0, 0, 0, // + 1, 1, 1, 0, 0, 0, 0, 0, // + 1, 1, 1, 1, 0, 0, 0, 0, // + ], // AFV2 + [ + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 1, // + 0, 0, 0, 0, 0, 0, 1, 1, // + 0, 0, 0, 0, 0, 1, 1, 1, // + ], // AFV3 + [ ] // invalid + ]; + +#pragma warning restore + + public static ReadOnlySpan TypeColor(int rawStrategy) + { + DebugGuard.IsTrue(JxlAcStrategy.IsRawStrategyValid(rawStrategy), $"Raw strategy must be a valid strategy: {rawStrategy}"); + rawStrategy = Math.Clamp(rawStrategy, 0, JxlAcStrategy.NumberOfValidStrategies); + return TypeColors[rawStrategy]; + } + + public static ReadOnlySpan TypeMask(int rawStrategy) + { + DebugGuard.IsTrue(JxlAcStrategy.IsRawStrategyValid(rawStrategy), $"Raw strategy must be a valid strategy: {rawStrategy}"); + rawStrategy = Math.Clamp(rawStrategy, 0, JxlAcStrategy.NumberOfValidStrategies); + return Mask[rawStrategy]; + } + + public static bool MultiBlockTransformCrossesHorizontalBoundary(JxlAcStrategyImage acStrategyImage, int startX, int y, int endX) + { + if (startX >= acStrategyImage.XSize || y >= acStrategyImage.YSize) + { + return false; + } + + if (y % 8 == 0) + { + // Nothing crosses 64x64 boundaries, and the memory on the other side + // of the 64x64 block may still uninitialized. + return false; + } + + endX = Math.Min(endX, acStrategyImage.XSize); + + // The first multiblock might be before the start_x, let's adjust it + // to point to the first IsFirstBlock() == true block we find by backward + // tracing. + JxlAcStrategyRow row = acStrategyImage.GetRow(y); + int startXLimit = startX & ~7; + + while (startX != startXLimit && !row[startX].IsFirstBlock) + { + startX--; + } + + for (int x = startX; x < endX;) + { + if (row[x].IsFirstBlock) + { + x += row[x].CoveredBlocksX; + } + else + { + return true; + } + } + + return false; + } + + public static bool MultiBlockTransformCrossesVerticalBoundary(JxlAcStrategyImage acStrategyImage, int x, int startY, int endY) + { + if (x >= acStrategyImage.XSize || startY >= acStrategyImage.YSize) + { + return false; + } + + if (x % 8 == 0) + { + // Nothing crosses 64x64 boundaries, and the memory on the other side + // of the 64x64 block may still uninitialized. + return false; + } + + endY = Math.Min(endY, acStrategyImage.YSize); + + // The first multiblock might be before the start_y, let's adjust it + // to point to the first IsFirstBlock() == true block we find by backward + // tracing. + int startYLimit = startY & ~7; + while (startY != startYLimit && !acStrategyImage.GetRow(startY)[x].IsFirstBlock) + { + startY--; + } + + for (int y = startY; y < endY;) + { + JxlAcStrategyRow row = acStrategyImage.GetRow(y); + + if (row[x].IsFirstBlock) + { + y += row[x].CoveredBlocksY; + } + else + { + return true; + } + } + + return false; + } +} From 06598eee415ceadfde3d925d276b633d58986482 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 8 Sep 2026 20:33:23 +0400 Subject: [PATCH 158/328] Blank transforms encoder --- .../Jxl/Processing/Encoder/JxlTransformsEncoder.cs | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlTransformsEncoder.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlTransformsEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlTransformsEncoder.cs new file mode 100644 index 0000000000..f3565a75d9 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlTransformsEncoder.cs @@ -0,0 +1,8 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; + +internal static class JxlTransformsEncoder +{ +} From 5b8cd178e303c535164c35d6b05278575df60f51 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 8 Sep 2026 21:26:51 +0400 Subject: [PATCH 159/328] Add fields tests --- .../Formats/Jxl/Fields/JxlBundle.cs | 2 +- .../Formats/Jxl/IO/Metadata/JxlSizeHeader.cs | 1 + .../Jxl/Processing/Encoder/JxlBitWriter.cs | 12 + .../Formats/Jxl/IO/FieldsTests.cs | 674 ++++++++++++++++++ 4 files changed, 688 insertions(+), 1 deletion(-) create mode 100644 tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs index 866e00f317..f7b8c921b1 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs @@ -135,7 +135,7 @@ public static bool Write(IJxlFields fields, JxlBitWriter writer, JxlLayerType la public static bool WriteCodestreamHeaders(JxlCodecMetadata metadata, JxlBitWriter writer, JxlAuxiliaryOutput auxOut) { // Marker/signature - if (!writer.WithMaxBits(16, JxlLayerType.Header, auxOut, () => + if (!writer.WithMaxBits(16, () => { writer.Write(8, 0xFF); writer.Write(8, JxlShared.CodestreamMarker); diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlSizeHeader.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlSizeHeader.cs index 5da30b8a0d..e40720a53c 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlSizeHeader.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlSizeHeader.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlBitWriter.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlBitWriter.cs index e497da0a34..924dac7c92 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlBitWriter.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlBitWriter.cs @@ -57,6 +57,18 @@ public void Write(int nBits, ulong bits) } } + public void ZeroPadToByte() + { + long remainderBits = JxlMath.RoundUpBitsToByteMultiple((int)this.BitsWritten) - this.BitsWritten; + + if (remainderBits == 0) + { + return; + } + + this.Write((int)remainderBits, 0); + } + public bool WithMaxBits(ulong maxBits, Func func) { bool previousIsLimited = this.isLimited; diff --git a/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs new file mode 100644 index 0000000000..b715b48a60 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs @@ -0,0 +1,674 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; +using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Processing; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl.IO; + +public class FieldsTests +{ + private sealed class OldBundle : IJxlFields + { + private uint oldSmall; + private float oldF; + private uint oldLarge; + private ulong extensions; + + public OldBundle() => JxlBundle.Init(this); + + public uint OldSmall + { + get => this.oldSmall; + set => this.oldSmall = value; + } + + public float OldF + { + get => this.oldF; + set => this.oldF = value; + } + + public uint OldLarge + { + get => this.oldLarge; + set => this.oldLarge = value; + } + + public ulong Extensions + { + get => this.extensions; + set => this.extensions = value; + } + + public bool Visit(JxlVisitor visitor) + { + if (!visitor.U32( + JxlFieldExpressions.Value(1), + JxlFieldExpressions.Bits(2), + JxlFieldExpressions.Bits(3), + JxlFieldExpressions.Bits(4), + 1, + ref this.oldSmall)) + { + return false; + } + + if (!visitor.F16(1.125f, ref this.oldF)) + { + return false; + } + + if (!visitor.U32( + JxlFieldExpressions.Bits(7), + JxlFieldExpressions.Bits(12), + JxlFieldExpressions.Bits(16), + JxlFieldExpressions.Bits(32), + 0, + ref this.oldLarge)) + { + return false; + } + + if (!visitor.BeginExtensions(ref this.extensions)) + { + return false; + } + + return visitor.EndExtensions(); + } + } + + private sealed class NewBundle : IJxlFields + { + private uint oldSmall; + private float oldF; + private uint oldLarge; + private ulong extensions; + private uint newSmall = 2; + private float newF = -2.0f; + private uint newLarge; + + public NewBundle() => JxlBundle.Init(this); + + public uint OldSmall + { + get => this.oldSmall; + set => this.oldSmall = value; + } + + public float OldF + { + get => this.oldF; + set => this.oldF = value; + } + + public uint OldLarge + { + get => this.oldLarge; + set => this.oldLarge = value; + } + + public ulong Extensions + { + get => this.extensions; + set => this.extensions = value; + } + + public uint NewSmall + { + get => this.newSmall; + set => this.newSmall = value; + } + + public float NewF + { + get => this.newF; + set => this.newF = value; + } + + public uint NewLarge + { + get => this.newLarge; + set => this.newLarge = value; + } + + public bool Visit(JxlVisitor visitor) + { + if (!visitor.U32( + JxlFieldExpressions.Value(1), + JxlFieldExpressions.Bits(2), + JxlFieldExpressions.Bits(3), + JxlFieldExpressions.Bits(4), + 1, + ref this.oldSmall)) + { + return false; + } + + if (!visitor.F16(1.125f, ref this.oldF)) + { + return false; + } + + if (!visitor.U32( + JxlFieldExpressions.Bits(7), + JxlFieldExpressions.Bits(12), + JxlFieldExpressions.Bits(16), + JxlFieldExpressions.Bits(32), + 0, + ref this.oldLarge)) + { + return false; + } + + if (!visitor.BeginExtensions(ref this.extensions)) + { + return false; + } + + if (visitor.Conditional((this.Extensions & 1) != 0)) + { + if (!visitor.U32( + JxlFieldExpressions.Value(2), + JxlFieldExpressions.Bits(2), + JxlFieldExpressions.Bits(3), + JxlFieldExpressions.Bits(4), + 2, + ref this.newSmall)) + { + return false; + } + + if (!visitor.F16(-2.0f, ref this.newF)) + { + return false; + } + } + + if (visitor.Conditional((this.Extensions & 2) != 0)) + { + if (!visitor.U32( + JxlFieldExpressions.Bits(9), + JxlFieldExpressions.Bits(12), + JxlFieldExpressions.Bits(16), + JxlFieldExpressions.Bits(32), + 0, + ref this.newLarge)) + { + return false; + } + } + + return visitor.EndExtensions(); + } + } + + private static void TestU32Coder(uint value, int expectedBitsWritten) + { + JxlU32Enc enc = new( + JxlFieldExpressions.Value(0), + JxlFieldExpressions.Bits(4), + JxlFieldExpressions.Value(0x7FFFFFFF), + JxlFieldExpressions.Bits(32)); + + using MemoryStream memoryStream = new(); + JxlBitWriter bitWriter = new(memoryStream); + + Assert.True(bitWriter.WithMaxBits( + (ulong)JxlMath.RoundUpBitsToByteMultiple(JxlU32Coder.MaxEncodedBits(enc)), + () => + { + int encodedBits = 0; + Assert.True(JxlU32Coder.CanEncode(in enc, value, ref encodedBits)); + Assert.Equal(expectedBitsWritten, encodedBits); + + Assert.True(JxlU32Coder.Write(in enc, value, bitWriter)); + Assert.Equal(expectedBitsWritten, bitWriter.BitsWritten); + + bitWriter.ZeroPadToByte(); + + return true; + })); + + memoryStream.Position = 0; + JxlBitReader reader = new(memoryStream); + uint decodedValue = JxlU32Coder.Read(in enc, reader); + + Assert.Equal(value, decodedValue); + } + + private static void TestU64Coder(ulong value, int expectedBitsWritten) + { + using MemoryStream memoryStream = new(); + JxlBitWriter bitWriter = new(memoryStream); + + Assert.True(bitWriter.WithMaxBits( + (ulong)JxlMath.RoundUpBitsToByteMultiple(JxlU64Coder.MaxEncodedBits()), + () => + { + int encodedBits = 0; + Assert.True(JxlU64Coder.CanEncode(value, ref encodedBits)); + Assert.Equal(expectedBitsWritten, encodedBits); + + Assert.True(JxlU64Coder.Write(value, bitWriter)); + Assert.Equal(expectedBitsWritten, bitWriter.BitsWritten); + + bitWriter.ZeroPadToByte(); + + return true; + })); + + memoryStream.Position = 0; + JxlBitReader reader = new(memoryStream); + ulong decodedValue = JxlU64Coder.Read(reader); + + Assert.Equal(value, decodedValue); + } + + private static void TestF16Coder(float value) + { + int maxEncodedBits = 0; + if (JxlF16Coder.CanEncode(value, ref maxEncodedBits)) + { + throw new InvalidOperationException("This F16 value cannot be encoded: " + value); + } + + Assert.Equal(JxlF16Coder.MaxEncodedBits(), maxEncodedBits); + + using MemoryStream memoryStream = new(); + JxlBitWriter bitWriter = new(memoryStream); + + Assert.True(bitWriter.WithMaxBits( + (ulong)JxlMath.RoundUpBitsToByteMultiple(maxEncodedBits), + () => + { + Assert.True(JxlF16Coder.Write(value, bitWriter)); + Assert.Equal(JxlF16Coder.MaxEncodedBits(), bitWriter.BitsWritten); + bitWriter.ZeroPadToByte(); + return true; + })); + + memoryStream.Position = 0; + JxlBitReader reader = new(memoryStream); + + float decodedValue = 0; + Assert.True(JxlF16Coder.Read(reader, ref decodedValue)); + Assert.Equal(value, decodedValue); + } + + [Fact] + public void U32CoderTest() + { + TestU32Coder(0, 2); + TestU32Coder(1, 6); + TestU32Coder(15, 6); + TestU32Coder(0x7FFFFFFF, 2); + TestU32Coder(128, 34); + TestU32Coder(0x7FFFFFFEu, 34); + TestU32Coder(0x80000000u, 34); + TestU32Coder(0xFFFFFFFFu, 34); + } + + [Fact] + public void U64CoderTest() + { + // Values that should take 2 bits (selector 00): 0 + TestU64Coder(0, 2); + + // Values that should take 6 bits (2 for selector, 4 for value): 1..16 + TestU64Coder(1, 6); + TestU64Coder(2, 6); + TestU64Coder(8, 6); + TestU64Coder(15, 6); + TestU64Coder(16, 6); + + // Values that should take 10 bits (2 for selector, 8 for value): 17..272 + TestU64Coder(17, 10); + TestU64Coder(18, 10); + TestU64Coder(100, 10); + TestU64Coder(271, 10); + TestU64Coder(272, 10); + + // Values that should take 15 bits (2 for selector, 12 for value, 1 for varint + // end): (0)..273..4095 + TestU64Coder(273, 15); + TestU64Coder(274, 15); + TestU64Coder(1000, 15); + TestU64Coder(4094, 15); + TestU64Coder(4095, 15); + + // Take 24 bits (of which 20 actual value): (0)..4096..1048575 + TestU64Coder(4096, 24); + TestU64Coder(4097, 24); + TestU64Coder(10000, 24); + TestU64Coder(1048574, 24); + TestU64Coder(1048575, 24); + + // Take 33 bits (of which 28 actual value): (0)..1048576..268435455 + TestU64Coder(1048576, 33); + TestU64Coder(1048577, 33); + TestU64Coder(10000000, 33); + TestU64Coder(268435454, 33); + TestU64Coder(268435455, 33); + + // Take 42 bits (of which 36 actual value): (0)..268435456..68719476735 + TestU64Coder(268435456uL, 42); + TestU64Coder(268435457uL, 42); + TestU64Coder(1000000000uL, 42); + TestU64Coder(68719476734uL, 42); + TestU64Coder(68719476735uL, 42); + + // Take 51 bits (of which 44 actual value): (0)..68719476736..17592186044415 + TestU64Coder(68719476736uL, 51); + TestU64Coder(68719476737uL, 51); + TestU64Coder(1000000000000uL, 51); + TestU64Coder(17592186044414uL, 51); + TestU64Coder(17592186044415uL, 51); + + // Take 60 bits (of which 52 actual value): + // (0)..17592186044416..4503599627370495 + TestU64Coder(17592186044416uL, 60); + TestU64Coder(17592186044417uL, 60); + TestU64Coder(100000000000000uL, 60); + TestU64Coder(4503599627370494uL, 60); + TestU64Coder(4503599627370495uL, 60); + + // Take 69 bits (of which 60 actual value): + // (0)..4503599627370496..1152921504606846975 + TestU64Coder(4503599627370496uL, 69); + TestU64Coder(4503599627370497uL, 69); + TestU64Coder(10000000000000000uL, 69); + TestU64Coder(1152921504606846974uL, 69); + TestU64Coder(1152921504606846975uL, 69); + + // Take 73 bits (of which 64 actual value): + // (0)..1152921504606846976..18446744073709551615 + TestU64Coder(1152921504606846976uL, 73); + TestU64Coder(1152921504606846977uL, 73); + TestU64Coder(10000000000000000000uL, 73); + TestU64Coder(18446744073709551614uL, 73); + TestU64Coder(18446744073709551615uL, 73); + } + + [Fact] + public void F16CoderTest() + { + ReadOnlySpan signs = [-1.0f, 1.0f]; + ReadOnlySpan magnitudes = [0.0f, 0.5f, 1.0f, 2.0f, 2.5f, 16.015625f, 1.0f / 4096, 1.0f / 16384, 65504.0f]; + + foreach (float sign in signs) + { + foreach (float magnitude in magnitudes) + { + TestF16Coder(sign * magnitude); + } + } + + // These values are out of range and cannot be represented + // by an F16 coder, so they're expected to throw. + Assert.Throws(() => TestF16Coder(65504.01f)); + Assert.Throws(() => TestF16Coder(-65505.0f)); + } + + [Fact] + public void TestRoundtripSize() + { + for (int i = 0; i < 8; i++) + { + JxlSizeHeader size = new(); + size.Set(123 + (77 * i), 7 + i); // It'll throw if it can't set + + int extensionBits = 999; + long totalBits = 999; + + Assert.True(JxlBundle.CanEncode(size, ref extensionBits, ref totalBits)); + Assert.Equal(0, extensionBits); + + using MemoryStream ms = new(); + JxlBitWriter writer = new(ms); + + Assert.True(JxlBundle.WriteSizeHeader(size, writer, JxlLayerType.Header, new JxlAuxiliaryOutput())); + Assert.Equal(totalBits, writer.BitsWritten); + + writer.ZeroPadToByte(); + + JxlSizeHeader size2 = new(); + + ms.Position = 0; + JxlBitReader reader = new(ms); + + Assert.True(JxlBundle.Read(reader, size2)); + Assert.Equal(totalBits, reader.TotalBitsConsumed); + + Assert.Equal(size.XSize, size2.XSize); + Assert.Equal(size.YSize, size2.YSize); + } + } + + [Fact] + public void TestCropRect() + { + JxlCodecMetadata metadata = new(); + + for (int i = -999; i < 19000; i++) + { + JxlFrameHeader f = new() + { + Metadata = metadata, + CustomSizeOrOrigin = true, + FrameOrigin = new Point(i), + FrameSize = new(1000 + i) + }; + + int extensionBits = 0; + long totalBits = 0; + + Assert.True(JxlBundle.CanEncode(f, ref extensionBits, ref totalBits)); + Assert.Equal(0, extensionBits); + Assert.True(totalBits >= 9); + } + } + + [Fact] + public void TestPreview() + { + for (int i = 1; i < 4360; i++) + { + JxlPreviewHeader p = new(); + p.Set(i, i); // Throws if it fails + + int extensionBits = 0; + long totalBits = 0; + + Assert.True(JxlBundle.CanEncode(p, ref extensionBits, ref totalBits)); + Assert.Equal(0, extensionBits); + Assert.True(totalBits >= 6); + } + } + + [Fact] + public void TestRoundtripFrame() + { + JxlCodecMetadata metadata = new(); + + JxlFrameHeader h = new() + { + Metadata = metadata, + Extensions = 0x800 + }; + + int extensionBits = 999; + long totalBits = 999; + + Assert.True(JxlBundle.CanEncode(h, ref extensionBits, ref totalBits)); + Assert.Equal(0, extensionBits); + + using MemoryStream ms = new(); + JxlBitWriter writer = new(ms); + + Assert.True(JxlBundle.WriteFrameHeader(h, writer, null)); + Assert.Equal(totalBits, writer.BitsWritten); + + writer.ZeroPadToByte(); + + JxlFrameHeader h2 = new() + { + Metadata = metadata + }; + + ms.Position = 0; + JxlBitReader reader = new(ms); + + Assert.True(JxlBundle.Read(reader, h2)); + Assert.Equal(totalBits, reader.TotalBitsConsumed); + + Assert.Equal(h.Extensions, h2.Extensions); + Assert.Equal(h.Flags, h2.Flags); + } + + [Fact] + public void TestOutOfRange() + { + JxlSizeHeader h = new(); + h.Set(unchecked((int)0xFFFFFFFF), unchecked((int)0xFFFFFFFF)); + + int extensionBits = 999; + long totalBits = 999; + + Assert.False(JxlBundle.CanEncode(h, ref extensionBits, ref totalBits)); + } + + [Fact] + public void TestNewDecoderOldData() + { + OldBundle oldBundle = new() + { + OldLarge = 123, + OldF = 3.75f, + Extensions = 0 + }; + + const int maxOutBytes = 999; + + using MemoryStream ms = new(); + JxlBitWriter writer = new(ms); + + int extensionBits = 12345; + long totalBits = 12345; + + Assert.True(JxlBundle.CanEncode(oldBundle, ref extensionBits, ref totalBits)); + Assert.True(totalBits <= maxOutBytes * JxlMath.BitsPerByte); + Assert.Equal(0, extensionBits); + + JxlAuxiliaryOutput auxOut = new(); + Assert.True( + JxlBundle.Write( + oldBundle, + writer, + JxlLayerType.Header, + auxOut)); + + Assert.True(writer.WithMaxBits( + (ulong)((maxOutBytes * JxlMath.BitsPerByte) - totalBits), + () => + { + writer.Write(20, 0xA55A); + writer.ZeroPadToByte(); + return true; + })); + + ms.Position = 0; + JxlBitReader reader = new(ms); + NewBundle newBundle = new(); + + Assert.True(JxlBundle.Read(reader, newBundle)); + + Assert.Equal( + reader.TotalBitsConsumed, + auxOut.Layers[(int)JxlLayerType.Header].TotalBits); + + Assert.Equal(0xA55A, (int)reader.ReadBits32(20)); + + // Old fields are the same in both. + Assert.Equal(oldBundle.Extensions, newBundle.Extensions); + Assert.Equal(oldBundle.OldSmall, newBundle.OldSmall); + Assert.Equal(oldBundle.OldF, newBundle.OldF); + Assert.Equal(oldBundle.OldLarge, newBundle.OldLarge); + + // New fields match their defaults. + Assert.Equal(2u, newBundle.NewSmall); + Assert.Equal(-2.0f, newBundle.NewF); + Assert.Equal(0u, newBundle.NewLarge); + } + + [Fact] + public void TestOldDecoderNewData() + { + NewBundle newBundle = new() + { + OldLarge = 123, + Extensions = 3, + NewF = 999.0f, + NewLarge = 456 + }; + + const int maxOutBytes = 999; + + using MemoryStream ms = new(); + JxlBitWriter writer = new(ms); + + int extensionBits = 12345; + long totalBits = 12345; + + Assert.True(JxlBundle.CanEncode( + newBundle, + ref extensionBits, + ref totalBits)); + + Assert.NotEqual(0, extensionBits); + + JxlAuxiliaryOutput auxOut = new(); + Assert.True(JxlBundle.Write( + newBundle, + writer, + JxlLayerType.Header, + auxOut)); + + int headerBits = auxOut.Layers[(int)JxlLayerType.Header].TotalBits; + + Assert.True(headerBits <= maxOutBytes * JxlMath.BitsPerByte); + + Assert.True(writer.WithMaxBits( + (ulong)((maxOutBytes * JxlMath.BitsPerByte) - headerBits), + () => + { + writer.Write(20, 0xA55A); + writer.ZeroPadToByte(); + return true; + })); + + ms.Position = 0; + JxlBitReader reader = new(ms); + OldBundle oldBundle = new(); + + Assert.True(JxlBundle.Read(reader, oldBundle)); + + Assert.Equal( + reader.TotalBitsConsumed, + auxOut.Layers[(int)JxlLayerType.Header].TotalBits); + + Assert.Equal(0xA55A, (int)reader.ReadBits32(20)); + + // Old fields are the same in both. + Assert.Equal(newBundle.Extensions, oldBundle.Extensions); + Assert.Equal(newBundle.OldSmall, oldBundle.OldSmall); + Assert.Equal(newBundle.OldF, oldBundle.OldF); + Assert.Equal(newBundle.OldLarge, oldBundle.OldLarge); + } +} From 0520ab33b99bc77bdbd115dc085dc48f50edc5ca Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 8 Sep 2026 23:59:20 +0400 Subject: [PATCH 160/328] Add encoder image comparators --- .../Comparator/ButteraugliJxlComparator.cs | 83 +++++++++ .../Encoder/Comparator/JxlComparator.cs | 27 +++ .../JxlComparatorScoreComputation.cs | 175 ++++++++++++++++++ .../Encoder/Comparator/NullComparator.cs | 23 +++ 4 files changed, 308 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/ButteraugliJxlComparator.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparator.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparatorScoreComputation.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/NullComparator.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/ButteraugliJxlComparator.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/ButteraugliJxlComparator.cs new file mode 100644 index 0000000000..919446bfca --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/ButteraugliJxlComparator.cs @@ -0,0 +1,83 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics.Tensors; +using SixLabors.ImageSharp.Formats.Jxl.Cms; +using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Comparator; + +/// +/// Compares images using Google Butteraugli. +/// +internal sealed class ButteraugliJxlComparator(ButteraugliParameters parameters, JxlCmsInterface cms) + : JxlComparator +{ + private int width; + private int height; + private float intensityTarget; + private ButteraugliComparator? comparator; + + public override float GoodQualityScore => Butteraugli.Butteraugli.ButteraugliFuzzyInverse(1.5f); + + public override float BadQualityScore => Butteraugli.Butteraugli.ButteraugliFuzzyInverse(0.5f); + + public override void CompareWith(Configuration configuration, JxlImageBundle actual, JxlImageF? diffMap, out float score) + { + DebugGuard.NotNull(this.comparator, nameof(this.comparator)); + + if (this.width != actual.XSize || this.height != actual.YSize) + { + throw new InvalidOperationException("Image must have same size"); + } + + JxlImageMetadata metadata = actual.Metadata!; + JxlImageBundle store = new(metadata); + + JxlTransformsEncoder.TransformIfNeeded(configuration, actual, JxlColorEncoding.LinearSRgb(actual.IsGray), cms, store, out JxlImageBundle actualLinearSRgb); + + JxlImageF tempDiffmap = new(configuration, this.width, this.height); + JxlImage3F scaledActualLinearSRgb = actualLinearSRgb.Color!; + + if (this.intensityTarget != 0 && actual.Metadata!.IntensityTarget != this.intensityTarget) + { + JxlImage3F scaledActualLinearSRgbStore = new(configuration, this.width, this.height); + scaledActualLinearSRgb = scaledActualLinearSRgbStore; + + float scale = actual.Metadata!.IntensityTarget / this.intensityTarget; + for (int c = 0; c < 3; ++c) + { + for (int y = 0; y < this.height; ++y) + { + Span souceRow = actualLinearSRgb.Color!.PlaneRow(c, y); + Span scaledRow = scaledActualLinearSRgb.PlaneRow(c, y); + TensorPrimitives.Multiply(souceRow, scale, scaledRow); + } + } + } + + if (!this.comparator.Diffmap(configuration, scaledActualLinearSRgb, tempDiffmap)) + { + throw new InvalidOperationException("Diffmap failed"); + } + + score = Butteraugli.Butteraugli.ButteraugliScoreFromDiffmap(tempDiffmap, parameters); + diffMap?.Swap(tempDiffmap); + } + + public override void SetReferenceImage(Configuration configuration, JxlImageBundle reference) + { + JxlImageMetadata metadata = reference.Metadata!; + JxlImageBundle store = new(metadata); + + JxlTransformsEncoder.TransformIfNeeded(configuration, reference, JxlColorEncoding.LinearSRgb(reference.IsGray), cms, store, out JxlImageBundle refLinearSRgb); + this.comparator = ButteraugliComparator.Make(configuration, refLinearSRgb.Color!, parameters); + + this.width = reference.XSize; + this.height = reference.YSize; + this.intensityTarget = reference.Metadata!.IntensityTarget; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparator.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparator.cs new file mode 100644 index 0000000000..920e929099 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparator.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Comparator; + +/// +/// Compares images. +/// +internal abstract class JxlComparator +{ + public abstract float GoodQualityScore { get; } + + public abstract float BadQualityScore { get; } + + // Sets the reference image, the first to compare. + public abstract void SetReferenceImage(Configuration configuration, JxlImageBundle reference); + + // Sets the actual image, the second to compare. + public abstract void CompareWith( + Configuration configuration, + JxlImageBundle actual, + JxlImageF? diffMap, + out float score); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparatorScoreComputation.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparatorScoreComputation.cs new file mode 100644 index 0000000000..8dec22e152 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparatorScoreComputation.cs @@ -0,0 +1,175 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics.Tensors; +using SixLabors.ImageSharp.Formats.Jxl.Cms; +using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Comparator; + +internal static class JxlComparatorScoreComputation +{ + private static void AlphaBlend(JxlImage3F input, int c, float backgroundLinear, JxlImageF alpha, JxlImage3F output) + { + float background = (float)JxlGammaCorrect.LinearToSRgb8Direct(backgroundLinear); + + for (int y = 0; y < output.YSize; y++) + { + ReadOnlySpan rowA = alpha.GetRow(y); + ReadOnlySpan rowI = input.PlaneRow(c, y); + Span rowO = output.PlaneRow(c, y); + + for (int x = 0; x < output.XSize; x++) + { + float a = rowA[x]; + + if (a <= 0.0f) + { + rowO[x] = backgroundLinear; + } + else if (a >= 1.0f) + { + rowO[x] = rowI[x]; + } + else + { + float wFg = a; + float wBg = 1.0f - wFg; + float fg = wFg * (float)JxlGammaCorrect.LinearToSRgb8Direct(rowI[x]); + float bg = wBg * background; + + rowO[x] = (float)JxlGammaCorrect.SRgb8ToLinearDirect(fg + bg); + } + } + } + } + + private static void AlphaBlend(float backgroundLinear, JxlImageBundle image) + { + if (!image.ContainsAlpha) + { + return; + } + + for (int c = 0; c < 3; c++) + { + AlphaBlend(image.Color!, c, backgroundLinear, image.Alpha!, image.Color!); + } + } + + private static void ComputeScoreImpl(Configuration configuration, JxlImageBundle rgb0, JxlImageBundle rgb1, JxlComparator comparator, JxlImageF? diffMap, out float score) + { + comparator.SetReferenceImage(rgb0); + comparator.CompareWith(configuration, rgb1, diffMap, out score); + } + + public static void ComputeScore( + Configuration configuration, + JxlImageBundle rgb0, + JxlImageBundle rgb1, + JxlComparator comparator, + JxlCmsInterface cms, + out float score, + JxlImageF? diffMap = null, + bool ignoreAlpha = false) + { + JxlImageMetadata metadata0 = rgb0.Metadata.Copy(); + + JxlImageBundle store0 = new(metadata0); + + JxlTransformsEncoder.TransformIfNeeded( + rgb0, + JxlColorEncoding.LinearSRgb(rgb0.IsGray), + cms, + pool, + store0, + out JxlImageBundle linearSrgb0); + + JxlImageMetadata metadata1 = rgb1.Metadata.Copy(); + + JxlImageBundle store1 = new(metadata1); + + JxlTransformsEncoder.TransformIfNeeded( + rgb1, + JxlColorEncoding.LinearSRgb(rgb1.IsGray), + cms, + pool, + store1, + out JxlImageBundle linearSrgb1); + + if (ignoreAlpha || (!rgb0.ContainsAlpha && !rgb1.ContainsAlpha)) + { + ComputeScoreImpl( + configuration, + linearSrgb0, + linearSrgb1, + comparator, + diffMap, + out score); + } + + JxlImageF diffMapBlack = new(); + float distBlack; + { + const float black = 0.0f; + + JxlImageBundle blendedBlack0 = linearSrgb0.Copy(configuration); + JxlImageBundle blendedBlack1 = linearSrgb1.Copy(configuration); + + AlphaBlend(black, blendedBlack0); + AlphaBlend(black, blendedBlack1); + + ComputeScoreImpl( + configuration, + blendedBlack0, + blendedBlack1, + comparator, + diffMapBlack, + out distBlack); + } + + JxlImageF diffMapWhite = new(); + float distWhite; + { + const float white = 1.0f; + + JxlImageBundle blendedWhite0 = linearSrgb0.Copy(configuration); + JxlImageBundle blendedWhite1 = linearSrgb1.Copy(configuration); + + AlphaBlend(white, blendedWhite0); + AlphaBlend(white, blendedWhite1); + + ComputeScoreImpl( + configuration, + blendedWhite0, + blendedWhite1, + comparator, + diffMapWhite, + out distWhite); + } + + if (diffMap is not null) + { + int xSize = rgb0.XSize; + int ySize = rgb0.YSize; + + JxlImageF outputDiffMap = new( + configuration, + xSize, + ySize); + + for (int y = 0; y < ySize; y++) + { + ReadOnlySpan rowBlack = diffMapBlack.GetRow(y); + ReadOnlySpan rowWhite = diffMapWhite.GetRow(y); + Span rowOut = diffMap.GetRow(y); + + TensorPrimitives.Max(rowBlack, rowWhite, rowOut); + } + } + + score = MathF.Max(distBlack, distWhite); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/NullComparator.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/NullComparator.cs new file mode 100644 index 0000000000..810c4c59a0 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/NullComparator.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Comparator; + +/// +/// Performs no image comparison at all. +/// +internal sealed class NullComparator : JxlComparator +{ + public override float GoodQualityScore => 0; + + public override float BadQualityScore => 0; + + public override void CompareWith(Configuration configuration, JxlImageBundle actual, JxlImageF? diffMap, out float score) => score = 0; + + public override void SetReferenceImage(Configuration configuration, JxlImageBundle reference) + { + } +} From 0c2c3a1acfc15f29b0959f79e1681669aa0ab3c9 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 9 Sep 2026 00:20:22 +0400 Subject: [PATCH 161/328] Mark transforms encoder as TODO --- .../Formats/Jxl/Processing/Encoder/JxlTransformsEncoder.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlTransformsEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlTransformsEncoder.cs index f3565a75d9..d7c7b63b4b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlTransformsEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlTransformsEncoder.cs @@ -3,6 +3,9 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; +/// +/// TODO +/// internal static class JxlTransformsEncoder { } From ec32e422759b98cf47e701d9f69556ffaff07f09 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 9 Sep 2026 00:26:29 +0400 Subject: [PATCH 162/328] Refactor --- .../Formats/Jxl/IO/Jpeg/JpegBitWriter.cs | 13 ++--- .../Jxl/IO/Jpeg/JpegSerializationStage.cs | 2 +- .../Formats/Jxl/IO/Jpeg/JpegWriter.cs | 56 +++++++++---------- 3 files changed, 34 insertions(+), 37 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegBitWriter.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegBitWriter.cs index bb97c941b3..805eb934f6 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegBitWriter.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegBitWriter.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers.Binary; + namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg; internal sealed class JpegBitWriter @@ -62,14 +64,9 @@ public void EmitMarker(int marker) public void StoreBe64(ulong value) { - this.buffer[this.pos++] = (byte)(value >> 56); - this.buffer[this.pos++] = (byte)(value >> 48); - this.buffer[this.pos++] = (byte)(value >> 40); - this.buffer[this.pos++] = (byte)(value >> 32); - this.buffer[this.pos++] = (byte)(value >> 24); - this.buffer[this.pos++] = (byte)(value >> 16); - this.buffer[this.pos++] = (byte)(value >> 8); - this.buffer[this.pos++] = (byte)value; + Span span = this.buffer.AsSpan(this.pos); + BinaryPrimitives.WriteUInt64BigEndian(span, value); + this.pos += 8; } public void DischargeBitBuffer(int nBits, ulong bits) diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegSerializationStage.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegSerializationStage.cs index 47d1b2d3ad..4beb4e51cf 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegSerializationStage.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegSerializationStage.cs @@ -3,7 +3,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg; -internal enum JpegSerializationStage +internal enum JpegSerializationStage : byte { Initialize, SerializeSection, diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegWriter.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegWriter.cs index 62ea5230e5..0df15a4bea 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegWriter.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegWriter.cs @@ -226,18 +226,18 @@ private static bool EncodeSOF(JpegData jpg, byte marker, JpegSerializationState state.OutputQueue.Add([(byte)(markerLength + 2)]); Span data = CollectionsMarshal.AsSpan(state.OutputQueue[^1]); - int pos = 0; - - data[pos++] = 0xff; - data[pos++] = marker; - data[pos++] = (byte)(markerLength >> 8); - data[pos++] = (byte)markerLength; - data[pos++] = JpegPrecision; - data[pos++] = (byte)(jpg.Height >> 8); - data[pos++] = (byte)jpg.Height; - data[pos++] = (byte)(jpg.Width >> 8); - data[pos++] = (byte)jpg.Width; - data[pos++] = (byte)componentCount; + int pos = 10; + + data[0] = 0xff; + data[1] = marker; + data[2] = (byte)(markerLength >> 8); + data[3] = (byte)markerLength; + data[4] = JpegPrecision; + data[5] = (byte)(jpg.Height >> 8); + data[6] = (byte)jpg.Height; + data[7] = (byte)(jpg.Width >> 8); + data[8] = (byte)jpg.Width; + data[9] = (byte)componentCount; for (int i = 0; i < componentCount; ++i) { @@ -267,13 +267,13 @@ private static bool EncodeSOS(JpegData jpg, JpegScanInfo scanInfo, JpegSerializa state.OutputQueue.Add([(byte)(markerLength + 2)]); Span data = CollectionsMarshal.AsSpan(state.OutputQueue[^1]); - int pos = 0; + int pos = 5; - data[pos++] = 0xff; - data[pos++] = 0xda; - data[pos++] = (byte)(markerLength >> 8); - data[pos++] = (byte)markerLength; - data[pos++] = (byte)scanCount; + data[0] = 0xff; + data[1] = 0xda; + data[2] = (byte)(markerLength >> 8); + data[3] = (byte)markerLength; + data[4] = (byte)scanCount; for (int i = 0; i < scanCount; ++i) { @@ -327,12 +327,12 @@ private static bool EncodeDHT(JpegData jpg, JpegSerializationState state) state.OutputQueue.Add([(byte)(markerLength + 2)]); Span data = CollectionsMarshal.AsSpan(state.OutputQueue[^1]); - int pos = 0; + int pos = 4; - data[pos++] = 0xff; - data[pos++] = 0xc4; - data[pos++] = (byte)(markerLength >> 8); - data[pos++] = (byte)markerLength; + data[0] = 0xff; + data[1] = 0xc4; + data[2] = (byte)(markerLength >> 8); + data[3] = (byte)markerLength; while (true) { @@ -428,12 +428,12 @@ private static bool EncodeDQT(JpegData jpg, JpegSerializationState state) state.OutputQueue.Add([(byte)(markerLength + 2)]); Span data = CollectionsMarshal.AsSpan(state.OutputQueue[^1]); - int pos = 0; + int pos = 4; - data[pos++] = 0xff; - data[pos++] = 0xdb; - data[pos++] = (byte)(markerLength >> 8); - data[pos++] = (byte)markerLength; + data[0] = 0xff; + data[1] = 0xdb; + data[2] = (byte)(markerLength >> 8); + data[3] = (byte)markerLength; while (true) { From b058eba8d6186b2ff73061a499813e23a59b2449 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 9 Sep 2026 00:49:57 +0400 Subject: [PATCH 163/328] Add context map decoder --- .../Decoder/JxlContextMapDecoder.cs | 105 +++++++++++++++++- 1 file changed, 103 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlContextMapDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlContextMapDecoder.cs index 78b989910a..9695384b48 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlContextMapDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlContextMapDecoder.cs @@ -1,8 +1,109 @@ -using System; +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics.Tensors; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; -public class JxlContextMapDecoder +internal static class JxlContextMapDecoder { + private const int MaxClusters = 256; + + public static void VerifyContextMap(List contextMap, int numHTrees) + { + Span haveHTree = stackalloc bool[numHTrees]; + + int numFound = 0; + + foreach (byte htree in contextMap) + { + if (htree >= numHTrees) + { + throw new InvalidOperationException("Invalid histogram index in context map"); + } + + if (!haveHTree[htree]) + { + haveHTree[htree] = true; + numFound++; + } + } + + if (numFound != numHTrees) + { + throw new InvalidOperationException("Incomplete context map"); + } + } + + public static void DecodeContextMap(Configuration configuration, List contextMap, ref int numHTrees, JxlBitReader input) + { + // True - simple context map, without any ANS entropy coding, + // false - the context map uses entropy coding and LZ77. + bool isSimpleContextMap = input.ReadBoolean(); + + Span cmSpan = CollectionsMarshal.AsSpan(contextMap); + + if (isSimpleContextMap) + { + uint bitsPerEntry = input.ReadBits32(2); + + if (bitsPerEntry != 0) + { + for (int i = 0; i < cmSpan.Length; i++) + { + cmSpan[i] = (byte)input.ReadBits32(bitsPerEntry); + } + } + else + { + // No context map at all + cmSpan.Clear(); + } + } + else + { + bool useMtf = input.ReadBoolean(); // use Move to Front transform? + JxlAnsCode ansCode = new(); + List sinkContextMap = []; + + if (!JxlAnsReader.DecodeHistograms(configuration, input, 1, ansCode, sinkContextMap, contextMap.Count <= 2)) + { + throw new InvalidOperationException("Context map histogram couldn't be decoded"); + } + + JxlAnsSymbolReader ans = JxlAnsSymbolReader.Create(ansCode, input); + + int i = 0; + int maxSymbols = 0; + + while (i < cmSpan.Length) + { + uint sym = ans.ReadHybridUInt(true, 0, input, sinkContextMap); + maxSymbols = sym > maxSymbols ? (int)sym : maxSymbols; + cmSpan[i] = (byte)sym; + i++; + } + + if (maxSymbols >= MaxClusters) + { + throw new InvalidOperationException("Invalid cluster ID"); + } + + if (!ans.CheckAnsFinalState()) + { + throw new InvalidOperationException("Invalid context map"); + } + + if (useMtf) + { + JxlInverseMtf.InverseMoveToFrontTransform(cmSpan); + } + } + numHTrees = TensorPrimitives.Max((ReadOnlySpan)cmSpan) + 1; + VerifyContextMap(contextMap, numHTrees); + } } From f872a81117a435a3b2224379f88fe728a67c1944 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 9 Sep 2026 21:29:30 +0400 Subject: [PATCH 164/328] Remove SIMD vectors from FJXL, add some ANS symbol reader work --- .../Decoder/Ans/JxlAnsSymbolReader.cs | 5 + .../Encoder/JxlFastLosslessEncoder.cs | 254 ------------------ 2 files changed, 5 insertions(+), 254 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsSymbolReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsSymbolReader.cs index 115b4fad68..2de97c74f7 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsSymbolReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsSymbolReader.cs @@ -27,4 +27,9 @@ private sealed class Checkpoint private JxlHuffmanDecodingData huffmanData; private bool usePrefixCode; private uint state = AnsSignature << 16u; + private List configs = []; + private int logAlphaSize; + private int logEntrySize; + private int entrySizeMinus1; + } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs index bea22c76a3..5a4c57bdbf 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs @@ -583,260 +583,6 @@ private static void EncodeHybridUintLz77(int value, out int token, out int nBits } } - /// - /// SIMD Mask32 - /// - private struct Mask32 - { - /// - /// Actual mask. - /// - public ushort Mask; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly uint CountPrefix() => CtzNonZero(~(uint)this.Mask); - } - - /// - /// Wrapper over a 32-bit integer vector. - /// - /// Underlying vector. - private struct SimdVec32(Vector vector) - { - /// - /// The actual vector for this simd vector. - /// - public Vector Vec = vector; - - /// - /// Adds both vectors. - /// - /// First vector - /// Second vector - /// a + b - public static SimdVec32 operator +(SimdVec32 a, SimdVec32 b) => new(a.Vec + b.Vec); - - /// - /// Subtracts both vectors. - /// - /// First vector - /// Second vector - /// a - b - public static SimdVec32 operator -(SimdVec32 a, SimdVec32 b) => new(a.Vec - b.Vec); - - /// - /// XORs both vectors. - /// - /// First vector - /// Second vector - /// a ^ b - public static SimdVec32 operator ^(SimdVec32 a, SimdVec32 b) => new(a.Vec ^ b.Vec); - - /// - /// Sets bits to all 1 if vector items are equal, otherwise to all 0. - /// For example, if vector a is {5, 2, 4, 1} and vector b is {7, 3, 4, 5} - /// the result is {0, 0, 0xFFFFFFFF, 0}. - /// - /// First vector - /// Second vector - /// a == b - public static SimdVec32 operator ==(SimdVec32 a, SimdVec32 b) => new(Vector.Equals(a.Vec, b.Vec)); - - // We don't use this. It's to remove an error where == requires !=. - public static SimdVec32 operator !=(SimdVec32 a, SimdVec32 b) => new(Vector.Equals(a.Vec, b.Vec)); - - /// - /// Sets bits to all 1 if vector items are larger, otherwise to all 0. - /// For example, if vector a is {5, 2, 4, 1} and vector b is {3, 3, 4, 5} - /// the result is {0xFFFFFFFF, 0, 0, 0}. - /// - /// First vector - /// Second vector - /// a > b - public static SimdVec32 operator >(SimdVec32 a, SimdVec32 b) => new(Vector.GreaterThan(a.Vec, b.Vec)); - - /// - /// Sets bits to all 1 if vector items are lower, otherwise to all 0. - /// For example, if vector a is {5, 2, 4, 1} and vector b is {3, 3, 4, 5} - /// the result is {0, 0xFFFFFFFF, 0, 0xFFFFFFFF}. - /// - /// First vector - /// Second vector - /// a < b - public static SimdVec32 operator <(SimdVec32 a, SimdVec32 b) => new(Vector.LessThan(a.Vec, b.Vec)); - - /// - /// Converts this vector to a mask. - /// - /// - /// Mask where bits are 1 if the item - /// at the index is set to all 1, otherwise 0. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly Mask32 ToMask() - { - int mask = 0; - - for (int i = 0; i < 16 && i < Vector.Count; i++) - { - if (this.Vec[i] == uint.MaxValue) - { - mask |= 1 << i; - } - } - - return new() { Mask = (ushort)mask }; - } - - public static SimdVec32 Load(Span data) => new(new Vector(data)); - - public static SimdVec32 Value(uint value) => new(new Vector(value)); - - public readonly SimdVec32 ValueToToken() => new(new Vector(32u) - GetLzcnt(this.Vec)); - - public readonly SimdVec32 SaturateSubtract(SimdVec32 toSubtract) => new(Vector.Max(this.Vec, toSubtract.Vec) - toSubtract.Vec); - - public readonly SimdVec32 Pow2() => new(Vector.ShiftLeft(Vector.One, unchecked((int)this.Vec[0]))); - - public readonly void Store(Span data) => this.Vec.CopyTo(data); - - // We don't use this. - public override readonly bool Equals(object? obj) => false; - - // We don't use this. - public override readonly int GetHashCode() => this.Vec.GetHashCode(); - } - - /// - /// SIMD Mask16 - /// - private struct Mask16 - { - /// - /// Actual mask. - /// - public uint Mask; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly uint CountPrefix() => CtzNonZero(~this.Mask); - } - - /// - /// Wrapper over a 16-bit integer vector. - /// - /// Underlying vector. - private struct SimdVec16(Vector vector) - { - /// - /// The actual vector for this simd vector. - /// - public Vector Vec = vector; - - /// - /// Adds both vectors. - /// - /// First vector - /// Second vector - /// a + b - public static SimdVec16 operator +(SimdVec16 a, SimdVec16 b) => new(a.Vec + b.Vec); - - /// - /// Subtracts both vectors. - /// - /// First vector - /// Second vector - /// a - b - public static SimdVec16 operator -(SimdVec16 a, SimdVec16 b) => new(a.Vec - b.Vec); - - /// - /// XORs both vectors. - /// - /// First vector - /// Second vector - /// a ^ b - public static SimdVec16 operator ^(SimdVec16 a, SimdVec16 b) => new(a.Vec ^ b.Vec); - - /// - /// Sets bits to all 1 if vector items are equal, otherwise to all 0. - /// For example, if vector a is {5, 2, 4, 1} and vector b is {7, 3, 4, 5} - /// the result is {0, 0, 0xFFFF, 0}. - /// - /// First vector - /// Second vector - /// a == b - public static SimdVec16 operator ==(SimdVec16 a, SimdVec16 b) => new(Vector.Equals(a.Vec, b.Vec)); - - // We don't use this. It's to remove an error where == requires !=. - public static SimdVec16 operator !=(SimdVec16 a, SimdVec16 b) => new(Vector.Equals(a.Vec, b.Vec)); - - /// - /// Sets bits to all 1 if vector items are larger, otherwise to all 0. - /// For example, if vector a is {5, 2, 4, 1} and vector b is {3, 3, 4, 5} - /// the result is {0xFFFF, 0, 0, 0}. - /// - /// First vector - /// Second vector - /// a > b - public static SimdVec16 operator >(SimdVec16 a, SimdVec16 b) => new(Vector.GreaterThan(a.Vec, b.Vec)); - - /// - /// Sets bits to all 1 if vector items are lower, otherwise to all 0. - /// For example, if vector a is {5, 2, 4, 1} and vector b is {3, 3, 4, 5} - /// the result is {0, 0xFFFF, 0, 0xFFFF}. - /// - /// First vector - /// Second vector - /// a < b - public static SimdVec16 operator <(SimdVec16 a, SimdVec16 b) => new(Vector.LessThan(a.Vec, b.Vec)); - - /// - /// Converts this vector to a mask. - /// - /// - /// Mask where bits are 1 if the item - /// at the index is set to all 1, otherwise 0. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly Mask16 ToMask() - { - uint mask = 0; - - for (int i = 0; i < 32 && i < Vector.Count; i++) - { - if (this.Vec[i] == ushort.MaxValue) - { - mask |= 1u << i; - } - } - - return new() { Mask = mask }; - } - - public static SimdVec16 FromTwo32(SimdVec32 lo, SimdVec32 hi) - { - Vector narrow = Vector.Narrow(lo.Vec, hi.Vec); - return new(narrow); - } - - public static SimdVec16 Load(Span data) => new(new Vector(data)); - - public static SimdVec16 Value(ushort value) => new(new Vector(value)); - - public readonly SimdVec16 ValueToToken() => new(new Vector(16) - GetLzcnt(this.Vec)); - - public readonly SimdVec16 SaturateSubtract(SimdVec16 toSubtract) => new(Vector.Max(this.Vec, toSubtract.Vec) - toSubtract.Vec); - - public readonly SimdVec16 Pow2() => new(Vector.ShiftLeft(Vector.One, unchecked(this.Vec[0]))); - - public readonly void Store(Span data) => this.Vec.CopyTo(data); - - // We don't use this. - public override readonly bool Equals(object? obj) => false; - - // We don't use this. - public override readonly int GetHashCode() => this.Vec.GetHashCode(); - } - /// /// Pair of two vectors. /// From 4d49215f89cd6f23bb7676aafe0086503803b7ce Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 9 Sep 2026 21:59:28 +0400 Subject: [PATCH 165/328] Add SimdUtils.Lzcnt --- src/ImageSharp/Common/Helpers/SimdUtils.cs | 35 ++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/ImageSharp/Common/Helpers/SimdUtils.cs b/src/ImageSharp/Common/Helpers/SimdUtils.cs index 3e0abcd6ef..fb6f2a74d6 100644 --- a/src/ImageSharp/Common/Helpers/SimdUtils.cs +++ b/src/ImageSharp/Common/Helpers/SimdUtils.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Runtime.Intrinsics; using System.Runtime.Intrinsics.Arm; using System.Runtime.Intrinsics.X86; @@ -108,6 +109,40 @@ internal static Vector ReciprocalEstimate(this Vector v) } } + /// + /// Finds the leading zero count on each item of the vector. + /// + /// The vector to compute leading zero count for. + /// Input vector with leading zero count on each element. + internal static Vector Lzcnt(Vector vector) + { + if (Avx512CD.IsSupported) + { + Vector512 v512 = vector.AsVector512(); + Vector512 lzcnt = Avx512CD.LeadingZeroCount(v512); + return lzcnt.AsVector(); + } + else if (AdvSimd.IsSupported) + { + Vector128 v128 = vector.AsVector128(); + Vector128 lzcnt = AdvSimd.LeadingZeroCount(v128); + return lzcnt.AsVector(); + } + else + { + Span data = stackalloc int[Vector.Count]; + ref int dataPtr = ref MemoryMarshal.GetReference(data); + vector.StoreUnsafe(ref dataPtr); + + for (int i = 0; i < data.Length; i++) + { + Unsafe.Add(ref dataPtr, i) = BitOperations.LeadingZeroCount((nuint)Unsafe.Add(ref dataPtr, i)); + } + + return Vector.LoadUnsafe(ref dataPtr); + } + } + [Conditional("DEBUG")] private static void DebugVerifySpanInput(ReadOnlySpan source, ReadOnlySpan dest, int shouldBeDivisibleBy) { From 1119f88407a26043aba13504695cfc38691e0043 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 10 Sep 2026 05:03:45 +0400 Subject: [PATCH 166/328] Add some FJXL work --- src/ImageSharp/Common/Helpers/SimdUtils.cs | 34 +- .../Encoder/FastLossless/FjxlBitWriter.cs | 88 ++ .../Encoder/FastLossless/FjxlPrefixCode.cs | 461 +++++++ .../FastLossless/JxlFastLosslessEncoder.cs | 645 ++++++++++ .../FjxlChunkEncoderExactly14Bits.cs | 61 + .../FjxlChunkEncoderFrom9To13Bits.cs | 70 ++ .../FjxlChunkEncoderMoreThan14Bits.cs | 77 ++ .../FjxlChunkEncoderUpTo8Bits.cs | 67 + .../Encoder/FastLossless/Simd/FjxlHuffman.cs | 66 + .../FastLossless/Simd/FjxlSimdUtils.cs | 14 + .../FastLossless/Simd/FjxlStoreSimd.cs | 101 ++ .../FastLossless/Simd/FjxlTokenization.cs | 45 + .../Encoder/JxlFastLosslessEncoder.cs | 1114 ----------------- 13 files changed, 1727 insertions(+), 1116 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/FjxlBitWriter.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/FjxlPrefixCode.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/JxlFastLosslessEncoder.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderExactly14Bits.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderFrom9To13Bits.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderMoreThan14Bits.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderUpTo8Bits.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlHuffman.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlSimdUtils.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlStoreSimd.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlTokenization.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs diff --git a/src/ImageSharp/Common/Helpers/SimdUtils.cs b/src/ImageSharp/Common/Helpers/SimdUtils.cs index fb6f2a74d6..7e78581c33 100644 --- a/src/ImageSharp/Common/Helpers/SimdUtils.cs +++ b/src/ImageSharp/Common/Helpers/SimdUtils.cs @@ -116,13 +116,13 @@ internal static Vector ReciprocalEstimate(this Vector v) /// Input vector with leading zero count on each element. internal static Vector Lzcnt(Vector vector) { - if (Avx512CD.IsSupported) + if (Avx512CD.IsSupported && Vector.Count == 16) { Vector512 v512 = vector.AsVector512(); Vector512 lzcnt = Avx512CD.LeadingZeroCount(v512); return lzcnt.AsVector(); } - else if (AdvSimd.IsSupported) + else if (AdvSimd.IsSupported && Vector.Count == 4) { Vector128 v128 = vector.AsVector128(); Vector128 lzcnt = AdvSimd.LeadingZeroCount(v128); @@ -143,6 +143,36 @@ internal static Vector Lzcnt(Vector vector) } } + /// + /// Raises 2 to the power of each item in the vector. + /// + /// Input vector. + /// 2^x for each item in the vector. + internal static Vector Pow2(Vector vector) + { + if (Avx512F.IsSupported && Vector.Count == 16) + { + return Avx512F.ShiftLeftLogicalVariable(Vector512.One, vector.AsVector512().AsUInt32()).AsVector(); + } + else if (Avx2.IsSupported && Vector.Count == 8) + { + return Avx2.ShiftLeftLogicalVariable(Vector256.One, vector.AsVector256().AsUInt32()).AsVector(); + } + else + { + Span data = stackalloc int[Vector.Count]; + ref int dataPtr = ref MemoryMarshal.GetReference(data); + vector.StoreUnsafe(ref dataPtr); + + for (int i = 0; i < data.Length; i++) + { + Unsafe.Add(ref dataPtr, i) = 1 << Unsafe.Add(ref dataPtr, i); + } + + return Vector.LoadUnsafe(ref dataPtr); + } + } + [Conditional("DEBUG")] private static void DebugVerifySpanInput(ReadOnlySpan source, ReadOnlySpan dest, int shouldBeDivisibleBy) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/FjxlBitWriter.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/FjxlBitWriter.cs new file mode 100644 index 0000000000..54277d41a9 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/FjxlBitWriter.cs @@ -0,0 +1,88 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless; + +/// +/// Simple MSB-first bit-stream writer implementation built on top +/// of a stream. +/// +/// Output bytes are written here. +internal sealed class FjxlBitWriter(Stream stream) : IDisposable +{ + /// + /// Temporary cache used to store pending written bits + /// before they're written to the output stream. + /// + private ulong buffer; + + /// + /// Gets the total number of bytes written to the output buffer so far. + /// + public long BytesWritten { get; private set; } + + /// + /// Gets the number of bits actively in the bit cache. + /// This is used to track how many bits were written into + /// the cache prior to sending the cache to the stream. + /// + public int BitsInBuffer { get; private set; } + + /// + /// Writes the specified bits in the Most Significant Byte (MSB) + /// order. + /// + /// Represents the number of bits to write to the bit-stream. + /// Represents the value to write to the bit-stream. + public void Write(int count, ulong bits) + { + DebugGuard.MustBeBetweenOrEqualTo(count, 0, 56, nameof(count)); + + if (count < 64) + { + bits &= (1UL << count) - 1; + } + + this.buffer |= bits << this.BitsInBuffer; + this.BitsInBuffer += count; + + this.FlushBytes(); + } + + /// + /// Internal method used to flush bytes from the cache + /// () into the output stream. + /// + private void FlushBytes() + { + int bytes = this.BitsInBuffer / 8; + + for (int i = 0; i < bytes; i++) + { + stream.WriteByte((byte)this.buffer); + this.BytesWritten++; + this.buffer >>= 8; + } + + this.BitsInBuffer -= bytes * 8; + } + + /// + /// Used by the dispose method to flush the remaining bits + /// that are not byte-aligned. F.e. if we dispose this reader + /// and we have 5 bits left, those final 5 bits are set to all 0 + /// and the byte is written to the stream. + /// + public void ZeroPadToByte() + { + if (this.BitsInBuffer != 0) + { + this.Write(8 - this.BitsInBuffer, 0); + } + } + + /// + /// Flushes out the final bytes. + /// + public void Dispose() => this.ZeroPadToByte(); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/FjxlPrefixCode.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/FjxlPrefixCode.cs new file mode 100644 index 0000000000..4663ed74ee --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/FjxlPrefixCode.cs @@ -0,0 +1,461 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless; + +/// +/// The prefix code is used for encoding LZ77-compressed coefficients. +/// +internal sealed class FjxlPrefixCode +{ +#pragma warning disable SA1401 // Fields should be private + + /// + /// Maximum number of raw symbols for prefix coding. + /// + private const int MaxNumSymbols = JxlFastLosslessEncoder.NumRawSymbols + 1 < JxlFastLosslessEncoder.NumLz77 ? JxlFastLosslessEncoder.NumLz77 : JxlFastLosslessEncoder.NumRawSymbols + 1; + + /// + /// Gets or sets the Huffman raw bit lengths. + /// + public InlineArray19 RawLengths; + + /// + /// Gets or sets the Huffman raw code values. + /// + public InlineArray19 RawCodes; + + /// + /// Gets or sets the Huffman LZ77 bit lengths. + /// + public InlineArray33 Lz77Lengths; + + /// + /// Gets or sets the Huffman LZ77 code values. + /// + public InlineArray33 Lz77Codes; + + /// + /// Gets or sets the Huffman LZ77 cache code values. + /// + public InlineArray32 Lz77CacheBits; + + /// + /// Gets or sets the Huffman LZ77 cache bit lengths. + /// + public InlineArray32 Lz77CacheLengths; + + public FjxlPrefixCode(Span rawCounts, Span lz77Counts) + { + Span level1Counts = stackalloc ulong[JxlFastLosslessEncoder.NumRawSymbols + 1]; + rawCounts[..JxlFastLosslessEncoder.NumRawSymbols].CopyTo(level1Counts); + + this.RawCount = JxlFastLosslessEncoder.NumRawSymbols; + + while (this.RawCount > 0 && level1Counts[this.RawCount - 1] == 0) + { + this.RawCount--; + } + + level1Counts[this.RawCount] = 0; + + for (int i = 0; i < JxlFastLosslessEncoder.NumLz77; i++) + { + level1Counts[this.RawCount] += lz77Counts[i]; + } + + Span level1Lengths = stackalloc byte[JxlFastLosslessEncoder.NumRawSymbols + 1]; + level1Lengths.Clear(); + + ComputeCodeLengths(level1Counts, this.RawCount + 1, JxlFastLosslessEncoder.MinimumRawLength, JxlFastLosslessEncoder.MaximumRawLength, level1Lengths); + + Span level2Lengths = stackalloc byte[JxlFastLosslessEncoder.NumLz77]; + Span minLengths = stackalloc byte[JxlFastLosslessEncoder.NumLz77]; + + level2Lengths.Clear(); + minLengths.Clear(); + + int l = 15 - level1Lengths[this.RawCount]; + Span maxLengths = stackalloc byte[JxlFastLosslessEncoder.NumLz77]; + maxLengths.Fill((byte)l); + + int numLz77 = JxlFastLosslessEncoder.NumLz77; + while (numLz77 > 0 && lz77Counts[numLz77 - 1] == 0) + { + numLz77--; + } + + ComputeCodeLengths(lz77Counts, numLz77, minLengths, maxLengths, level2Lengths); + + level1Lengths[..this.RawCount].CopyTo(this.RawLengths); + + for (int i = 0; i < numLz77; i++) + { + this.Lz77Lengths[i] = (byte)(level2Lengths[i] != 0 ? level1Lengths[this.RawCount] + level2Lengths[i] : 0); + } + + ComputeCanonicalCode(this.RawLengths, this.RawCodes, this.Lz77Lengths, this.Lz77Codes); + + // Prepare the LZ77 cache + for (int count = 0; count < JxlFastLosslessEncoder.Lz77CacheSize; count++) + { + EncodeHybridUintLz77(count, out int token, out int nbits, out int bits); + this.Lz77CacheLengths[count] = (byte)(this.Lz77Lengths[token] + nbits + this.RawLengths[0]); + this.Lz77CacheBits[count] = + (ulong)((((bits << this.Lz77Lengths[token]) | this.Lz77Codes[token]) << this.RawLengths[0]) | + this.RawLengths[0]); + } + } + + /// + /// Gets a lookup used to reverse integers bit-wise. + /// + private static ReadOnlySpan ReverseNibbleLookup => + [ + 0b0000, 0b1000, 0b0100, 0b1100, 0b0010, 0b1010, 0b0110, 0b1110, + 0b0001, 0b1001, 0b0101, 0b1101, 0b0011, 0b1011, 0b0111, 0b1111, + ]; + +#pragma warning restore SA1401 // Fields should be private + + /// + /// Gets or sets the number of raw codes. + /// + public int RawCount { get; set; } + + /// + /// Reverses the integer bit-wise. + /// + /// Number of bits for the integer. + /// Actual bits to reverse. + /// + /// Input integer but reversed. F.e. 10010 becomes 01001. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ushort BitReverse(int nbits, ushort bits) + { + unchecked + { + ushort rev16 = (ushort)((ReverseNibbleLookup[bits & 0xF] << 12) | + (ReverseNibbleLookup[(bits >> 4) & 0xF] << 8) | + (ReverseNibbleLookup[(bits >> 8) & 0xF] << 4) | + ReverseNibbleLookup[bits >> 12]); + return (ushort)(rev16 >> (16 - nbits)); + } + } + + private static void ComputeCanonicalCode(Span firstChunkLengths, Span firstChunkCodes, Span secondChunkLengths, Span secondChunkCodes) + { + const int maxCodeLength = 15; + + Span codeLengthCounts = stackalloc byte[maxCodeLength + 1]; + codeLengthCounts.Clear(); + + for (int i = 0; i < firstChunkCodes.Length; i++) + { + codeLengthCounts[firstChunkLengths[i]]++; + + if (firstChunkLengths[i] > 8) + { + throw new InvalidOperationException("First chunk length is too large"); + } + + if (firstChunkLengths[i] <= 0) + { + throw new InvalidOperationException("First chunk length cannot be <= 0"); + } + } + + for (int i = 0; i < secondChunkCodes.Length; i++) + { + codeLengthCounts[secondChunkLengths[i]]++; + + if (secondChunkLengths[i] > maxCodeLength) + { + throw new InvalidOperationException("Second chunk length is too large"); + } + } + + Span nextCode = stackalloc ushort[maxCodeLength + 1]; + nextCode.Clear(); + + ushort code = 0; + + for (int i = 1; i < maxCodeLength + 1; i++) + { + code = unchecked((ushort)((code + codeLengthCounts[i - 1]) << 1)); + nextCode[i] = code; + } + + unchecked + { + for (int i = 0; i < firstChunkCodes.Length; i++) + { + firstChunkCodes[i] = (byte)BitReverse(firstChunkLengths[i], nextCode[firstChunkLengths[i]]++); + } + + for (int i = 0; i < secondChunkCodes.Length; i++) + { + secondChunkCodes[i] = (byte)BitReverse(secondChunkLengths[i], nextCode[secondChunkLengths[i]]++); + } + } + } + + private static void ComputeCodeLengthsNonZeroImpl( + Span freqs, + int n, + int precision, + T infty, + Span minLimit, + Span maxLimit, + Span nbits) + where T : unmanaged, INumber + { + DebugGuard.MustBeLessThan(precision, 15, nameof(precision)); + DebugGuard.MustBeLessThanOrEqualTo(n, MaxNumSymbols, nameof(n)); + + int scale = 1 << precision; + int width = scale + 1; + + Span dynp = stackalloc T[width * (n + 1)]; + dynp.Fill(infty); + dynp[0] = T.Zero; + + for (int sym = 0; sym < n; sym++) + { + for (int bits = minLimit[sym]; bits <= maxLimit[sym]; bits++) + { + int offsetDelta = 1 << (precision - bits); + T cost = T.CreateChecked(freqs[sym]) * T.CreateChecked(bits); + + for (int off = 0; off + offsetDelta <= scale; off++) + { + int current = (sym * width) + off; + int next = ((sym + 1) * width) + off + offsetDelta; + + dynp[next] = T.Min(dynp[current] + cost, dynp[next]); + } + } + } + + int offFinal = scale; + + for (int sym = n - 1; sym >= 0; sym--) + { + if (offFinal <= 0) + { + throw new InvalidOperationException("Offset should be greater than zero"); + } + + for (int bits = minLimit[sym]; bits <= maxLimit[sym]; bits++) + { + int offsetDelta = 1 << (precision - bits); + + if (offsetDelta <= offFinal) + { + int current = (sym * width) + offFinal; + int previous = (sym * width) + offFinal - offsetDelta; + + T cost = T.CreateChecked(freqs[sym]) * T.CreateChecked(bits); + + if (dynp[current] == dynp[previous] + cost) + { + offFinal -= offsetDelta; + nbits[sym] = (byte)bits; + break; + } + } + } + } + } + + private static void ComputeCodeLengthsNonZero(Span freqs, int n, Span minLimit, Span maxLimit, Span nbits) + { + int precision = 0; + int shortestLength = 255; + ulong frequencySum = 0; + + for (int i = 0; i < n; i++) + { + frequencySum += freqs[i]; + + if (minLimit[i] < 1) + { + minLimit[i] = 1; + } + + precision = Math.Max(maxLimit[i], precision); + shortestLength = Math.Min(minLimit[i], shortestLength); + } + + precision -= shortestLength - 1; + ulong infinity = frequencySum * (ulong)precision; + + if (infinity < uint.MaxValue / 2) + { + ComputeCodeLengthsNonZeroImpl(freqs, n, precision, (uint)infinity, minLimit, maxLimit, nbits); + } + else + { + ComputeCodeLengthsNonZeroImpl(freqs, n, precision, infinity, minLimit, maxLimit, nbits); + } + } + + private static void ComputeCodeLengths(Span freqs, int n, ReadOnlySpan minLimitIn, ReadOnlySpan maxLimitIn, Span nbits) + { + DebugGuard.MustBeLessThanOrEqualTo(n, MaxNumSymbols, nameof(n)); + + Span compactFreqs = stackalloc ulong[MaxNumSymbols]; + Span minLimit = stackalloc byte[MaxNumSymbols]; + Span maxLimit = stackalloc byte[MaxNumSymbols]; + + int ni = 0; + for (int i = 0; i < n; i++) + { + if (freqs[i] != 0) + { + compactFreqs[ni] = freqs[i]; + minLimit[ni] = minLimitIn[i]; + maxLimit[ni] = maxLimitIn[i]; + ni++; + } + } + + compactFreqs[ni..].Clear(); + minLimit[ni..].Clear(); + maxLimit[ni..].Clear(); + + Span numBits = stackalloc byte[MaxNumSymbols]; + numBits.Clear(); + + ComputeCodeLengthsNonZero(compactFreqs, ni, minLimit, maxLimit, numBits); + + ni = 0; + + for (int i = 0; i < n; i++) + { + nbits[i] = 0; + if (freqs[i] != 0) + { + nbits[i] = numBits[ni++]; + } + } + } + + /// + /// Writes this LZ77 prefix code into the bit-stream. + /// + /// The bit-stream to write the prefix code into. + public void Write(FjxlBitWriter writer) + { + Span codeLengthCounts = stackalloc ulong[32].Slice(0, 18); + codeLengthCounts.Clear(); + codeLengthCounts[17] = 3 + (2 * (JxlFastLosslessEncoder.NumLz77 - 1)); + + for (int i = 0; i < 19; i++) + { + byte rawLength = this.RawLengths[i]; + + codeLengthCounts[rawLength]++; + } + + for (int i = 0; i < 33; i++) + { + byte lz77Length = this.Lz77Lengths[i]; + + codeLengthCounts[lz77Length]++; + } + + // Lengths for representing the code length + Span codeLengthLengths = stackalloc byte[32].Slice(0, 18); + Span codeLengthLengthsMinimum = stackalloc byte[32].Slice(0, 18); + Span codeLengthLengthsMaximum = stackalloc byte[32].Slice(0, 18); + + codeLengthLengths.Clear(); + codeLengthLengthsMinimum.Clear(); + codeLengthLengthsMaximum.Fill(5); + + ComputeCodeLengths(codeLengthCounts, 18, codeLengthLengthsMinimum, codeLengthLengthsMaximum, codeLengthLengths); + + writer.Write(2, 0b00); // HSKIP = 0 (Don't skip code lengths) + + // As per Brotli RFC + Span codeLengthOrder = [1, 2, 3, 4, 0, 5, 17, 6, 16, + 7, 8, 9, 10, 11, 12, 13, 14, 15]; + + // Lengths & codes for representing lengths of code lengths + Span codeLengthLengthLengths = [2, 4, 3, 2, 2, 4]; + Span codeLengthLengthCodes = [0, 7, 3, 2, 1, 15]; + + // Maximum number of code lengths + int numCodeLengths = 18; + while (codeLengthLengths[codeLengthOrder[numCodeLengths - 1]] == 0) + { + numCodeLengths--; + } + + // Max bits written in this loop: 18 * 4 = 72 + for (int i = 0; i < numCodeLengths; i++) + { + int symbol = codeLengthLengths[codeLengthOrder[i]]; + writer.Write(codeLengthLengthLengths[symbol], codeLengthLengthCodes[symbol]); + } + + Span codeLengthBits = stackalloc ushort[32].Slice(0, 18); + codeLengthBits.Clear(); + ComputeCanonicalCode([], [], codeLengthLengths, codeLengthBits); + + for (int i = 0; i < 19; i++) + { + byte rawLength = this.RawLengths[i]; + + writer.Write(codeLengthLengths[rawLength], codeLengthBits[rawLength]); + } + + int numLz77 = JxlFastLosslessEncoder.NumLz77; + while (this.Lz77Lengths[numLz77 - 1] == 0) + { + numLz77--; + } + + // Max bits in this block: 24 + writer.Write(codeLengthLengths[17], codeLengthBits[17]); + writer.Write(3, 0b010); // 5 + writer.Write(codeLengthLengths[17], codeLengthBits[17]); + writer.Write(3, 0b000); // (5 - 2) * 8 + 3 = 27 + writer.Write(codeLengthLengths[17], codeLengthBits[17]); + writer.Write(3, 0b010); // (27 - 2) * 8 + 5 = 205 + + // Encode LZ77 symbols with values 224 + i. + // Max. bits in this loop: 33 * 5 = 165 + for (int i = 0; i < numLz77; i++) + { + writer.Write(codeLengthLengths[this.Lz77Lengths[i]], codeLengthBits[this.Lz77Lengths[i]]); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void EncodeHybridUintLz77(int value, out int token, out int nBits, out int bits) + { + unchecked + { + int n = (int)JxlFastLosslessEncoder.FloorLog2((uint)value); + + if (value < 16) + { + token = value; + nBits = 0; + bits = 0; + } + else + { + token = 16 + n - 4; + nBits = n; + bits = value - (1 << n); + } + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/JxlFastLosslessEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/JxlFastLosslessEncoder.cs new file mode 100644 index 0000000000..308e604277 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/JxlFastLosslessEncoder.cs @@ -0,0 +1,645 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd; + +// Suppress IDE0057. This is so we can stack-allocate +// a powers of 2 and then slice it to the appropriate +// length (which produces better code). +// +// Without this suppression, the analyzer produces a warning, +// recommending changing this: +// stackalloc ulong[32].Slice(0, 18) +// to: +// (stackalloc ulong[32])[..18] +// +// But then the analyzer produces a new warning, recommending +// to remove the paranthesis, changing this: +// (stackalloc ulong[32])[..18] +// to: +// stackalloc ulong[32][..18] +// +// which is invalid C# syntax. +#pragma warning disable IDE0057 // Use range operator + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless; + +/// +/// Extreme performance JPEG XL encoder which provides minimal lossless compression. +/// It also uses minimal dependencies. +/// +internal sealed class JxlFastLosslessEncoder +{ + /// + /// Specifies maximum number of bytes a frame header may use. + /// + public const int MaxFrameHeaderSize = 5; + + public const int NumRawSymbols = 19; + + public const int NumLz77 = 33; + + /// + /// Cache/dictionary size for LZ77 + /// + public const int Lz77CacheSize = 32; + + public const int Lz77Offset = 224; + + public const int Lz77MinLength = 7; + + private static readonly int LogChunkSize = + JxlSimdTarget.Using512BitVectors + ? 5 + : JxlSimdTarget.Using256BitVectors + ? 4 + : 3; + + /// + /// Input frame data is stored here. + /// + private readonly FjxlFrameInputSource input; + + /// + /// Image width of the input image. + /// + private readonly int width; + + /// + /// Image height of the input image. + /// + private readonly int height; + + /// + /// Image width in groups. + /// + private readonly int numGroupsX; + + /// + /// Image height in groups. + /// + private readonly int numGroupsY; + + /// + /// Image width in groups (DC). + /// + private readonly int numDcGroupsX; + + /// + /// Image height in groups (DC). + /// + private readonly int numDcGroupsY; + + /// + /// Number of channels. (f.e. RGBA is 4, YUV is 3) + /// + private readonly int channels; + + /// + /// Number of bits represented per pixel. (f.e. 8 means pixels + /// have a 0-255 range) + /// + /// + /// Higher bit depths can represent more colors. + /// + private readonly int bitDepth; + + /// + /// Should the output image be stored in big-endian order? + /// + private readonly bool isBigEndian; + + private readonly int effort; + + private readonly bool collided; + + /// + /// Prefix codes for LZ77. + /// + private InlineArray4 hcode; + + private readonly List lookup = []; + + /// + /// Bit writer to write the JPEG XL headers. + /// + private readonly FjxlBitWriter header; + + /// + /// Bit writers for writing JPEG XL groups. + /// + private readonly List> groupData = []; + + /// + /// Sizes for each group. + /// + private readonly List groupSizes = []; + + private int acGroupDataOffset; + + private int minDcGlobalSize; + + private int currentBitWriter; + + private int bitWriterBytePos; + + private int bitsInBuffer; + + private long bitBuffer; + + private bool processDone; + + /// + /// Abstracts access to a raster frame data required for encoding. + /// + internal abstract class FjxlFrameInputSource : IDisposable + { + /// + public abstract void Dispose(); + + /// + /// Returns a span that wraps over channel color data at the + /// specified rectangular position. + /// + /// Target type of the color data. + /// Left offset + /// Right offset + /// Selection width + /// Selection height + /// The actual offset of the row in row-major order is stored here. + /// + /// A wrapper over the color data of the channel at the specified + /// position. + /// + public abstract Span GetColorChannelData(int x, int y, int width, int height, out long rowOffset) + where T : unmanaged; + } + + /// + /// Gets minimum raw lengths for prefix coding. + /// + public static ReadOnlySpan MinimumRawLength => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + + /// + /// Gets maximum raw lengths for prefix coding. + /// + public static ReadOnlySpan MaximumRawLength => [7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 10]; + + /// + /// Gets a lookup used by the method + /// to translate a bucket into a base group size. + /// + private static ReadOnlySpan GroupSizeOffset => + [ + 0, + 1024, + 17408, + 4211712 + ]; + + /// + /// Gets a lookup to determine how many bits a TOC bucket uses. + /// + private static ReadOnlySpan TocBits => [12, 16, 24, 32]; + + /// + /// Approximates Floor(Log2(v)) using integers. + /// + /// Value to retrieve Floor(Log2(v)) of. + /// Floor of second logarithm of v, or 31 if v is equal to 0. + /// This method may use CPU intrinsics provided by the .NET Runtime (e.g. BMI1 on x86). + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint FloorLog2(uint v) => v == 0 ? 0 : 31u - (uint)BitOperations.LeadingZeroCount(v); + + /// + /// Approximates count trailing zeros of v using integers. + /// + /// Value to retrieve number of 0 bits after last 1 bit of. + /// After the least significant 1 bit, returns the number of 0 bits. E.g. 1000 1000 00 -> 5. + /// This method may use CPU intrinsics provided by the .NET Runtime (e.g. BMI1 on x86). + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint CtzNonZero(ulong v) => (uint)BitOperations.TrailingZeroCount(v); + + /// + /// Returns a TOC bucket based on the group size. + /// + /// Specified group size. + /// TOC bucket matching the appropriate group size. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int TocBucket(int groupSize) + { + int bucket = 0; + + while (bucket < 3 && groupSize >= GroupSizeOffset[bucket + 1]) + { + bucket++; + } + + return bucket; + } + + /// + /// Returns the total number of bits required to represent + /// all given group sizes in the TOC. + /// + /// Group sizes to calculate bit sizes of. + /// Accumulated number of bits required to represent each group size. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int TocSize(Span groupSizes) + { + int tocBits = 0; + + ref int unsafeRef = ref MemoryMarshal.GetReference(groupSizes); + + for (int i = 0; i < groupSizes.Length; i++) + { + // TODO: we can try using AVX2 gather intrinsics, + // especially because TocBits can absolutely fit + // in the L1 cache + int groupSize = Unsafe.Add(ref unsafeRef, i); + int bucketForGroupSize = TocBucket(groupSize); + int bitsUsedByBucket = TocBits[bucketForGroupSize]; + + tocBits += bitsUsedByBucket; + } + + return tocBits; + } + + /// + /// Returns the number of bytes for the frame header. + /// + /// Indicates presence of the alpha channel. + /// Indicates whether this is the final frame. + /// Frame header size in bytes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int FrameHeaderSize(bool containsAlpha, bool isLast) + { + // Original code (from libjxl): + // + // size_t nbits = 28 + (have_alpha ? 4 : 0) + (is_last ? 0 : 2); + // return (nbits + 7) / 8; + // + // In this implementation we just use constants to shave a few CPU cycles. + // The total amount of branches is reduced by one (for the !containsAlpha case), + // but we remove the arithmetic/shifting instructions. + unchecked + { + if (containsAlpha) + { + if (isLast) + { + return 5; // (34 + 7) / 8 + } + else + { + return 4; // (32 + 7) / 8 + } + } + else + { + return 4; // (30 + 7) / 8 AND (28 + 7) / 8 yield the same result + } + } + } + + private static long GetSectionSize(InlineArray4 groupData) + { + long size = 0; + + for (int j = 0; j < 4; j++) + { + FjxlBitWriter writer = groupData[j]; + + size += (writer.BytesWritten * 8) + writer.BitsInBuffer; + } + + return (size + 7) / 8; + } + + /// + /// Approximates number of bytes needed for the output image buffer. + /// + /// Bytes for the frame buffer. + private long GetOutputSize() + { + long totalSizeGroups = 0; + + Span> groups = CollectionsMarshal.AsSpan(this.groupData); + + for (int i = 0; i < groups.Length; i++) + { + InlineArray4 section = groups[i]; + + totalSizeGroups += GetSectionSize(section); + } + + return this.header.BytesWritten + totalSizeGroups; + } + + /// + /// Returns the maximum amount of bytes potentially required for the image buffer. + /// + /// Upper bound of bytes for frame buffer. + private long GetMaxRequiredOutput() => this.GetOutputSize() + 32; + + private void WriteHeader(bool addImageHeader, bool isLast) + { + FjxlBitWriter output = this.header; + bool haveAlpha = this.channels is 2 or 4; + + if (addImageHeader) + { + // File signature. This signature specifies + // a raw codestream. No container format here. + output.Write(16, 0x0AFF); + + // Handcrafted size header. + output.Write(1, 0); // Not small + + WriteSize(this.height); + output.Write(3, 0b000); // No special ratio + WriteSize(this.width); + + // Handcrafted image metadata + output.Write(1, 0); // all_default = 0 (don't assume values to be set to their defaults) + output.Write(1, 0); // extra_fields = 0 (extra fields are disabled and therefore not present) + output.Write(1, 0); // bit_depth.floating_point_sample = 0 (samples are integers) + + if (this.bitDepth == 8) + { + output.Write(2, 0b00); // bit_depth.bits_per_sample = 8 (predefined bit depth of 8 bits) + } + else if (this.bitDepth == 10) + { + output.Write(2, 0b01); // bit_depth.bits_per_sample = 10 (predefined bit depth of 10 bits) + } + else if (this.bitDepth == 12) + { + output.Write(2, 0b10); // bit_depth.bits_per_sample = 12 (predefined bit depth of 12 bits) + } + else + { + output.Write(2, 0b11); // Custom bit depth + output.Write(6, (ulong)this.bitDepth - 1); // bit depth minus 1 (so 0 becomes 1, 9 becomes 10, etc) + } + + if (this.bitDepth <= 14) + { + output.Write(1, 1); // 16-bit-buffer is sufficient + } + else + { + output.Write(1, 0); // 16-bit-buffer is NOT sufficient + } + + if (haveAlpha) + { + output.Write(2, 0b01); // Emit one extra channel (the alpha channel) + + if (this.bitDepth == 8) + { + output.Write(1, 1); // all_default = 1 (8-bit alpha is the default) + } + else + { + output.Write(1, 0); // all_default = 0 + output.Write(2, 0); // type = alpha + output.Write(1, 0); // samples are not floating point + + if (this.bitDepth == 10) + { + output.Write(2, 0b01); // bit_depth.bits_per_sample = 10 (predefined bit depth of 10 bits) + } + else if (this.bitDepth == 12) + { + output.Write(2, 0b10); // bit_depth.bits_per_sample = 12 (predefined bit depth of 12 bits) + } + else + { + output.Write(2, 0b11); // Custom bit depth + output.Write(6, (ulong)this.bitDepth - 1); // bit depth minus 1 (so 0 becomes 1, 9 becomes 10, etc) + } + + output.Write(2, 0); // dim_shift = 0 + output.Write(2, 0); // name_len = 0 + output.Write(1, 0); // alpha_associated = 0 + } + } + else + { + output.Write(2, 0b00); // 0 extra channels + } + + output.Write(1, 0); // not XYB + + if (this.channels > 2) + { + output.Write(1, 1); // color_encoding.all_default = 1 (sRGB) + } + else + { + output.Write(1, 0); // color_encoding.all_default = 0 + output.Write(1, 0); // color_encoding.want_icc = 0 + output.Write(2, 0b01); // Grayscale + output.Write(2, 0b01); // D65 + output.Write(1, 0); // No gamma transfer function + output.Write(2, 0b10); // transfer function: 2 + u(4) + output.Write(4, 11); // transfer function (specifies sRGB) + output.Write(2, 1); // relative rendering intent + } + + output.Write(2, 0b00); // No extensions + output.Write(1, 1); // all_default transform data + output.ZeroPadToByte(); // No ICC and no preview. Frame should start at byte boundary. + } + + // Handcrafted frame header + output.Write(1, 0); // all_default = 0 (non-default values) + output.Write(2, 0b00); // regular frame + output.Write(1, 1); // modular + output.Write(2, 0b00); // default flags + output.Write(1, 0); // not Y'Cb'Cr + output.Write(2, 0b00); // no upsampling + + if (haveAlpha) + { + output.Write(2, 0b00); // no alpha upsampling + } + + output.Write(2, 0b01); // default group size + output.Write(2, 0b00); // exactly one pass + output.Write(1, 0); // no custom size or origin + output.Write(2, 0b00); // Replace blending mode + + if (haveAlpha) + { + output.Write(2, 0b00); // Replace blending mode for alpha channel + } + + output.Write(2, 0b00); // a frame has no name + output.Write(1, 0); // loop filter is not all_default + output.Write(1, 0); // no Gaborish transform + output.Write(2, 0b00); // 0 EPF filters + output.Write(2, 0b00); // no LF extensions + output.Write(2, 0b00); // no FH extensions + + output.Write(1, 0); // no TOC permutation + output.ZeroPadToByte(); // TOC is byte aligned + + Span groupSizes = CollectionsMarshal.AsSpan(this.groupSizes); + + for (int i = 0; i < groupSizes.Length; i++) + { + int groupSize = groupSizes[i]; + + int bucket = TocBucket(groupSize); + output.Write(2, (ulong)bucket); + output.Write(TocBits[bucket] - 2, (ulong)(groupSize - GroupSizeOffset[bucket])); + } + + output.ZeroPadToByte(); // Groups are byte-aligned + + // Sizes are coded using a special variable-length + // kind of coding. This method does that here. + // + // It has a prefix of 2 bits, followed by the suffix of N + // bits which depend on the prefix: + // + // prefix 0b00: 9 consecutive bits + // prefix 0b01: 13 consecutive bits + // prefix 0b10: 18 consecutive bits + // prefix 0b11: 30 consecutive bits + void WriteSize(int size) + { + ulong sizeMinus1 = (ulong)size - 1uL; + + if (sizeMinus1 < (1 << 9)) + { + output.Write(2, 0b00); // 9 bits + output.Write(9, sizeMinus1); + } + else if (sizeMinus1 < (1 << 13)) + { + output.Write(2, 0b01); // 13 bits + output.Write(13, sizeMinus1); + } + else if (sizeMinus1 < (1 << 18)) + { + output.Write(2, 0b10); // 18 bits + output.Write(18, sizeMinus1); + } + else + { + output.Write(2, 0b11); // 30 bits + output.Write(30, sizeMinus1); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int ComputeDcGlobalPadding(Span groupSizes, int acGroupDataOffset, int minDcGlobalSize, bool containsAlpha, bool isLast) + { + // Libjxl reference implements this method like this: + /* + size_t ComputeDcGlobalPadding(const std::vector& group_sizes, + size_t ac_group_data_offset, + size_t min_dc_global_size, bool have_alpha, + bool is_last) { + std::vector new_group_sizes = group_sizes; + new_group_sizes[0] = min_dc_global_size; + size_t toc_size = TOCSize(new_group_sizes); + size_t actual_offset = + FrameHeaderSize(have_alpha, is_last) + toc_size + group_sizes[0]; + return ac_group_data_offset - actual_offset; + } + */ + // The reference implementation copies the entire vector so that + // element 0 can be modified without affecting the original. + // Since TocSize() does not throw, temporarily modify element 0 + // instead, avoiding the allocation and copy. + int firstItem = groupSizes[0]; + groupSizes[0] = minDcGlobalSize; + int tocSize = TocSize(groupSizes); + int actualOffset = FrameHeaderSize(containsAlpha, isLast) + tocSize + firstItem; + groupSizes[0] = firstItem; + return acGroupDataOffset - actualOffset; + } + + private static int PredictPixels(Span pixels, Span pixelsLeft, Span pixelsTop, Span pixelsTopleft, Span residuals, T negativeOne) + where T : unmanaged + { + Vector px = Vector.Create(pixels); + Vector left = Vector.Create(pixelsLeft); + Vector top = Vector.Create(pixelsTop); + Vector topleft = Vector.Create(pixelsTopleft); + + Vector ac = left - topleft; + Vector ab = left - top; + Vector bc = top - topleft; + Vector grad = ac + top; + Vector d = ab ^ bc; + Vector zero = Vector.Zero; + Vector clamp = Vector.ConditionalSelect(Vector.GreaterThan(zero, d), top, left); + Vector s = ac ^ bc; + Vector pred = Vector.ConditionalSelect(Vector.GreaterThan(zero, s), grad, clamp); + + Vector res = px - pred; + Vector resTimes2 = res + res; + res = Vector.ConditionalSelect(Vector.GreaterThan(zero, res), Vector.Create(negativeOne) - resTimes2, resTimes2); + res.CopyTo(residuals); + + return FjxlSimdUtils.CountPrefix(Vector.Equals(res, zero)); + } + + private static void EncodeHybridUint000(uint value, out uint token, out uint nbits, out uint bits) + { + if (value == 0) + { + token = 0; + nbits = 0; + bits = 0; + return; + } + + uint n = FloorLog2(value); + token = n + 1; + nbits = n; + bits = value - (1u << (int)n); + } + + private static void GenericEncodeChunk(ReadOnlySpan residuals, int n, int skip, FjxlPrefixCode code, ref FjxlBitWriter output) + { + for (int ix = skip; ix < n; ix++) + { + EncodeHybridUint000(residuals[ix], out uint token, out uint nbits, out uint bits); + output.Write((int)(code.RawLengths[(int)token] + nbits), code.RawCodes[(int)token] | (bits << code.RawLengths[(int)token])); + } + } + + /// + /// Pair of two vectors. + /// + /// Type of the vector. + /// Low vector + /// High vector + private struct VectorPair(T lo, T hi) + where T : unmanaged + { + public T Low = lo; + public T High = hi; + } + + internal static class SimdVector32 + { + public static Vector ValueToToken(Vector vec) => Vector.Create(32) - SimdUtils.Lzcnt(vec); + + public static Vector SaturateSubtract(Vector a, Vector b) => Vector.Max(a, b) - b; + + public static Vector Pow2(Vector x) => SimdUtils.Pow2(x); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderExactly14Bits.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderExactly14Bits.cs new file mode 100644 index 0000000000..4d7eff4a13 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderExactly14Bits.cs @@ -0,0 +1,61 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd.ChunkEncoders; + +internal readonly struct FjxlChunkEncoderExactly14Bits +{ + public const int BitDepth = 14; + public const int InputBytes = 2; + + public static ReadOnlySpan MinRawLength => + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 8, 7 + ]; + + public static ReadOnlySpan MaxRawLength => + [ + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 10 + ]; + + public static int MaxEncodedBitsPerSample() => 23; + + public static void PrepareForSimd(ReadOnlySpan nbits, ReadOnlySpan bits, Span nbitsSimd, Span bitsSimd) + { + CheckHuffmanBitsSimd(bits[15], nbits[15], bits[16], nbits[16]); + + nbits[..16].CopyTo(nbitsSimd); + bits[..16].CopyTo(bitsSimd); + } + + public static void EncodeChunkSimd(Span residuals, int n, int skip, ReadOnlySpan rawNBitsSimd, ReadOnlySpan rawBitsSimd, ref FjxlBitWriter output) + { + Span bits32 = stackalloc FjxlBits32[2 * ChunkSize / FjxlSimdVec16.Lanes]; + Span bits = stackalloc ushort[FjxlSimdVec16.Lanes]; + Span nbits = stackalloc ushort[FjxlSimdVec16.Lanes]; + Span bitsHuff = stackalloc ushort[FjxlSimdVec16.Lanes]; + Span nbitsHuff = stackalloc ushort[FjxlSimdVec16.Lanes]; + Span token = stackalloc ushort[FjxlSimdVec16.Lanes]; + + for (int i = 0; i < ChunkSize; i += FjxlSimdVec16.Lanes) + { + TokenizeSimd(residuals[i..], token, nbits, bits); + HuffmanSimd14(token, rawNBitsSimd, rawBitsSimd, nbitsHuff, bitsHuff); + + StoreSimdUpTo14( + nbits, + bits, + nbitsHuff, + bitsHuff, + Math.Max(n, i) - i, + Math.Max(skip, i) - i, + bits32[(2 * i / FjxlSimdVec16.Lanes)..]); + } + + StoreToWriter(bits32, ref output); + } + + public static int NumSymbols() => 17; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderFrom9To13Bits.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderFrom9To13Bits.cs new file mode 100644 index 0000000000..7d8b3dc218 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderFrom9To13Bits.cs @@ -0,0 +1,70 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd.ChunkEncoders; + +internal readonly struct FjxlChunkEncoderFrom9To13Bits +{ + public const int InputBytes = 2; + + private readonly int bitDepth; + + public FjxlChunkEncoderFrom9To13Bits(int bitDepth) + { + DebugGuard.MustBeLessThanOrEqualTo(bitDepth, 13, nameof(bitDepth)); + DebugGuard.MustBeGreaterThanOrEqualTo(bitDepth, 9, nameof(bitDepth)); + + this.bitDepth = bitDepth; + } + + public static ReadOnlySpan MinRawLength => + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ]; + + public static ReadOnlySpan MaxRawLength => + [ + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 10 + ]; + + public static int MaxEncodedBitsPerSample() => 22; + + public static void PrepareForSimd(ReadOnlySpan nbits, ReadOnlySpan bits, int n, Span nbitsSimd, Span bitsSimd) + { + DebugGuard.MustBeLessThanOrEqualTo(n, 16, nameof(n)); + + nbits[..16].CopyTo(nbitsSimd); + bits[..16].CopyTo(bitsSimd); + } + + public static void EncodeChunkSimd(Span residuals, int n, int skip, ReadOnlySpan rawNBitsSimd, ReadOnlySpan rawBitsSimd, ref FjxlBitWriter output) + { + Span bits32 = stackalloc FjxlBits32[2 * ChunkSize / FjxlSimdVec16.Lanes]; + Span bits = stackalloc ushort[FjxlSimdVec16.Lanes]; + Span nbits = stackalloc ushort[FjxlSimdVec16.Lanes]; + Span bitsHuff = stackalloc ushort[FjxlSimdVec16.Lanes]; + Span nbitsHuff = stackalloc ushort[FjxlSimdVec16.Lanes]; + Span token = stackalloc ushort[FjxlSimdVec16.Lanes]; + + for (int i = 0; i < ChunkSize; i += FjxlSimdVec16.Lanes) + { + TokenizeSimd(residuals[i..], token, nbits, bits); + HuffmanSimdUpTo13(token, rawNBitsSimd, rawBitsSimd, nbitsHuff, bitsHuff); + + StoreSimdUpTo14( + nbits, + bits, + nbitsHuff, + bitsHuff, + Math.Max(n, i) - i, + Math.Max(skip, i) - i, + bits32[(2 * i / FjxlSimdVec16.Lanes)..]); + } + + StoreToWriter(bits32, ref output); + } + + public int NumSymbols(bool doingYcocgOrLargePalette) => this.bitDepth + (doingYcocgOrLargePalette ? 3 : 2); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderMoreThan14Bits.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderMoreThan14Bits.cs new file mode 100644 index 0000000000..dfef97f74c --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderMoreThan14Bits.cs @@ -0,0 +1,77 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd.ChunkEncoders; + +internal readonly struct FjxlChunkEncoderMoreThan14Bits +{ + public const int InputBytes = 2; + + private readonly int bitDepth; + + public FjxlChunkEncoderMoreThan14Bits(int bitDepth) + { + DebugGuard.MustBeGreaterThan(bitDepth, 14, nameof(bitDepth)); + DebugGuard.MustBeLessThanOrEqualTo(bitDepth, 16, nameof(bitDepth)); + + this.bitDepth = bitDepth; + } + + public static ReadOnlySpan MinRawLength => + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 8, 8, 8, 8, 8, 8, 7 + ]; + + public static ReadOnlySpan MaxRawLength => + [ + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 8, 8, 8, 8, 8, 8, 10 + ]; + + public static int MaxEncodedBitsPerSample() => 25; + + public static void PrepareForSimd(ReadOnlySpan nbits, ReadOnlySpan bits, Span nbitsSimd, Span bitsSimd) + { + CheckHuffmanBitsSimd(bits[13], nbits[13], bits[14], nbits[14]); + CheckHuffmanBitsSimd(bits[15], nbits[15], bits[16], nbits[16]); + CheckHuffmanBitsSimd(bits[17], nbits[17], bits[18], nbits[18]); + + nbits[..14].CopyTo(nbitsSimd); + bits[..14].CopyTo(bitsSimd); + + nbitsSimd[14] = nbits[15]; + bitsSimd[14] = bits[15]; + nbitsSimd[15] = nbits[17]; + bitsSimd[15] = bits[17]; + } + + public static void EncodeChunkSimd(Span residuals, int n, int skip, ReadOnlySpan rawNBitsSimd, ReadOnlySpan rawBitsSimd, ref FjxlBitWriter output) + { + Span bits32 = stackalloc FjxlBits32[2 * ChunkSize / FjxlSimdVec16.Lanes]; + Span bits = stackalloc uint[FjxlSimdVec16.Lanes]; + Span nbits = stackalloc uint[FjxlSimdVec16.Lanes]; + Span bitsHuff = stackalloc ushort[FjxlSimdVec16.Lanes]; + Span nbitsHuff = stackalloc ushort[FjxlSimdVec16.Lanes]; + Span token = stackalloc ushort[FjxlSimdVec16.Lanes]; + + for (int i = 0; i < ChunkSize; i += FjxlSimdVec16.Lanes) + { + TokenizeSimd(residuals[i..], token, nbits, bits); + HuffmanSimdAbove14(token, rawNBitsSimd, rawBitsSimd, nbitsHuff, bitsHuff); + + StoreSimdAbove14( + nbits, + bits, + nbitsHuff, + bitsHuff, + Math.Max(n, i) - i, + Math.Max(skip, i) - i, + bits32[(2 * i / FjxlSimdVec16.Lanes)..]); + } + + StoreToWriter(bits32, ref output); + } + + public static int NumSymbols() => 19; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderUpTo8Bits.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderUpTo8Bits.cs new file mode 100644 index 0000000000..eee2aa401e --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderUpTo8Bits.cs @@ -0,0 +1,67 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd.ChunkEncoders; + +internal readonly struct FjxlChunkEncoderUpTo8Bits +{ + public const int InputBytes = 1; + + private readonly int bitDepth; + + public FjxlChunkEncoderUpTo8Bits(int bitDepth) + { + DebugGuard.MustBeLessThanOrEqualTo(bitDepth, 8, nameof(bitDepth)); + + this.bitDepth = bitDepth; + } + + public static ReadOnlySpan MinRawLength => + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ]; + + public static ReadOnlySpan MaxRawLength => + [ + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 10 + ]; + + public static int MaxEncodedBitsPerSample() => 16; + + public static void PrepareForSimd(ReadOnlySpan nbits, ReadOnlySpan bits, int n, Span nbitsSimd, Span bitsSimd) + { + DebugGuard.MustBeLessThanOrEqualTo(n, 16, nameof(n)); + + nbits[..16].CopyTo(nbitsSimd); + bits[..16].CopyTo(bitsSimd); + } + + public static void EncodeChunkSimd(Span residuals, int n, int skip, ReadOnlySpan rawNBitsSimd, ReadOnlySpan rawBitsSimd, ref FjxlBitWriter output) + { + Span bits32 = stackalloc FjxlBits32[ChunkSize / FjxlSimdVec16.Lanes]; + Span bits = stackalloc ushort[FjxlSimdVec16.Lanes]; + Span nbits = stackalloc ushort[FjxlSimdVec16.Lanes]; + Span bitsHuff = stackalloc ushort[FjxlSimdVec16.Lanes]; + Span nbitsHuff = stackalloc ushort[FjxlSimdVec16.Lanes]; + Span token = stackalloc ushort[FjxlSimdVec16.Lanes]; + + for (int i = 0; i < ChunkSize; i += FjxlSimdVec16.Lanes) + { + TokenizeSimd(residuals[i..], token, nbits, bits); + HuffmanSimdUpTo13(token, rawNBitsSimd, rawBitsSimd, nbitsHuff, bitsHuff); + + StoreSimdUpTo8( + nbits, + bits, + nbitsHuff, + bitsHuff, + Math.Max(n, i) - i, + Math.Max(skip, i) - i, + bits32[(int)(i / FjxlSimdVec16.Lanes)..]); + } + + StoreToWriter(bits32, ref output); + } + + public int NumSymbols(bool doingYcocgOrLargePalette) => this.bitDepth + (doingYcocgOrLargePalette ? 3 : 2); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlHuffman.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlHuffman.cs new file mode 100644 index 0000000000..13c702f73f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlHuffman.cs @@ -0,0 +1,66 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd; + +internal static class FjxlHuffman +{ + public static void HuffmanSimdUpTo13(Span tokens, ReadOnlySpan rawNbitsSimd, ReadOnlySpan rawBitsSimd, Span nbitsOut, Span bitsOut) + { + Vector tok = FjxlSimdUtils.PrepareForU8Lookup(Vector.Create(tokens)); + + FjxlSimdUtils.U8Lookup(tok, rawNbitsSimd).CopyTo(nbitsOut); + FjxlSimdUtils.U8Lookup(tok, rawBitsSimd).CopyTo(bitsOut); + } + + public static void HuffmanSimd14(Span tokens, ReadOnlySpan rawNbitsSimd, ReadOnlySpan rawBitsSimd, Span nbitsOut, Span bitsOut) + { + Vector tokenCap = Vector.Create((short)15); + Vector tok = Vector.Create(tokens); + + Vector tokIndex = FjxlSimdUtils.PrepareForU8Lookup(Vector.Min(tok, tokenCap)); + + Vector huffBitsPre = FjxlSimdUtils.U8Lookup(tokIndex, rawBitsSimd); + + Vector needsHighBit = Vector.Equals(tok, Vector.Create((short)16)); + + Vector huffBits = Vector.ConditionalSelect( + needsHighBit, + huffBitsPre | Vector.Create((short)128), + huffBitsPre); + + huffBits.CopyTo(bitsOut); + + FjxlSimdUtils.U8Lookup(tokIndex, rawNbitsSimd) + .CopyTo(nbitsOut); + } + + public static void HuffmanSimdAbove14(Span tokens, ReadOnlySpan rawNbitsSimd, ReadOnlySpan rawBitsSimd, Span nbitsOut, Span bitsOut) + { + Vector tok = Vector.Create(tokens); + Vector above = Vector.GreaterThan(tok, Vector.Create((short)12)); + + Vector remapTok = Vector.ConditionalSelect( + above, + FjxlSimdUtils.HorizontalAdd(tok, Vector.Create((short)13)), + tok); + + Vector tokIndex = FjxlSimdUtils.PrepareForU8Lookup(remapTok); + Vector huffBitsPre = FjxlSimdUtils.U8Lookup(tokIndex, rawBitsSimd); + + Vector evenTok = tok & Vector.Create(unchecked((short)0xFFF)); + Vector needsHighBit = above & Vector.Equals(tok, evenTok); + + Vector huffBits = Vector.ConditionalSelect( + needsHighBit, + huffBitsPre | Vector.Create((short)128), + huffBitsPre); + + huffBits.CopyTo(bitsOut); + + FjxlSimdUtils.U8Lookup(tokIndex, rawNbitsSimd) + .CopyTo(nbitsOut); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlSimdUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlSimdUtils.cs new file mode 100644 index 0000000000..6cdaf7f57b --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlSimdUtils.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd; + +/// +/// SIMD utilities highly specific to the Fast Lossless JXL encoder. +/// +internal static class FjxlSimdUtils +{ + public static Vector HorizontalAdd(this Vector a, Vector b) => (a + b) >> 1; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlStoreSimd.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlStoreSimd.cs new file mode 100644 index 0000000000..03f78a92d8 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlStoreSimd.cs @@ -0,0 +1,101 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd; + +internal static class FjxlStoreSimd +{ + public static void StoreSimdUpTo8(Span nbitsTok, Span bitsTok, Span nbitsHuff, Span bitsHuff, int n, int skip, Span bitsOut) + { + FjxlBits16 bits = FjxlBits16.FromRaw( + Vector.Create(nbitsTok), + Vector.Create(bitsTok)); + + FjxlBits16 huffBits = FjxlBits16.FromRaw( + Vector.Create(nbitsHuff), + Vector.Create(bitsHuff)); + + bits.Interleave(huffBits); + bits.ClipTo(n); + bits.Skip(skip); + + bitsOut[0] = bits.Merge(); + } + + public static void StoreSimdUpTo14(Span nbitsTok, Span bitsTok, Span nbitsHuff, Span bitsHuff, int n, int skip, Span bitsOut) + { + VecPair> bits = Vector.Create(bitsTok).Interleave(Vector.Create(bitsHuff)); + VecPair> nbits = Vector.Create(nbitsTok).Interleave(Vector.Create(nbitsHuff)); + + Bits16 low = Bits16.FromRaw(nbits.Low, bits.Low); + Bits16 hi = Bits16.FromRaw(nbits.High, bits.High); + + int lanes = Vector.Count; + + low.ClipTo(2 * n); + low.Skip(2 * skip); + + hi.ClipTo(Math.Max(2 * n, lanes) - lanes); + hi.Skip(Math.Max(2 * skip, lanes) - lanes); + + bitsOut[0] = low.Merge(); + bitsOut[1] = hi.Merge(); + } + + public static void StoreSimdAbove14(Span nbitsTok, Span bitsTok, Span nbitsHuff, Span bitsHuff, int n, int skip, Span bitsOut) + { + Vector nbitsTokLo = Vector.Create(nbitsTok); + Vector bitsTokLo = Vector.Create(bitsTok); + + Vector nbitsTokHi = Vector.Create(nbitsTok[Vector.Count..]); + Vector bitsTokHi = Vector.Create(bitsTok[Vector.Count..]); + + Bits32 bitsLow = Bits32.FromRaw(nbitsTokLo, bitsTokLo); + Bits32 bitsHi = Bits32.FromRaw(nbitsTokHi, bitsTokHi); + + VecPair> huffBits = FjxlSimdUtils.Upcast(Vector.Create(bitsHuff)); + VecPair> huffNbits = FjxlSimdUtils.Upcast(Vector.Create(nbitsHuff)); + + Bits32 huffLow = Bits32.FromRaw(huffNbits.Low, huffBits.Low); + Bits32 huffHi = Bits32.FromRaw(huffNbits.High, huffBits.High); + + bitsLow.Interleave(huffLow); + bitsLow.ClipTo(n); + bitsLow.Skip(skip); + bitsOut[0] = bitsLow; + + int lanes = Vector.Count; + + bitsHi.Interleave(huffHi); + bitsHi.ClipTo(Math.Max(n, lanes) - lanes); + bitsHi.Skip(Math.Max(skip, lanes) - lanes); + bitsOut[1] = bitsHi; + } + + public static void StoreToWriter(ReadOnlySpan bits, FjxlBitWriter output, int n) + { + Span nbits64 = stackalloc ulong[Bits64.Lanes * n]; + Span bits64 = stackalloc ulong[Bits64.Lanes * n]; + + bits[0].Merge().Store(nbits64, bits64); + + if (n > 1) + { + bits[1].Merge().Store(nbits64[Bits64.Lanes..], bits64[Bits64.Lanes..]); + } + + if (n > 2) + { + bits[2].Merge().Store(nbits64[(2 * Bits64.Lanes)..], bits64[(2 * Bits64.Lanes)..]); + } + + if (n > 3) + { + bits[3].Merge().Store(nbits64[(3 * Bits64.Lanes)..], bits64[(3 * Bits64.Lanes)..]); + } + + output.WriteMultiple(nbits64, bits64, Bits64.Lanes * n); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlTokenization.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlTokenization.cs new file mode 100644 index 0000000000..aed8a20576 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlTokenization.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd; + +internal static class FjxlTokenization +{ + public static void Tokenize16(Span residuals, Span output, Span nbitsOut, Span bitsOut) + { + Vector res = Vector.Create(residuals); + Vector token = FjxlSimdUtils.ValueToToken(res); + Vector nbits = FjxlSimdUtils.SaturateSubtract(token, Vector.One); + Vector bits = FjxlSimdUtils.SaturateSubtract(res, FjxlSimdUtils.Pow2(nbits)); + + token.CopyTo(output); + nbits.CopyTo(nbitsOut); + bits.CopyTo(bitsOut); + } + + public static void Tokenize32(Span residuals, Span tokenOut, Span nbitsOut, Span bitsOut) + { + Vector resLo = Vector.Create(residuals); + Vector resHi = Vector.Create(residuals[Vector.Count..]); + + Vector tokenLo = FjxlSimdUtils.ValueToToken(resLo); + Vector tokenHi = FjxlSimdUtils.ValueToToken(resHi); + + Vector nbitsLo = FjxlSimdUtils.SaturateSubtract(tokenLo, Vector.One); + Vector nbitsHi = FjxlSimdUtils.SaturateSubtract(tokenHi, Vector.One); + + Vector bitsLo = FjxlSimdUtils.SaturateSubtract(resLo, FjxlSimdUtils.Pow2(nbitsLo)); + Vector bitsHi = FjxlSimdUtils.SaturateSubtract(resHi, FjxlSimdUtils.Pow2(nbitsHi)); + + Vector token = FjxlSimdUtils.FromTwo32(tokenLo, tokenHi); + token.CopyTo(tokenOut); + + nbitsLo.CopyTo(nbitsOut); + nbitsHi.CopyTo(nbitsOut[Vector.Count..]); + + bitsLo.CopyTo(bitsOut); + bitsHi.CopyTo(bitsOut[Vector.Count..]); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs deleted file mode 100644 index 5a4c57bdbf..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs +++ /dev/null @@ -1,1114 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// Suppress IDE0057. This is so we can stack-allocate -// a powers of 2 and then slice it to the appropriate -// length (which produces better code). -// -// Without this suppression, the analyzer produces a warning, -// recommending changing this: -// stackalloc ulong[32].Slice(0, 18) -// to: -// (stackalloc ulong[32])[..18] -// -// But then the analyzer produces a new warning, recommending -// to remove the paranthesis, changing this: -// (stackalloc ulong[32])[..18] -// to: -// stackalloc ulong[32][..18] -// -// which is invalid C# syntax. -#pragma warning disable IDE0057 // Use range operator - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; - -/// -/// Extreme performance JPEG XL encoder which provides minimal lossless compression. -/// It also uses minimal dependencies. -/// -internal sealed class JxlFastLosslessEncoder -{ - /// - /// Specifies maximum number of bytes a frame header may use. - /// - private const int MaxFrameHeaderSize = 5; - - private const int NumRawSymbols = 19; - - private const int NumLz77 = 33; - - /// - /// Cache/dictionary size for LZ77 - /// - private const int Lz77CacheSize = 32; - - private const int Lz77Offset = 224; - - private const int Lz77MinLength = 7; - - /// - /// Input frame data is stored here. - /// - private readonly FjxlFrameInputSource input; - - /// - /// Image width of the input image. - /// - private readonly int width; - - /// - /// Image height of the input image. - /// - private readonly int height; - - /// - /// Image width in groups. - /// - private readonly int numGroupsX; - - /// - /// Image height in groups. - /// - private readonly int numGroupsY; - - /// - /// Image width in groups (DC). - /// - private readonly int numDcGroupsX; - - /// - /// Image height in groups (DC). - /// - private readonly int numDcGroupsY; - - /// - /// Number of channels. (f.e. RGBA is 4, YUV is 3) - /// - private readonly int channels; - - /// - /// Number of bits represented per pixel. (f.e. 8 means pixels - /// have a 0-255 range) - /// - /// - /// Higher bit depths can represent more colors. - /// - private readonly int bitDepth; - - /// - /// Should the output image be stored in big-endian order? - /// - private readonly bool isBigEndian; - - private readonly int effort; - - private readonly bool collided; - - /// - /// Prefix codes for LZ77. - /// - private InlineArray4 hcode; - - private readonly List lookup = []; - - /// - /// Bit writer to write the JPEG XL headers. - /// - private readonly BitWriter header; - - /// - /// Bit writers for writing JPEG XL groups. - /// - private readonly List> groupData = []; - - /// - /// Sizes for each group. - /// - private readonly List groupSizes = []; - - private int acGroupDataOffset; - - private int minDcGlobalSize; - - private int currentBitWriter; - - private int bitWriterBytePos; - - private int bitsInBuffer; - - private long bitBuffer; - - private bool processDone; - - /// - /// Abstracts access to a raster frame data required for encoding. - /// - internal abstract class FjxlFrameInputSource : IDisposable - { - /// - public abstract void Dispose(); - - /// - /// Returns a span that wraps over channel color data at the - /// specified rectangular position. - /// - /// Target type of the color data. - /// Left offset - /// Right offset - /// Selection width - /// Selection height - /// The actual offset of the row in row-major order is stored here. - /// - /// A wrapper over the color data of the channel at the specified - /// position. - /// - public abstract Span GetColorChannelData(int x, int y, int width, int height, out long rowOffset) - where T : unmanaged; - } - - /// - /// Gets minimum raw lengths for prefix coding. - /// - private static ReadOnlySpan MinimumRawLength => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - - /// - /// Gets maximum raw lengths for prefix coding. - /// - private static ReadOnlySpan MaximumRawLength => [7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 10]; - - /// - /// Gets a lookup used by the method - /// to translate a bucket into a base group size. - /// - private static ReadOnlySpan GroupSizeOffset => - [ - 0, - 1024, - 17408, - 4211712 - ]; - - /// - /// Gets a lookup to determine how many bits a TOC bucket uses. - /// - private static ReadOnlySpan TocBits => [12, 16, 24, 32]; - - /// - /// Approximates Floor(Log2(v)) using integers. - /// - /// Value to retrieve Floor(Log2(v)) of. - /// Floor of second logarithm of v, or 31 if v is equal to 0. - /// This method may use CPU intrinsics provided by the .NET Runtime (e.g. BMI1 on x86). - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint FloorLog2(uint v) => v == 0 ? 0 : 31u - (uint)BitOperations.LeadingZeroCount(v); - - /// - /// Approximates count trailing zeros of v using integers. - /// - /// Value to retrieve number of 0 bits after last 1 bit of. - /// After the least significant 1 bit, returns the number of 0 bits. E.g. 1000 1000 00 -> 5. - /// This method may use CPU intrinsics provided by the .NET Runtime (e.g. BMI1 on x86). - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint CtzNonZero(ulong v) => (uint)BitOperations.TrailingZeroCount(v); - - /// - /// Returns a TOC bucket based on the group size. - /// - /// Specified group size. - /// TOC bucket matching the appropriate group size. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int TocBucket(int groupSize) - { - int bucket = 0; - - while (bucket < 3 && groupSize >= GroupSizeOffset[bucket + 1]) - { - bucket++; - } - - return bucket; - } - - /// - /// Returns the total number of bits required to represent - /// all given group sizes in the TOC. - /// - /// Group sizes to calculate bit sizes of. - /// Accumulated number of bits required to represent each group size. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int TocSize(Span groupSizes) - { - int tocBits = 0; - - ref int unsafeRef = ref MemoryMarshal.GetReference(groupSizes); - - for (int i = 0; i < groupSizes.Length; i++) - { - // TODO: we can try using AVX2 gather intrinsics, - // especially because TocBits can absolutely fit - // in the L1 cache - int groupSize = Unsafe.Add(ref unsafeRef, i); - int bucketForGroupSize = TocBucket(groupSize); - int bitsUsedByBucket = TocBits[bucketForGroupSize]; - - tocBits += bitsUsedByBucket; - } - - return tocBits; - } - - /// - /// Returns the number of bytes for the frame header. - /// - /// Indicates presence of the alpha channel. - /// Indicates whether this is the final frame. - /// Frame header size in bytes. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int FrameHeaderSize(bool containsAlpha, bool isLast) - { - // Original code (from libjxl): - // - // size_t nbits = 28 + (have_alpha ? 4 : 0) + (is_last ? 0 : 2); - // return (nbits + 7) / 8; - // - // In this implementation we just use constants to shave a few CPU cycles. - // The total amount of branches is reduced by one (for the !containsAlpha case), - // but we remove the arithmetic/shifting instructions. - unchecked - { - if (containsAlpha) - { - if (isLast) - { - return 5; // (34 + 7) / 8 - } - else - { - return 4; // (32 + 7) / 8 - } - } - else - { - return 4; // (30 + 7) / 8 AND (28 + 7) / 8 yield the same result - } - } - } - - private static long GetSectionSize(InlineArray4 groupData) - { - long size = 0; - - for (int j = 0; j < 4; j++) - { - BitWriter writer = groupData[j]; - - size += (writer.BytesWritten * 8) + writer.BitsInBuffer; - } - - return (size + 7) / 8; - } - - /// - /// Approximates number of bytes needed for the output image buffer. - /// - /// Bytes for the frame buffer. - private long GetOutputSize() - { - long totalSizeGroups = 0; - - Span> groups = CollectionsMarshal.AsSpan(this.groupData); - - for (int i = 0; i < groups.Length; i++) - { - InlineArray4 section = groups[i]; - - totalSizeGroups += GetSectionSize(section); - } - - return this.header.BytesWritten + totalSizeGroups; - } - - /// - /// Returns the maximum amount of bytes potentially required for the image buffer. - /// - /// Upper bound of bytes for frame buffer. - private long GetMaxRequiredOutput() => this.GetOutputSize() + 32; - - private void WriteHeader(bool addImageHeader, bool isLast) - { - BitWriter output = this.header; - bool haveAlpha = this.channels is 2 or 4; - - if (addImageHeader) - { - // File signature. This signature specifies - // a raw codestream. No container format here. - output.Write(16, 0x0AFF); - - // Handcrafted size header. - output.Write(1, 0); // Not small - - WriteSize(this.height); - output.Write(3, 0b000); // No special ratio - WriteSize(this.width); - - // Handcrafted image metadata - output.Write(1, 0); // all_default = 0 (don't assume values to be set to their defaults) - output.Write(1, 0); // extra_fields = 0 (extra fields are disabled and therefore not present) - output.Write(1, 0); // bit_depth.floating_point_sample = 0 (samples are integers) - - if (this.bitDepth == 8) - { - output.Write(2, 0b00); // bit_depth.bits_per_sample = 8 (predefined bit depth of 8 bits) - } - else if (this.bitDepth == 10) - { - output.Write(2, 0b01); // bit_depth.bits_per_sample = 10 (predefined bit depth of 10 bits) - } - else if (this.bitDepth == 12) - { - output.Write(2, 0b10); // bit_depth.bits_per_sample = 12 (predefined bit depth of 12 bits) - } - else - { - output.Write(2, 0b11); // Custom bit depth - output.Write(6, (ulong)this.bitDepth - 1); // bit depth minus 1 (so 0 becomes 1, 9 becomes 10, etc) - } - - if (this.bitDepth <= 14) - { - output.Write(1, 1); // 16-bit-buffer is sufficient - } - else - { - output.Write(1, 0); // 16-bit-buffer is NOT sufficient - } - - if (haveAlpha) - { - output.Write(2, 0b01); // Emit one extra channel (the alpha channel) - - if (this.bitDepth == 8) - { - output.Write(1, 1); // all_default = 1 (8-bit alpha is the default) - } - else - { - output.Write(1, 0); // all_default = 0 - output.Write(2, 0); // type = alpha - output.Write(1, 0); // samples are not floating point - - if (this.bitDepth == 10) - { - output.Write(2, 0b01); // bit_depth.bits_per_sample = 10 (predefined bit depth of 10 bits) - } - else if (this.bitDepth == 12) - { - output.Write(2, 0b10); // bit_depth.bits_per_sample = 12 (predefined bit depth of 12 bits) - } - else - { - output.Write(2, 0b11); // Custom bit depth - output.Write(6, (ulong)this.bitDepth - 1); // bit depth minus 1 (so 0 becomes 1, 9 becomes 10, etc) - } - - output.Write(2, 0); // dim_shift = 0 - output.Write(2, 0); // name_len = 0 - output.Write(1, 0); // alpha_associated = 0 - } - } - else - { - output.Write(2, 0b00); // 0 extra channels - } - - output.Write(1, 0); // not XYB - - if (this.channels > 2) - { - output.Write(1, 1); // color_encoding.all_default = 1 (sRGB) - } - else - { - output.Write(1, 0); // color_encoding.all_default = 0 - output.Write(1, 0); // color_encoding.want_icc = 0 - output.Write(2, 0b01); // Grayscale - output.Write(2, 0b01); // D65 - output.Write(1, 0); // No gamma transfer function - output.Write(2, 0b10); // transfer function: 2 + u(4) - output.Write(4, 11); // transfer function (specifies sRGB) - output.Write(2, 1); // relative rendering intent - } - - output.Write(2, 0b00); // No extensions - output.Write(1, 1); // all_default transform data - output.ZeroPadToByte(); // No ICC and no preview. Frame should start at byte boundary. - } - - // Handcrafted frame header - output.Write(1, 0); // all_default = 0 (non-default values) - output.Write(2, 0b00); // regular frame - output.Write(1, 1); // modular - output.Write(2, 0b00); // default flags - output.Write(1, 0); // not Y'Cb'Cr - output.Write(2, 0b00); // no upsampling - - if (haveAlpha) - { - output.Write(2, 0b00); // no alpha upsampling - } - - output.Write(2, 0b01); // default group size - output.Write(2, 0b00); // exactly one pass - output.Write(1, 0); // no custom size or origin - output.Write(2, 0b00); // Replace blending mode - - if (haveAlpha) - { - output.Write(2, 0b00); // Replace blending mode for alpha channel - } - - output.Write(2, 0b00); // a frame has no name - output.Write(1, 0); // loop filter is not all_default - output.Write(1, 0); // no Gaborish transform - output.Write(2, 0b00); // 0 EPF filters - output.Write(2, 0b00); // no LF extensions - output.Write(2, 0b00); // no FH extensions - - output.Write(1, 0); // no TOC permutation - output.ZeroPadToByte(); // TOC is byte aligned - - Span groupSizes = CollectionsMarshal.AsSpan(this.groupSizes); - - for (int i = 0; i < groupSizes.Length; i++) - { - int groupSize = groupSizes[i]; - - int bucket = TocBucket(groupSize); - output.Write(2, (ulong)bucket); - output.Write(TocBits[bucket] - 2, (ulong)(groupSize - GroupSizeOffset[bucket])); - } - - output.ZeroPadToByte(); // Groups are byte-aligned - - // Sizes are coded using a special variable-length - // kind of coding. This method does that here. - // - // It has a prefix of 2 bits, followed by the suffix of N - // bits which depend on the prefix: - // - // prefix 0b00: 9 consecutive bits - // prefix 0b01: 13 consecutive bits - // prefix 0b10: 18 consecutive bits - // prefix 0b11: 30 consecutive bits - void WriteSize(int size) - { - ulong sizeMinus1 = (ulong)size - 1uL; - - if (sizeMinus1 < (1 << 9)) - { - output.Write(2, 0b00); // 9 bits - output.Write(9, sizeMinus1); - } - else if (sizeMinus1 < (1 << 13)) - { - output.Write(2, 0b01); // 13 bits - output.Write(13, sizeMinus1); - } - else if (sizeMinus1 < (1 << 18)) - { - output.Write(2, 0b10); // 18 bits - output.Write(18, sizeMinus1); - } - else - { - output.Write(2, 0b11); // 30 bits - output.Write(30, sizeMinus1); - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int ComputeDcGlobalPadding(Span groupSizes, int acGroupDataOffset, int minDcGlobalSize, bool containsAlpha, bool isLast) - { - // Libjxl reference implements this method like this: - /* - size_t ComputeDcGlobalPadding(const std::vector& group_sizes, - size_t ac_group_data_offset, - size_t min_dc_global_size, bool have_alpha, - bool is_last) { - std::vector new_group_sizes = group_sizes; - new_group_sizes[0] = min_dc_global_size; - size_t toc_size = TOCSize(new_group_sizes); - size_t actual_offset = - FrameHeaderSize(have_alpha, is_last) + toc_size + group_sizes[0]; - return ac_group_data_offset - actual_offset; - } - */ - // The reference implementation copies the entire vector so that - // element 0 can be modified without affecting the original. - // Since TocSize() does not throw, temporarily modify element 0 - // instead, avoiding the allocation and copy. - int firstItem = groupSizes[0]; - groupSizes[0] = minDcGlobalSize; - int tocSize = TocSize(groupSizes); - int actualOffset = FrameHeaderSize(containsAlpha, isLast) + tocSize + firstItem; - groupSizes[0] = firstItem; - return acGroupDataOffset - actualOffset; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void EncodeHybridUintLz77(int value, out int token, out int nBits, out int bits) - { - unchecked - { - int n = (int)FloorLog2((uint)value); - - if (value < 16) - { - token = value; - nBits = 0; - bits = 0; - } - else - { - token = 16 + n - 4; - nBits = n; - bits = value - (1 << n); - } - } - } - - /// - /// Pair of two vectors. - /// - /// Type of the vector. - /// Low vector - /// High vector - private struct VectorPair(T lo, T hi) - where T : unmanaged - { - public T Low = lo; - public T High = hi; - } - - /// - /// The prefix code is used for encoding LZ77-compressed coefficients. - /// - private sealed class PrefixCode - { -#pragma warning disable SA1401 // Fields should be private - - /// - /// Maximum number of raw symbols for prefix coding. - /// - private const int MaxNumSymbols = NumRawSymbols + 1 < NumLz77 ? NumLz77 : NumRawSymbols + 1; - - /// - /// Gets or sets the Huffman raw bit lengths. - /// - public InlineArray19 RawLengths; - - /// - /// Gets or sets the Huffman raw code values. - /// - public InlineArray19 RawCodes; - - /// - /// Gets or sets the Huffman LZ77 bit lengths. - /// - public InlineArray33 Lz77Lengths; - - /// - /// Gets or sets the Huffman LZ77 code values. - /// - public InlineArray33 Lz77Codes; - - /// - /// Gets or sets the Huffman LZ77 cache code values. - /// - public InlineArray32 Lz77CacheBits; - - /// - /// Gets or sets the Huffman LZ77 cache bit lengths. - /// - public InlineArray32 Lz77CacheLengths; - - public PrefixCode(Span rawCounts, Span lz77Counts) - { - Span level1Counts = stackalloc ulong[NumRawSymbols + 1]; - rawCounts[..NumRawSymbols].CopyTo(level1Counts); - - this.RawCount = NumRawSymbols; - - while (this.RawCount > 0 && level1Counts[this.RawCount - 1] == 0) - { - this.RawCount--; - } - - level1Counts[this.RawCount] = 0; - - for (int i = 0; i < NumLz77; i++) - { - level1Counts[this.RawCount] += lz77Counts[i]; - } - - Span level1Lengths = stackalloc byte[NumRawSymbols + 1]; - level1Lengths.Clear(); - - ComputeCodeLengths(level1Counts, this.RawCount + 1, MinimumRawLength, MaximumRawLength, level1Lengths); - - Span level2Lengths = stackalloc byte[NumLz77]; - Span minLengths = stackalloc byte[NumLz77]; - - level2Lengths.Clear(); - minLengths.Clear(); - - int l = 15 - level1Lengths[this.RawCount]; - Span maxLengths = stackalloc byte[NumLz77]; - maxLengths.Fill((byte)l); - - int numLz77 = NumLz77; - while (numLz77 > 0 && lz77Counts[numLz77 - 1] == 0) - { - numLz77--; - } - - ComputeCodeLengths(lz77Counts, numLz77, minLengths, maxLengths, level2Lengths); - - level1Lengths[..this.RawCount].CopyTo(this.RawLengths); - - for (int i = 0; i < numLz77; i++) - { - this.Lz77Lengths[i] = (byte)(level2Lengths[i] != 0 ? level1Lengths[this.RawCount] + level2Lengths[i] : 0); - } - - ComputeCanonicalCode(this.RawLengths, this.RawCodes, this.Lz77Lengths, this.Lz77Codes); - - // Prepare the LZ77 cache - for (int count = 0; count < Lz77CacheSize; count++) - { - EncodeHybridUintLz77(count, out int token, out int nbits, out int bits); - this.Lz77CacheLengths[count] = (byte)(this.Lz77Lengths[token] + nbits + this.RawLengths[0]); - this.Lz77CacheBits[count] = - (ulong)((((bits << this.Lz77Lengths[token]) | this.Lz77Codes[token]) << this.RawLengths[0]) | - this.RawLengths[0]); - } - } - - /// - /// Gets a lookup used to reverse integers bit-wise. - /// - private static ReadOnlySpan ReverseNibbleLookup => - [ - 0b0000, 0b1000, 0b0100, 0b1100, 0b0010, 0b1010, 0b0110, 0b1110, - 0b0001, 0b1001, 0b0101, 0b1101, 0b0011, 0b1011, 0b0111, 0b1111, - ]; - -#pragma warning restore SA1401 // Fields should be private - - /// - /// Gets or sets the number of raw codes. - /// - public int RawCount { get; set; } - - /// - /// Reverses the integer bit-wise. - /// - /// Number of bits for the integer. - /// Actual bits to reverse. - /// - /// Input integer but reversed. F.e. 10010 becomes 01001. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static ushort BitReverse(int nbits, ushort bits) - { - unchecked - { - ushort rev16 = (ushort)((ReverseNibbleLookup[bits & 0xF] << 12) | - (ReverseNibbleLookup[(bits >> 4) & 0xF] << 8) | - (ReverseNibbleLookup[(bits >> 8) & 0xF] << 4) | - ReverseNibbleLookup[bits >> 12]); - return (ushort)(rev16 >> (16 - nbits)); - } - } - - private static void ComputeCanonicalCode(Span firstChunkLengths, Span firstChunkCodes, Span secondChunkLengths, Span secondChunkCodes) - { - const int maxCodeLength = 15; - - Span codeLengthCounts = stackalloc byte[maxCodeLength + 1]; - codeLengthCounts.Clear(); - - for (int i = 0; i < firstChunkCodes.Length; i++) - { - codeLengthCounts[firstChunkLengths[i]]++; - - if (firstChunkLengths[i] > 8) - { - throw new InvalidOperationException("First chunk length is too large"); - } - - if (firstChunkLengths[i] <= 0) - { - throw new InvalidOperationException("First chunk length cannot be <= 0"); - } - } - - for (int i = 0; i < secondChunkCodes.Length; i++) - { - codeLengthCounts[secondChunkLengths[i]]++; - - if (secondChunkLengths[i] > maxCodeLength) - { - throw new InvalidOperationException("Second chunk length is too large"); - } - } - - Span nextCode = stackalloc ushort[maxCodeLength + 1]; - nextCode.Clear(); - - ushort code = 0; - - for (int i = 1; i < maxCodeLength + 1; i++) - { - code = unchecked((ushort)((code + codeLengthCounts[i - 1]) << 1)); - nextCode[i] = code; - } - - unchecked - { - for (int i = 0; i < firstChunkCodes.Length; i++) - { - firstChunkCodes[i] = (byte)BitReverse(firstChunkLengths[i], nextCode[firstChunkLengths[i]]++); - } - - for (int i = 0; i < secondChunkCodes.Length; i++) - { - secondChunkCodes[i] = (byte)BitReverse(secondChunkLengths[i], nextCode[secondChunkLengths[i]]++); - } - } - } - - private static void ComputeCodeLengthsNonZeroImpl( - Span freqs, - int n, - int precision, - T infty, - Span minLimit, - Span maxLimit, - Span nbits) - where T : unmanaged, INumber - { - DebugGuard.MustBeLessThan(precision, 15, nameof(precision)); - DebugGuard.MustBeLessThanOrEqualTo(n, MaxNumSymbols, nameof(n)); - - int scale = 1 << precision; - int width = scale + 1; - - Span dynp = stackalloc T[width * (n + 1)]; - dynp.Fill(infty); - dynp[0] = T.Zero; - - for (int sym = 0; sym < n; sym++) - { - for (int bits = minLimit[sym]; bits <= maxLimit[sym]; bits++) - { - int offsetDelta = 1 << (precision - bits); - T cost = T.CreateChecked(freqs[sym]) * T.CreateChecked(bits); - - for (int off = 0; off + offsetDelta <= scale; off++) - { - int current = (sym * width) + off; - int next = ((sym + 1) * width) + off + offsetDelta; - - dynp[next] = T.Min(dynp[current] + cost, dynp[next]); - } - } - } - - int offFinal = scale; - - for (int sym = n - 1; sym >= 0; sym--) - { - if (offFinal <= 0) - { - throw new InvalidOperationException("Offset should be greater than zero"); - } - - for (int bits = minLimit[sym]; bits <= maxLimit[sym]; bits++) - { - int offsetDelta = 1 << (precision - bits); - - if (offsetDelta <= offFinal) - { - int current = (sym * width) + offFinal; - int previous = (sym * width) + offFinal - offsetDelta; - - T cost = T.CreateChecked(freqs[sym]) * T.CreateChecked(bits); - - if (dynp[current] == dynp[previous] + cost) - { - offFinal -= offsetDelta; - nbits[sym] = (byte)bits; - break; - } - } - } - } - } - - private static void ComputeCodeLengthsNonZero(Span freqs, int n, Span minLimit, Span maxLimit, Span nbits) - { - int precision = 0; - int shortestLength = 255; - ulong frequencySum = 0; - - for (int i = 0; i < n; i++) - { - frequencySum += freqs[i]; - - if (minLimit[i] < 1) - { - minLimit[i] = 1; - } - - precision = Math.Max(maxLimit[i], precision); - shortestLength = Math.Min(minLimit[i], shortestLength); - } - - precision -= shortestLength - 1; - ulong infinity = frequencySum * (ulong)precision; - - if (infinity < uint.MaxValue / 2) - { - ComputeCodeLengthsNonZeroImpl(freqs, n, precision, (uint)infinity, minLimit, maxLimit, nbits); - } - else - { - ComputeCodeLengthsNonZeroImpl(freqs, n, precision, infinity, minLimit, maxLimit, nbits); - } - } - - private static void ComputeCodeLengths(Span freqs, int n, ReadOnlySpan minLimitIn, ReadOnlySpan maxLimitIn, Span nbits) - { - DebugGuard.MustBeLessThanOrEqualTo(n, MaxNumSymbols, nameof(n)); - - Span compactFreqs = stackalloc ulong[MaxNumSymbols]; - Span minLimit = stackalloc byte[MaxNumSymbols]; - Span maxLimit = stackalloc byte[MaxNumSymbols]; - - int ni = 0; - for (int i = 0; i < n; i++) - { - if (freqs[i] != 0) - { - compactFreqs[ni] = freqs[i]; - minLimit[ni] = minLimitIn[i]; - maxLimit[ni] = maxLimitIn[i]; - ni++; - } - } - - compactFreqs[ni..].Clear(); - minLimit[ni..].Clear(); - maxLimit[ni..].Clear(); - - Span numBits = stackalloc byte[MaxNumSymbols]; - numBits.Clear(); - - ComputeCodeLengthsNonZero(compactFreqs, ni, minLimit, maxLimit, numBits); - - ni = 0; - - for (int i = 0; i < n; i++) - { - nbits[i] = 0; - if (freqs[i] != 0) - { - nbits[i] = numBits[ni++]; - } - } - } - - /// - /// Writes this LZ77 prefix code into the bit-stream. - /// - /// The bit-stream to write the prefix code into. - public void Write(BitWriter writer) - { - Span codeLengthCounts = stackalloc ulong[32].Slice(0, 18); - codeLengthCounts.Clear(); - codeLengthCounts[17] = 3 + (2 * (NumLz77 - 1)); - - for (int i = 0; i < 19; i++) - { - byte rawLength = this.RawLengths[i]; - - codeLengthCounts[rawLength]++; - } - - for (int i = 0; i < 33; i++) - { - byte lz77Length = this.Lz77Lengths[i]; - - codeLengthCounts[lz77Length]++; - } - - // Lengths for representing the code length - Span codeLengthLengths = stackalloc byte[32].Slice(0, 18); - Span codeLengthLengthsMinimum = stackalloc byte[32].Slice(0, 18); - Span codeLengthLengthsMaximum = stackalloc byte[32].Slice(0, 18); - - codeLengthLengths.Clear(); - codeLengthLengthsMinimum.Clear(); - codeLengthLengthsMaximum.Fill(5); - - ComputeCodeLengths(codeLengthCounts, 18, codeLengthLengthsMinimum, codeLengthLengthsMaximum, codeLengthLengths); - - writer.Write(2, 0b00); // HSKIP = 0 (Don't skip code lengths) - - // As per Brotli RFC - Span codeLengthOrder = [1, 2, 3, 4, 0, 5, 17, 6, 16, - 7, 8, 9, 10, 11, 12, 13, 14, 15]; - - // Lengths & codes for representing lengths of code lengths - Span codeLengthLengthLengths = [2, 4, 3, 2, 2, 4]; - Span codeLengthLengthCodes = [0, 7, 3, 2, 1, 15]; - - // Maximum number of code lengths - int numCodeLengths = 18; - while (codeLengthLengths[codeLengthOrder[numCodeLengths - 1]] == 0) - { - numCodeLengths--; - } - - // Max bits written in this loop: 18 * 4 = 72 - for (int i = 0; i < numCodeLengths; i++) - { - int symbol = codeLengthLengths[codeLengthOrder[i]]; - writer.Write(codeLengthLengthLengths[symbol], codeLengthLengthCodes[symbol]); - } - - Span codeLengthBits = stackalloc ushort[32].Slice(0, 18); - codeLengthBits.Clear(); - ComputeCanonicalCode([], [], codeLengthLengths, codeLengthBits); - - for (int i = 0; i < 19; i++) - { - byte rawLength = this.RawLengths[i]; - - writer.Write(codeLengthLengths[rawLength], codeLengthBits[rawLength]); - } - - int numLz77 = NumLz77; - while (this.Lz77Lengths[numLz77 - 1] == 0) - { - numLz77--; - } - - // Max bits in this block: 24 - writer.Write(codeLengthLengths[17], codeLengthBits[17]); - writer.Write(3, 0b010); // 5 - writer.Write(codeLengthLengths[17], codeLengthBits[17]); - writer.Write(3, 0b000); // (5 - 2) * 8 + 3 = 27 - writer.Write(codeLengthLengths[17], codeLengthBits[17]); - writer.Write(3, 0b010); // (27 - 2) * 8 + 5 = 205 - - // Encode LZ77 symbols with values 224 + i. - // Max. bits in this loop: 33 * 5 = 165 - for (int i = 0; i < numLz77; i++) - { - writer.Write(codeLengthLengths[this.Lz77Lengths[i]], codeLengthBits[this.Lz77Lengths[i]]); - } - } - } - - /// - /// Simple MSB-first bit-stream writer implementation built on top - /// of a stream. - /// - /// Output bytes are written here. - private sealed class BitWriter(Stream stream) : IDisposable - { - /// - /// Temporary cache used to store pending written bits - /// before they're written to the output stream. - /// - private ulong buffer; - - /// - /// Gets the total number of bytes written to the output buffer so far. - /// - public long BytesWritten { get; private set; } - - /// - /// Gets the number of bits actively in the bit cache. - /// This is used to track how many bits were written into - /// the cache prior to sending the cache to the stream. - /// - public int BitsInBuffer { get; private set; } - - /// - /// Writes the specified bits in the Most Significant Byte (MSB) - /// order. - /// - /// Represents the number of bits to write to the bit-stream. - /// Represents the value to write to the bit-stream. - public void Write(int count, ulong bits) - { - DebugGuard.MustBeBetweenOrEqualTo(count, 0, 56, nameof(count)); - - if (count < 64) - { - bits &= (1UL << count) - 1; - } - - this.buffer |= bits << this.BitsInBuffer; - this.BitsInBuffer += count; - - this.FlushBytes(); - } - - /// - /// Internal method used to flush bytes from the cache - /// () into the output stream. - /// - private void FlushBytes() - { - int bytes = this.BitsInBuffer / 8; - - for (int i = 0; i < bytes; i++) - { - stream.WriteByte((byte)this.buffer); - this.BytesWritten++; - this.buffer >>= 8; - } - - this.BitsInBuffer -= bytes * 8; - } - - /// - /// Used by the dispose method to flush the remaining bits - /// that are not byte-aligned. F.e. if we dispose this reader - /// and we have 5 bits left, those final 5 bits are set to all 0 - /// and the byte is written to the stream. - /// - public void ZeroPadToByte() - { - if (this.BitsInBuffer != 0) - { - this.Write(8 - this.BitsInBuffer, 0); - } - } - - /// - /// Flushes out the final bytes. - /// - public void Dispose() => this.ZeroPadToByte(); - } -} From 030fb54526493bba979c7a7c01100fb6f906bf6a Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 10 Sep 2026 05:35:16 +0400 Subject: [PATCH 167/328] Add coeff order encoder --- .../Encoder/JxlCoefficientOrderEncoder.cs | 422 ++++++++++++++++++ .../Jxl/Processing/JxlCoefficientOrder.cs | 2 +- 2 files changed, 423 insertions(+), 1 deletion(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlCoefficientOrderEncoder.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlCoefficientOrderEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlCoefficientOrderEncoder.cs new file mode 100644 index 0000000000..98171f340a --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlCoefficientOrderEncoder.cs @@ -0,0 +1,422 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; + +internal static class JxlCoefficientOrderEncoder +{ + private struct PosAndCount + { + public uint Pos; + public ulong CountAndIdx; + } + + private static (uint UsedOrders, uint CustomizeOrders) ComputeUsedOrders(JxlSpeedTier speed, JxlAcStrategyImage acStrategy, Rectangle rect) + { + if (speed >= JxlSpeedTier.Falcon) + { + return (1, 1); + } + + uint ret = 0; + uint retCustomize = 0; + int xSizeBlocks = rect.Width; + int ySizeBlocks = rect.Height; + + for (int by = 0; by < ySizeBlocks; by++) + { + JxlAcStrategyRow acsRow = acStrategy.GetRow(rect, by); + + for (int bx = 0; bx < xSizeBlocks; bx++) + { + byte ord = JxlCoefficientOrder.StrategyOrder[acsRow[bx].RawStrategy]; + + ret |= 1u << ord; + + if (ord > 6) + { + continue; + } + + retCustomize |= 1u << ord; + } + } + + if (acStrategy.XSize < 5 && acStrategy.YSize < 5) + { + return (ret, 0); + } + + return (ret, retCustomize); + } + + private static bool ComputeCoeffOrder(JxlSpeedTier speed, IJxlDctAcImage acImage, JxlAcStrategyImage acStrategy, JxlFrameDimensions frameDim, ref uint allUsedOrders, uint prevUsedAcs, uint currentUsedAcs, uint currentUsedOrders, Span order) + { + int[] numZeros = new int[JxlCoefficientOrder.CoefficientOrderMaxSize]; + double blockFraction = 1.0; + + if (speed >= JxlSpeedTier.Squirrel && currentUsedOrders == 1) + { + blockFraction = 0.5; + } + + if (currentUsedOrders != 0) + { + ulong threshold = uint.MaxValue * (ulong)blockFraction; + + ulong s0 = 0x94D049BB133111EBUL; + ulong s1 = 0xBF58476D1CE4E5B9UL; + + bool UseSample() + { + ulong s = s0; + ulong s0Local = s1; + ulong bits = s + s0Local; + + s0 = s0Local; + + s ^= s << 23; + s ^= s0Local ^ (s >> 18) ^ (s0Local >> 5); + s1 = s; + + return (bits >> 32) <= threshold; + } + + for (int groupIndex = 0; groupIndex < frameDim.NumGroups; ++groupIndex) + { + int gx = groupIndex % frameDim.XSizeGroups; + int gy = groupIndex / frameDim.XSizeGroups; + + Rectangle rect = RectangleUtils.CreateRectangle( + gx * JxlFrameDimensions.GroupDimensionsInBlocks, + gy * JxlFrameDimensions.GroupDimensionsInBlocks, + JxlFrameDimensions.GroupDimensionsInBlocks, + JxlFrameDimensions.GroupDimensionsInBlocks, + frameDim.XSizeBlocks, + frameDim.YSizeBlocks); + + int acOffset = 0; + JxlDctAcType type = acImage.Type; + + for (int by = 0; by < rect.Height; ++by) + { + JxlAcStrategyRow acsRow = acStrategy.GetRow(rect, by); + + for (int bx = 0; bx < rect.Width; ++bx) + { + JxlAcStrategy acs = acsRow[bx]; + + if (!acs.IsFirstBlock) + { + continue; + } + + if (!UseSample()) + { + continue; + } + + int size = JxlFrameDimensions.DctBlockSize << acs.Log2CoveredBlocks; + + for (int c = 0; c < 3; ++c) + { + int orderOffset = JxlCoefficientOrder.CoeffOrderOffset(JxlCoefficientOrder.StrategyOrder[acs.RawStrategy], c); + + if (type == JxlDctAcType.Ac16) + { + for (int k = 0; k < size; ++k) + { + bool isZero = acImage.GetPlaneRow(c, groupIndex, 0).Pointer16[acOffset + k] == 0; + if (isZero) + { + ++numZeros[orderOffset + k]; + } + } + } + else + { + for (int k = 0; k < size; ++k) + { + bool isZero = acImage.GetPlaneRow(c, groupIndex, 0).Pointer32[acOffset + k] == 0; + if (isZero) + { + ++numZeros[orderOffset + k]; + } + } + } + + int cx = acs.CoveredBlocksX; + int cy = acs.CoveredBlocksY; + + JxlForwardCoefficientOrder.CoefficientLayout(ref cy, ref cx); + + for (int iy = 0; iy < cy; ++iy) + { + for (int ix = 0; ix < cx; ++ix) + { + numZeros[orderOffset + (iy * JxlFrameDimensions.BlockDimensions * cx) + ix] = -1; + } + } + } + + acOffset += size; + } + } + } + } + + PosAndCount[] posAndVal = new PosAndCount[JxlAcStrategy.MaximumCoefficientArea]; + int[] naturalOrderBuffer = []; + + ushort computed = 0; + + for (byte o = 0; o < JxlAcStrategy.NumberOfValidStrategies; ++o) + { + byte ord = JxlCoefficientOrder.StrategyOrder[o]; + + if ((computed & (1 << ord)) != 0) + { + continue; + } + + computed |= (ushort)(1 << ord); + + JxlAcStrategy acs = new(o); + + int sz = JxlFrameDimensions.DctBlockSize * acs.CoveredBlocksX * acs.CoveredBlocksY; + + if (sz > (1 << 16)) + { + throw new InvalidOperationException("Inavlid size"); + } + + if (((1u << ord) & ~currentUsedAcs) != 0) + { + continue; + } + + if (((1u << ord) & prevUsedAcs) != 0) + { + continue; + } + + if (((1u << ord) & allUsedOrders) != 0) + { + continue; + } + + if (naturalOrderBuffer.Length < sz) + { + // Not sure how large can sz get... + naturalOrderBuffer = new int[sz]; + } + + acs.ComputeNaturalCoefficientOrder(naturalOrderBuffer); + + if (((1u << ord) & ~currentUsedOrders) != 0) + { + for (int c = 0; c < 3; ++c) + { + int offset = JxlCoefficientOrder.CoeffOrderOffset(ord, c); + + if (JxlCoefficientOrder.CoeffOrderOffset(ord, c + 1) - offset != sz) + { + throw new InvalidOperationException("Invalid coefficient order"); + } + + naturalOrderBuffer.AsSpan(0, sz).CopyTo(order.Slice(offset, sz)); + } + + continue; + } + + bool isNonDefault = false; + + for (byte c = 0; c < 3; ++c) + { + int offset = JxlCoefficientOrder.CoeffOrderOffset(ord, c); + + if (JxlCoefficientOrder.CoeffOrderOffset(ord, c + 1) - offset != sz) + { + throw new InvalidOperationException("Invalid coefficient order"); + } + + float invSqrtSz = 1.0f / MathF.Sqrt(sz); + + for (int i = 0; i < sz; ++i) + { + int pos = naturalOrderBuffer[i]; + + posAndVal[i].Pos = (uint)pos; + ulong count = (ulong)((numZeros[offset + pos] * invSqrtSz) + 0.1f); + + if (count >= 1uL << 48) + { + throw new InvalidOperationException("Count is too large"); + } + + posAndVal[i].CountAndIdx = (count << 16) | (uint)i; + } + + Array.Sort(posAndVal, 0, sz, PosAndCountComparer.Instance); + + for (int i = 0; i < sz; ++i) + { + order[offset + i] = (int)posAndVal[i].Pos; + + isNonDefault |= naturalOrderBuffer[i] != posAndVal[i].Pos; + } + } + + if (!isNonDefault) + { + currentUsedOrders &= ~(1u << ord); + } + } + + allUsedOrders |= currentUsedOrders; + return true; + } + + private static bool TokenizePermutation(ReadOnlySpan order, int skip, int size, List tokens) + { + uint[] lehmer = new uint[size]; + uint[] temp = new uint[size + 1]; + + if (!JxlLehmerCode.ComputeLehmerCode(order, temp, size, lehmer)) + { + return false; + } + + int end = size; + while (end > skip && lehmer[end - 1] == 0) + { + --end; + } + + tokens.Add(new JxlToken((JxlMaTreeContext)JxlCoefficientOrder.CoeffOrderContext((uint)size), (uint)(end - skip))); + + uint last = 0; + for (int i = skip; i < end; ++i) + { + tokens.Add(new JxlToken((JxlMaTreeContext)JxlCoefficientOrder.CoeffOrderContext(last), lehmer[i])); + last = lehmer[i]; + } + + return true; + } + + private static bool EncodePermutation(ReadOnlySpan order, int skip, int size, JxlBitWriter writer) + { + List[] tokens = [[]]; + + if (!TokenizePermutation(order, skip, size, tokens[0])) + { + return false; + } + + JxlEntropyEncodingData codes = new(); + + if (!BuildAndEncodeHistograms(configuration, new JxlHistogramParameters(), PermutationContext, tokens, out codes, writer, out _)) + { + return false; + } + + return WriteTokens(tokens[0], codes, 0, writer); + } + + private static bool EncodeCoeffOrder(ReadOnlySpan order, JxlAcStrategy acs, List tokens, Span orderZigzag, Span naturalOrderLut) + { + int llf = acs.CoveredBlocksX * acs.CoveredBlocksY; + int size = JxlFrameDimensions.DctBlockSize * llf; + + for (int i = 0; i < size; ++i) + { + orderZigzag[i] = (uint)naturalOrderLut[(int)order[i]]; + } + + return TokenizePermutation(orderZigzag[..size], llf, size, tokens); + } + + public static bool EncodeCoeffOrders(ushort usedOrders, ReadOnlySpan order, JxlBitWriter writer, JxlLayerType layer, JxlAuxiliaryOutput auxOut) + { + int memSize = JxlAcStrategy.MaximumCoefficientArea; + + // TODO: pool this? or allocate with Configuration.MemoryAllocator? memSize=65536 + uint[] orderZigzag = new uint[memSize]; + + ushort computed = 0; + List[] tokens = [[]]; + int[] naturalOrderLut = []; + + for (byte o = 0; o < JxlAcStrategy.NumberOfValidStrategies; ++o) + { + byte ord = JxlCoefficientOrder.StrategyOrder[o]; + + if ((computed & (1 << ord)) != 0) + { + continue; + } + + computed |= (ushort)(1 << ord); + + if ((usedOrders & (1 << ord)) == 0) + { + continue; + } + + JxlAcStrategy acs = new(o); + + int llf = acs.CoveredBlocksX * acs.CoveredBlocksY; + int size = JxlFrameDimensions.DctBlockSize * llf; + + if (naturalOrderLut.Length < size) + { + naturalOrderLut = new int[size]; + } + + acs.ComputeNaturalCoefficientOrderLookup(naturalOrderLut); + + for (int c = 0; c < 3; ++c) + { + int offset = JxlCoefficientOrder.CoeffOrderOffset(ord, c); + + if (!EncodeCoeffOrder(order.Slice(offset, size), acs, tokens[0], orderZigzag.AsSpan(0, size), naturalOrderLut)) + { + return false; + } + } + } + + if (usedOrders != 0) + { + JxlEntropyEncodingData codes = new(); + + if (!BuildAndEncodeHistograms(configuration, new JxlHistogramParameters(), PermutationContexts, tokens, out codes, writer, layer, auxOut, out _)) + { + return false; + } + + if (!WriteTokens(tokens[0], codes, 0, writer, layer, auxOut)) + { + return false; + } + } + + return true; + } + + private sealed class PosAndCountComparer : IComparer + { + public static readonly PosAndCountComparer Instance = new(); + + public int Compare(PosAndCount a, PosAndCount b) => a.CountAndIdx.CompareTo(b.CountAndIdx); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs index c240746a3a..ac3f377741 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs @@ -41,7 +41,7 @@ internal static class JxlCoefficientOrder ]; [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int CoeffOrderOffset(int o, int c) => CoefficientOrderOffsets[(3 * o) + c] * JxlFrameDimensions.DctBlockSize; + public static int CoeffOrderOffset(int o, int c) => CoefficientOrderOffsets[(3 * o) + c] * JxlFrameDimensions.DctBlockSize; [MethodImpl(MethodImplOptions.AggressiveInlining)] public static uint CoeffOrderContext(uint value) From e81085cc833833776e45b9ea95f3dbd8f89ec73b Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 10 Sep 2026 05:43:38 +0400 Subject: [PATCH 168/328] Implement remaining SIMD utils in JXL --- .../Formats/Jxl/Processing/JxlSimdUtils.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs index 042406649d..750f910383 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using System.Numerics.Tensors; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; @@ -14,6 +15,8 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing; /// internal static partial class JxlSimdUtils { + public static int MaxVectorSize => Vector.Count * sizeof(float); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 ConcatLowerLower(Vector256 a, Vector256 b) where T : unmanaged => Vector256.Create(a.GetLower(), b.GetLower()); @@ -505,6 +508,39 @@ public static Vector FastPowf( Vector exponent) => FastPow2f(Vector.Log2(@base) * exponent); + public static uint MaxValue(Span data) + { + int lanes = Vector.Count; + int lastFull = lanes * (data.Length / lanes); + + Vector max = Vector.Zero; + ref uint dataRef = ref MemoryMarshal.GetReference(data); + + for (int i = 0; i < lastFull; i += lanes) + { + max = Vector.Max(max, Vector.LoadUnsafe(ref Unsafe.Add(ref dataRef, i))); + } + + if (lastFull < data.Length) + { + Vector stop = Vector.Create((uint)data.Length); + Vector fence = Iota((uint)lastFull); + Vector take = Vector.LessThan(fence, stop); + max = Vector.Max( + max, + Vector.ConditionalSelect( + take, + Vector.LoadUnsafe(ref Unsafe.Add(ref dataRef, lastFull)), + Vector.Zero)); + } + + // The following part is to find the largest number in + // the vector. + Span copy = stackalloc uint[Vector.Count]; + max.CopyTo(copy); + return TensorPrimitives.Max(copy); + } + /// /// Incrementing values to compute the Iota function. /// From 1a23a1a28224d1537d0b7b46a9a76b3f521b0955 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 10 Sep 2026 06:05:04 +0400 Subject: [PATCH 169/328] Add TOC encoder --- .../Encoder/JxlCoefficientOrderEncoder.cs | 2 +- .../Jxl/Processing/Encoder/JxlTocEncoder.cs | 55 +++++++++++++++++++ .../Formats/Jxl/Processing/JxlToc.cs | 16 ++++-- 3 files changed, 67 insertions(+), 6 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlTocEncoder.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlCoefficientOrderEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlCoefficientOrderEncoder.cs index 98171f340a..650717ee7d 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlCoefficientOrderEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlCoefficientOrderEncoder.cs @@ -313,7 +313,7 @@ private static bool TokenizePermutation(ReadOnlySpan order, int skip, int return true; } - private static bool EncodePermutation(ReadOnlySpan order, int skip, int size, JxlBitWriter writer) + public static bool EncodePermutation(ReadOnlySpan order, int skip, int size, JxlBitWriter writer) { List[] tokens = [[]]; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlTocEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlTocEncoder.cs new file mode 100644 index 0000000000..c0c38df9fb --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlTocEncoder.cs @@ -0,0 +1,55 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.Fields; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; + +internal static class JxlTocEncoder +{ + public static void WriteTocPermutation(Memory permutation, JxlBitWriter writer) + { + int nPermutation = permutation.Length; + + _ = writer.WithMaxBits((ulong)JxlToc.MaxBits(0), () => + { + if (nPermutation > 0) + { + writer.Write(1, 1); // permutation present + if (!JxlCoefficientOrderEncoder.EncodePermutation( + MemoryMarshal.Cast(permutation.Span), + 0, + permutation.Length, + writer)) + { + return false; + } + } + else + { + writer.Write(1, 0); // no permutation + } + + writer.ZeroPadToByte(); + return true; + }); + } + + public static void WriteTocSizes(Memory groupSizes, JxlBitWriter writer) => + _ = writer.WithMaxBits((ulong)JxlToc.MaxBits(groupSizes.Length), () => + { + Span groupSizesSpan = groupSizes.Span; + + for (int i = 0; i < groupSizesSpan.Length; i++) + { + if (!JxlU32Coder.Write(in JxlToc.TocDistribution, unchecked((uint)groupSizesSpan[i]), writer)) + { + return false; + } + } + + writer.ZeroPadToByte(); + return true; + }); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlToc.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlToc.cs index b6dfd4e39f..d3de060b83 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlToc.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlToc.cs @@ -11,12 +11,22 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing; /// internal static class JxlToc { - private static readonly JxlU32Enc TocDistribution = new( + private const int BitsPerByte = 8; + + private const int MaxTocEntries = 65536; + + public static readonly JxlU32Enc TocDistribution = new( JxlFieldExpressions.Bits(10), JxlFieldExpressions.BitsOffset(14, 2024), JxlFieldExpressions.BitsOffset(22, 17408), JxlFieldExpressions.BitsOffset(30, 4211712)); + public static int MaxBits(int numSizes) + { + int entryBits = JxlU32Coder.MaxEncodedBits(TocDistribution) * numSizes; + return 1 + JxlMath.BitsPerByte + entryBits + JxlMath.BitsPerByte; + } + public static int AcGroupIndex(int pass, int group, int numGroups, int numDcGroups) => 2 + numDcGroups + (pass * numGroups) + group; @@ -30,10 +40,6 @@ public static int NumberOfTocEntries(int numGroups, int numDcGroups, int numPass return AcGroupIndex(0, 0, numGroups, numDcGroups) + (numGroups * numPasses); } - private const int BitsPerByte = 8; - - private const int MaxTocEntries = 65536; - public static bool ReadToc( Configuration configuration, int tocEntries, From 5223e06b7023daf2bdb7843b20f93f311d41a993 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 10 Sep 2026 06:36:41 +0400 Subject: [PATCH 170/328] Add JPEG XL optimize --- .../Encoder/JxlTransformsEncoder.cs | 3 - .../Optimize/JxlOptimizeArrayFactory{T}.cs | 12 ++ .../Encoder/Optimize/JxlOptimizeArray{T}.cs | 204 ++++++++++++++++++ .../Optimize/JxlOptimizeFunction{T}.cs | 9 + 4 files changed, 225 insertions(+), 3 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Optimize/JxlOptimizeArrayFactory{T}.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Optimize/JxlOptimizeArray{T}.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Optimize/JxlOptimizeFunction{T}.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlTransformsEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlTransformsEncoder.cs index d7c7b63b4b..f3565a75d9 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlTransformsEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlTransformsEncoder.cs @@ -3,9 +3,6 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; -/// -/// TODO -/// internal static class JxlTransformsEncoder { } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Optimize/JxlOptimizeArrayFactory{T}.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Optimize/JxlOptimizeArrayFactory{T}.cs new file mode 100644 index 0000000000..8b14fad0d4 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Optimize/JxlOptimizeArrayFactory{T}.cs @@ -0,0 +1,12 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Optimize; + +internal class JxlOptimizeArrayFactory(Configuration configuration, int length) + where T : unmanaged, INumber +{ + public JxlOptimizeArray CreateArray(T defaultValue = default) => new(configuration, length, defaultValue); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Optimize/JxlOptimizeArray{T}.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Optimize/JxlOptimizeArray{T}.cs new file mode 100644 index 0000000000..2a89b1cd4b --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Optimize/JxlOptimizeArray{T}.cs @@ -0,0 +1,204 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Numerics; +using System.Numerics.Tensors; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Optimize; + +internal struct JxlOptimizeArray : IDisposable + where T : unmanaged, INumber +{ + private readonly IMemoryOwner memoryOwner; + private readonly int length; + private readonly Configuration configuration; + private bool isDisposed; + + public JxlOptimizeArray(Configuration configuration, int length, T value) + { + this.memoryOwner = configuration.MemoryAllocator.Allocate(length); + this.memoryOwner.Memory.Span.Fill(value); + this.length = length; + this.configuration = configuration; + } + + public readonly Span Span => this.memoryOwner.Memory.Span; + + public readonly ref T this[int index] + { + get + { + DebugGuard.MustBeLessThan(index, this.length, nameof(index)); + return ref this.memoryOwner.Memory.Span[index]; + } + } + + public static JxlOptimizeArray operator +(JxlOptimizeArray a, JxlOptimizeArray b) + { + JxlOptimizeArray z = new(a.configuration, a.length, default); + TensorPrimitives.Add(a.Span, b.Span, z.Span); + return z; + } + + public static JxlOptimizeArray operator -(JxlOptimizeArray a, JxlOptimizeArray b) + { + JxlOptimizeArray z = new(a.configuration, a.length, default); + TensorPrimitives.Subtract(a.Span, b.Span, z.Span); + return z; + } + + public static JxlOptimizeArray operator *(T a, JxlOptimizeArray b) + { + JxlOptimizeArray z = new(b.configuration, b.length, default); + TensorPrimitives.Multiply(b.Span, a, z.Span); + return z; + } + + public readonly T DotProduct(JxlOptimizeArray y) => TensorPrimitives.Dot(this.Span, y.Span); + + public static JxlOptimizeArray OptimizeWithScaledConjugateGradientMethod( + JxlOptimizeArrayFactory factory, + JxlOptimizeFunction function, + JxlOptimizeArray w0, + T gradientNormalThreshold, + int maxIterations) + where T : unmanaged, INumber, + IBinaryFloatingPointIeee754 + { + // These variable names look cryptic, but they're part of + // the Scaled Conjugate Gradient method. See the reference + // implementation: + // https://github.com/libjxl/libjxl/blob/main/lib/jxl/enc_optimize.h#L81-L188 + int n = w0.length; + T rsqThreshold = gradientNormalThreshold * gradientNormalThreshold; + + T sigma0 = T.CreateSaturating(0.0001); + T lMin = T.CreateSaturating(1.0e-15); + T lMax = T.CreateSaturating(1.0e15); + + JxlOptimizeArray w = w0; + + JxlOptimizeArray wp = factory.CreateArray(); + JxlOptimizeArray r = factory.CreateArray(); + JxlOptimizeArray rt = factory.CreateArray(); + JxlOptimizeArray e = factory.CreateArray(); + JxlOptimizeArray p = factory.CreateArray(); + + T psq = default; + T fp; + T D = default; + T d; + T m = default; + T a; + T b; + T s; + T t = default; + + T fw = function(w, ref r); + T rsq = r.DotProduct(r); + e = r; + p = r; + + T l = T.CreateSaturating(1.0); + bool success = true; + long nSuccess = 0; + long k = 0; + + // Hot loop + while (k++ < maxIterations) + { + if (success) + { + m = -p.DotProduct(r); + + if (m >= T.Zero) + { + p = r; + m = -p.DotProduct(r); + } + + psq = p.DotProduct(p); + s = sigma0 / T.Sqrt(psq); + _ = function(w + (s * p), ref rt); + t = p.DotProduct(r - rt) / s; + } + + d = t + (l * psq); + if (d <= T.Zero) + { + d = l * psq; + l -= t / psq; + } + + a = -m / d; + wp = w + (a * p); + fp = function(wp, ref rt); + + D = T.CreateSaturating(2.0) * (fp - fw) / (a * m); + if (D >= T.Zero) + { + success = true; + nSuccess++; + w = wp; + } + else + { + success = false; + } + + if (success) + { + e = r; + r = rt; + rsq = r.DotProduct(r); + fw = fp; + + if (rsq <= rsqThreshold) + { + break; + } + } + + if (D < T.CreateSaturating(0.25)) + { + l = T.Min(T.CreateSaturating(4) * l, lMax); + } + else if (D > T.CreateSaturating(0.75)) + { + l = T.Max(T.CreateSaturating(0.25) * l, lMax); + } + + if ((nSuccess % n) == 0) + { + p = r; + l = T.CreateSaturating(1.0); + } + else if (success) + { + b = (e - r).DotProduct(r) / m; + p = (b * p) + r; + } + } + + // clean up + wp.Dispose(); + r.Dispose(); + rt.Dispose(); + e.Dispose(); + p.Dispose(); + + return w; + } + + public void Dispose() + { + if (this.isDisposed) + { + return; + } + + this.isDisposed = true; + this.memoryOwner.Dispose(); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Optimize/JxlOptimizeFunction{T}.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Optimize/JxlOptimizeFunction{T}.cs new file mode 100644 index 0000000000..be9533b148 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Optimize/JxlOptimizeFunction{T}.cs @@ -0,0 +1,9 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Optimize; + +internal delegate T JxlOptimizeFunction(JxlOptimizeArray a, ref JxlOptimizeArray b) + where T : unmanaged, INumber; 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 171/328] 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); + } + } +} From 37766b8d1a611def002bd8f649502820a376b792 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 10 Sep 2026 07:44:53 +0400 Subject: [PATCH 172/328] Implement field visitor for metadata --- .../Jxl/IO/Metadata/JxlExtraChannelInfo.cs | 131 ++++++++- .../Jxl/IO/Metadata/JxlImageMetadata.cs | 278 ++++++++++++++++-- .../Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs | 70 ++++- .../Formats/Jxl/IO/Metadata/JxlToneMapping.cs | 90 +++++- 4 files changed, 532 insertions(+), 37 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs index 1ad671969e..2d53a5f380 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs @@ -1,39 +1,152 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Fields; namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; internal sealed class JxlExtraChannelInfo : IJxlFields { - public bool AllDefault { get; set; } + private bool allDefault; + private JxlExtraChannel type; + private JxlBitDepthMetadata? bitDepth; + private int dimensionShift; + private string? name; + private bool alphaAssociated; + private InlineArray4 spotColor; + private int cfaChannel; - public JxlExtraChannel Type { get; set; } + public bool AllDefault + { + get => this.allDefault; + set => this.allDefault = value; + } + + public JxlExtraChannel Type + { + get => this.type; + set => this.type = value; + } - public JxlBitDepthMetadata? BitDepth { get; set; } + public JxlBitDepthMetadata? BitDepth + { + get => this.bitDepth; + set => this.bitDepth = value; + } - public int DimensionShift { get; set; } + public int DimensionShift + { + get => this.dimensionShift; + set => this.dimensionShift = value; + } - public string? Name { get; set; } + public string? Name + { + get => this.name; + set => this.name = value; + } - public bool AlphaAssociated { get; set; } + public bool AlphaAssociated + { + get => this.alphaAssociated; + set => this.alphaAssociated = value; + } - public InlineArray4 SpotColor { get; set; } + public InlineArray4 SpotColor + { + get => this.spotColor; + set => this.spotColor = value; + } - public int CfaChannel { get; set; } + public int CfaChannel + { + get => this.cfaChannel; + set => this.cfaChannel = value; + } public bool Visit(JxlVisitor visitor) { if (visitor.AllDefault(this, ref this.allDefault)) { + // Overwrite all serialized fields, but not any nonserialized_*. visitor.SetDefault(this); return true; } - if (!visitor.Enum(JxlExtraChannel.Alpha, ref this.Type)) + // General + if (!visitor.Enum(JxlExtraChannel.Alpha, ref this.type)) + { + return false; + } + + if (!visitor.VisitNested(this.bitDepth!)) { return false; } + + if (!visitor.U32( + JxlFieldExpressions.Value(0u), + JxlFieldExpressions.Value(3u), + JxlFieldExpressions.Value(4u), + JxlFieldExpressions.BitsOffset(3u, 1u), + 0u, + ref Unsafe.As(ref this.dimensionShift))) + { + return false; + } + + if ((1u << this.dimensionShift) > 8u) + { + return false; + } + + if (!VisitNameString(visitor, ref this.name)) + { + return false; + } + + // Conditional + if (visitor.Conditional(this.type == JxlExtraChannel.Alpha)) + { + if (!visitor.Boolean(false, ref this.alphaAssociated)) + { + return false; + } + } + + if (visitor.Conditional(this.type == JxlExtraChannel.SpotColor)) + { + for (int i = 0; i < 4; i++) + { + if (!visitor.F16(0F, ref this.spotColor[i])) + { + return false; + } + } + } + + if (visitor.Conditional(this.type == JxlExtraChannel.Cfa)) + { + if (!visitor.U32( + JxlFieldExpressions.Value(1u), + JxlFieldExpressions.Bits(2u), + JxlFieldExpressions.BitsOffset(4u, 3u), + JxlFieldExpressions.BitsOffset(8u, 19u), + 1u, + ref Unsafe.As(ref this.cfaChannel))) + { + return false; + } + } + + if (this.type is JxlExtraChannel.Unknown or + >= JxlExtraChannel.Reserved0 and + <= JxlExtraChannel.Reserved7) + { + return false; + } + + return true; } } diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs index 9b8d6e220e..500fefb1c8 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Cms; using SixLabors.ImageSharp.Formats.Jxl.Fields; @@ -8,37 +9,120 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; internal sealed class JxlImageMetadata : IJxlFields { - public bool AllDefault { get; set; } + private bool allDefault; + private JxlBitDepthMetadata? bitDepth; + private bool modular16BitBufferSufficient; + private bool xybEncoded; + private JxlColorEncoding? colorEncoding; + private int orientation = 1; + private bool havePreview; + private bool haveAnimation; + private bool haveIntrinsicSize; + private JxlSizeHeader intrinsicSize = new(); + private JxlToneMapping? toneMapping; + private int extraChannelCount; + private List extraChannels = []; + private JxlPreviewHeader previewSize = new(); + private JxlAnimationHeader animation = new(); + private long extensions; + + public JxlImageMetadata() => JxlBundle.Init(this); + + public bool AllDefault + { + get => this.allDefault; + set => this.allDefault = value; + } - public JxlBitDepthMetadata? BitDepth { get; set; } + public JxlBitDepthMetadata? BitDepth + { + get => this.bitDepth; + set => this.bitDepth = value; + } - public bool Modular16BitBufferSufficient { get; set; } // Otherwise, 32 is + public bool Modular16BitBufferSufficient + { + get => this.modular16BitBufferSufficient; + set => this.modular16BitBufferSufficient = value; + } - public bool XybEncoded { get; set; } + public bool XybEncoded + { + get => this.xybEncoded; + set => this.xybEncoded = value; + } - public JxlColorEncoding? ColorEncoding { get; set; } + public JxlColorEncoding? ColorEncoding + { + get => this.colorEncoding; + set => this.colorEncoding = value; + } - public int Orientation { get; set; } = 1; + public int Orientation + { + get => this.orientation; + set => this.orientation = value; + } - public bool HavePreview { get; set; } + public bool HavePreview + { + get => this.havePreview; + set => this.havePreview = value; + } - public bool HaveAnimation { get; set; } + public bool HaveAnimation + { + get => this.haveAnimation; + set => this.haveAnimation = value; + } - public bool HaveIntrinsicSize { get; set; } + public bool HaveIntrinsicSize + { + get => this.haveIntrinsicSize; + set => this.haveIntrinsicSize = value; + } - public JxlSizeHeader IntrinsicSize { get; set; } = new(); + public JxlSizeHeader IntrinsicSize + { + get => this.intrinsicSize; + set => this.intrinsicSize = value; + } - public JxlToneMapping? ToneMapping { get; set; } + public JxlToneMapping? ToneMapping + { + get => this.toneMapping; + set => this.toneMapping = value; + } - public int ExtraChannelCount { get; set; } + public int ExtraChannelCount + { + get => this.extraChannelCount; + set => this.extraChannelCount = value; + } - public List ExtraChannels { get; set; } = []; + public List ExtraChannels + { + get => this.extraChannels; + set => this.extraChannels = value; + } - public JxlPreviewHeader PreviewSize { get; set; } = new(); + public JxlPreviewHeader PreviewSize + { + get => this.previewSize; + set => this.previewSize = value; + } - public JxlAnimationHeader Animation { get; set; } = new(); + public JxlAnimationHeader Animation + { + get => this.animation; + set => this.animation = value; + } - public long Extensions { get; set; } + public long Extensions + { + get => this.extensions; + set => this.extensions = value; + } public bool NonserializedOnlyParseBasicInfos { get; set; } @@ -148,5 +232,165 @@ public void SetIntensityTarget() } } - public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); + public bool Visit(JxlVisitor visitor) + { + if (visitor.AllDefault(this, ref this.allDefault)) + { + // Overwrite all serialized fields, but not any nonserialized_*. + visitor.SetDefault(this); + return true; + } + + // Bundle.AllDefault does not allow usage when reading because it may abort + // when a codestream has invalid values. When reading, extraFields is + // overwritten below, so AllDefault is not needed. + bool toneMappingDefault = !visitor.IsReading && JxlBundle.AllDefault(this.toneMapping!); + + bool extraFields = this.orientation != 1 || + this.havePreview || + this.haveAnimation || + this.haveIntrinsicSize || + !toneMappingDefault; + + if (!visitor.Boolean(false, ref extraFields)) + { + return false; + } + + if (visitor.Conditional(extraFields)) + { + this.orientation--; + + if (!visitor.Bits(3, 0, ref Unsafe.As(ref this.orientation))) + { + return false; + } + + this.orientation++; + + // No bounds checking is necessary because exactly 3 bits are read. + if (!visitor.Boolean(false, ref this.haveIntrinsicSize)) + { + return false; + } + + if (visitor.Conditional(this.haveIntrinsicSize)) + { + if (!visitor.VisitNested(this.intrinsicSize)) + { + return false; + } + } + + if (!visitor.Boolean(false, ref this.havePreview)) + { + return false; + } + + if (visitor.Conditional(this.havePreview)) + { + if (!visitor.VisitNested(this.previewSize)) + { + return false; + } + } + + if (!visitor.Boolean(false, ref this.haveAnimation)) + { + return false; + } + + if (visitor.Conditional(this.haveAnimation)) + { + if (!visitor.VisitNested(this.animation)) + { + return false; + } + } + } + else + { + this.orientation = 1; + this.haveIntrinsicSize = false; + this.havePreview = false; + this.haveAnimation = false; + } + + if (!visitor.VisitNested(this.bitDepth!)) + { + return false; + } + + if (!visitor.Boolean(true, ref this.modular16BitBufferSufficient)) + { + return false; + } + + this.extraChannelCount = this.extraChannels.Count; + + if (!visitor.U32( + JxlFieldExpressions.Value(0u), + JxlFieldExpressions.Value(1u), + JxlFieldExpressions.BitsOffset(4u, 2u), + JxlFieldExpressions.BitsOffset(12u, 1u), + 0u, + ref Unsafe.As(ref this.extraChannelCount))) + { + return false; + } + + if (visitor.Conditional(this.extraChannelCount != 0)) + { + if (visitor.IsReading) + { + this.extraChannels.Clear(); + this.extraChannels.Capacity = this.extraChannelCount; + + for (int i = 0; i < this.extraChannelCount; i++) + { + this.extraChannels.Add(new JxlExtraChannelInfo()); + } + } + + for (int i = 0; i < this.extraChannels.Count; i++) + { + if (!visitor.VisitNested(this.extraChannels[i])) + { + return false; + } + } + } + + if (!visitor.Boolean(true, ref this.xybEncoded)) + { + return false; + } + + if (!visitor.VisitNested(this.colorEncoding!)) + { + return false; + } + + if (visitor.Conditional(extraFields)) + { + if (!visitor.VisitNested(this.toneMapping!)) + { + return false; + } + } + + // Treat as if only the fields up to extra channels exist. + if (visitor.IsReading && this.NonserializedOnlyParseBasicInfos) + { + return true; + } + + if (!visitor.BeginExtensions(ref Unsafe.As(ref this.extensions))) + { + return false; + } + + // Extensions: in chronological order of being added to the format. + return visitor.EndExtensions(); + } } diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs index 346e374148..9148e7dc77 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs @@ -11,13 +11,73 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; internal sealed class JxlOpsinInverseMatrix : IJxlFields { - public InlineArray3 OpsinBiases; + private InlineArray3 opsinBiases; + private InlineArray3 quantBiases; + private bool allDefault; + private JxlMatrix3x3F inverseMatrix; - public InlineArray3 QuantBiases; + public InlineArray3 OpsinBiases + { + get => this.opsinBiases; + set => this.opsinBiases = value; + } - public bool AllDefault { get; set; } + public InlineArray3 QuantBiases + { + get => this.quantBiases; + set => this.quantBiases = value; + } - public JxlMatrix3x3F InverseMatrix { get; set; } + public bool AllDefault + { + get => this.allDefault; + set => this.allDefault = value; + } - public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); + public JxlMatrix3x3F InverseMatrix + { + get => this.inverseMatrix; + set => this.inverseMatrix = value; + } + + public bool Visit(JxlVisitor visitor) + { + if (visitor.AllDefault(this, ref this.allDefault)) + { + // Overwrite all serialized fields, but not any nonserialized_*. + visitor.SetDefault(this); + return true; + } + + JxlMatrix3x3F defaultInverse = JxlCms.DefaultInverseOpsinAbsorbanceMatrix(); + + for (int j = 0; j < 3; j++) + { + for (int i = 0; i < 3; i++) + { + if (!visitor.F16(defaultInverse[j][i], ref this.inverseMatrix[j][i])) + { + return false; + } + } + } + + for (int i = 0; i < 3; i++) + { + if (!visitor.F16(JxlCms.NegOpsinAbsorbanceBiasRgb[i], ref this.opsinBiases[i])) + { + return false; + } + } + + for (int i = 0; i < 4; i++) + { + if (!visitor.F16(DefaultQuantBias[i], ref this.quantBiases[i])) + { + return false; + } + } + + return true; + } } diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlToneMapping.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlToneMapping.cs index 9a399dc0b0..3c3b5a498a 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlToneMapping.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlToneMapping.cs @@ -7,15 +7,93 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; internal sealed class JxlToneMapping : IJxlFields { - public bool AllDefault { get; set; } + private bool allDefault; + private float intensityTarget; + private float lowerBoundIntensityLevel; + private bool relativeToMaxDisplay; + private float linearBelow; + private float minNits; - public float IntensityTarget { get; set; } + public bool AllDefault + { + get => this.allDefault; + set => this.allDefault = value; + } - public float LowerBoundIntensityLevel { get; set; } + public float IntensityTarget + { + get => this.intensityTarget; + set => this.intensityTarget = value; + } - public bool RelativeToMaxDisplay { get; set; } + public float LowerBoundIntensityLevel + { + get => this.lowerBoundIntensityLevel; + set => this.lowerBoundIntensityLevel = value; + } - public float LinearBelow { get; set; } + public bool RelativeToMaxDisplay + { + get => this.relativeToMaxDisplay; + set => this.relativeToMaxDisplay = value; + } - public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); + public float LinearBelow + { + get => this.linearBelow; + set => this.linearBelow = value; + } + + public float MinimumNits + { + get => this.minNits; + set => this.minNits = value; + } + + public bool Visit(JxlVisitor visitor) + { + if (visitor.AllDefault(this, ref this.allDefault)) + { + // Overwrite all serialized fields, but not any nonserialized_*. + visitor.SetDefault(this); + return true; + } + + if (!visitor.F16(JxlFieldExpressions.Value(JxlConstants.DefaultIntensityTarget), ref this.intensityTarget)) + { + return false; + } + + if (this.intensityTarget <= 0F) + { + return false; + } + + if (!visitor.F16(0F, ref this.minNits)) + { + return false; + } + + if (this.minNits < 0F || this.minNits > this.intensityTarget) + { + return false; + } + + if (!visitor.Boolean(false, ref this.relativeToMaxDisplay)) + { + return false; + } + + if (!visitor.F16(0F, ref this.linearBelow)) + { + return false; + } + + if (this.linearBelow < 0F || (this.relativeToMaxDisplay && this.linearBelow > 1F)) + { + return false; + } + + return true; + } } From d43064b1d8478e4dd2fb48732371812a638f21fd Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Fri, 11 Sep 2026 20:45:22 +0400 Subject: [PATCH 173/328] Finish JXL->JPEG decoder, split codestream-processing work (incomplete), add tests (incomplete), add compressed DC (incomplete), update folder structure --- src/ImageSharp/Formats/Jxl/IO/JxlCodecIO.cs | 63 ++ .../Formats/Jxl/IO/JxlCodestreamProcessor.cs | 100 ++ .../Jxl/IO/Metadata/JxlExtraChannelInfo.cs | 28 +- .../{ => FrameDecoder}/JxlFrameDecoder.cs | 2 +- .../{ => FrameDecoder}/JxlSectionInfo.cs | 2 +- .../{ => FrameDecoder}/JxlSectionStatus.cs | 2 +- .../Decoder/{ => FrameDecoder}/JxlTocEntry.cs | 2 +- .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 142 +-- .../Decoder/JxlPassesDecoderState.cs | 3 +- .../Processing/Decoder/JxlToJpegDecoder.cs | 84 ++ .../Formats/Jxl/Processing/JxlCompressedDc.cs | 38 + ...JxlSimdUtils.StoreInterleaved.Generated.cs | 75 ++ .../JxlSimdUtils.StoreInterleaved.tt | 2 +- .../Formats/Jxl/IO/ImageBundleTests.cs | 51 + .../Processing/Decoder/JxlDecoderCoreTests.cs | 917 ++++++++++++++++++ .../Formats/Jxl/Processing/SimdUtilsTests.cs | 68 ++ 16 files changed, 1427 insertions(+), 152 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/IO/JxlCodecIO.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/JxlCodestreamProcessor.cs rename src/ImageSharp/Formats/Jxl/Processing/Decoder/{ => FrameDecoder}/JxlFrameDecoder.cs (98%) rename src/ImageSharp/Formats/Jxl/Processing/Decoder/{ => FrameDecoder}/JxlSectionInfo.cs (79%) rename src/ImageSharp/Formats/Jxl/Processing/Decoder/{ => FrameDecoder}/JxlSectionStatus.cs (88%) rename src/ImageSharp/Formats/Jxl/Processing/Decoder/{ => FrameDecoder}/JxlTocEntry.cs (66%) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlCompressedDc.cs create mode 100644 tests/ImageSharp.Tests/Formats/Jxl/IO/ImageBundleTests.cs create mode 100644 tests/ImageSharp.Tests/Formats/Jxl/Processing/Decoder/JxlDecoderCoreTests.cs create mode 100644 tests/ImageSharp.Tests/Formats/Jxl/Processing/SimdUtilsTests.cs diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlCodecIO.cs b/src/ImageSharp/Formats/Jxl/IO/JxlCodecIO.cs new file mode 100644 index 0000000000..215c8808a2 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/JxlCodecIO.cs @@ -0,0 +1,63 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO; + +internal sealed class JxlCodecIO +{ + public JxlCodecIO() + { + this.PreviewFrame = new(this.CodecMetadata.ImageMetadata); + this.Frames = [new(this.CodecMetadata.ImageMetadata)]; + } + + public JxlCodecMetadata CodecMetadata { get; } = new(); + + public JxlImageBundle PreviewFrame { get; set; } + + public List Frames { get; } = []; + + public int LastStillFrame + { + get + { + int last = 0; + + for (int i = 0; i < this.Frames.Count; i++) + { + last = i; + + if (this.Frames[i].Duration > 0) + { + break; + } + } + + return last; + } + } + + public JxlImageBundle Main => this.Frames[this.LastStillFrame]; + + public int Width => this.CodecMetadata.Size!.XSize; + + public int Height => this.CodecMetadata.Size!.YSize; + + public void SetSize(int width, int height) => this.CodecMetadata.Size!.Set(width, height); + + public void ShrinkTo(int width, int height) + { + foreach (JxlImageBundle ib in this.Frames) + { + if (!ib.ShrinkTo(width, height)) + { + throw new InvalidOperationException("Couldn't shrink an image bundle"); + } + } + + this.SetSize(width, height); + } +} diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlCodestreamProcessor.cs b/src/ImageSharp/Formats/Jxl/IO/JxlCodestreamProcessor.cs new file mode 100644 index 0000000000..e83d4a1481 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/JxlCodestreamProcessor.cs @@ -0,0 +1,100 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.Processing; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO; + +internal sealed class JxlCodestreamProcessor +{ + private bool parsedCodestreamSignature; + private bool parsedBasicInfo; + private long codestreamSizeInBits; + private readonly Stream stream; + private readonly JxlBitReader bitReader; + + public JxlCodestreamProcessor(Stream stream) + { + this.stream = stream; + this.bitReader = new(stream); + } + + public void ReadBasicInfo() + { + this.MaybeParseCodestreamSignature(); + + long total = this.bitReader.TotalBitsConsumed; + + this.ReadBundle(this.metadata!.Size!); + this.ReadBundle(this.metadata!.ImageMetadata!); + + long delta = total - this.bitReader.TotalBitsConsumed; + this.UpdateCodestreamSize(delta); + this.parsedBasicInfo = true; + this.basicInfoSizeHint = 0; + this.imageMetadata = this.metadata.ImageMetadata; + + if (!CheckSizeLimit(this.metadata.Size!.XSize, this.metadata.Size.YSize)) + { + throw new InvalidOperationException("The image is too large"); + } + } + + /// + /// Reads a single bundle into . + /// + /// Type of the bundle to read. + /// The bundle to parse. + private void ReadBundle(T bundle) + where T : IJxlFields + { + if (!JxlBundle.Read(this.bitReader, bundle)) + { + JxlCodestreamProcessorThrowHelper.ThrowNotEnoughSourceDataForBundle(); + } + } + + /// + /// Checks if width * height can be represented safely as a + /// positive integer after rounding the width up to the next + /// multiple of 32. + /// + /// Input width. + /// Input height. + /// + /// Boolean indicating whether the padded image dimensions fit + /// within a signed 32-bit integer when calculating the total + /// pixel count. + /// + /// + /// Negative values aren't rejected, but will produce incorrect + /// results. This method is meant to be used with positive values only. + /// + private static bool CheckSizeLimit(int width, int height) + { + if (width == 0 || height == 0) + { + return true; + } + + int paddedWidth = JxlMath.DivCeil(width, 32) * 32; + + if (paddedWidth < width) + { + // Overflow + return false; + } + + int pixelCount = paddedWidth * height; + + if (pixelCount / paddedWidth != height) + { + // Overflow + return false; + } + + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs index 2d53a5f380..da134d8940 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs @@ -6,17 +6,21 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +// One field should be public because we can't change +// an InlineArray values if it's a property +#pragma warning disable SA1401 // Fields should be private + internal sealed class JxlExtraChannelInfo : IJxlFields { private bool allDefault; private JxlExtraChannel type; - private JxlBitDepthMetadata? bitDepth; private int dimensionShift; private string? name; private bool alphaAssociated; - private InlineArray4 spotColor; private int cfaChannel; + public InlineArray4 SpotColor; + public bool AllDefault { get => this.allDefault; @@ -29,11 +33,7 @@ public JxlExtraChannel Type set => this.type = value; } - public JxlBitDepthMetadata? BitDepth - { - get => this.bitDepth; - set => this.bitDepth = value; - } + public JxlBitDepthMetadata? BitDepth { get; set; } public int DimensionShift { @@ -53,12 +53,6 @@ public bool AlphaAssociated set => this.alphaAssociated = value; } - public InlineArray4 SpotColor - { - get => this.spotColor; - set => this.spotColor = value; - } - public int CfaChannel { get => this.cfaChannel; @@ -80,7 +74,7 @@ public bool Visit(JxlVisitor visitor) return false; } - if (!visitor.VisitNested(this.bitDepth!)) + if (!visitor.VisitNested(this.BitDepth!)) { return false; } @@ -119,7 +113,7 @@ ref Unsafe.As(ref this.dimensionShift))) { for (int i = 0; i < 4; i++) { - if (!visitor.F16(0F, ref this.spotColor[i])) + if (!visitor.F16(0F, ref this.SpotColor[i])) { return false; } @@ -141,8 +135,8 @@ ref Unsafe.As(ref this.cfaChannel))) } if (this.type is JxlExtraChannel.Unknown or - >= JxlExtraChannel.Reserved0 and - <= JxlExtraChannel.Reserved7) + (>= JxlExtraChannel.Reserved0 and + <= JxlExtraChannel.Reserved7)) { return false; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/FrameDecoder/JxlFrameDecoder.cs similarity index 98% rename from src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs rename to src/ImageSharp/Formats/Jxl/Processing/Decoder/FrameDecoder/JxlFrameDecoder.cs index 5e0eefc9ef..4343bab95f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/FrameDecoder/JxlFrameDecoder.cs @@ -7,7 +7,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.FrameDecoder; internal sealed class JxlFrameDecoder { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlSectionInfo.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/FrameDecoder/JxlSectionInfo.cs similarity index 79% rename from src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlSectionInfo.cs rename to src/ImageSharp/Formats/Jxl/Processing/Decoder/FrameDecoder/JxlSectionInfo.cs index 4637bacbda..36be2b01fd 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlSectionInfo.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/FrameDecoder/JxlSectionInfo.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.FrameDecoder; internal sealed class JxlSectionInfo(JxlBitReader reader, int id, int index) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlSectionStatus.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/FrameDecoder/JxlSectionStatus.cs similarity index 88% rename from src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlSectionStatus.cs rename to src/ImageSharp/Formats/Jxl/Processing/Decoder/FrameDecoder/JxlSectionStatus.cs index eefcf9ad90..1e2286a66c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlSectionStatus.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/FrameDecoder/JxlSectionStatus.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.FrameDecoder; /// /// Status of processing a section. diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlTocEntry.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/FrameDecoder/JxlTocEntry.cs similarity index 66% rename from src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlTocEntry.cs rename to src/ImageSharp/Formats/Jxl/Processing/Decoder/FrameDecoder/JxlTocEntry.cs index 4e0cfac6bc..4e5947bb43 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlTocEntry.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/FrameDecoder/JxlTocEntry.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.FrameDecoder; internal struct JxlTocEntry { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index 9fefcde018..4831b7f8a4 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -10,6 +10,7 @@ using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.FrameDecoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using SixLabors.ImageSharp.IO; @@ -119,12 +120,12 @@ internal sealed class JxlDecoderCore : ImageDecoderCore, IDisposable /// /// Type of the box currently being decoded. /// - private JxlBoxType boxType; + private int boxType; /// /// Underlying type for brob boxes. /// - private JxlBoxType boxDecodedType; + private int boxDecodedType; private bool boxEvent; @@ -358,6 +359,11 @@ internal sealed class JxlDecoderCore : ImageDecoderCore, IDisposable private bool bufferingJxlpIsLast; + /// + /// The JPEG decoder allows lossless decoding from JPEG XL to JPEG. + /// + private readonly JxlToJpegDecoder jpegDecoder = new(); + /// /// Decompresses box contents. /// @@ -1188,48 +1194,6 @@ public bool TryRequestMoreInput() /// True if the decoder can use code stream input. Otherwise false. public bool CanUseMoreCodestreamInput() => this.decoderStage != JxlDecoderStage.CodeStreamFinished; - /// - /// Checks if width * height can be represented safely as a - /// positive integer after rounding the width up to the next - /// multiple of 32. - /// - /// Input width. - /// Input height. - /// - /// Boolean indicating whether the padded image dimensions fit - /// within a signed 32-bit integer when calculating the total - /// pixel count. - /// - /// - /// Negative values aren't rejected, but will produce incorrect - /// results. This method is meant to be used with positive values only. - /// - public static bool CheckSizeLimit(int width, int height) - { - if (width == 0 || height == 0) - { - return true; - } - - int paddedWidth = JxlMath.DivCeil(width, 32) * 32; - - if (paddedWidth < width) - { - // Overflow - return false; - } - - int pixelCount = paddedWidth * height; - - if (pixelCount / paddedWidth != height) - { - // Overflow - return false; - } - - return true; - } - /// /// Resets the decoder state to its default values, and, /// additionally, releases memory used by buffers and replaces @@ -1465,83 +1429,6 @@ private void BeforeUpdateState(string propertyName) } } - /// - /// Reads a single bundle into . - /// - /// Type of the bundle to read. - /// Stream to read data from. - /// Bit reader to continue from. - /// The bundle to parse. - /// Status of parsing the bundle. - private bool ReadBundle(Stream stream, JxlBitReader br, T bundle) - where T : IJxlFields - { - JxlBitReader reader = new(stream); - reader.SkipBits64((ulong)br.TotalBitsConsumed); - - bool canRead = JxlBundle.CanRead(reader, bundle); - - if (!canRead) - { - return this.TryRequestMoreInput(); - } - - if (!JxlBundle.Read(reader, bundle)) - { - return false; - } - - return true; - } - - /// - /// Reads all basic metadata and headers. - /// - /// Thrown if the data is incorrect. - /// Thrown if the data is malformed. - public void ReadBasicInfo(Stream stream) - { - if (!this.gotCodestreamSignature) - { - Span fileSignature = [0, 0]; - stream.ReadExactly(fileSignature); - - if (fileSignature[0] != 0xFF || fileSignature[1] != JxlShared.CodestreamMarker) - { - throw new InvalidOperationException("The file signature is invalid"); - } - - this.gotCodestreamSignature = true; - } - - JxlBitReader bitReader = new(stream); - - if (!this.ReadBundle(stream, bitReader, this.metadata!.Size!)) - { - throw new InvalidDataException("Could not parse the size header"); - } - - if (!this.ReadBundle(stream, bitReader, this.metadata!.ImageMetadata!)) - { - throw new InvalidDataException("Could not parse the image metadata"); - } - - long totalBits = bitReader.TotalBitsConsumed; - - (long div, long rem) = Math.DivRem(totalBits, JxlMath.BitsPerByte); - this.AdvanceCodeStream(div); - - this.codestreamBitsAhead = rem; - this.gotBasicInfo = true; - this.basicInfoSizeHint = 0; - this.imageMetadata = this.metadata.ImageMetadata; - - if (!CheckSizeLimit(this.metadata.Size!.XSize, this.metadata.Size.YSize)) - { - throw new InvalidOperationException("The image is too large"); - } - } - /// /// Parses all necessary headers. /// @@ -1738,10 +1625,7 @@ public int ProcessCodestream() this.imageBundle ??= new(this.imageMetadata!); - if (!this.jpegDecoder.SetImageBundleJpegData(this.imageBundle!)) - { - throw new InvalidOperationException("Cannot set JXL->JPEG decoder image bundle"); - } + this.jpegDecoder.SetImageBundleJpegData(this.imageBundle!); this.frameDecoder = new(this.passesState!, this.metadata!, useSlowRenderingPipeline: false); this.frameHeader = new() @@ -2241,12 +2125,12 @@ public int ProcessBoxes(Stream stream) if (this.reconstructionExifSize > 0) { - JxlToJpegDecoder.TrySetExif(this.exifMetadata!.Memory.Span, jpegData); + _ = JxlToJpegDecoder.TrySetExif(this.exifMetadata!.Memory.Span, jpegData); } if (this.reconstructionXmpSize > 0) { - JxlToJpegDecoder.TrySetXmp(this.xmpMetadata!.Memory.Span, jpegData); + _ = JxlToJpegDecoder.TrySetXmp(this.xmpMetadata!.Memory.Span, jpegData); } this.reconstructionOutputJpeg = JpegReconstructionStage.Output; @@ -2254,7 +2138,7 @@ public int ProcessBoxes(Stream stream) if (this.reconstructionOutputJpeg == JpegReconstructionStage.Output && !this.JbrdNeedsMoreBoxes()) { - this.jpegDecoder!.WriteOutput(this.imageBundle!.JpegData); + this.jpegDecoder!.WriteOutput(this.imageBundle!.JpegData!); this.reconstructionOutputJpeg = JpegReconstructionStage.None; this.imageBundle.Reset(); @@ -2715,7 +2599,7 @@ public void DecodeInput(Stream stream) } } - private static void ThrowNotEnoughData() => throw new InvalidOperationException("Not enough data"); + private static void ThrowNotEnoughData() => throw new EndOfStreamException("Not enough data"); protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) => throw new NotImplementedException(); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs index adc70f36c3..336ac33c5c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs @@ -6,6 +6,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; +using SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; @@ -38,7 +39,7 @@ public JxlPassesDecoderState(JxlFrameHeader header, Configuration configuration) public JxlPassesSharedState Shared { get; set; } - public JxlRenderPipelineStage[] Upsampler8x { get; set; } = []; + public RenderPipelineStageBase[] Upsampler8x { get; set; } = []; public List Code { get; set; } = []; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlToJpegDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlToJpegDecoder.cs index 995b74852a..cf15210deb 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlToJpegDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlToJpegDecoder.cs @@ -2,12 +2,96 @@ // Licensed under the Six Labors Split License. using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg; using SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +/// +/// Decodes JPEG XL files to JPEG. +/// internal class JxlToJpegDecoder { + /// + /// Content of the most recently parsed JPEG reconstruction box if any. + /// + private MemoryStream buffer = new(); + + /// + /// Decoded content of the most recently parsed JPEG reconstruction box + /// is stored here. + /// + private readonly JpegData jpegData = new(); + + /// + /// True if the JPEG reconstruction box has undefined size (keeps going + /// till End Of File). + /// + private bool boxUntilEof; + + /// + /// Size of the most recently parsed JPEG reconstruction box contents. + /// + private long boxSize; + + /// + /// Gets a value indicating whether the JPEG reconstruction output + /// has been provided. + /// + public bool IsOutputSet => this.Output is not null; + + /// + /// Gets a value indicating whether JPEG box was parsed. + /// + public bool IsParsingBox { get; private set; } + + /// + /// Gets or sets a stream where JPEG reconstruction output will be written to. + /// + public Stream? Output { get; set; } + + public JpegData GetJpegData() => this.jpegData; + + public void StartBox(bool boxUntilEof, long contentsSize) + { + this.buffer.Dispose(); + this.buffer = new MemoryStream(); + this.IsParsingBox = true; + + if (boxUntilEof) + { + this.boxUntilEof = true; + } + else + { + this.boxSize = contentsSize; + } + } + + public void SetImageBundleJpegData(JxlImageBundle bundle) + { + if (this.IsOutputSet && this.jpegData is not null) + { + JpegData.SetJpegDataFromIcc(bundle.Metadata!.ColorEncoding!.Icc, this.jpegData); + bundle.JpegData = this.jpegData; + } + } + + public void WriteOutput(JpegData jpegData) + { + long Write(Span buf) + { + this.Output!.Write(buf); + return buf.Length; + } + + if (!JpegWriter.WriteJpeg(jpegData, Write)) + { + throw new InvalidOperationException("Couldn't emit JPEG data"); + } + } + /// /// Returns the number of EXIF markers in the JPEG file. /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlCompressedDc.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlCompressedDc.cs new file mode 100644 index 0000000000..9e92a09e79 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlCompressedDc.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal static class JxlCompressedDc +{ + private const float W1 = 0.20345139757231578f; + private const float W2 = 0.0334829185968739f; + private const float W0 = 1.0f - (4.0f * (W1 + W2)); + + private static unsafe void ComputePixelChannelPacked(float dcFactor, Span rowTop, Span row, Span rowBottom, out Vector mc, out Vector sm, out Vector gap, int x) + { + // Use ptr for aligned vector loads + fixed (float* pRowTop = rowTop) + { + fixed (float* pRow = row) + { + fixed (float* pRowBottom = rowBottom) + { + // libjxl seems to use the unaligned and aligned + // vector loads in a specific pattern here. + // Additionally, use nontemporal writes as we only + // iterate through parts of the image once. + Vector tl = Vector.Load(pRowTop + x - 1); + Vector tc = Vector.LoadAlignedNonTemporal(pRowTop + x); + Vector tr = Vector.Load(pRowTop + x + 1); + + // TODO + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.Generated.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.Generated.cs index 8e4495e0f0..227af2d8ec 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.Generated.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.Generated.cs @@ -55,6 +55,31 @@ public static unsafe void StoreInterleaved(Vector v1, Vector v2, Vector v6.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 5)); } + public static unsafe void StoreInterleaved(Vector v1, Vector v2, Vector v3, Vector v4, Vector v5, Vector v6, Vector v7, ref T memory) + where T : unmanaged + { + v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 3)); + v5.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 4)); + v6.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 5)); + v7.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 6)); + } + + public static unsafe void StoreInterleaved(Vector v1, Vector v2, Vector v3, Vector v4, Vector v5, Vector v6, Vector v7, Vector v8, ref T memory) + where T : unmanaged + { + v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 3)); + v5.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 4)); + v6.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 5)); + v7.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 6)); + v8.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 7)); + } + public static unsafe void StoreInterleaved(Vector128 v1, Vector128 v2, ref T memory) where T : unmanaged { @@ -100,6 +125,31 @@ public static unsafe void StoreInterleaved(Vector128 v1, Vector128 v2, v6.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 5)); } + public static unsafe void StoreInterleaved(Vector128 v1, Vector128 v2, Vector128 v3, Vector128 v4, Vector128 v5, Vector128 v6, Vector128 v7, ref T memory) + where T : unmanaged + { + v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 3)); + v5.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 4)); + v6.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 5)); + v7.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 6)); + } + + public static unsafe void StoreInterleaved(Vector128 v1, Vector128 v2, Vector128 v3, Vector128 v4, Vector128 v5, Vector128 v6, Vector128 v7, Vector128 v8, ref T memory) + where T : unmanaged + { + v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 3)); + v5.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 4)); + v6.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 5)); + v7.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 6)); + v8.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 7)); + } + public static unsafe void StoreInterleaved(Vector256 v1, Vector256 v2, ref T memory) where T : unmanaged { @@ -145,4 +195,29 @@ public static unsafe void StoreInterleaved(Vector256 v1, Vector256 v2, v6.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 5)); } + public static unsafe void StoreInterleaved(Vector256 v1, Vector256 v2, Vector256 v3, Vector256 v4, Vector256 v5, Vector256 v6, Vector256 v7, ref T memory) + where T : unmanaged + { + v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 3)); + v5.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 4)); + v6.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 5)); + v7.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 6)); + } + + public static unsafe void StoreInterleaved(Vector256 v1, Vector256 v2, Vector256 v3, Vector256 v4, Vector256 v5, Vector256 v6, Vector256 v7, Vector256 v8, ref T memory) + where T : unmanaged + { + v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 3)); + v5.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 4)); + v6.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 5)); + v7.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 6)); + v8.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 7)); + } + } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.tt b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.tt index d4dff7ee41..b97deb0086 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.tt +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.tt @@ -23,7 +23,7 @@ internal static partial class JxlSimdUtils "Vector256" ]; - const int maxVectorSize = 6; + const int maxVectorSize = 8; foreach (string vect in vectorTypes) { for (int i = 2; i <= maxVectorSize; i++) { diff --git a/tests/ImageSharp.Tests/Formats/Jxl/IO/ImageBundleTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/IO/ImageBundleTests.cs new file mode 100644 index 0000000000..ea5272edd2 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/IO/ImageBundleTests.cs @@ -0,0 +1,51 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl.IO; + +public class ImageBundleTests +{ + [Fact] + public void ExtraChannel() + { + using MemoryStream ms = new(); + JxlBitWriter bw = new(ms); + + Assert.True(bw.WithMaxBits(99, () => + { + JxlImageMetadata md = new() + { + ExtraChannels = + [ + new JxlExtraChannelInfo() + { + Type = JxlExtraChannel.Black, + Name = "testK" + } + ], + ExtraChannelCount = 1 + }; + + if (!JxlBundle.Write(md, bw, JxlLayerType.Header, new JxlAuxiliaryOutput())) + { + return false; + } + + bw.ZeroPadToByte(); + return true; + })); + + ms.Position = 0; + JxlBitReader reader = new(ms); + JxlImageMetadata result = new(); + + Assert.True(JxlBundle.Read(reader, result)); + Assert.Equal("testK", result.FindExtraChannel(JxlExtraChannel.Black)?.Name); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/Decoder/JxlDecoderCoreTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Decoder/JxlDecoderCoreTests.cs new file mode 100644 index 0000000000..da39c56499 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Decoder/JxlDecoderCoreTests.cs @@ -0,0 +1,917 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +#nullable enable + +using System.Buffers; +using System.Runtime.InteropServices; +using System.Text; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Jxl.Cms; +using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.IO.Container; +using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; +using SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg; +using SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; +using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Memory; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing.Decoder; + +public class JxlDecoderCoreTests +{ + // Unknown boxes and their data for testing. + private const string Unk1BoxType = "unk1"; + private const string Unk1BoxContents = "abcdefghijklmnopqrstuvwxyz"; + private const string Unk2BoxType = "unk2"; + private const string Unk2BoxContents = "0123456789"; + private const string Unk3BoxType = "unk3"; + private const string Unk3BoxContents = "ABCDEF123456"; + + private const int BoxBrobExifSize = 64; + private const int ExifUncompressedSize = 94; + + private static ReadOnlySpan BoxBrobExif => "\0\0\0@brobExif\xA1\xE8\x02\xC0\x7F\xA4v\xAA5\xC4\xF0\x17=?\xB7{\x1B\x1F\xCC\xDA\x8CQX17PT\"\xAE\0\0\x82s\x8C\xCBt\xDB\xC8\xD0k\x10\xBE\x18\x84\xBFl$\xD6c#\x01\b"u8; + + private static ReadOnlySpan ExifUncompressed => "\"\\0\\0\\0\\0MM\\0*\\0\\0\\0\\b\\0\\5\\1\\22\\0\\3\\0\\0\\0\\1\\0\\5\\0\\0\\1\\32\\0\\5\\0\\0\\0\\1\\0\\0\\0J\\1\\33\\0\\5\\0\\0\\0\\1\\0\\0\\0R\\1(\\0\\3\\0\\0\\0\\1\\0\\1\\0\\0\\2\\23\\0\\3\\0\\0\\0\\1\\0\\1\\0\\0\\0\\0\\0\\0\\0\\0\\0\\1\\0\\0\\0\\1\\0\\0\\0\\1\\0\\0\\0\\1\""u8; + + /// + /// What type of codestream format in the boxes to use for testing. + /// + private enum CodeStreamBoxFormat : byte + { + /// + /// Do not use box format at all. Only pure codestream. + /// + None, + + /// + /// Have a single codestream box, with its actual size given in + /// the box header. + /// + Single, + + /// + /// Have a single codestream box, with box size 0 (final box + /// running to end). + /// + SingleZeroTerminated, + + /// + /// Single codestream box, with another unknown box behind it + /// + SingleOther, + + /// + /// Have multiple partial codestream boxes + /// + Multi, + + /// + /// Have multiple partial codestream boxes, with final box size 0 + /// (running to end) + /// + MultiZeroTerminated, + + /// + /// Have multiple partial codestream boxes, terminated by + /// non-codestream box + /// + MultiOtherTerminated, + + /// + /// Have multiple partial codestream boxes, terminated by non-codestream + /// box that has its size set to 0 (running to end) + /// + MultiOtherZeroTerminated, + + /// + /// Have multiple partial codestream boxes, and the first one has a content + /// of zero length + /// + MultiFirstEmpty, + + /// + /// Have multiple partial codestream boxes, and the last one has a content + /// of zero length and there is an unknown empty box at the end + /// + MultiLastEmptyOther, + + /// + /// Have a compressed exif box before a regular codestream box. + /// + BrobExif, + + /// + /// Not a value but used for counting amount of enum entries + /// + NumEntries + } + + private struct PixelTestConfiguration + { + private static readonly string[] Colors = [string.Empty, "G", "GA", "RGB", "RGBA"]; + + public bool Grayscale { get; set; } + + public bool IncludeAlpha { get; set; } + + public int Width { get; set; } + + public int Height { get; set; } + + public PreviewMode PreviewMode { get; set; } + + public bool AddIntrinsicSize { get; set; } + + public ByteOrder Endianness { get; set; } + + public JxlDataType DataType { get; set; } + + public uint OutputChannels { get; set; } + + public CodeStreamBoxFormat AddContainer { get; set; } + + public bool UseCallback { get; set; } + + public bool SetBufferEarly { get; set; } + + public bool UseResizableRunner { get; set; } + + public JxlExifOrientation Orientation { get; set; } + + public bool KeepOrientation { get; set; } + + public int Upsampling { get; set; } + + // For debugging purposes + public void Format(StringBuilder sb) + { + // Dimensions + sb.Append("Dimensions: "); + sb.Append(this.Width); + sb.Append('x'); + sb.Append(this.Height); + sb.AppendLine(); + + // Colors + sb.Append("Colors: "); + sb.Append(Colors[(this.Grayscale ? 1 : 3) + (this.IncludeAlpha ? 1 : 0)]); + sb.Append(" to "); + sb.Append(Colors[this.OutputChannels]); + sb.AppendLine(); + + // Data type + sb.Append("Data type: "); + sb.Append(this.DataType switch + { + JxlDataType.Byte => "u8", + JxlDataType.UInt16 => "u16", + JxlDataType.Single => "f32", + JxlDataType.Half => "f16", + _ => "???" + }); + sb.AppendLine(); + + // Endianness + sb.Append("Endianness: "); + sb.Append(this.Endianness is ByteOrder.BigEndian ? "Big Endian" : "Little Endian"); + sb.AppendLine(); + + // File type + sb.Append("File type: "); + sb.Append(this.AddContainer == CodeStreamBoxFormat.None ? "Plain codestream" : "Container"); + sb.AppendLine(); + + // Preview + sb.Append("Preview type: "); + sb.Append(this.PreviewMode switch + { + PreviewMode.NoPreview => "No preview", + PreviewMode.SmallPreview => "Small preview", + PreviewMode.BigPreview => "Big preview", + _ => "???" + }); + sb.AppendLine(); + + // Other flags + sb.Append("Other flags: "); + + if (this.AddIntrinsicSize) + { + sb.Append("Intrinsic Size; "); + } + + if (this.UseCallback) + { + sb.Append("Use Callback; "); + } + + if (this.SetBufferEarly) + { + sb.Append("Set Buffer Early; "); + } + + if (this.UseResizableRunner) + { + sb.Append("Use Resizable Runner; "); + } + + if (this.Orientation != (JxlExifOrientation)1) + { + sb.Append("Orientation=" + this.Orientation + "; "); + } + + if (this.KeepOrientation) + { + sb.Append("Keep Orientation; "); + } + + if (this.Upsampling > 1) + { + sb.Append(null, $"Upsampling x{this.Upsampling}; "); + } + } + + public override string ToString() + { + StringBuilder sb = new(); + this.Format(sb); + return sb.ToString(); + } + } + + private enum PreviewMode : byte + { + NoPreview, + SmallPreview, + BigPreview, + NumPreviewModes + } + + private static void AppendU32BE(uint u32, List bytes) + { + bytes.Add((byte)(u32 >> 24)); + bytes.Add((byte)(u32 >> 16)); + bytes.Add((byte)(u32 >> 8)); + bytes.Add((byte)(u32 >> 0)); + } + + private static void AppendTestBox(ReadOnlySpan type, ReadOnlySpan contents, bool unbounded, List bytes) + { + AppendU32BE((uint)(contents.Length + 8), bytes); + bytes.Add((byte)type[0]); + bytes.Add((byte)type[1]); + bytes.Add((byte)type[2]); + bytes.Add((byte)type[3]); + + ReadOnlySpan contentsU = MemoryMarshal.Cast(type); + for (int i = 0; i < contentsU.Length; i++) + { + bytes.Add(contentsU[i]); + } + } + + private static void GeneratePreview(Configuration configuration, PreviewMode previewMode, JxlImageBundle ib) + { + if (previewMode == PreviewMode.SmallPreview) + { + Assert.True(ib.ShrinkTo(ib.XSize / 7, ib.YSize / 7)); + } + else if (previewMode == PreviewMode.BigPreview) + { + static void Upsample7(JxlPlane input, JxlPlane output) + { + for (int y = 0; y < output.YSize; ++y) + { + Span rowY = output.GetRow(y); + Span rowYDiv7 = input.GetRow(y / 7); + + for (int x = 0; x < output.XSize; ++x) + { + rowY[x] = rowYDiv7[x / 7]; + } + } + } + + JxlImage3F preview = new(configuration, ib.XSize * 7, ib.YSize * 7); + + for (int c = 0; c < 3; c++) + { + Upsample7(ib.Color!.Plane(c), preview.Plane(c)); + } + + List extraChannels = []; + + foreach (JxlImageF extraChannel in ib.EnumerateExtraChannels()) + { + JxlImageF ec = new(configuration, ib.XSize * 7, ib.YSize * 7); + + Upsample7(extraChannel, ec); + extraChannels.Add(ec); + } + + ib.RemoveColor(); + ib.ClearExtraChannels(); + + Assert.True(ib.SetFromImage(preview, ib.CurrentColorEncoding!)); + Assert.True(ib.TrySetExtraChannels(extraChannels)); + } + } + + private static List CreateTestJpegXLCodeStream(List pixels, int width, int height, int numChannels, TestCodestreamParameters parameters) + => CreateTestJpegXLCodeStream(TestEnvironment.Configuration, CollectionsMarshal.AsSpan(pixels), width, height, numChannels, parameters); + + // Input pixels always given as 16-bit RGBA, 8 bytes per pixel. + // include_alpha determines if the encoded image should contain the alpha + // channel. + // add_icc_profile: if false, encodes the image as sRGB using the JXL fields, + // for grayscale or RGB images. If true, encodes the image using the ICC profile + // returned by GetIccTestProfile, without the JXL fields, this requires the + // image is RGB, not grayscale. + // Providing jpeg_codestream will populate the jpeg_codestream with compressed + // JPEG bytes, and make it possible to reconstruct those exact JPEG bytes using + // the return value _if_ add_container indicates a box format. + // + // (comment from libjxl source) + private static List CreateTestJpegXLCodeStream(Configuration configuration, Span pixels, int width, int height, int numChannels, TestCodestreamParameters parameters) + { + bool grayscale = numChannels <= 2; + bool haveAlpha = (numChannels & 1) == 0; + bool includeAlpha = haveAlpha && parameters.JpegCodestream is null; + int bitDepth = parameters.JpegCodestream is null ? 16 : 8; + + JxlColorEncoding colorEncoding = new(); + + if (parameters.AddIccProfile) + { + Assert.False(grayscale); + Assert.Equal(0, parameters.ColorSpace?.Length); + Assert.True(colorEncoding.SetIcc(GetIccTestProfile(), JxlGetDefaultCms())); + } + else if (parameters.ColorSpace?.Length > 0) + { + JxlColorEncoding c = new(); + Assert.True(ParseDescription(parameters.ColorSpace!, c)); + Assert.True(colorEncoding.FromExternal(c)); + Assert.Equal(grayscale, colorEncoding.IsGray); + } + else + { + colorEncoding = JxlColorEncoding.SRgb(grayscale); + } + + JxlCodecIO io = new(); + io.SetSize(width, height); + + io.CodecMetadata.ImageMetadata!.SetUIntSamples(bitDepth); + + if (includeAlpha) + { + io.CodecMetadata.ImageMetadata!.SetAlphaBits(bitDepth); + } + + if (parameters.IntensityTarget != 0) + { + io.CodecMetadata.ImageMetadata!.SetIntensityTarget(parameters.IntensityTarget); + } + + JxlPixelFormat format = new() + { + Channels = numChannels, + DataType = JxlDataType.UInt16, + Endianness = ByteOrder.BigEndian, + Align = 0 + }; + + io.CodecMetadata.ImageMetadata.ColorEncoding = colorEncoding; + + JxlExternalImageDecoder.ConvertFromExternal(configuration, pixels, width, height, colorEncoding, 16, format, io.Main, includeAlpha); + + List encodedJpegBytes = []; + + if (parameters.JpegCodestream is not null) + { + if (CanDecode(JxlCodecType.Jpeg)) + { + List jpegBytes = []; + + // We encode the JPEG and ensure that the JPEG writer in JPEG XL + // codec for use in JPEG<->JPEG XL Lossless coding can properly + // code data. + // First let's set up the JPEG encoder parameters, really the only + // thing we have to configure there is set the quality to 70%. + JpegEncoder encoder = new() + { + Quality = 70 + }; + + // The following chain of if's is just encoding the image with input pixel format + // depending on the number of channels. Since our data type is always + // 16-bits per channel, that means a single grayscale channel maps + // very well into L16. 3 channels map into Rgb48. We can have up to + // 4 channels in this context, and that's Rgba64. + if (numChannels == 1) + { + // Each sample is 16-bit and we have one grayscale channel, so + // feed the encoder an L16. + // Total bit depth is 1 * 16. + EncodeJpeg(pixels); + } + else if (numChannels == 2) + { + // 2 channel is not a thing. 🚫 + throw new InvalidOperationException("Cannot encode a 2-channel JPEG"); + } + else if (numChannels == 3) + { + // RGB with each component being 16-bit. + // Total bit depth is 3 * 16. + EncodeJpeg(pixels); + } + else if (numChannels == 4) + { + // RGBA with each component being 16-bit. + // Total bit depth is 4 * 16. + EncodeJpeg(pixels); + } + else + { + // Only 1, 3, and 4 channels are allowed. + throw new InvalidOperationException($"Invalid number of channels: {numChannels}"); + } + + // JPEG encoded successfully. So parse its components. + JpegData jpegData = new(); + Assert.True(JpegReader.ReadJpeg(CollectionsMarshal.AsSpan(encodedJpegBytes), JpegReadMode.ReadEverything, jpegData)); + Assert.True(EncodeJpegData(io.Main.JpegData!, encodedJpegBytes, parameters.CompressParameters)); + io.CodecMetadata.ImageMetadata!.XybEncoded = false; + + // This is a simple helper so we don't have to duplicate + // code. It just takes in a pixel format and encodes a + // JPEG using ImageSharp existing JPEG codec. Very simple. + // We should also pass the pixels Span explicitly because + // a local function cannot use a ByRef variable. + void EncodeJpeg(Span pixels) + where TPixelFormat : unmanaged, IPixel + { + using Image data = new(width, height); + + // Just some copying from `pixels` variable to the data image. + Assert.True(data.DangerousTryGetSinglePixelMemory(out Memory memory)); + Span bytes = MemoryMarshal.Cast(memory.Span); + Assert.Equal(bytes.Length, pixels.Length); + pixels.CopyTo(bytes); + + // We store a temporary MemoryStream here and this is where the + // encoded JPEG file output will go. + using MemoryStream memoryStream = new(); + data.Save(memoryStream, encoder); + + // JPEG file is saved inside memoryStream, now we have + // to copy it to a List. + // encodedJpegBytes is initially empty, so we can construct + // a new List on top. Note that memoryStream.ToArray() + // allocates a new array for us, but in theory it should + // be faster than calling .Add() elementwise. + encodedJpegBytes = [.. memoryStream.ToArray()]; + } + } + else + { + throw new InvalidOperationException("JPEG cannot be encoded"); + } + } + + if (parameters.PreviewMode != PreviewMode.NoPreview) + { + io.PreviewFrame = io.Main.Copy(configuration); + GeneratePreview(configuration, parameters.PreviewMode, io.PreviewFrame); + io.CodecMetadata.ImageMetadata!.HavePreview = true; + + // This will throw on failure. + io.CodecMetadata.ImageMetadata.PreviewSize.Set(io.PreviewFrame.XSize, io.PreviewFrame.YSize); + } + + if (parameters.AddIntrinsicSize) + { + // This will also throw on failure. + io.CodecMetadata.ImageMetadata!.PreviewSize.Set(io.PreviewFrame.XSize / 3, io.PreviewFrame.YSize / 3); + } + + io.CodecMetadata.ImageMetadata.Orientation = (int)parameters.Orientation; + + List compressed = []; + EncodeFile(parameters.CompressParameters, io, compressed); + + CodeStreamBoxFormat addContainer = parameters.BoxFormat; + + if (addContainer != CodeStreamBoxFormat.None) + { + // We can encode the container format. + // + // Header with signature box and ftyp box + Span header = + [ + 0, 0, 0, 0xc, 0x4a, 0x58, 0x4c, 0x20, + 0xd, 0xa, 0x87, 0xa, 0, 0, 0, 0x14, + 0x66, 0x74, 0x79, 0x70, 0x6a, 0x78, 0x6c, 0x20, + 0, 0, 0, 0, 0x6a, 0x78, 0x6c, 0x20 + ]; + + bool isMulti = addContainer is CodeStreamBoxFormat.Multi + or CodeStreamBoxFormat.MultiZeroTerminated + or CodeStreamBoxFormat.MultiOtherTerminated + or CodeStreamBoxFormat.MultiOtherZeroTerminated + or CodeStreamBoxFormat.MultiFirstEmpty + or CodeStreamBoxFormat.MultiLastEmptyOther; + + if (isMulti) + { + int third = compressed.Count / 3; + Span compressedData = CollectionsMarshal.AsSpan(compressed); + + Span compressed0 = compressedData[..third]; + Span compressed1 = compressedData[third..(2 * third)]; + Span compressed2 = compressedData[(2 * third)..]; + + List c = []; + c.AddRange(header); + + if (parameters.JpegCodestream is not null) + { + AppendBoxHeader(JxlBoxHeader.TypeFromString("jbrd"), encodedJpegBytes.Count, false, c); + c.AddRange(encodedJpegBytes); + } + + uint jxlpIndex = 0; + + if (addContainer == CodeStreamBoxFormat.MultiFirstEmpty) + { + AppendU32BE(12, c); + c.Add((byte)'j'); + c.Add((byte)'x'); + c.Add((byte)'l'); + c.Add((byte)'p'); + AppendU32BE(jxlpIndex++, c); + } + + // First codestream part + AppendU32BE((uint)compressed0.Length + 12, c); + c.Add((byte)'j'); + c.Add((byte)'x'); + c.Add((byte)'l'); + c.Add((byte)'p'); + AppendU32BE(jxlpIndex++, c); + c.AddRange(compressed0); + + // A few non-codestream boxes in between + AppendTestBox(Unk1BoxType, Unk1BoxContents, false, c); + AppendTestBox(Unk2BoxType, Unk2BoxContents, false, c); + + // Empty placeholder codestream part + AppendU32BE(12, c); + c.Add((byte)'j'); + c.Add((byte)'x'); + c.Add((byte)'l'); + c.Add((byte)'p'); + AppendU32BE(jxlpIndex++, c); + + // Second codestream part + AppendU32BE((uint)compressed1.Length + 12, c); + c.Add((byte)'j'); + c.Add((byte)'x'); + c.Add((byte)'l'); + c.Add((byte)'p'); + AppendU32BE(jxlpIndex++, c); + c.AddRange(compressed1); + + // Third (last) codestream part + AppendU32BE( + addContainer == CodeStreamBoxFormat.MultiZeroTerminated + ? 0u + : ((uint)compressed2.Length + 12), + c); + c.Add((byte)'j'); + c.Add((byte)'x'); + c.Add((byte)'l'); + c.Add((byte)'p'); + + if (addContainer != CodeStreamBoxFormat.MultiLastEmptyOther) + { + AppendU32BE(jxlpIndex++ | 0x80000000u, c); + } + else + { + AppendU32BE(jxlpIndex++, c); + } + + c.AddRange(compressed2); + if (addContainer == CodeStreamBoxFormat.MultiLastEmptyOther) + { + // Empty placeholder codestream part + AppendU32BE(12, c); + c.Add((byte)'j'); + c.Add((byte)'x'); + c.Add((byte)'l'); + c.Add((byte)'p'); + AppendU32BE(jxlpIndex++ | 0x80000000, c); + AppendTestBox(Unk3BoxType, Unk3BoxContents, false, c); + } + + if (addContainer == CodeStreamBoxFormat.MultiOtherTerminated) + { + AppendTestBox(Unk3BoxType, Unk3BoxContents, false, c); + } + + if (addContainer == CodeStreamBoxFormat.MultiOtherZeroTerminated) + { + AppendTestBox(Unk3BoxType, Unk3BoxContents, true, c); + } + + RuntimeUtility.Swap(ref compressed, ref c); + } + else + { + List c = []; + c.AddRange(header); + + if (parameters.JpegCodestream is not null) + { + AppendBoxHeader(JxlBoxHeader.TypeFromString("jbrd"), encodedJpegBytes.Count, c); + c.AddRange(encodedJpegBytes); + } + + if (addContainer == CodeStreamBoxFormat.BrobExif) + { + c.AddRange(BoxBrobExif); + } + + AppendU32BE( + addContainer == CodeStreamBoxFormat.SingleZeroTerminated + ? 0 + : ((uint)compressed.Count + 8), + c); + + c.Add((byte)'j'); + c.Add((byte)'x'); + c.Add((byte)'l'); + c.Add((byte)'c'); + + c.AddRange(compressed); + + if (addContainer == CodeStreamBoxFormat.SingleOther) + { + AppendTestBox(Unk1BoxType, Unk1BoxContents, false, c); + } + + RuntimeUtility.Swap(ref compressed, ref c); + } + } + + return compressed; + } + + /// + /// Tests to make sure that JXLP boxes that are out of order + /// don't result in the decoder crashing. + /// + [Fact] + public void TestOutOfOrderJxlpDuplicateIndexTest() + { + static void AppendTag(ReadOnlySpan tagData, List output) => output.AddRange(tagData); + + List data = []; + + // Adding the JPEG XL signature box + AppendU32BE(12, data); + AppendTag("JXL "u8, data); + data.AddRange([0x0D, 0x0A, 0x87, 0x0A]); + + // Adding the FTYP box declaring file format version 1, which + // enables out-of-order JXLP + AppendU32BE(20, data); + AppendTag("ftyp"u8, data); + AppendTag("jxl "u8, data); + AppendU32BE(1, data); + AppendTag("jxl "u8, data); + + // Two jxlp boxes that both carry out-of-order index 1 (the expected first + // index is 0). The first is buffered; the second is a duplicate. + for (int i = 0; i < 2; i++) + { + // box size: 8-byte header + 8-byte contents + AppendU32BE(16, data); + AppendTag("jxlp"u8, data); + + // jxlp index 1, high bit unset (not the last box) + AppendU32BE(1, data); + + // payload + data.AddRange([0xAA, 0xBB, 0xCC, 0xDD]); + } + + byte[] byteArrayData = [.. data]; + using MemoryStream ms = new(byteArrayData); + ms.Position = 0; + + using JxlDecoderCore decoderCore = new(DecoderOptions.Default); + decoderCore.SubscribeEvents(JxlDecoderStatus.BasicInfo); + + decoderCore.DecodeInput(ms); + + // We just have to make sure that processing out-of-order (OOO) JXLP + // boxes don't throw. + } + + [Fact] + public void TestSpotColors() + { + JxlCodecIO io = new(); + int width = 55; + int height = 257; + + io.CodecMetadata.ImageMetadata!.ColorEncoding = JxlColorEncoding.LinearSRgb(); + + using JxlImage3F main = new(TestEnvironment.Configuration, width, height); + using JxlImageF spot = new(TestEnvironment.Configuration, width, height); + JxlImageOperations.ZeroFillImage(main); + JxlImageOperations.ZeroFillImage(spot); + + for (int y = 0; y < height; y++) + { + Span rowm = main.PlaneRow(1, y); + Span rows = spot.GetRow(y); + + for (int x = 0; x < width; x++) + { + rowm[x] = (x + y) * (1.0f / 255.0f); + rows[x] = ((x ^ y) & 255) * (1.0f / 255.0f); + } + } + + io.SetFromImage(main, JxlColorEncoding.LinearSRgb()); + + JxlExtraChannelInfo info = new() + { + BitDepth = new() + { + BitsPerSample = 8 + }, + DimensionShift = 0, + Type = JxlExtraChannel.SpotColor + }; + + info.SpotColor[0] = 0.5f; + info.SpotColor[1] = 0.2f; + info.SpotColor[2] = 1.0f; + info.SpotColor[3] = 0.5f; + + io.CodecMetadata.ImageMetadata!.ExtraChannels.Add(info); + + List ec = [spot]; + Assert.True(io.Frames[0].TrySetExtraChannels(ec)); + + JxlCompressParameters compressParameters = new() + { + SpeedTier = JxlSpeedTier.Lightning, + ModularMode = true, + ColorTransform = JxlColorTransform.None, + ButteraugliDistance = 0f + }; + + List compressed = []; + Assert.True(EncodeFile(compressParameters, io, compressed)); + + for (int renderSpot = 0; renderSpot < 2; renderSpot++) + { + JxlPixelFormat format = new() + { + Channels = 3, + DataType = JxlDataType.Byte, + Endianness = ByteOrder.LittleEndian, + Align = 0 + }; + + using JxlDecoderCore decoder = new(DecoderOptions.Default); + decoder.SubscribeEvents(JxlDecoderStatus.BasicInfo | JxlDecoderStatus.FullImage); + + if (renderSpot == 0) + { + decoder.RenderSpotcolors = false; + } + + using MemoryStream ms = new([.. compressed]); + decoder.DecodeInput(ms); + + JxlBasicInfo basicInfo = decoder.GetBasicInfo(); + Assert.Equal(1u, basicInfo.ExtraChannelCount); + Assert.Equal(width, basicInfo.XSize); + Assert.Equal(height, basicInfo.YSize); + + JxlExtraChannelInfo extraInfo = decoder.GetExtraChannelInfo(0); + Assert.Equal(JxlExtraChannel.SpotColor, extraInfo.Type); + + // Should throw as image output buffer wasn't provided. + Assert.Throws(() => decoder.DecodeInput(ms)); + + long bufferSize = decoder.GetImageOutputBufferSize(); + long extraSize = decoder.GetExtraChannelBufferSize(0); + + using Image image = new(basicInfo.XSize, basicInfo.YSize); + IMemoryOwner extra = TestEnvironment.Configuration.MemoryAllocator.Allocate(basicInfo.XSize * basicInfo.YSize); + + Assert.True(image.DangerousTryGetSinglePixelMemory(out Memory imageMemory)); + + decoder.SetOutputBuffer(JxlTestingTools.ChangeMemoryType(imageMemory)); + decoder.SetExtraChannelBuffer(0, extra.Memory); + + decoder.DecodeInput(ms); + decoder.StatusMustBeFullImage(); + + decoder.DecodeInput(ms); + decoder.StatusMustBeSuccess(); + + Span imageSpan = imageMemory.Span; + Span extraSpan = extra.Memory.Span; + + int stride = basicInfo.XSize; + + for (int y = 0; y < height; y++) + { + Span rowm = imageSpan[(stride * y)..]; + Span rows = extraSpan[(stride * y)..]; + + for (int x = 0; x < width; x++) + { + if (renderSpot == 0) + { + // if spot color isn't rendered, main image should be as we made it + // (red and blue are all zeroes) + ref Rgb24 rgb = ref rowm[x]; + Assert.Equal(0, rgb.R); + Assert.Equal(x + y > 255 ? 255 : x + y, rgb.G); + Assert.Equal(0, rgb.B); + } + + if (renderSpot != 0) + { + // if spot color is rendered, expect red and blue to look like the + // spot color channel + Assert.True(MathF.Abs(rowm[x].R - (rows[x] * 0.25f)) < 1.0f); + Assert.True(MathF.Abs(rowm[x].B - (rows[x] * 0.5f)) < 1.0f); + } + + Assert.Equal(rows[x], (x ^ y) & 255); + } + } + } + } + + /// + /// The DC event still exists, but is no longer implemented, it is deprecated. + /// + [Fact] + public void TestDcNotGettable() + { + ReadOnlySpan compressed = "\xFF\n\0\x10\xB0\x13\0H\x80(\0\xDC\0U\x0F\0\0\xA8P\x19e\xDC\xE0\xE5\\\xCF\x97\x1F:,\xA6m\\gh\xABm\vK\x12E\xC6\xB1I\xAAC&pH\x12\xEB \xF3\x06\x12\bp\0\x80\x9F\x1C\x99W2d\xAD$\x01"u8; + using JxlDecoderCore decoder = new(DecoderOptions.Default); + decoder.SubscribeEvents(JxlDecoderStatus.BasicInfo); + + using MemoryStream ms = new(compressed.ToArray()); + + decoder.DecodeInput(ms); + decoder.StatusMustBeBasicInfo(); + + decoder.DecodeInput(ms); + decoder.StatusMustBeSuccess(); + } + + private class TestCodestreamParameters + { + public JxlCompressParameters CompressParameters { get; set; } = new(); + + public CodeStreamBoxFormat BoxFormat { get; set; } = CodeStreamBoxFormat.None; + + public JxlExifOrientation Orientation { get; set; } = JxlExifOrientation.Identity; + + public PreviewMode PreviewMode { get; set; } = PreviewMode.NoPreview; + + public bool AddIntrinsicSize { get; set; } + + public bool AddIccProfile { get; set; } + + public float IntensityTarget { get; set; } + + public string? ColorSpace { get; set; } + + public List? JpegCodestream { get; set; } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/SimdUtilsTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/SimdUtilsTests.cs new file mode 100644 index 0000000000..1000c4ac2d --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/SimdUtilsTests.cs @@ -0,0 +1,68 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Numerics; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.Processing; + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; + +public class SimdUtilsTests +{ + [Fact] + public void TestInterleave2() + { + Span mem = [0.0f, 0.0f]; + + Vector vec1 = JxlSimdUtils.Iota(0f); + Vector vec2 = JxlSimdUtils.Iota(128.0f); + + JxlSimdUtils.StoreInterleaved(vec1, vec2, ref MemoryMarshal.GetReference(mem)); + + for (int j = 0; j < 2; j++) + { + Assert.Equal(mem[j], j * 128.0f); + } + } + + [Fact] + public void TestInterleave4() + { + Span mem = [0.0f, 0.0f, 0.0f, 0.0f]; + + Vector vec1 = JxlSimdUtils.Iota(0f); + Vector vec2 = JxlSimdUtils.Iota(128.0f); + Vector vec3 = JxlSimdUtils.Iota(256.0f); + Vector vec4 = JxlSimdUtils.Iota(384.0f); + + JxlSimdUtils.StoreInterleaved(vec1, vec2, vec3, vec4, ref MemoryMarshal.GetReference(mem)); + + for (int j = 0; j < 4; j++) + { + Assert.Equal(mem[j], j * 128.0f); + } + } + + [Fact] + public void TestInterleave8() + { + Span mem = [0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f]; + + Vector vec1 = JxlSimdUtils.Iota(0f); + Vector vec2 = JxlSimdUtils.Iota(1 * 128.0f); + Vector vec3 = JxlSimdUtils.Iota(2 * 128.0f); + Vector vec4 = JxlSimdUtils.Iota(3 * 128.0f); + Vector vec5 = JxlSimdUtils.Iota(4 * 128.0f); + Vector vec6 = JxlSimdUtils.Iota(5 * 128.0f); + Vector vec7 = JxlSimdUtils.Iota(6 * 128.0f); + Vector vec8 = JxlSimdUtils.Iota(7 * 128.0f); + + JxlSimdUtils.StoreInterleaved(vec1, vec2, vec3, vec4, vec5, vec6, vec7, vec8, ref MemoryMarshal.GetReference(mem)); + + for (int j = 0; j < 8; j++) + { + Assert.Equal(mem[j], j * 128.0f); + } + } +} From 09066a0099c1027304b2adaa78bf91e8b8053edf Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Fri, 11 Sep 2026 21:19:04 +0400 Subject: [PATCH 174/328] Add reinterpreting IDCT --- .../Encoder/JxlTransformsEncoder.cs | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlTransformsEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlTransformsEncoder.cs index f3565a75d9..95741c5a62 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlTransformsEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlTransformsEncoder.cs @@ -1,8 +1,61 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; + namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; internal static class JxlTransformsEncoder { + private const int MaxBlocks = 32; + + public static void ReinterpretingInverseDct( + int dctRows, + int dctCols, + int lfRows, + int lfCols, + int rows, + int cols, + Span input, + int inputStride, + Span output, + int outputStride, + Span scratch) + { + if (rows < cols) + { + ReadOnlySpan sp1 = JxlDctScales.GetResampleScales(dctRows, rows); + ReadOnlySpan sp2 = JxlDctScales.GetResampleScales(dctCols, cols); + + for (int y = 0; y < lfRows; y++) + { + int yCols = y * cols; + int yInputStride = y * inputStride; + + for (int x = 0; x < lfCols; x++) + { + scratch[yCols + x] = input[yInputStride + x] * sp1[y] * sp2[y]; + } + } + } + else + { + ReadOnlySpan sp2 = JxlDctScales.GetResampleScales(dctRows, rows); + ReadOnlySpan sp1 = JxlDctScales.GetResampleScales(dctCols, cols); + + for (int y = 0; y < lfCols; y++) + { + int yRows = y * rows; + int yInputStride = y * inputStride; + + for (int x = 0; x < lfRows; x++) + { + scratch[yRows + x] = input[yInputStride + x] * sp1[y] * sp2[y]; + } + } + } + + Span scratchSpace = scratch[(MaxBlocks * MaxBlocks)..]; + JxlDct.ComputeScaledInverseDct(rows, cols, scratch, new JxlDctOutput(output, outputStride), scratchSpace); + } } From 82d8bbd73c3947be891a7c660370b0edf3bd965b Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 12 Sep 2026 17:44:10 +0400 Subject: [PATCH 175/328] Add transforms encoder --- .../Encoder/JxlTransformsEncoder.cs | 1143 +++++++++++++++++ 1 file changed, 1143 insertions(+) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlTransformsEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlTransformsEncoder.cs index 95741c5a62..85c69cb80c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlTransformsEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlTransformsEncoder.cs @@ -1,7 +1,10 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; +using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; @@ -9,6 +12,301 @@ internal static class JxlTransformsEncoder { private const int MaxBlocks = 32; + /// + /// Lookup for forward AFV DCT 4x4. + /// + private static readonly float[][] AfvBasisTranspose4x4 = + [ + [ + 0.2500000000000000f, + 0.8769029297991420f, + 0.0000000000000000f, + 0.0000000000000000f, + 0.0000000000000000f, + -0.4105377591765233f, + 0.0000000000000000f, + 0.0000000000000000f, + 0.0000000000000000f, + 0.0000000000000000f, + 0.0000000000000000f, + 0.0000000000000000f, + 0.0000000000000000f, + 0.0000000000000000f, + 0.0000000000000000f, + 0.0000000000000000f, + ], + [ + 0.2500000000000000f, + 0.2206518106944235f, + 0.0000000000000000f, + 0.0000000000000000f, + -0.7071067811865474f, + 0.6235485373547691f, + 0.0000000000000000f, + 0.0000000000000000f, + 0.0000000000000000f, + 0.0000000000000000f, + 0.0000000000000000f, + 0.0000000000000000f, + 0.0000000000000000f, + 0.0000000000000000f, + 0.0000000000000000f, + 0.0000000000000000f, + ], + [ + 0.2500000000000000f, + -0.1014005039375376f, + 0.4067007583026075f, + -0.2125574805828875f, + 0.0000000000000000f, + -0.0643507165794627f, + -0.4517556589999482f, + -0.3046847507248690f, + 0.3017929516615495f, + 0.4082482904638627f, + 0.1747866975480809f, + -0.2110560104933578f, + -0.1426608480880726f, + -0.1381354035075859f, + -0.1743760259965107f, + 0.1135498731499434f, + ], + [ + 0.2500000000000000f, + -0.1014005039375375f, + 0.4444481661973445f, + 0.3085497062849767f, + 0.0000000000000000f, + -0.0643507165794627f, + 0.1585450355184006f, + 0.5112616136591823f, + 0.2579236279634118f, + 0.0000000000000000f, + 0.0812611176717539f, + 0.1856718091610980f, + -0.3416446842253372f, + 0.3302282550303788f, + 0.0702790691196284f, + -0.0741750459581035f, + ], + [ + 0.2500000000000000f, + 0.2206518106944236f, + 0.0000000000000000f, + 0.0000000000000000f, + 0.7071067811865476f, + 0.6235485373547694f, + 0.0000000000000000f, + 0.0000000000000000f, + 0.0000000000000000f, + 0.0000000000000000f, + 0.0000000000000000f, + 0.0000000000000000f, + 0.0000000000000000f, + 0.0000000000000000f, + 0.0000000000000000f, + 0.0000000000000000f, + ], + [ + 0.2500000000000000f, + -0.1014005039375378f, + 0.0000000000000000f, + 0.4706702258572536f, + 0.0000000000000000f, + -0.0643507165794628f, + -0.0403851516082220f, + 0.0000000000000000f, + 0.1627234014286620f, + 0.0000000000000000f, + 0.0000000000000000f, + 0.0000000000000000f, + 0.7367497537172237f, + 0.0875511500058708f, + -0.2921026642334881f, + 0.1940289303259434f, + ], + [ + 0.2500000000000000f, + -0.1014005039375377f, + 0.1957439937204294f, + -0.1621205195722993f, + 0.0000000000000000f, + -0.0643507165794628f, + 0.0074182263792424f, + -0.2904801297289980f, + 0.0952002265347504f, + 0.0000000000000000f, + -0.3675398009862027f, + 0.4921585901373873f, + 0.2462710772207515f, + -0.0794670660590957f, + 0.3623817333531167f, + -0.4351904965232280f, + ], + [ + 0.2500000000000000f, + -0.1014005039375376f, + 0.2929100136981264f, + 0.0000000000000000f, + 0.0000000000000000f, + -0.0643507165794627f, + 0.3935103426921017f, + -0.0657870154914280f, + 0.0000000000000000f, + -0.4082482904638628f, + -0.3078822139579090f, + -0.3852501370925192f, + -0.0857401903551931f, + -0.4613374887461511f, + 0.0000000000000000f, + 0.2191868483885747f, + ], + [ + 0.2500000000000000f, + -0.1014005039375376f, + -0.4067007583026072f, + -0.2125574805828705f, + 0.0000000000000000f, + -0.0643507165794627f, + -0.4517556589999464f, + 0.3046847507248840f, + 0.3017929516615503f, + -0.4082482904638635f, + -0.1747866975480813f, + 0.2110560104933581f, + -0.1426608480880734f, + -0.1381354035075829f, + -0.1743760259965108f, + 0.1135498731499426f, + ], + [ + 0.2500000000000000f, + -0.1014005039375377f, + -0.1957439937204287f, + -0.1621205195722833f, + 0.0000000000000000f, + -0.0643507165794628f, + 0.0074182263792444f, + 0.2904801297290076f, + 0.0952002265347505f, + 0.0000000000000000f, + 0.3675398009862011f, + -0.4921585901373891f, + 0.2462710772207514f, + -0.0794670660591026f, + 0.3623817333531165f, + -0.4351904965232251f, + ], + [ + 0.2500000000000000f, + -0.1014005039375375f, + 0.0000000000000000f, + -0.4706702258572528f, + 0.0000000000000000f, + -0.0643507165794627f, + 0.1107416575309343f, + 0.0000000000000000f, + -0.1627234014286617f, + 0.0000000000000000f, + 0.0000000000000000f, + 0.0000000000000000f, + 0.1488339922711357f, + 0.4972464710953509f, + 0.2921026642334879f, + 0.5550443808910661f, + ], + [ + 0.2500000000000000f, + -0.1014005039375377f, + 0.1137907446044809f, + -0.1464291867126764f, + 0.0000000000000000f, + -0.0643507165794628f, + 0.0829816309488205f, + -0.2388977352334460f, + -0.3531238544981630f, + -0.4082482904638630f, + 0.4826689115059883f, + 0.1741941265991622f, + -0.0476868035022925f, + 0.1253805944856366f, + -0.4326608024727445f, + -0.2546827712406646f, + ], + [ + 0.2500000000000000f, + -0.1014005039375377f, + -0.4444481661973438f, + 0.3085497062849487f, + 0.0000000000000000f, + -0.0643507165794628f, + 0.1585450355183970f, + -0.5112616136592012f, + 0.2579236279634129f, + 0.0000000000000000f, + -0.0812611176717504f, + -0.1856718091610990f, + -0.3416446842253373f, + 0.3302282550303805f, + 0.0702790691196282f, + -0.0741750459581023f, + ], + [ + 0.2500000000000000f, + -0.1014005039375376f, + -0.2929100136981264f, + 0.0000000000000000f, + 0.0000000000000000f, + -0.0643507165794627f, + 0.3935103426921022f, + 0.0657870154914254f, + 0.0000000000000000f, + 0.4082482904638634f, + 0.3078822139579031f, + 0.3852501370925211f, + -0.0857401903551927f, + -0.4613374887461554f, + 0.0000000000000000f, + 0.2191868483885728f, + ], + [ + 0.2500000000000000f, + -0.1014005039375376f, + -0.1137907446044814f, + -0.1464291867126654f, + 0.0000000000000000f, + -0.0643507165794627f, + 0.0829816309488214f, + 0.2388977352334547f, + -0.3531238544981624f, + 0.4082482904638630f, + -0.4826689115059858f, + -0.1741941265991621f, + -0.0476868035022928f, + 0.1253805944856431f, + -0.4326608024727457f, + -0.2546827712406641f, + ], + [ + 0.2500000000000000f, + -0.1014005039375374f, + 0.0000000000000000f, + 0.4251149611657548f, + 0.0000000000000000f, + -0.0643507165794626f, + -0.4517556589999480f, + 0.0000000000000000f, + -0.6035859033230976f, + 0.0000000000000000f, + 0.0000000000000000f, + 0.0000000000000000f, + -0.1426608480880724f, + -0.1381354035075845f, + 0.3487520519930227f, + 0.1135498731499429f, + ], + ]; + public static void ReinterpretingInverseDct( int dctRows, int dctCols, @@ -58,4 +356,849 @@ public static void ReinterpretingInverseDct( Span scratchSpace = scratch[(MaxBlocks * MaxBlocks)..]; JxlDct.ComputeScaledInverseDct(rows, cols, scratch, new JxlDctOutput(output, outputStride), scratchSpace); } + + public static void Dct2TopBlock(int s, Span block, int stride, Span output) + { + Span temp = stackalloc float[JxlFrameDimensions.DctBlockSize]; + int num2x2 = s >>> 1; // Logical right shift (no sign-extend) + + for (int y = 0; y < num2x2; y++) + { + // TODO: convert repeated arithmetic (e.g. y * DctBlockSize) + // into variables? + for (int x = 0; x < num2x2; x++) + { + float c00 = block[(y * 2 * stride) + (x * 2)]; + float c01 = block[(y * 2 * stride) + (x * 2) + 1]; + float c10 = block[(((y * 2) + 1) * stride) + (x * 2)]; + float c11 = block[(((y * 2) + 1) * stride) + (x * 2) + 1]; + + float r00 = c00 + c01 + c10 + c11; + float r01 = c00 + c01 - c10 - c11; + float r10 = c00 - c01 + c10 - c11; + float r11 = c00 - c01 - c10 + c11; + + r00 *= 0.25f; + r01 *= 0.25f; + r10 *= 0.25f; + r11 *= 0.25f; + + temp[(y * JxlFrameDimensions.DctBlockSize) + x] = r00; + temp[(y * JxlFrameDimensions.DctBlockSize) + num2x2 + x] = r01; + temp[((y + num2x2) * JxlFrameDimensions.DctBlockSize) + x] = r10; + temp[((y + num2x2) * JxlFrameDimensions.DctBlockSize) + num2x2 + x] = r11; + } + } + + for (int y = 0; y < s; y++) + { + for (int x = 0; x < s; x++) + { + output[(y * JxlFrameDimensions.BlockDimensions) + x] = temp[(y * JxlFrameDimensions.BlockDimensions) + x]; + } + } + } + + public static void AfvDct4x4(Span coeffs, Span pixels) + { + if (!Vector.IsHardwareAccelerated || Vector.Count > 16) + { + // Vector.Count can be > 16 on some CPUs, + // e.g. some ARM SVE2 processors can reach 2048 bits + for (int i = 0; i < 16; i++) + { + float scalar = 0f; + + for (int j = 0; j < 16; j++) + { + float px = pixels[j]; + float basis = AfvBasisTranspose4x4[j][i]; + scalar = (px * basis) + scalar; + } + + coeffs[i] = scalar; + } + } + else + { + for (int i = 0; i < 16; i += Vector.Count) + { + Vector scalar = Vector.Zero; + + for (int j = 0; j < 16; j++) + { + Vector px = Vector.Create(pixels[j]); + Vector basis = Vector.Create(AfvBasisTranspose4x4[j].AsSpan(i)); + scalar = (px * basis) + scalar; + } + + scalar.CopyTo(pixels[i..]); + } + } + } + + private static void AfvTransformFromPixels(int afvKind, Span pixels, int pixelsStride, Span coefficients) + { + Span scratchSpace = stackalloc float[4 * 8 * 5]; + + // Equivalent of Math.DivRem(afvKind, 2) + int afvX = afvKind & 1; + int afvY = afvKind >>> 1; + + Span block = stackalloc float[4 * 8]; + block.Clear(); + + for (int iy = 0; iy < 4; iy++) + { + for (int ix = 0; ix < 4; ix++) + { + block[((afvY == 1 ? 3 - iy : iy) * 4) + (afvX == 1 ? 3 - ix : ix)] = pixels[((iy + (4 * afvY)) * pixelsStride) + ix + (4 * afvX)]; + } + } + + Span coeff = stackalloc float[4 * 4]; + AfvDct4x4(block, coeff); + + for (int iy = 0; iy < 4; iy++) + { + for (int ix = 0; ix < 4; ix++) + { + coefficients[(iy * 2 * 8) + (ix * 2)] = coeff[(iy * 4) + ix]; + } + } + + JxlDct.ComputeScaledDct( + 4, + 4, + new JxlDctSource(pixels[((afvY * 4 * pixelsStride) + (afvX == 1 ? 0 : 4))..], pixelsStride), + block, + scratchSpace); + + for (int iy = 0; iy < 4; iy++) + { + for (int ix = 0; ix < 8; ix++) + { + coefficients[(iy * 2 * 8) + (ix * 2) + 1] = block[(iy * 4) + ix]; + } + } + + JxlDct.ComputeScaledDct( + 4, + 8, + new JxlDctSource(pixels[((afvY == 1 ? 0 : 4) * pixelsStride)..], pixelsStride), + block, + scratchSpace); + + for (int iy = 0; iy < 4; iy++) + { + for (int ix = 0; ix < 8; ix++) + { + coefficients[((1 + (iy * 2)) * 8) + ix] = block[(iy * 8) + ix]; + } + } + + float block00 = coefficients[0] * 0.25f; + float block01 = coefficients[1]; + float block10 = coefficients[8]; + coefficients[0] = (block00 + block01 + (2 * block10)) * 0.25f; + coefficients[1] = (block00 - block01) * 0.5f; + coefficients[8] = (block00 + block01 - (2 * block10)) * 0.25f; + } + + public static void TransformFromPixels(JxlAcStrategyType strategy, Span pixels, int pixelsStride, Span coefficients, Span scratchSpace) + { + switch (strategy) + { + case JxlAcStrategyType.IDENTITY: + { + for (int y = 0; y < 2; y++) + { + for (int x = 0; x < 2; x++) + { + float blockDc = 0; + + for (int iy = 0; iy < 4; iy++) + { + for (int ix = 0; ix < 4; ix++) + { + blockDc += pixels[(((y * 4) + iy) * pixelsStride) + (x * 4) + ix]; + } + } + + blockDc *= 1.0f / 16; + + for (int iy = 0; iy < 4; iy++) + { + for (int ix = 0; ix < 4; ix++) + { + if (ix == 1 && iy == 1) + { + continue; + } + + coefficients[((y + (iy * 2)) * 8) + x + (ix * 2)] = + pixels[(((y * 4) + iy) * pixelsStride) + (x * 4) + ix] - + pixels[(((y * 4) + 1) * pixelsStride) + (x * 4) + 1]; + } + } + + coefficients[((y + 2) * 8) + x + 2] = coefficients[(y * 8) + x]; + coefficients[(y * 8) + x] = blockDc; + } + } + + float block00 = coefficients[0]; + float block01 = coefficients[1]; + float block10 = coefficients[8]; + float block11 = coefficients[9]; + + coefficients[0] = (block00 + block01 + block10 + block11) * 0.25f; + coefficients[1] = (block00 + block01 - block10 - block11) * 0.25f; + coefficients[8] = (block00 - block01 + block10 - block11) * 0.25f; + coefficients[9] = (block00 - block01 - block10 + block11) * 0.25f; + + break; + } + + case JxlAcStrategyType.DCT8X4: + { + Span block = stackalloc float[4 * 8]; + + for (int x = 0; x < 2; x++) + { + JxlDct.ComputeScaledDct( + 8, + 4, + new JxlDctSource(pixels[(x * 4)..], pixelsStride), + block, + scratchSpace); + + for (int iy = 0; iy < 4; iy++) + { + for (int ix = 0; ix < 8; ix++) + { + // Store transposed. + coefficients[((x + (iy * 2)) * 8) + ix] = block[(iy * 8) + ix]; + } + } + } + + float block0 = coefficients[0]; + float block1 = coefficients[8]; + + coefficients[0] = (block0 + block1) * 0.5f; + coefficients[8] = (block0 - block1) * 0.5f; + + break; + } + + case JxlAcStrategyType.DCT4X8: + { + Span block = stackalloc float[4 * 8]; + + for (int y = 0; y < 2; y++) + { + JxlDct.ComputeScaledDct( + 4, + 8, + new JxlDctSource(pixels[(y * 4 * pixelsStride)..], pixelsStride), + block, + scratchSpace); + + for (int iy = 0; iy < 4; iy++) + { + for (int ix = 0; ix < 8; ix++) + { + coefficients[((y + (iy * 2)) * 8) + ix] = block[(iy * 8) + ix]; + } + } + } + + float block0 = coefficients[0]; + float block1 = coefficients[8]; + + coefficients[0] = (block0 + block1) * 0.5f; + coefficients[8] = (block0 - block1) * 0.5f; + + break; + } + + case JxlAcStrategyType.DCT4X4: + { + Span block = stackalloc float[4 * 4]; + + for (int y = 0; y < 2; y++) + { + for (int x = 0; x < 2; x++) + { + JxlDct.ComputeScaledDct( + 4, + 4, + new JxlDctSource(pixels[((y * 4 * pixelsStride) + (x * 4))..], pixelsStride), + block, + scratchSpace); + + for (int iy = 0; iy < 4; iy++) + { + for (int ix = 0; ix < 4; ix++) + { + coefficients[((y + (iy * 2)) * 8) + x + (ix * 2)] = block[(iy * 4) + ix]; + } + } + } + } + + float block00 = coefficients[0]; + float block01 = coefficients[1]; + float block10 = coefficients[8]; + float block11 = coefficients[9]; + + coefficients[0] = (block00 + block01 + block10 + block11) * 0.25f; + coefficients[1] = (block00 + block01 - block10 - block11) * 0.25f; + coefficients[8] = (block00 - block01 + block10 - block11) * 0.25f; + coefficients[9] = (block00 - block01 - block10 + block11) * 0.25f; + + break; + } + + case JxlAcStrategyType.DCT2X2: + { + Dct2TopBlock(8, pixels, pixelsStride, coefficients); + Dct2TopBlock(4, coefficients, JxlFrameDimensions.BlockDimensions, coefficients); + Dct2TopBlock(2, coefficients, JxlFrameDimensions.BlockDimensions, coefficients); + + break; + } + + case JxlAcStrategyType.DCT16X16: + { + JxlDct.ComputeScaledDct( + 16, + 16, + new JxlDctSource(pixels, pixelsStride), + coefficients, + scratchSpace); + break; + } + + case JxlAcStrategyType.DCT16X8: + { + JxlDct.ComputeScaledDct( + 16, + 8, + new JxlDctSource(pixels, pixelsStride), + coefficients, + scratchSpace); + break; + } + + case JxlAcStrategyType.DCT8X16: + { + JxlDct.ComputeScaledDct( + 8, + 16, + new JxlDctSource(pixels, pixelsStride), + coefficients, + scratchSpace); + break; + } + + case JxlAcStrategyType.DCT32X8: + { + JxlDct.ComputeScaledDct( + 32, + 8, + new JxlDctSource(pixels, pixelsStride), + coefficients, + scratchSpace); + break; + } + + case JxlAcStrategyType.DCT8X32: + { + JxlDct.ComputeScaledDct( + 8, + 32, + new JxlDctSource(pixels, pixelsStride), + coefficients, + scratchSpace); + break; + } + + case JxlAcStrategyType.DCT32X16: + { + JxlDct.ComputeScaledDct( + 32, + 16, + new JxlDctSource(pixels, pixelsStride), + coefficients, + scratchSpace); + break; + } + + case JxlAcStrategyType.DCT16X32: + { + JxlDct.ComputeScaledDct( + 16, + 32, + new JxlDctSource(pixels, pixelsStride), + coefficients, + scratchSpace); + break; + } + + case JxlAcStrategyType.DCT32X32: + { + JxlDct.ComputeScaledDct( + 32, + 32, + new JxlDctSource(pixels, pixelsStride), + coefficients, + scratchSpace); + break; + } + + case JxlAcStrategyType.DCT: + { + JxlDct.ComputeScaledDct( + 8, + 8, + new JxlDctSource(pixels, pixelsStride), + coefficients, + scratchSpace); + break; + } + + case JxlAcStrategyType.AFV0: + { + AfvTransformFromPixels(0, pixels, pixelsStride, coefficients); + break; + } + + case JxlAcStrategyType.AFV1: + { + AfvTransformFromPixels(1, pixels, pixelsStride, coefficients); + break; + } + + case JxlAcStrategyType.AFV2: + { + AfvTransformFromPixels(2, pixels, pixelsStride, coefficients); + break; + } + + case JxlAcStrategyType.AFV3: + { + AfvTransformFromPixels(3, pixels, pixelsStride, coefficients); + break; + } + + case JxlAcStrategyType.DCT64X64: + { + JxlDct.ComputeScaledDct( + 64, + 64, + new JxlDctSource(pixels, pixelsStride), + coefficients, + scratchSpace); + break; + } + + case JxlAcStrategyType.DCT64X32: + { + JxlDct.ComputeScaledDct( + 64, + 32, + new JxlDctSource(pixels, pixelsStride), + coefficients, + scratchSpace); + break; + } + + case JxlAcStrategyType.DCT32X64: + { + JxlDct.ComputeScaledDct( + 32, + 64, + new JxlDctSource(pixels, pixelsStride), + coefficients, + scratchSpace); + break; + } + + case JxlAcStrategyType.DCT128X128: + { + JxlDct.ComputeScaledDct( + 128, + 128, + new JxlDctSource(pixels, pixelsStride), + coefficients, + scratchSpace); + break; + } + + case JxlAcStrategyType.DCT128X64: + { + JxlDct.ComputeScaledDct( + 128, + 64, + new JxlDctSource(pixels, pixelsStride), + coefficients, + scratchSpace); + break; + } + + case JxlAcStrategyType.DCT64X128: + { + JxlDct.ComputeScaledDct( + 64, + 128, + new JxlDctSource(pixels, pixelsStride), + coefficients, + scratchSpace); + break; + } + + case JxlAcStrategyType.DCT256X256: + { + JxlDct.ComputeScaledDct( + 256, + 256, + new JxlDctSource(pixels, pixelsStride), + coefficients, + scratchSpace); + break; + } + + case JxlAcStrategyType.DCT256X128: + { + JxlDct.ComputeScaledDct( + 256, + 128, + new JxlDctSource(pixels, pixelsStride), + coefficients, + scratchSpace); + break; + } + + case JxlAcStrategyType.DCT128X256: + { + JxlDct.ComputeScaledDct( + 128, + 256, + new JxlDctSource(pixels, pixelsStride), + coefficients, + scratchSpace); + break; + } + } + } + + public static void DcFromLowestFrequencies(JxlAcStrategyType strategy, Span block, Span dc, int dcStride, Span scratchSpace) + { + switch (strategy) + { + case JxlAcStrategyType.DCT16X8: + { + ReinterpretingInverseDct( + 2 * JxlFrameDimensions.BlockDimensions, + JxlFrameDimensions.BlockDimensions, + 2, + 1, + 2, + 1, + block, + 2 * JxlFrameDimensions.BlockDimensions, + dc, + dcStride, + scratchSpace); + break; + } + + case JxlAcStrategyType.DCT8X16: + { + ReinterpretingInverseDct( + JxlFrameDimensions.BlockDimensions, + 2 * JxlFrameDimensions.BlockDimensions, + 1, + 2, + 1, + 2, + block, + 2 * JxlFrameDimensions.BlockDimensions, + dc, + dcStride, + scratchSpace); + break; + } + + case JxlAcStrategyType.DCT16X16: + { + ReinterpretingInverseDct( + 2 * JxlFrameDimensions.BlockDimensions, + 2 * JxlFrameDimensions.BlockDimensions, + 2, + 2, + 2, + 2, + block, + 2 * JxlFrameDimensions.BlockDimensions, + dc, + dcStride, + scratchSpace); + break; + } + + case JxlAcStrategyType.DCT32X8: + { + ReinterpretingInverseDct( + 4 * JxlFrameDimensions.BlockDimensions, + JxlFrameDimensions.BlockDimensions, + 4, + 1, + 4, + 1, + block, + 4 * JxlFrameDimensions.BlockDimensions, + dc, + dcStride, + scratchSpace); + break; + } + + case JxlAcStrategyType.DCT8X32: + { + ReinterpretingInverseDct( + JxlFrameDimensions.BlockDimensions, + 4 * JxlFrameDimensions.BlockDimensions, + 1, + 4, + 1, + 4, + block, + 4 * JxlFrameDimensions.BlockDimensions, + dc, + dcStride, + scratchSpace); + break; + } + + case JxlAcStrategyType.DCT32X16: + { + ReinterpretingInverseDct( + 4 * JxlFrameDimensions.BlockDimensions, + 2 * JxlFrameDimensions.BlockDimensions, + 4, + 2, + 4, + 2, + block, + 4 * JxlFrameDimensions.BlockDimensions, + dc, + dcStride, + scratchSpace); + break; + } + + case JxlAcStrategyType.DCT16X32: + { + ReinterpretingInverseDct( + 2 * JxlFrameDimensions.BlockDimensions, + 4 * JxlFrameDimensions.BlockDimensions, + 2, + 4, + 2, + 4, + block, + 4 * JxlFrameDimensions.BlockDimensions, + dc, + dcStride, + scratchSpace); + break; + } + + case JxlAcStrategyType.DCT32X32: + { + ReinterpretingInverseDct( + 4 * JxlFrameDimensions.BlockDimensions, + 4 * JxlFrameDimensions.BlockDimensions, + 4, + 4, + 4, + 4, + block, + 4 * JxlFrameDimensions.BlockDimensions, + dc, + dcStride, + scratchSpace); + break; + } + + case JxlAcStrategyType.DCT64X32: + { + ReinterpretingInverseDct( + 8 * JxlFrameDimensions.BlockDimensions, + 4 * JxlFrameDimensions.BlockDimensions, + 8, + 4, + 8, + 4, + block, + 8 * JxlFrameDimensions.BlockDimensions, + dc, + dcStride, + scratchSpace); + break; + } + + case JxlAcStrategyType.DCT32X64: + { + ReinterpretingInverseDct( + 4 * JxlFrameDimensions.BlockDimensions, + 8 * JxlFrameDimensions.BlockDimensions, + 4, + 8, + 4, + 8, + block, + 8 * JxlFrameDimensions.BlockDimensions, + dc, + dcStride, + scratchSpace); + break; + } + + case JxlAcStrategyType.DCT64X64: + { + ReinterpretingInverseDct( + 8 * JxlFrameDimensions.BlockDimensions, + 8 * JxlFrameDimensions.BlockDimensions, + 8, + 8, + 8, + 8, + block, + 8 * JxlFrameDimensions.BlockDimensions, + dc, + dcStride, + scratchSpace); + break; + } + + case JxlAcStrategyType.DCT128X64: + { + ReinterpretingInverseDct( + 16 * JxlFrameDimensions.BlockDimensions, + 8 * JxlFrameDimensions.BlockDimensions, + 16, + 8, + 16, + 8, + block, + 16 * JxlFrameDimensions.BlockDimensions, + dc, + dcStride, + scratchSpace); + break; + } + + case JxlAcStrategyType.DCT64X128: + { + ReinterpretingInverseDct( + 8 * JxlFrameDimensions.BlockDimensions, + 16 * JxlFrameDimensions.BlockDimensions, + 8, + 16, + 8, + 16, + block, + 16 * JxlFrameDimensions.BlockDimensions, + dc, + dcStride, + scratchSpace); + break; + } + + case JxlAcStrategyType.DCT128X128: + { + ReinterpretingInverseDct( + 16 * JxlFrameDimensions.BlockDimensions, + 16 * JxlFrameDimensions.BlockDimensions, + 16, + 16, + 16, + 16, + block, + 16 * JxlFrameDimensions.BlockDimensions, + dc, + dcStride, + scratchSpace); + break; + } + + case JxlAcStrategyType.DCT256X128: + { + ReinterpretingInverseDct( + 32 * JxlFrameDimensions.BlockDimensions, + 16 * JxlFrameDimensions.BlockDimensions, + 32, + 16, + 32, + 16, + block, + 32 * JxlFrameDimensions.BlockDimensions, + dc, + dcStride, + scratchSpace); + break; + } + + case JxlAcStrategyType.DCT128X256: + { + ReinterpretingInverseDct( + 16 * JxlFrameDimensions.BlockDimensions, + 32 * JxlFrameDimensions.BlockDimensions, + 16, + 32, + 16, + 32, + block, + 32 * JxlFrameDimensions.BlockDimensions, + dc, + dcStride, + scratchSpace); + break; + } + + case JxlAcStrategyType.DCT256X256: + { + ReinterpretingInverseDct( + 32 * JxlFrameDimensions.BlockDimensions, + 32 * JxlFrameDimensions.BlockDimensions, + 32, + 32, + 32, + 32, + block, + 32 * JxlFrameDimensions.BlockDimensions, + dc, + dcStride, + scratchSpace); + break; + } + + case JxlAcStrategyType.DCT: + case JxlAcStrategyType.DCT2X2: + case JxlAcStrategyType.DCT4X4: + case JxlAcStrategyType.DCT4X8: + case JxlAcStrategyType.DCT8X4: + case JxlAcStrategyType.AFV0: + case JxlAcStrategyType.AFV1: + case JxlAcStrategyType.AFV2: + case JxlAcStrategyType.AFV3: + case JxlAcStrategyType.IDENTITY: + dc[0] = block[0]; + break; + } + } } From ea8a62973646225224d2d669450f76621d5efe95 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 12 Sep 2026 19:15:14 +0400 Subject: [PATCH 176/328] Add quantizer weights encoder --- .../Dct/JxlDctQuantWeightParameters.cs | 5 +- .../Encoder/JxlQuantizerWeightsEncoder.cs | 331 ++++++++++++++++++ .../Quantization/JxlDequantMatrices.cs | 10 + 3 files changed, 345 insertions(+), 1 deletion(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlQuantizerWeightsEncoder.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctQuantWeightParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctQuantWeightParameters.cs index 2c0573668c..b2d9ae183e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctQuantWeightParameters.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctQuantWeightParameters.cs @@ -5,7 +5,10 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; internal sealed class JxlDctQuantWeightParameters { - private const int Log2MaxDistanceBands = 4; + /// + /// Log2 of the maximum number of distance bands. + /// + public const int Log2MaxDistanceBands = 4; private const int MaxDistanceBands = 1 + (1 << Log2MaxDistanceBands); public JxlDctQuantWeightParameters() diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlQuantizerWeightsEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlQuantizerWeightsEncoder.cs new file mode 100644 index 0000000000..1c63ff26e1 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlQuantizerWeightsEncoder.cs @@ -0,0 +1,331 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; + +/// +/// Writes transform parameters, such as quantization and DCT, +/// into the bit-stream. +/// +internal static class JxlQuantizerWeightsEncoder +{ + public static bool EncodeDctParameters(JxlDctQuantWeightParameters parameters, JxlBitWriter writer) + { + if (parameters.NumDistanceBands < 1) + { + // at least 1 distance band is required + return false; + } + + writer.Write(JxlDctQuantWeightParameters.Log2MaxDistanceBands, parameters.NumDistanceBands - 1); + + for (int c = 0; c < 3; c++) + { + Span bands = parameters.DistanceBands[c]; + + for (int i = 0; i < parameters.NumDistanceBands; i++) + { + // each DCT distance band is a 16-bit half-precision floating-point number + if (!JxlF16Coder.Write(bands[i] * (i == 0 ? (1 / 64.0f) : 1.0f), writer)) + { + return false; + } + } + } + + return true; + } + + public static bool EncodeQuant(Configuration configuration, JxlQuantizerEncoding encoding, int idx, int sizeX, int sizeY, JxlBitWriter writer, JxlModularFrameEncoder encoder) + { + writer.Write(JxlQuantizerConstants.Log2NumQuantModes, (int)encoding.Mode); + + sizeX *= JxlFrameDimensions.BlockDimensions; + sizeY *= JxlFrameDimensions.BlockDimensions; + + switch (encoding.Mode) + { + case JxlQuantMode.Library: + writer.Write(JxlQuantizerConstants.CeilLog2NumPredefinedTables, encoding.Predefined); + break; + + case JxlQuantMode.Id: + for (int c = 0; c < 3; c++) + { + Span idWeight = encoding.IdWeights![c]; + + for (int i = 0; i < 3; i++) + { + if (!JxlF16Coder.Write(idWeight[i] * (1.0f / 64), writer)) + { + return false; + } + } + } + + break; + + case JxlQuantMode.Dct2: + for (int c = 0; c < 3; c++) + { + Span dct2Weight = encoding.Dct2Weights![c]; + + for (int i = 0; i < 6; i++) + { + if (!JxlF16Coder.Write(dct2Weight[i] * (1.0f / 64), writer)) + { + return false; + } + } + } + + break; + + case JxlQuantMode.Dct4x8: + Span multipliers = encoding.Dct4x8Multipliers!.AsSpan(); + + if (!JxlF16Coder.Write(multipliers[0], writer) || + !JxlF16Coder.Write(multipliers[1], writer) || + !JxlF16Coder.Write(multipliers[2], writer)) + { + return false; + } + + if (!EncodeDctParameters(encoding.DctParameters!, writer)) + { + return false; + } + + break; + + case JxlQuantMode.Dct4: + for (int c = 0; c < 3; c++) + { + Span dct4Mul = encoding.Dct4Multipliers![c]; + + if (!JxlF16Coder.Write(dct4Mul[0], writer) || !JxlF16Coder.Write(dct4Mul[1], writer)) + { + return false; + } + } + + if (!EncodeDctParameters(encoding.DctParameters!, writer)) + { + return false; + } + + break; + + case JxlQuantMode.Dct: + if (!EncodeDctParameters(encoding.DctParameters!, writer)) + { + return false; + } + + break; + + case JxlQuantMode.Raw: + if (!JxlModularFrameEncoder.EncodeQuantTable(configuration, sizeX, sizeY, writer, encoding, idx, encoder)) + { + return false; + } + + break; + + case JxlQuantMode.Afv: + for (int c = 0; c < 3; c++) + { + Span afvWeights = encoding.AfvWeights![c]; + + for (int i = 0; i < 9; i++) + { + if (!JxlF16Coder.Write(afvWeights[i] * (i < 6 ? 1.0f / 64 : 1.0f), writer)) + { + return false; + } + } + } + + if (!EncodeDctParameters(encoding.DctParameters!, writer)) + { + return false; + } + + if (!EncodeDctParameters(encoding.DctParametersAfv4x4!, writer)) + { + return false; + } + + break; + } + + return true; + } + + public static bool DequantMatricesEncode(Configuration configuration, JxlDequantMatrices matrices, JxlBitWriter writer, JxlModularFrameEncoder? encoder) + { + bool allDefault = true; + + // We need a Memory as well as Span. + // Memory - so we can use it in lambda expression + // Span - micro-optimization, offers slightly better performance + Memory encodingsMemory = matrices.GetEncodingsMemory(); + Span encodings = encodingsMemory.Span; + + for (int i = 0; i < encodings.Length; i++) + { + JxlQuantizerEncoding enc = encodings[i]; + + if (enc.Mode != JxlQuantMode.Library || enc.Predefined != 0) + { + allDefault = false; + } + } + + return writer.WithMaxBits(512 * 1024, () => + { + writer.Write(1, allDefault ? 1 : 0); + + if (allDefault) + { + for (int i = 0; i < encodingsMemory.Length; i++) + { + if (!EncodeQuant(configuration, encodingsMemory.Span[i], i, JxlDequantMatrices.RequiredSizeX[i], JxlDequantMatrices.RequiredSizeY[i], writer, encoder!)) + { + return false; + } + } + } + + return true; + }); + } + + public static bool DequantMatricesEncodeDc(JxlDequantMatrices matrices, JxlBitWriter writer) + { + Span dcQuant = matrices.GetDcQuants(); + bool allDefault = dcQuant[0] != JxlQuantizerConstants.DcQuant[0] + || dcQuant[1] != JxlQuantizerConstants.DcQuant[1] + || dcQuant[2] != JxlQuantizerConstants.DcQuant[2]; + + return writer.WithMaxBits(1 + (sizeof(float) * JxlMath.BitsPerByte * 3), () => + { + Span dcQuant = matrices.GetDcQuants(); + writer.Write(1, allDefault ? 1 : 0); + + if (!allDefault) + { + if (!JxlF16Coder.Write(dcQuant[0] * 128.0f, writer) || + !JxlF16Coder.Write(dcQuant[1] * 128.0f, writer) || + !JxlF16Coder.Write(dcQuant[2] * 128.0f, writer)) + { + return false; + } + } + + return true; + }); + } + + public static bool DequantMatricesSetCustomDc(Configuration configuration, JxlDequantMatrices matrices, InlineArray3 dc) + { + matrices.SetDcQuant(dc); + + // TODO: we might need a fixed-size memory stream to reduce GC + // pressure on MemoryStream's underlying array resizes, the + // encoded value should be < 12 bytes + using MemoryStream ms = new(); + JxlBitWriter writer = new(ms); + + if (!DequantMatricesEncodeDc(matrices, writer)) + { + return false; + } + + writer.ZeroPadToByte(); + + ms.Position = 0; + JxlBitReader reader = new(ms); + + if (!matrices.DecodeDc(reader)) + { + return false; + } + + return true; + } + + public static bool DequantMatricesScaleDc(Configuration configuration, JxlDequantMatrices matrices, float scale) + { + InlineArray3 dc = default; + dc[0] = matrices.GetInverseDcQuant(0) * (1.0f / scale); + dc[1] = matrices.GetInverseDcQuant(1) * (1.0f / scale); + dc[2] = matrices.GetInverseDcQuant(2) * (1.0f / scale); + + return DequantMatricesSetCustomDc(configuration, matrices, dc); + } + + public static bool DequantMatricesRoundtrip(Configuration configuration, JxlDequantMatrices matrices) + { + // Up to 64KB of data + // TODO: pool 64KB of data & use it as fixed-size data for a custom stream? + using MemoryStream ms = new(); + JxlBitWriter writer = new(ms); + + if (!DequantMatricesEncode(configuration, matrices, writer, null)) + { + return false; + } + + writer.ZeroPadToByte(); + + ms.Position = 0; + JxlBitReader reader = new(ms); + + if (!matrices.Decode(configuration, reader)) + { + return false; + } + + return true; + } + + public static bool DequantMatricesSetCustom(Configuration configuration, JxlDequantMatrices matrices, JxlQuantizerEncoding[] encodings, JxlModularFrameEncoder encoder) + { + if (encodings.Length != JxlQuantizerConstants.NumberOfQuantizerTables) + { + return false; + } + + matrices.SetEncodings(encodings); + + for (int i = 0; i < encodings.Length; i++) + { + if (encodings[i].Mode == JxlQuantMode.Raw) + { + if (!encoder.AddQuantTable( + JxlDequantMatrices.RequiredSizeX[i] * JxlFrameDimensions.BlockDimensions, + JxlDequantMatrices.RequiredSizeY[i] * JxlFrameDimensions.BlockDimensions, + encodings[i], + i)) + { + return false; + } + } + } + + if (!DequantMatricesRoundtrip(configuration, matrices)) + { + return false; + } + + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlDequantMatrices.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlDequantMatrices.cs index 8841f0ceac..40fbbfe817 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlDequantMatrices.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlDequantMatrices.cs @@ -264,4 +264,14 @@ public void SetEncodings(JxlQuantizerEncoding[] encodings) /// is simply an empty span. /// public Span GetEncodings() => this.encodings; + + /// + /// Returns quantizer encodings for this dequant matrices instance. + /// + /// + /// Encodings set by the method. + /// By default (when the aforementioned method wasn't invoked), the result + /// is simply an empty span. + /// + public Memory GetEncodingsMemory() => this.encodings; } From 169262303d9436a3f70a2fda303972f290345919 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 14 Sep 2026 20:40:52 +0400 Subject: [PATCH 177/328] Add group decoder --- .../Jxl/Cms/JxlChromaticAdaptationMatrices.cs | 26 + .../Jxl/Cms/JxlExtraTransferFunction.cs | 32 + .../Jxl/IO/FrameHeader/JxlFrameHeader.cs | 38 + .../Formats/Jxl/IO/Jpeg/Data/JpegComponent.cs | 14 + .../Formats/Jxl/Memory/JxlMemoryHelpers.cs | 39 + .../Formats/Jxl/Memory/JxlPlaneBase.cs | 9 + .../Formats/Jxl/Memory/JxlPlane{T}.cs | 7 + .../Decoder/FrameDecoder/JxlFrameDecoder.cs | 1 + .../Decoder/Group/BlockLoader/IJxlGetBlock.cs | 34 + .../BlockLoader/JxlBitStreamBlockLoader.cs | 232 +++++ .../BlockLoader/JxlEncoderBlockLoader.cs | 101 +++ .../Processing/Decoder/Group/JxlDrawMode.cs | 10 + .../{ => Group}/JxlGroupBorderAssigner.cs | 2 +- .../Decoder/Group/JxlGroupDecoder.cs | 847 ++++++++++++++++++ .../{ => Group}/JxlGroupDecoderCache.cs | 2 +- .../Decoder/JxlPassesDecoderState.cs | 2 +- .../Processing/Image/JxlImageOperations.cs | 2 +- .../Formats/Jxl/Processing/JxlCompressedDc.cs | 21 +- .../Quantization/JxlQuantizerSimd.cs | 2 +- .../RenderPipeline/RenderPipelineStageBase.cs | 2 +- 20 files changed, 1415 insertions(+), 8 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlChromaticAdaptationMatrices.cs create mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlExtraTransferFunction.cs create mode 100644 src/ImageSharp/Formats/Jxl/Memory/JxlMemoryHelpers.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/BlockLoader/IJxlGetBlock.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/BlockLoader/JxlBitStreamBlockLoader.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/BlockLoader/JxlEncoderBlockLoader.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlDrawMode.cs rename src/ImageSharp/Formats/Jxl/Processing/Decoder/{ => Group}/JxlGroupBorderAssigner.cs (99%) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs rename src/ImageSharp/Formats/Jxl/Processing/Decoder/{ => Group}/JxlGroupDecoderCache.cs (74%) diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlChromaticAdaptationMatrices.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlChromaticAdaptationMatrices.cs new file mode 100644 index 0000000000..9c75d633b7 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlChromaticAdaptationMatrices.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms; + +/// +/// Chromatic adaption matrix lookups. +/// +internal static class JxlChromaticAdaptationMatrices +{ + public static readonly JxlMatrix3x3F Bradford = new( + [ + [0.8951f, 0.2664f, -0.1614f], + [-0.7502f, 1.7135f, 0.0367f], + [0.0389f, -0.0685f, 1.0296f] + ]); + + public static readonly JxlMatrix3x3F InverseBradford = new( + [ + [0.9869929f, -0.1470543f, 0.1599627f], + [0.9869929f, -0.1470543f, 0.1599627f], + [-0.0085287f, 0.0400428f, 0.9684867f] + ]); +} diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlExtraTransferFunction.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlExtraTransferFunction.cs new file mode 100644 index 0000000000..c3e1705400 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlExtraTransferFunction.cs @@ -0,0 +1,32 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Cms.TransferFunctions; + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms; + +/// +/// Specifies the kind of extra transfer function. +/// +internal enum JxlExtraTransferFunction : byte +{ + /// + /// No transfer functions. + /// + None, + + /// + /// Perceptual Quantization. + /// + Pq, + + /// + /// Hybrid Log Gamma. . + /// + Hlg, + + /// + /// sRGB. . + /// + SRgb, +} diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs index c1cf766b0e..e39739db8b 100644 --- a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs +++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs @@ -6,6 +6,7 @@ // so we can use the ref keyword on them directly. #pragma warning disable IDE0032 // Use auto property +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Processing; @@ -346,6 +347,43 @@ public JxlFrameDimensions FrameDimensions this.frameType != JxlFrameType.DcFrame && (this.animationFrame?.Duration == 0 || this.saveAsReference != 0); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static InlineArray3 JpegOrder(JxlColorTransform transform, bool isGray) + { + if (isGray) + { + return default; // {0, 0, 0} + } + + switch (transform) + { + case JxlColorTransform.YCbCr: + { + // {1, 0, 2} + InlineArray3 result = default; + result[0] = 1; + result[2] = 2; + return result; + } + + case JxlColorTransform.None: + { + // {0, 1, 2} + InlineArray3 result = default; + result[1] = 1; + result[2] = 2; + return result; + } + + case JxlColorTransform.Xyb: + // JPEG does not support XYB + throw new InvalidOperationException("Color transform cannot be XYB"); + + default: + throw new InvalidOperationException("Invalid color transform for JPEG order"); + } + } + private void UpdateFlag(bool condition, ulong flag) { if (condition) diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegComponent.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegComponent.cs index 297acacc0a..194863eb7b 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegComponent.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegComponent.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Runtime.InteropServices; + namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; // We may need to get a ref to fields, so don't @@ -50,4 +52,16 @@ internal sealed class JpegComponent /// Gets or sets DCT coefficients. /// public List Coefficients { get; set; } = []; + + /// + /// Gets the as a . + /// + /// Lightweight wrapper over data. + /// + /// ⚠️ Adding or removing things from and using an old + /// wrapper means the span's length wouldn't be updated. This would + /// result in an access violation exception if items out of bounds were accessed + /// through the span. + /// + public Span DangerousCoefficientsAsSpan() => CollectionsMarshal.AsSpan(this.Coefficients); } diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlMemoryHelpers.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlMemoryHelpers.cs new file mode 100644 index 0000000000..17f2119487 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlMemoryHelpers.cs @@ -0,0 +1,39 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Memory; + +/// +/// Memory management utilities. +/// +internal static class JxlMemoryHelpers +{ + public static Memory CastMemory(Memory memory) + where TSourceType : unmanaged + where TOutputType : unmanaged + { + CastingMemoryManager memoryManager = new(memory); + return memoryManager.Memory; + } + + private sealed class CastingMemoryManager(Memory memory) + : MemoryManager + where TSourceType : unmanaged + where TOutputType : unmanaged + { + public override Span GetSpan() => MemoryMarshal.Cast(memory.Span); + + // We don't use these. + public override MemoryHandle Pin(int elementIndex = 0) => throw new NotImplementedException(); + + public override void Unpin() => throw new NotImplementedException(); + + protected override void Dispose(bool disposing) + { + // blank + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs index 5d2300c2a0..f2e5235f9a 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs @@ -175,6 +175,15 @@ protected Span GetRowPlusBase(int y, int plus) return MemoryMarshal.Cast(row); } + protected Memory GetRowMemoryBase(int y) + where T : unmanaged + { + DebugGuard.MustBeLessThan(y, this.YSize, nameof(y)); + + Memory row = this.Bytes[(y * this.BytesPerRow)..]; + return JxlMemoryHelpers.CastMemory(row); + } + /// /// Swaps properties & data of this image with the specified image. /// diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs index 489b529221..d420a16f95 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs @@ -80,6 +80,13 @@ public static JxlPlane Create(Configuration configuration, int xSize, int ySi /// A span which covers memory for the specified row. public Span GetRowPlus(int y, int plus) => this.GetRowMinusBase(y, plus); + /// + /// Returns a memory for the specified row. + /// + /// The row index. + /// A memory which covers data for the specified row. + public Memory GetRowMemory(int y) => this.GetRowMemoryBase(y); + /// /// Returns a span for the specified row within the specified rectangle bounds. /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/FrameDecoder/JxlFrameDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/FrameDecoder/JxlFrameDecoder.cs index 4343bab95f..64f77f5b2e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/FrameDecoder/JxlFrameDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/FrameDecoder/JxlFrameDecoder.cs @@ -4,6 +4,7 @@ using SixLabors.ImageSharp.Formats.Jxl.IO; using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Group; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/BlockLoader/IJxlGetBlock.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/BlockLoader/IJxlGetBlock.cs new file mode 100644 index 0000000000..5fe20e5cc0 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/BlockLoader/IJxlGetBlock.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Group.BlockLoader; + +/// +/// Interface for reading groups for group decoder. +/// +internal interface IJxlGetBlock +{ + /// + /// Signals the start of a row. + /// + /// Y coordinate of the block. + public void StartRow(int by); + + /// + /// Tries to load a block. + /// + /// X coordinate of the block. + /// Y coordinate of the block. + /// AC strategy. + /// Block size. + /// Log2(N) where N=number of covered blocks. + /// First block + /// Second block + /// Third block + /// AC type + /// Status of the operation. + public bool TryLoadBlock(int bx, int by, JxlAcStrategy acs, int size, int log2CoveredBlocks, JxlDctAcPointer block0, JxlDctAcPointer block1, JxlDctAcPointer block2, JxlDctAcType acType); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/BlockLoader/JxlBitStreamBlockLoader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/BlockLoader/JxlBitStreamBlockLoader.cs new file mode 100644 index 0000000000..6b52794d86 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/BlockLoader/JxlBitStreamBlockLoader.cs @@ -0,0 +1,232 @@ +// 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.Jxl.IO.FrameHeader; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Group.BlockLoader; + +/// +/// Loads coefficient blocks from the JPEG XL bitstream. +/// +internal sealed class JxlBitStreamBlockLoader : IJxlGetBlock +{ + private readonly JxlGroupDecoderCache groupDecCache; + private readonly JxlBlockContextMap blockCtxMap; + private readonly JxlImageI rawQuantField; + private readonly JxlImageB quantDc; + private readonly JxlAnsSymbolReader[] decoders; + private readonly JxlBitReader[] readers; + + private IMemoryOwner? shiftForPass; + private byte[]? coeffOrders; + private List[]? contextMaps; + + private int coeffOrderSize; + private int numPasses; + private int[] ctxOffset; + private int nzerosStride; + + private InlineArray3 hshift; + private InlineArray3 vshift; + + private int currentBy; + private Memory qfRow; + private Memory quantDcRow; + + private Rectangle rect; + + // 11 because JxlShared.MaximumNumberOfPasses = 11 + private InlineArray3>> rowNZeroes; + private InlineArray3>> rowNZeroesTop; + + public JxlBitStreamBlockLoader( + JxlGroupDecoderCache groupDecCache, + JxlBlockContextMap blockCtxMap, + JxlImageI rawQuantField, + JxlImageB quantDc, + JxlAnsSymbolReader[] decoders, + JxlBitReader[] readers) + { + this.groupDecCache = groupDecCache; + this.blockCtxMap = blockCtxMap; + this.rawQuantField = rawQuantField; + this.quantDc = quantDc; + this.decoders = decoders; + this.readers = readers; + this.shiftForPass = null; + this.coeffOrders = null; + this.coeffOrderSize = 0; + this.numPasses = 0; + this.ctxOffset = []; + this.nzerosStride = 0; + this.hshift = default; + this.vshift = default; + this.currentBy = 0; + this.qfRow = Memory.Empty; + this.quantDcRow = Memory.Empty; + } + + public JxlAnsSymbolReader[] Decoders => this.decoders; + + /// + public void StartRow(int by) + { + this.currentBy = by; + this.qfRow = this.rawQuantField.GetRowMemory(by); + + for (int c = 0; c < 3; c++) + { + int sby = by >> this.vshift[c]; + this.quantDcRow = this.quantDc.GetRowMemory(this.rect.Y0() + by)[this.rect.X0()..]; + + for (int i = 0; i < this.numPasses; i++) + { + this.rowNZeroes[i][c] = this.groupDecCache.NumberOfNonZeroes[i].PlaneRowMemory(c, sby); + this.rowNZeroesTop[i][c] = + sby == 0 + ? default + : this.groupDecCache.NumberOfNonZeroes[i].PlaneRowMemory(c, sby - 1); + } + } + } + + /// + public bool TryLoadBlock( + int bx, + int by, + JxlAcStrategy acs, + int size, + int log2CoveredBlocks, + JxlDctAcPointer block0, + JxlDctAcPointer block1, + JxlDctAcPointer block2, + JxlDctAcType acType) + { + for (int c = 0; c < 3; c++) + { + int sbx = bx >> this.hshift[c]; + int sby = by >> this.vshift[c]; + + if ((sbx << this.hshift[c]) != bx || (sby << this.vshift[c]) != by) + { + continue; + } + + for (int pass = 0; pass < this.numPasses; pass++) + { + Span rowNzeros = this.groupDecCache.NumberOfNonZeroes[pass].PlaneRow(c, sby); + ReadOnlySpan rowNzerosTop = sby == 0 + ? default + : this.groupDecCache.NumberOfNonZeroes[pass].PlaneRow(c, sby - 1); + + JxlDctAcPointer block = c switch + { + 0 => block0, + 1 => block1, + _ => block2 + }; + + if (!JxlGroupDecoder.DecodeAcVarBlock( + acType, + this.decoders[pass].UsesLz77, + this.ctxOffset[pass], + log2CoveredBlocks, + rowNzeros, + rowNzerosTop, + this.nzerosStride, + c, + sbx, + bx, + acs, + this.coeffOrders!.AsSpan()[(pass * this.coeffOrderSize)..], + this.readers[pass], + this.decoders[pass], + CollectionsMarshal.AsSpan(this.contextMaps![pass]), + this.quantDcRow, + this.qfRow, + this.blockCtxMap, + block, + (int)this.shiftForPass!.Memory.Span[pass])) + { + return false; + } + } + } + + return true; + } + + public bool Init( + JxlFrameHeader frameHeader, + JxlBitReader[] readers, + int numPasses, + int groupIndex, + int histogramSelectorBits, + Rectangle rect, + JxlPassesDecoderState decState, + int firstPass) + { + for (int i = 0; i < 3; i++) + { + this.hshift[i] = frameHeader.ChromaSubsampling!.HShift(i); + this.vshift[i] = frameHeader.ChromaSubsampling.VShift(i); + } + + this.coeffOrderSize = decState.Shared.CoeffOrderSize; + this.coeffOrders = decState.Shared.CoeffOrders.ToArray()[(firstPass * this.coeffOrderSize)..]; + + this.contextMaps = new List[numPasses]; + for (int i = 0; i < numPasses; i++) + { + this.contextMaps[i] = decState.ContextMap[firstPass + i]; + } + + this.numPasses = numPasses; + this.shiftForPass = decState.Shared.FrameHeader.Passes.Shift[firstPass..]; + this.nzerosStride = this.groupDecCache!.NumberOfNonZeroes[0].PixelsPerRow; + + this.ctxOffset = new int[numPasses]; + + for (int pass = 0; pass < numPasses; pass++) + { + int currentHistogram = 0; + + if (histogramSelectorBits != 0) + { + currentHistogram = (int)readers[pass].ReadBits32((uint)histogramSelectorBits); + } + + if (currentHistogram >= decState.Shared.NumHistograms) + { + return false; + } + + this.ctxOffset[pass] = currentHistogram * this.blockCtxMap.AcContextCount; + + this.decoders[pass] = + JxlAnsSymbolReader.Create( + decState.Code[pass + firstPass], + readers[pass]); + } + + for (int i = 1; i < numPasses; i++) + { + if (this.nzerosStride != this.groupDecCache.NumberOfNonZeroes[i].PixelsPerRow) + { + return false; + } + } + + this.rect = rect; + + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/BlockLoader/JxlEncoderBlockLoader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/BlockLoader/JxlEncoderBlockLoader.cs new file mode 100644 index 0000000000..3a04fd22d5 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/BlockLoader/JxlEncoderBlockLoader.cs @@ -0,0 +1,101 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Group.BlockLoader; + +internal sealed class JxlEncoderBlockLoader : IJxlGetBlock +{ + private readonly IReadOnlyList quantizedAc; + private readonly int[][] rows; + private readonly uint[] shiftForPass; + + private int offset; + + public JxlEncoderBlockLoader( + IReadOnlyList quantizedAc, + int[][] rows, + uint[] shiftForPass) + { + this.quantizedAc = quantizedAc; + this.rows = rows; + this.shiftForPass = shiftForPass; + this.offset = 0; + } + + public void StartRow(int by) + { + } + + public bool TryLoadBlock( + int bx, + int by, + JxlAcStrategy acs, + int size, + int log2CoveredBlocks, + JxlDctAcPointer block0, + JxlDctAcPointer block1, + JxlDctAcPointer block2, + JxlDctAcType acType) + { + if (acType != JxlDctAcType.Ac32) + { + return false; + } + + for (int c = 0; c < 3; c++) + { + JxlDctAcPointer block = c switch + { + 0 => block0, + 1 => block1, + _ => block2 + }; + + for (int i = 0; i < this.quantizedAc.Count; i++) + { + Span block32 = block.Pointer32; + + int shift = (int)this.shiftForPass[i]; + int scale = 1 << shift; + ReadOnlySpan row = this.rows[(i * 3) + c].AsSpan(); + + for (int k = 0; k < size; k++) + { + block32[k] += row[this.offset + k] * scale; + } + } + } + + this.offset += size; + return true; + } + + public static JxlEncoderBlockLoader Create( + IReadOnlyList ac, + int groupIndex, + ReadOnlySpan shiftForPass) + { + int[][] rows = new int[ac.Count * 3][]; + + for (int i = 0; i < ac.Count; i++) + { + if (ac[i].Type != JxlDctAcType.Ac32) + { + throw new InvalidOperationException("AC image must use 32-bit coefficients."); + } + + for (int c = 0; c < 3; c++) + { + rows[(i * 3) + c] = ac[i].GetPlaneRow(c, groupIndex, 0).Pointer32.ToArray(); + } + } + + return new JxlEncoderBlockLoader( + ac, + rows, + shiftForPass.ToArray()); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlDrawMode.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlDrawMode.cs new file mode 100644 index 0000000000..c1eec3caca --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlDrawMode.cs @@ -0,0 +1,10 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Group; + +internal enum JxlDrawMode : byte +{ + Draw, + DoNotDraw +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlGroupBorderAssigner.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupBorderAssigner.cs similarity index 99% rename from src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlGroupBorderAssigner.cs rename to src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupBorderAssigner.cs index 3833c5ede2..dbaf45e384 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlGroupBorderAssigner.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupBorderAssigner.cs @@ -4,7 +4,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using SixLabors.ImageSharp.Memory; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Group; internal sealed class JxlGroupBorderAssigner { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs new file mode 100644 index 0000000000..26f47a6646 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs @@ -0,0 +1,847 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; +using SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; +using SixLabors.ImageSharp.Formats.Jxl.Memory; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Group.BlockLoader; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; +using SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Group; + +internal static class JxlGroupDecoder +{ + public static void Transpose8x8InPlace(Span block) + { + for (int x = 0; x < 8; x++) + { + for (int y = 0; y < 8; y++) + { + RuntimeUtility.Swap(ref block[(y * 8) + x], ref block[(x * 8) + y]); + } + } + } + + public static void DequantLane( + JxlDctAcType acType, + Vector scaledDequantX, + Vector scaledDequantY, + Vector scaledDequantB, + ReadOnlySpan dequantMatrices, + int size, + int k, + Vector xCCMul, + Vector bCCMul, + ReadOnlySpan biases, + JxlDctAcPointer qblock0, + JxlDctAcPointer qblock1, + JxlDctAcPointer qblock2, + Span block) + { + Vector xMul = Vector.Create(dequantMatrices[k..]) * scaledDequantX; + Vector yMul = Vector.Create(dequantMatrices[(k + size)..]) * scaledDequantY; + Vector bMul = Vector.Create(dequantMatrices[(k + (size * 2))..]) * scaledDequantB; + + Vector quantizedXInt; + Vector quantizedYInt; + Vector quantizedBInt; + + if (acType == JxlDctAcType.Ac16) + { + quantizedXInt = Vector.WidenLower(Vector.Create(qblock0.Pointer16[k..])); + quantizedYInt = Vector.WidenLower(Vector.Create(qblock1.Pointer16[k..])); + quantizedBInt = Vector.WidenLower(Vector.Create(qblock2.Pointer16[k..])); + } + else + { + quantizedXInt = Vector.Create(qblock0.Pointer32[k..]); + quantizedYInt = Vector.Create(qblock1.Pointer32[k..]); + quantizedBInt = Vector.Create(qblock2.Pointer32[k..]); + } + + Vector dequantXCC = JxlQuantizerSimd.AdjustQuantBias(0, quantizedXInt, biases) * xMul; + Vector dequantY = JxlQuantizerSimd.AdjustQuantBias(1, quantizedYInt, biases) * yMul; + Vector dequantBCC = JxlQuantizerSimd.AdjustQuantBias(2, quantizedBInt, biases) * bMul; + + Vector dequantX = (xCCMul * dequantY) + dequantXCC; + Vector dequantB = (bCCMul * dequantY) + dequantBCC; + + dequantX.CopyTo(block[k..]); + dequantY.CopyTo(block[(k + size)..]); + dequantB.CopyTo(block[(k + (size * 2))..]); + } + + private static void DequantBlock( + JxlDctAcType acType, + float invGlobalScale, + int quant, + float xDmMultiplier, + float bDmMultiplier, + Vector xCcMul, + Vector bCcMul, + JxlAcStrategyType kind, + int size, + JxlQuantizer quantizer, + int coveredBlocks, + Span sbx, + Span dcRow, + int dcStride, + Span biases, + JxlDctAcPointer qblock0, + JxlDctAcPointer qblock1, + JxlDctAcPointer qblock2, + Span block, + Span scratch) + { + float scaledDequantS = invGlobalScale / quant; + + Vector scaledDequantX = new(scaledDequantS * xDmMultiplier); + Vector scaledDequantY = new(scaledDequantS); + Vector scaledDequantB = new(scaledDequantS * bDmMultiplier); + + ReadOnlySpan dequantMatrices = quantizer.DequantMatrix(kind, 0); + + for (int k = 0; k < coveredBlocks * JxlFrameDimensions.DctBlockSize; k += Vector.Count) + { + DequantLane(acType, scaledDequantX, scaledDequantY, scaledDequantB, dequantMatrices, size, k, xCcMul, bCcMul, biases, qblock0, qblock1, qblock2, block); + } + + for (int c = 0; c < 3; c++) + { + JxlTransformsDecoder.LowestFrequenciesFromDc(kind, dcRow[sbx[c]..], dcStride, block[(c * size)..], scratch); + } + } + + private static bool DecodeGroupCore( + JxlFrameHeader frameHeader, + JxlGetBlock getBlock, + JxlGroupDecoderCache groupDecCache, + JxlPassesDecoderState decState, + int thread, + int groupIdx, + JxlRenderPipelineInput renderPipelineInput, + JpegData? jpegData, + JxlDrawMode draw) + { + Rectangle blockRect = decState.Shared.FrameDimensions.BlockGroupRect(groupIdx); + JxlAcStrategyImage acStrategy = decState.Shared.AcStrategy; + + int xsizeBlocks = blockRect.Width; + int ysizeBlocks = blockRect.Height; + + int dcStride = decState.Shared.Dc.PixelsPerRow; + float invGlobalScale = decState.Shared.Quantizer.InverseGlobalScale; + + JxlYCbCrChromaSubsampling cs = frameHeader.ChromaSubsampling!; + + Vector jpegDctMin = new(-4095); + Vector jpegDctMax = new(4095); + + InlineArray3 idctStride = default; + for (int c = 0; c < 3; c++) + { + idctStride[c] = renderPipelineInput.GetBuffer(c).First.PixelsPerRow(); + } + + Span scaledQTable = stackalloc int[64 * 3]; + + JxlDctAcType acType = decState.Coefficients.Type; + + bool accumulate = !decState.Coefficients.IsEmpty; + int offset = 0; + + InlineArray3 jpegCMap = default; + bool jpegIsGray = false; + InlineArray3 dcOff = default; + + JxlColorCorrelation colorCorrelation = decState.Shared.ColorMap.Base; + + if (jpegData is not null) + { + if (!colorCorrelation.IsJpegCompatible) + { + return false; + } + + jpegIsGray = jpegData.Components.Count == 1; + + if (frameHeader.ColorTransform == JxlColorTransform.Xyb) + { + return false; + } + + jpegCMap = JxlFrameHeader.JpegOrder(frameHeader.ColorTransform, jpegIsGray); + + Span qe = decState.Shared.Matrices.GetEncodings(); + + if (qe.Length == 0 || + qe[0].Mode != JxlQuantMode.Raw || + MathF.Abs(qe[0].QuantizationTableDenominator - (1.0f / (8 * 255))) > 1e-8f) + { + return false; + } + + if (qe[0].QuantizationTable!.Length != 3 * 8 * 8) + { + return false; + } + + ReadOnlySpan qTable = qe[0].QuantizationTable.AsSpan(); + + for (int c = 0; c < 3; c++) + { + if (frameHeader.ColorTransform == JxlColorTransform.None) + { + dcOff[c] = 1024 / qTable[64 * c]; + } + + for (int i = 0; i < 64; i++) + { + int num = qTable[64 + i]; + int den = qTable[(64 * c) + i]; + + if (num <= 0 || den <= 0 || num >= 65536 || den >= 65536) + { + return false; + } + + scaledQTable[(64 * c) + ((i % 8) * 8) + (i / 8)] = (1 << JxlChromaFromLuma.CflFixedPointPrecision) * num / den; + } + } + } + + Span hShift = + [ + cs!.HShift(0), + cs.HShift(1), + cs.HShift(2) + ]; + + Span vShift = + [ + cs!.VShift(0), + cs.VShift(1), + cs.VShift(2) + ]; + + Span r = stackalloc Rectangle[3]; + r.Clear(); + + for (int i = 0; i < 3; i++) + { + r[i] = new Rectangle( + blockRect.X0() >> hShift[i], + blockRect.Y0() >> vShift[i], + blockRect.Width >> hShift[i], + blockRect.Height >> vShift[i]); + + if (!r[i].IsInside(new Rectangle( + 0, + 0, + decState.Shared.Dc.Plane(i).XSize, + decState.Shared.Dc.Plane(i).YSize))) + { + return false; + } + } + + Span transposedDctY = stackalloc int[JxlFrameDimensions.DctBlockSize]; + InlineArray3 sby = default; + + for (int by = 0; by < ysizeBlocks; by++) + { + getBlock.StartRow(by); + + sby[0] = by >> vShift[0]; + sby[1] = by >> vShift[1]; + sby[2] = by >> vShift[2]; + + ReadOnlySpan rowQuant = decState.Shared.RawQuantField.GetRow(blockRect, by); + + Span dcRow0 = decState.Shared.Dc.PlaneRow(r[0], 0, sby[0]); + Span dcRow1 = decState.Shared.Dc.PlaneRow(r[1], 1, sby[1]); + Span dcRow2 = decState.Shared.Dc.PlaneRow(r[2], 2, sby[2]); + + int ty = (blockRect.Y0() + by) / JxlChromaFromLuma.ColorTileDimensionInBlocks; + + JxlAcStrategyRow acsRow = acStrategy.GetRow(blockRect, by); + + ReadOnlySpan rowCMap0 = decState.Shared.ColorMap.YToXMap!.GetRow(ty); + ReadOnlySpan rowCMap2 = decState.Shared.ColorMap.YToBMap!.GetRow(ty); + + Span idctRow0; + Span idctRow1; + Span idctRow2; + + Span jpegRow0 = default; + Span jpegRow1 = default; + Span jpegRow2 = default; + + for (int c = 0; c < 3; c++) + { + var buffer = renderPipelineInput.GetBuffer(c); + Span idctRow = buffer.Second.Row(buffer.First, sby[c] * JxlFrameDimensions.BlockDimensions); + + if (c == 0) + { + idctRow0 = idctRow; + } + else if (c == 1) + { + idctRow1 = idctRow; + } + else + { + idctRow2 = idctRow; + } + + if (jpegData is not null) + { + JpegComponent component = jpegData.Components[jpegCMap[c]]; + + Span jpegRow = + component.DangerousCoefficientsAsSpan()[ + (((component.WidthInBlocks * (r[c].Y0() + sby[c])) + r[c].X0()) * + JxlFrameDimensions.DctBlockSize)..]; + + if (c == 0) + { + jpegRow0 = jpegRow; + } + else if (c == 1) + { + jpegRow1 = jpegRow; + } + else + { + jpegRow2 = jpegRow; + } + } + } + + int bx = 0; + + for (int tx = 0; + tx < JxlMath.DivCeil(xsizeBlocks, JxlChromaFromLuma.ColorTileDimensionInBlocks); + tx++) + { + int absTx = tx + (blockRect.X0() / JxlChromaFromLuma.ColorTileDimensionInBlocks); + + Vector xCcMul = new(colorCorrelation.YToXRatio(rowCMap0[absTx])); + Vector bCcMul = new(colorCorrelation.YToBRatio(rowCMap2[absTx])); + + for (; bx < xsizeBlocks && bx < (tx + 1) * JxlChromaFromLuma.ColorTileDimensionInBlocks;) + { + InlineArray3 sbx = default; + sbx[0] = bx >> hShift[0]; + sbx[1] = bx >> hShift[1]; + sbx[2] = bx >> hShift[2]; + + JxlAcStrategy acs = acsRow[bx]; + int llfX = acs.CoveredBlocksX; + + if (!acs.IsFirstBlock) + { + bx += llfX; + continue; + } + + int log2CoveredBlocks = acs.Log2CoveredBlocks; + int coveredBlocks = 1 << log2CoveredBlocks; + int size = coveredBlocks * JxlFrameDimensions.DctBlockSize; + + JxlDctAcPointer qblock0; + JxlDctAcPointer qblock1; + JxlDctAcPointer qblock2; + + if (accumulate) + { + qblock0 = decState.Coefficients.GetPlaneRow(0, groupIdx, offset); + qblock1 = decState.Coefficients.GetPlaneRow(1, groupIdx, offset); + qblock2 = decState.Coefficients.GetPlaneRow(2, groupIdx, offset); + } + else + { + if (draw != JxlDrawMode.Draw) + { + return false; + } + + if (acType == JxlDctAcType.Ac16) + { + groupDecCache.DecodedGroupQBlock16.Clear(); + + qblock0 = new JxlDctAcPointer(groupDecCache.DecodedGroupQBlock16.Slice(0, size)); + qblock1 = new JxlDctAcPointer(groupDecCache.DecodedGroupQBlock16.Slice(size, size)); + qblock2 = new JxlDctAcPointer(groupDecCache.DecodedGroupQBlock16.Slice(size * 2, size)); + } + else + { + groupDecCache.DecodedGroupQBlock.Clear(); + + qblock0 = new JxlDctAcPointer(groupDecCache.DecodedGroupQBlock.Slice(0, size)); + qblock1 = new JxlDctAcPointer(groupDecCache.DecodedGroupQBlock.Slice(size, size)); + qblock2 = new JxlDctAcPointer(groupDecCache.DecodedGroupQBlock.Slice(size * 2, size)); + } + } + + if (!getBlock.LoadBlock(bx, by, acs, size, log2CoveredBlocks, qblock0, qblock1, qblock2, acType)) + { + return false; + } + + offset += size; + + if (draw == JxlDrawMode.DoNotDraw) + { + bx += llfX; + continue; + } + + if (acs.Strategy != JxlAcStrategyType.DCT) + { + return false; + } + + foreach (int c in new[] { 1, 0, 2 }) + { + // Propagate only Y for grayscale. + if (jpegIsGray && c != 1) + { + continue; + } + + if ((sbx[c] << hShift[c]) != bx || (sby[c] << vShift[c]) != by) + { + continue; + } + + // We can't have a Span inside of an array, another Span, + // or even an inline array. + Span jpegPos = + c == 0 + ? jpegRow0[(sbx[c] * JxlFrameDimensions.DctBlockSize)..] + : c == 1 + ? jpegRow1[(sbx[c] * JxlFrameDimensions.DctBlockSize)..] + : jpegRow2[(sbx[c] * JxlFrameDimensions.DctBlockSize)..]; + + // JPEG XL is transposed, JPEG is not. + JxlDctAcPointer transposedDct = c switch + { + 0 => qblock0, + 1 => qblock1, + 2 => qblock2, + _ => throw new InvalidOperationException() + }; + + Transpose8x8InPlace(transposedDct.Pointer32); + + // No CfL, so there is no need to store the Y block converted to integers. + if (!cs.Is444 || (rowCMap0[absTx] == 0 && rowCMap2[absTx] == 0)) + { + for (int i = 0; i < JxlFrameDimensions.DctBlockSize; i += Vector.Count) + { + Vector ini = Vector.Create(transposedDct.Pointer32[i..]); + ini.CopyTo(jpegPos[i..]); + } + } + else if (c == 1) + { + // Y channel: save for restoring X/B, but nothing else to do. + for (int i = 0; i < JxlFrameDimensions.DctBlockSize; i += Vector.Count) + { + Vector ini = Vector.Create(transposedDct.Pointer32[i..]); + ini.CopyTo(transposedDctY[i..]); + ini.CopyTo(jpegPos[i..]); + } + } + else + { + // transposedDctY contains the Y channel block, transposed. + Vector scale = new(JxlColorCorrelation.RatioJpeg(c == 0 ? rowCMap0[absTx] : rowCMap2[absTx])); + Vector round = new(1 << (JxlChromaFromLuma.ColorTileDimensionInBlocks - 1)); + + for (int i = 0; i < JxlFrameDimensions.DctBlockSize; i += Vector.Count) + { + Vector input = Vector.Create(transposedDct.Pointer32[i..]); + Vector inputY = Vector.Create(transposedDctY[i..]); + Vector qt = Vector.Create(scaledQTable[((c * size) + i)..]); + + Vector coeffScale = ((qt * scale) + round) >> JxlChromaFromLuma.ColorTileDimensionInBlocks; + Vector cflFactor = ((inputY * coeffScale) + round) >> JxlChromaFromLuma.ColorTileDimensionInBlocks; + Vector result = input + cflFactor; + + result.CopyTo(jpegPos[i..]); + } + } + + jpegPos[0] = (int)Math.Clamp( + c == 0 + ? dcRow0[sbx[c] - dcOff[c]] + : c == 1 + ? dcRow1[sbx[c] - dcOff[c]] + : dcRow2[sbx[c] - dcOff[c]], + -2047.0f, + 2047.0f); + + Vector overflow = Vector.Zero; + Vector underflow = Vector.Zero; + + for (int i = 0; i < JxlFrameDimensions.DctBlockSize; i += Vector.Count) + { + Vector input = Vector.Create(jpegPos[i..]); + + overflow |= Vector.GreaterThan(input, jpegDctMax); + underflow |= Vector.LessThan(input, jpegDctMin); + } + + if (!Vector.All(overflow | underflow, 0)) + { + return false; + } + } + + bx += llfX; + } + } + } + + return true; + } + + public static bool DecodeAcVarBlock( + JxlDctAcType acType, + bool usesLz77, + int ctxOffset, + int log2CoveredBlocks, + Span rowNzeros, + ReadOnlySpan rowNzerosTop, + int nzerosStride, + int c, + int bx, + int lbx, + JxlAcStrategy acs, + ReadOnlySpan coeffOrder, + JxlBitReader br, + JxlAnsSymbolReader decoder, + ReadOnlySpan contextMap, + ReadOnlySpan qdcRow, + ReadOnlySpan qfRow, + JxlBlockContextMap blockCtxMap, + JxlDctAcPointer block, + int shift = 0) + { + // Equal to number of LLF coefficients. + int coveredBlocks = 1 << log2CoveredBlocks; + int size = coveredBlocks * JxlFrameDimensions.DctBlockSize; + + int predictedNzeros = JxlEntropyCoder.PredictFromTopAndLeft(rowNzerosTop, rowNzeros, bx, 32); + int ord = JxlCoefficientOrder.StrategyOrder[acs.RawStrategy]; + ReadOnlySpan order = coeffOrder[JxlCoefficientOrder.CoeffOrderOffset(ord, c)..]; + + int blockCtx = blockCtxMap.Context(qdcRow[lbx], (uint)qfRow[bx], ord, c); + int nzeroCtx = blockCtxMap.NonZeroContext(predictedNzeros, blockCtx) + ctxOffset; + int nzeros = decoder.ReadHybridUintInlined(usesLz77, nzeroCtx, br, contextMap); + + if (nzeros > size - coveredBlocks) + { + return false; + } + + for (int y = 0; y < acs.CoveredBlocksY; y++) + { + for (int x = 0; x < acs.CoveredBlocksX; x++) + { + rowNzeros[bx + x + (y * nzerosStride)] = (nzeros + coveredBlocks - 1) >> log2CoveredBlocks; + } + } + + int histoOffset = ctxOffset + blockCtxMap.ZeroDensityContextOffset(blockCtx); + + int prev = nzeros > size / 16 ? 0 : 1; + + for (int k = coveredBlocks; k < size && nzeros != 0; k++) + { + int ctx = + histoOffset + + JxlAcContext.ZeroDensityContext( + nzeros, + k, + coveredBlocks, + log2CoveredBlocks, + prev); + + int uCoeff = decoder.ReadHybridUintInlined(usesLz77, ctx, br, contextMap); + + // Hand-rolled version of UnpackSigned, shifting before conversion + // to signed integer to avoid undefined behavior. + int magnitude = uCoeff >> 1; + int negSign = (~uCoeff) & 1; + + int coeff = (magnitude ^ (negSign - 1)) << shift; + + if (acType == JxlDctAcType.Ac16) + { + block.Pointer16[order[k]] += (short)coeff; + } + else + { + block.Pointer32[order[k]] += coeff; + } + + prev = uCoeff != 0 ? 1 : 0; + nzeros -= prev; + } + + if (nzeros != 0) + { + return false; + } + + return true; + } + + public static bool DecodeGroup( + JxlFrameHeader frameHeader, + JxlBitReader[] readers, + int numPasses, + int groupIndex, + JxlPassesDecoderState decState, + JxlGroupDecoderCache groupDecCache, + int thread, + JxlRenderPipelineInput renderPipelineInput, + JpegData? jpegData, + int firstPass, + bool forceDraw, + bool dcOnly, + ref bool shouldRunPipeline) + { + bool draw = numPasses + firstPass == frameHeader.Passes.NumPasses || forceDraw; + + shouldRunPipeline = draw; + + if (draw && numPasses == 0 && firstPass == 0) + { + groupDecCache.InitializeDcBufferOnce(); + + JxlYCbCrChromaSubsampling cs = frameHeader.ChromaSubsampling; + + for (int c = 0; c < 3; c++) + { + int hs = cs!.HShift(c); + int vs = cs.VShift(c); + + Rectangle srcRectPrecs = decState.Shared.FrameDimensions.BlockGroupRect(groupIndex); + + Rectangle srcRect = new( + srcRectPrecs.X0() >> hs, + srcRectPrecs.Y0() >> vs, + srcRectPrecs.Width >> hs, + srcRectPrecs.Height >> vs); + + Rectangle copyRect = new( + RenderPipelineStageBase.RenderPipelineXOffset, + 2, + srcRect.Width, + srcRect.Height); + + JxlPlane dcPlane = decState.Shared.Dc.Plane(c); + + if (!JxlImageOperations.CopyImageToWithPadding( + srcRect, + dcPlane, + 2, + copyRect, + groupDecCache.DcBuffer)) + { + return false; + } + + // Mirror-pad the left and right edges. + // Interleaving the two padding pixels ensures that padding + // also works correctly when the DC size is one pixel. + for (int y = 0; y < srcRect.Height + 4; y++) + { + int xEnd = + RenderPipelineStageBase.RenderPipelineXOffset + + (dcPlane.XSize >> hs) - + srcRect.X0(); + + Span row = groupDecCache.DcBuffer.Row(y); + + for (int ix = 0; ix < 2; ix++) + { + if (srcRect.X0() == 0) + { + row[RenderPipelineStageBase.RenderPipelineXOffset - ix - 1] = row[RenderPipelineStageBase.RenderPipelineXOffset + ix]; + } + + if (srcRect.X0() + srcRect.Width + 2 >= (decState.Shared.Dc.XSize >> hs)) + { + row[xEnd + ix] = row[xEnd - ix - 1]; + } + } + } + + JxlRenderPipelineBuffer buffer = renderPipelineInput.GetBuffer(c); + + Rectangle dstRect = buffer.Bounds; + JxlImageF upsamplingDst = buffer.Image; + + if (!dstRect.IsInside(upsamplingDst.GetRectangle())) + { + return false; + } + + JxlRenderPipelineRowInfo inputRows = new(1, 5); + JxlRenderPipelineRowInfo outputRows = new(1, 8); + + for (int y = srcRect.Y0(); y < srcRect.Y0() + srcRect.Height; y++) + { + for (int iy = 0; iy < 5; iy++) + { + int inputY = + JxlImageOperations.Mirror( + y + iy - 2, + decState.Shared.Dc.Plane(c).YSize) + + 2 - + srcRect.Y0(); + + inputRows[0][iy] = + groupDecCache.DcBuffer.Row(inputY); + } + + for (int iy = 0; iy < 8; iy++) + { + outputRows[0][iy] = + upsamplingDst.GetRowMemory( + dstRect, + ((y - srcRect.Y0()) << 3) + iy)[ + RenderPipelineStageBase.RenderPipelineXOffset..]; + } + + // throws on failure + decState.Upsampler8x.ProcessRow( + inputRows, + outputRows, + 0, + 0, + srcRect.Width, + 0, + 0, + thread); + } + } + + return true; + } + + int histogramSelectorBits = 0; + + if (dcOnly) + { + if (numPasses != 0) + { + return false; + } + } + else + { + if (decState.Shared.NumHistograms <= 0) + { + return false; + } + + histogramSelectorBits = JxlMath.CeilLog2Nonzero(decState.Shared.NumHistograms); + } + + Rectangle blockGroupRect = decState.Shared.FrameDimensions.BlockGroupRect(groupIndex); + + JxlBitStreamBlockLoader getBlock = new(); + + if (!getBlock.Init( + frameHeader, + readers, + numPasses, + groupIndex, + histogramSelectorBits, + blockGroupRect, + decState, + firstPass)) + { + return false; + } + + if (!DecodeGroupCore( + frameHeader, + ref getBlock, + groupDecCache, + decState, + thread, + groupIndex, + renderPipelineInput, + jpegData, + draw)) + { + return false; + } + + for (int pass = 0; pass < numPasses; pass++) + { + if (!getBlock.Decoders[pass].CheckAnsFinalState()) + { + return false; + } + } + + return true; + } + + public static bool DecodeGroupForRoundtrip( + JxlFrameHeader frameHeader, + IReadOnlyList ac, + int groupIndex, + JxlPassesDecoderState decState, + JxlGroupDecoderCache groupDecCache, + int thread, + JxlRenderPipelineInput renderPipelineInput, + JpegData? jpegData, + JxlAuxiliaryOutput? auxOut) + { + JxlEncoderBlockLoader getBlock = + JxlEncoderBlockLoader.Create( + ac, + groupIndex, + frameHeader.Passes.Shift); + + if (!groupDecCache.InitializeOnce( + numPasses: 0, + usedAcs: (1 << JxlAcStrategy.NumberOfValidStrategies) - 1)) + { + return false; + } + + return DecodeGroupCore( + frameHeader, + ref getBlock, + groupDecCache, + decState, + thread, + groupIndex, + renderPipelineInput, + jpegData, + true); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlGroupDecoderCache.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoderCache.cs similarity index 74% rename from src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlGroupDecoderCache.cs rename to src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoderCache.cs index f82e332d8d..306256d7a2 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlGroupDecoderCache.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoderCache.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Group; /// /// Cache for the JPEG XL Group Decoder. diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs index 336ac33c5c..388ee1497f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs @@ -39,7 +39,7 @@ public JxlPassesDecoderState(JxlFrameHeader header, Configuration configuration) public JxlPassesSharedState Shared { get; set; } - public RenderPipelineStageBase[] Upsampler8x { get; set; } = []; + public RenderPipelineStageBase Upsampler8x { get; set; } = new(); public List Code { get; set; } = []; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs index 89badfa498..6ece90cb35 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs @@ -401,7 +401,7 @@ public static void ZeroFillImage(JxlPlane image) /// Core method for the WrapMirror function. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int Mirror(long x, long xSize) + public static int Mirror(long x, long xSize) { DebugGuard.MustBeGreaterThan(xSize, 0, nameof(xSize)); diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlCompressedDc.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlCompressedDc.cs index 9e92a09e79..4dc25c082c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlCompressedDc.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlCompressedDc.cs @@ -13,7 +13,7 @@ internal static class JxlCompressedDc private const float W2 = 0.0334829185968739f; private const float W0 = 1.0f - (4.0f * (W1 + W2)); - private static unsafe void ComputePixelChannelPacked(float dcFactor, Span rowTop, Span row, Span rowBottom, out Vector mc, out Vector sm, out Vector gap, int x) + private static unsafe void ComputePixelChannelPacked(float dcFactor, Span rowTop, Span row, Span rowBottom, out Vector mc, out Vector sm, ref Vector gap, int x) { // Use ptr for aligned vector loads fixed (float* pRowTop = rowTop) @@ -30,7 +30,24 @@ private static unsafe void ComputePixelChannelPacked(float dcFactor, Span Vector tc = Vector.LoadAlignedNonTemporal(pRowTop + x); Vector tr = Vector.Load(pRowTop + x + 1); - // TODO + Vector ml = Vector.Load(pRow + x - 1); + mc = Vector.LoadAlignedNonTemporal(pRow + x); + Vector mr = Vector.Load(pRow + x + 1); + + Vector bl = Vector.Load(pRowBottom + x - 1); + Vector bc = Vector.LoadAligned(pRowBottom + x); + Vector br = Vector.Load(pRowBottom + x + 1); + + Vector wCenter = Vector.Create(W0); + Vector wSide = Vector.Create(W1); + Vector wCorner = Vector.Create(W2); + + Vector corner = (tl + tr) + (bl + br); + Vector side = (ml + mr) + (tc + bc); + sm = (corner * wCorner) + ((side * wSide) + (mc * wCenter)); + + Vector dcQuant = Vector.Create(dcFactor); + gap = Vector.Max(gap, Vector.Abs((mc - sm) / dcQuant)); } } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerSimd.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerSimd.cs index 66d92467cb..09e182a1ab 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerSimd.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerSimd.cs @@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; /// internal static class JxlQuantizerSimd { - public static Vector AdjustQuantBias(int c, Vector quantI, Span biases) + public static Vector AdjustQuantBias(int c, Vector quantI, ReadOnlySpan biases) { Vector quant = quantI.As(); Vector constSign = Vector.Create(int.MinValue).As(); diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineStageBase.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineStageBase.cs index 07cb0f59db..1f4dd1331b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineStageBase.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineStageBase.cs @@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; [DebuggerDisplay($"{{{nameof(Name)}}}")] internal abstract class RenderPipelineStageBase(Configuration configuration) : IDisposable { - private const int RenderPipelineXOffset = 32; + public const int RenderPipelineXOffset = 32; /// /// Gets or sets the configuration for this render pipeline stage. From 4d3c02ffc65cbd0ccb782bc2698be048440b897e Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 14 Sep 2026 20:52:30 +0400 Subject: [PATCH 178/328] Apply optimizations --- .../Processing/Decoder/JxlTransformsDecoder.cs | 16 +++------------- .../Encoder/FastLossless/FjxlPrefixCode.cs | 7 ++++--- .../FjxlChunkEncoderExactly14Bits.cs | 12 +++++++----- .../FjxlChunkEncoderFrom9To13Bits.cs | 12 +++++++----- .../FjxlChunkEncoderMoreThan14Bits.cs | 14 +++++++++----- .../ChunkEncoders/FjxlChunkEncoderUpTo8Bits.cs | 12 +++++++----- 6 files changed, 37 insertions(+), 36 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlTransformsDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlTransformsDecoder.cs index 42304d1894..d52e009ade 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlTransformsDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlTransformsDecoder.cs @@ -450,13 +450,8 @@ public static void AfvTransformToPixels(int afvKind, Span coefficients, S Span coeff = stackalloc float[4 * 4]; // No need to zero-initialize coeff[0] = dcs[0]; - // TODO: unrolling this would lead to big performance benefits - for (int iy = 0; iy < 4; iy++) + for (int iy = 0, iy4 = 0, iy28 = 0; iy < 4; iy++, iy4 += 4, iy28 += 16) { - // Variables so we don't repeat multiplication over and over again - int iy4 = iy * 4; // iy multiplied by 4 - int iy28 = iy * 2 * 8; // iy multiplied by 2 multiplied by 8 - for (int ix = 0; ix < 4; ix++) { if (ix == 0 && iy == 0) @@ -489,11 +484,8 @@ public static void AfvTransformToPixels(int afvKind, Span coefficients, S // IDCT4x4 in (odd, even) positions. block[0] = dcs[1]; - for (int iy = 0; iy < 4; iy++) + for (int iy = 0, iy4 = 0, iy28 = 0; iy < 4; iy++, iy4 += 4, iy28 += 16) { - int iy4 = iy * 4; - int iy28 = iy * 2 * 8; - for (int ix = 0; ix < 4; ix++) { if (ix == 0 && iy == 0) @@ -514,10 +506,8 @@ public static void AfvTransformToPixels(int afvKind, Span coefficients, S block[0] = dcs[2]; - for (int iy = 0; iy < 4; iy++) + for (int iy = 0, iy8 = 0; iy < 4; iy++, iy8 += 8) { - int iy8 = iy * 8; - for (int ix = 0; ix < 8; ix++) { if (ix == 0 && iy == 0) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/FjxlPrefixCode.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/FjxlPrefixCode.cs index 4663ed74ee..f9b9c821b4 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/FjxlPrefixCode.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/FjxlPrefixCode.cs @@ -370,9 +370,10 @@ public void Write(FjxlBitWriter writer) } // Lengths for representing the code length - Span codeLengthLengths = stackalloc byte[32].Slice(0, 18); - Span codeLengthLengthsMinimum = stackalloc byte[32].Slice(0, 18); - Span codeLengthLengthsMaximum = stackalloc byte[32].Slice(0, 18); + Span codeLengthsBuffer = stackalloc byte[64]; // nearest power of 2 alignment for 18*3 = 64 + Span codeLengthLengths = codeLengthsBuffer.Slice(0, 18); + Span codeLengthLengthsMinimum = codeLengthsBuffer.Slice(18, 18); + Span codeLengthLengthsMaximum = codeLengthsBuffer.Slice(18 * 2, 18); codeLengthLengths.Clear(); codeLengthLengthsMinimum.Clear(); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderExactly14Bits.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderExactly14Bits.cs index 4d7eff4a13..0c79b56a5b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderExactly14Bits.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderExactly14Bits.cs @@ -33,11 +33,13 @@ public static void PrepareForSimd(ReadOnlySpan nbits, ReadOnlySpan b public static void EncodeChunkSimd(Span residuals, int n, int skip, ReadOnlySpan rawNBitsSimd, ReadOnlySpan rawBitsSimd, ref FjxlBitWriter output) { Span bits32 = stackalloc FjxlBits32[2 * ChunkSize / FjxlSimdVec16.Lanes]; - Span bits = stackalloc ushort[FjxlSimdVec16.Lanes]; - Span nbits = stackalloc ushort[FjxlSimdVec16.Lanes]; - Span bitsHuff = stackalloc ushort[FjxlSimdVec16.Lanes]; - Span nbitsHuff = stackalloc ushort[FjxlSimdVec16.Lanes]; - Span token = stackalloc ushort[FjxlSimdVec16.Lanes]; + + Span buffer = stackalloc ushort[FjxlSimdVec16.Lanes * 5]; + Span bits = buffer.Slice(0, FjxlSimdVec16.Lanes); + Span nbits = buffer.Slice(FjxlSimdVec16.Lanes, FjxlSimdVec16.Lanes); + Span bitsHuff = buffer.Slice(FjxlSimdVec16.Lanes * 2, FjxlSimdVec16.Lanes); + Span nbitsHuff = buffer.Slice(FjxlSimdVec16.Lanes * 3, FjxlSimdVec16.Lanes); + Span token = buffer.Slice(FjxlSimdVec16.Lanes * 4, FjxlSimdVec16.Lanes); for (int i = 0; i < ChunkSize; i += FjxlSimdVec16.Lanes) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderFrom9To13Bits.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderFrom9To13Bits.cs index 7d8b3dc218..7bb349abef 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderFrom9To13Bits.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderFrom9To13Bits.cs @@ -42,11 +42,13 @@ public static void PrepareForSimd(ReadOnlySpan nbits, ReadOnlySpan b public static void EncodeChunkSimd(Span residuals, int n, int skip, ReadOnlySpan rawNBitsSimd, ReadOnlySpan rawBitsSimd, ref FjxlBitWriter output) { Span bits32 = stackalloc FjxlBits32[2 * ChunkSize / FjxlSimdVec16.Lanes]; - Span bits = stackalloc ushort[FjxlSimdVec16.Lanes]; - Span nbits = stackalloc ushort[FjxlSimdVec16.Lanes]; - Span bitsHuff = stackalloc ushort[FjxlSimdVec16.Lanes]; - Span nbitsHuff = stackalloc ushort[FjxlSimdVec16.Lanes]; - Span token = stackalloc ushort[FjxlSimdVec16.Lanes]; + + Span buffer = stackalloc ushort[FjxlSimdVec16.Lanes * 5]; + Span bits = buffer.Slice(0, FjxlSimdVec16.Lanes); + Span nbits = buffer.Slice(FjxlSimdVec16.Lanes, FjxlSimdVec16.Lanes); + Span bitsHuff = buffer.Slice(FjxlSimdVec16.Lanes * 2, FjxlSimdVec16.Lanes); + Span nbitsHuff = buffer.Slice(FjxlSimdVec16.Lanes * 3, FjxlSimdVec16.Lanes); + Span token = buffer.Slice(FjxlSimdVec16.Lanes * 4, FjxlSimdVec16.Lanes); for (int i = 0; i < ChunkSize; i += FjxlSimdVec16.Lanes) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderMoreThan14Bits.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderMoreThan14Bits.cs index dfef97f74c..235f33508d 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderMoreThan14Bits.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderMoreThan14Bits.cs @@ -49,11 +49,15 @@ public static void PrepareForSimd(ReadOnlySpan nbits, ReadOnlySpan b public static void EncodeChunkSimd(Span residuals, int n, int skip, ReadOnlySpan rawNBitsSimd, ReadOnlySpan rawBitsSimd, ref FjxlBitWriter output) { Span bits32 = stackalloc FjxlBits32[2 * ChunkSize / FjxlSimdVec16.Lanes]; - Span bits = stackalloc uint[FjxlSimdVec16.Lanes]; - Span nbits = stackalloc uint[FjxlSimdVec16.Lanes]; - Span bitsHuff = stackalloc ushort[FjxlSimdVec16.Lanes]; - Span nbitsHuff = stackalloc ushort[FjxlSimdVec16.Lanes]; - Span token = stackalloc ushort[FjxlSimdVec16.Lanes]; + + Span bufferU32 = stackalloc uint[FjxlSimdVec16.Lanes * 2]; + Span bits = bufferU32.Slice(0, FjxlSimdVec16.Lanes); + Span nbits = bufferU32.Slice(FjxlSimdVec16, FjxlSimdVec16.Lanes); + + Span bufferU16 = stackalloc uint[FjxlSimdVec16.Lanes * 3]; + Span bitsHuff = bufferU16.Slice(0, FjxlSimdVec16.Lanes); + Span nbitsHuff = bufferU16.Slice(FjxlSimdVec16.Lanes, FjxlSimdVec16.Lanes); + Span token = bufferU16.Slice(FjxlSimdVec16.Lanes * 2, FjxlSimdVec16.Lanes); for (int i = 0; i < ChunkSize; i += FjxlSimdVec16.Lanes) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderUpTo8Bits.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderUpTo8Bits.cs index eee2aa401e..89faa2f2c5 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderUpTo8Bits.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderUpTo8Bits.cs @@ -39,11 +39,13 @@ public static void PrepareForSimd(ReadOnlySpan nbits, ReadOnlySpan b public static void EncodeChunkSimd(Span residuals, int n, int skip, ReadOnlySpan rawNBitsSimd, ReadOnlySpan rawBitsSimd, ref FjxlBitWriter output) { Span bits32 = stackalloc FjxlBits32[ChunkSize / FjxlSimdVec16.Lanes]; - Span bits = stackalloc ushort[FjxlSimdVec16.Lanes]; - Span nbits = stackalloc ushort[FjxlSimdVec16.Lanes]; - Span bitsHuff = stackalloc ushort[FjxlSimdVec16.Lanes]; - Span nbitsHuff = stackalloc ushort[FjxlSimdVec16.Lanes]; - Span token = stackalloc ushort[FjxlSimdVec16.Lanes]; + + Span buffer = stackalloc ushort[FjxlSimdVec16.Lanes * 5]; + Span bits = buffer.Slice(0, FjxlSimdVec16.Lanes); + Span nbits = buffer.Slice(FjxlSimdVec16.Lanes, FjxlSimdVec16.Lanes); + Span bitsHuff = buffer.Slice(FjxlSimdVec16.Lanes * 2, FjxlSimdVec16.Lanes); + Span nbitsHuff = buffer.Slice(FjxlSimdVec16.Lanes * 3, FjxlSimdVec16.Lanes); + Span token = buffer.Slice(FjxlSimdVec16.Lanes * 4, FjxlSimdVec16.Lanes); for (int i = 0; i < ChunkSize; i += FjxlSimdVec16.Lanes) { From d45edc4bb69dbe648d0f721fa273e4140e08bf54 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 14 Sep 2026 20:55:14 +0400 Subject: [PATCH 179/328] Convert into switch statement --- .../Jxl/Processing/Dct/JxlDctScales.cs | 77 ++++++++----------- 1 file changed, 30 insertions(+), 47 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctScales.cs b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctScales.cs index e6f4ecfa7e..ccaa87db14 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctScales.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctScales.cs @@ -392,54 +392,37 @@ internal static class JxlDctScales public static ReadOnlySpan GetResampleScales(int x, int y) { DebugGuard.IsTrue(x is 1 or 2 or 4 or 8 or 16 or 32 or 64 or 128 or 256, "x is invalid"); - if (x == 1) + switch (x) { - DebugGuard.IsTrue(y == 8, "Only y=8 resample scale for x=1 exists"); - return ResampleScales1_8; - } - else if (x == 2) - { - DebugGuard.IsTrue(y == 16, "Only y=16 resample scale for x=2 exists"); - return ResampleScales2_16; - } - else if (x == 4) - { - DebugGuard.IsTrue(y == 32, "Only y=32 resample scale for x=4 exists"); - return ResampleScales4_32; - } - else if (x == 8) - { - DebugGuard.IsTrue(y is 1 or 64, "Only y=1,64 resample scale for x=8 exists"); - return y == 1 ? ResampleScales8_1 : ResampleScales8_64; - } - else if (x == 16) - { - DebugGuard.IsTrue(y is 2 or 128, "Only y=2,128 resample scale for x=16 exists"); - return y == 2 ? ResampleScales16_2 : ResampleScales16_128; - } - else if (x == 32) - { - DebugGuard.IsTrue(y is 4 or 256, "Only y=4,256 resample scale for x=32 exists"); - return y == 4 ? ResampleScales32_4 : ResampleScales32_256; - } - else if (x == 64) - { - DebugGuard.IsTrue(y == 8, "Only y=8 resample scale for x=64 exists"); - return ResampleScales64_8; - } - else if (x == 128) - { - DebugGuard.IsTrue(y == 16, "Only y=16 resample scale for x=128 exists"); - return ResampleScales128_16; - } - else if (x == 256) - { - DebugGuard.IsTrue(y == 32, "Only y=32 resample scale for x=256 exists"); - return ResampleScales256_32; - } - else - { - return []; + case 1: + DebugGuard.IsTrue(y == 8, "Only y=8 resample scale for x=1 exists"); + return ResampleScales1_8; + case 2: + DebugGuard.IsTrue(y == 16, "Only y=16 resample scale for x=2 exists"); + return ResampleScales2_16; + case 4: + DebugGuard.IsTrue(y == 32, "Only y=32 resample scale for x=4 exists"); + return ResampleScales4_32; + case 8: + DebugGuard.IsTrue(y is 1 or 64, "Only y=1,64 resample scale for x=8 exists"); + return y == 1 ? ResampleScales8_1 : ResampleScales8_64; + case 16: + DebugGuard.IsTrue(y is 2 or 128, "Only y=2,128 resample scale for x=16 exists"); + return y == 2 ? ResampleScales16_2 : ResampleScales16_128; + case 32: + DebugGuard.IsTrue(y is 4 or 256, "Only y=4,256 resample scale for x=32 exists"); + return y == 4 ? ResampleScales32_4 : ResampleScales32_256; + case 64: + DebugGuard.IsTrue(y == 8, "Only y=8 resample scale for x=64 exists"); + return ResampleScales64_8; + case 128: + DebugGuard.IsTrue(y == 16, "Only y=16 resample scale for x=128 exists"); + return ResampleScales128_16; + case 256: + DebugGuard.IsTrue(y == 32, "Only y=32 resample scale for x=256 exists"); + return ResampleScales256_32; + default: + return []; } } } From 86e0ae03276f31f2b990471fccd2f005ee9f1662 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 16 Sep 2026 14:17:09 +0400 Subject: [PATCH 180/328] Add FJXL and LZ77 progress --- .../FastLossless/JxlFastLosslessEncoder.cs | 115 +++++++++++++++- .../FjxlChunkEncoderExactly14Bits.cs | 5 +- .../Encoder/FastLossless/Simd/FjxlBits32.cs | 26 ++++ .../Encoder/FastLossless/Simd/FjxlBits64.cs | 33 +++++ .../Encoder/FastLossless/Simd/FjxlRowFill.cs | 23 ++++ .../FastLossless/Simd/FjxlSimdVec16.cs | 14 ++ .../Encoder/JxlImageBundleEncoder.cs | 126 ++++++++++++++++++ .../Jxl/Processing/Encoder/Lz77/JxlLz77.cs | 51 +++++++ .../Processing/Encoder/Lz77/JxlLz77Costs.cs | 94 +++++++++++++ .../Lz77/JxlLz77SymbolCostEstimator.cs | 117 ++++++++++++++++ .../Jxl/Processing/Primitives/ArrayHelpers.cs | 30 +++++ 11 files changed, 631 insertions(+), 3 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlBits32.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlBits64.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlRowFill.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlSimdVec16.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlImageBundleEncoder.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77Costs.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77SymbolCostEstimator.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Primitives/ArrayHelpers.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/JxlFastLosslessEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/JxlFastLosslessEncoder.cs index 308e604277..e7ddf796b6 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/JxlFastLosslessEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/JxlFastLosslessEncoder.cs @@ -48,7 +48,7 @@ internal sealed class JxlFastLosslessEncoder /// public const int Lz77CacheSize = 32; - public const int Lz77Offset = 224; + public const int Lz77Offset = 224; // ⚠️ This must always be 224. Don't change this constant. Changing this will break compression. public const int Lz77MinLength = 7; @@ -621,6 +621,119 @@ private static void GenericEncodeChunk(ReadOnlySpan residuals, int n, int } } + public static void PrepareDcGlobalCommon(InlineArray4 code, FjxlBitWriter output) + { + // No patches, spline, or noise. + output.Write(1, 1); // Default DC dequantization factors + output.Write(1, 1); // Use global trees and histograms + output.Write(1, 0); // Don't use LZ77 compression for the tree. + + output.Write(1, 1); // Simple code for the tree's context map + output.Write(2, 0); // All contexts clustered together + output.Write(1, 1); // Use prefix code for tree + output.Write(4, 0); // 000 hybrid unsigned integer + output.Write(6, 0b100011); // Alphabet size is 4 + output.Write(2, 1); // Simple prefix code + output.Write(2, 3); // With 4 symbols + + output.Write(2, 0); + output.Write(2, 1); + output.Write(2, 2); + output.Write(2, 3); + output.Write(1, 0); // First tree encoding option + + // Huffman table and extra bits for the tree + Span symbolBits = [0b00, 0b10, 0b001, 0b101, 0b0011, 0b0111]; + Span symbolLengths = [2, 2, 3, 3, 4, 4]; + + Span predictors = + [ + 1, 2, 1, 4, 1, 0, 0, 5, 0, 0, 0, 0, 5, + 0, 0, 0, 0, 5, 0, 0, 0, 0, 5, 0, 0, 0 + ]; + + ref byte unsafePred = ref MemoryMarshal.GetReference(predictors); + + for (int i = 0; i < predictors.Length; i++) + { + byte predictor = Unsafe.AddByteOffset(ref unsafePred, i); + output.Write(symbolLengths[predictor], symbolBits[predictor]); + } + + output.Write(1, 1); // Enable LZ77 compression for the main bit-stream + output.Write(2, 0b00); // LZ77 offset 224 + output.Write(4, 0b1010); // LZ77 minimum length 7 + + // 400 hybrid uint configuration for LZ77 + output.Write(4, 4); + output.Write(3, 0); + output.Write(3, 0); + + output.Write(1, 1); // simple code for the context map + output.Write(2, 3); // 3 bits per entry + output.Write(3, 4); // channel 3 + output.Write(3, 3); // channel 2 + output.Write(3, 2); // channel 1 + output.Write(3, 1); // channel 0 + output.Write(3, 0); // distance histogram first + + output.Write(1, 1); // use prefix codes + output.Write(4, 0); // 000 hybrid uint configuration for distances (only need 0) + + for (int i = 0; i < 4; i++) + { + output.Write(4, 0); // 000 hybrid uint configuration for symbols (only <= 10) + } + + // Distance alphabet size + output.Write(5, 0b00001); // 2: just need 1 for RLE (i.e. distance 1) + + // Symbol + LZ77 alphabet size + for (int i = 0; i < 4; i++) + { + output.Write(1, 1); // > 1 + output.Write(4, 8); // <= 512 + output.Write(8, 256); // == 512 + } + + // Distance histogram + output.Write(2, 1); // simple prefix code + output.Write(2, 0); // With 1 symbol + output.Write(1, 1); // 1 + + // write all prefix codes + for (int i = 0; i < 4; i++) + { + code[i].Write(output); + } + + // group header for global modular image + output.Write(1, 1); // Global tree + output.Write(1, 1); // All default weighted prediction + } + + public static void PrepareDcGlobal(int numChannels, bool isSingleGroup, InlineArray4 codes, FjxlBitWriter output) + { + PrepareDcGlobalCommon(codes, output); + + if (numChannels > 2) + { + output.Write(2, 0b01); // 1 transform + output.Write(2, 0b00); // RCT + output.Write(5, 0b00000); // Starting from channel 0 + output.Write(2, 0b00); // YCoCg + } + else + { + output.Write(2, 0b00); // 0 transforms + } + + if (!isSingleGroup) + { + output.ZeroPadToByte(); + } + } + /// /// Pair of two vectors. /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderExactly14Bits.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderExactly14Bits.cs index 0c79b56a5b..f404e357d7 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderExactly14Bits.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderExactly14Bits.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Diagnostics; +using System.Runtime.InteropServices; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd.ChunkEncoders; @@ -43,8 +44,8 @@ public static void EncodeChunkSimd(Span residuals, int n, int skip, Read for (int i = 0; i < ChunkSize; i += FjxlSimdVec16.Lanes) { - TokenizeSimd(residuals[i..], token, nbits, bits); - HuffmanSimd14(token, rawNBitsSimd, rawBitsSimd, nbitsHuff, bitsHuff); + FjxlTokenization.TokenizeSimd(residuals[i..], token, nbits, bits); + FjxlHuffman.HuffmanSimd14(token, rawNBitsSimd, rawBitsSimd, nbitsHuff, bitsHuff); StoreSimdUpTo14( nbits, diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlBits32.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlBits32.cs new file mode 100644 index 0000000000..2bf012e441 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlBits32.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.Arm; +using static SharpImmintrin.CIntrinsicArmAdvSimd; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd; + +/// +/// Contains a 32-bit pair of code lengths and code values, useful +/// for Huffman encoding. +/// +internal struct FjxlBits32(Vector counts, Vector bits) +{ + /// + /// Gets or sets the code lengths. + /// + public Vector Counts { get; set; } = counts; + + /// + /// Gets or sets the code values. + /// + public Vector Bits { get; set; } = bits; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlBits64.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlBits64.cs new file mode 100644 index 0000000000..dca740cfbf --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlBits64.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd; + +/// +/// Contains a 64-bit pair of code lengths and code values, useful +/// for Huffman encoding. +/// +internal struct FjxlBits64(Vector counts, Vector bits) +{ + public static int Lanes { get; } = Vector.Count; + + /// + /// Gets or sets the code lengths. + /// + public Vector Counts { get; set; } = counts; + + /// + /// Gets or sets the code values. + /// + public Vector Bits { get; set; } = bits; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly void Store(ref ulong nbitsOut, ref ulong bitsOut) + { + this.Counts.StoreUnsafe(ref nbitsOut); + this.Bits.StoreUnsafe(ref bitsOut); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlRowFill.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlRowFill.cs new file mode 100644 index 0000000000..2c7217befa --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlRowFill.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd; + +internal static class FjxlRowFill +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void FillRowG8(Span rgba, int oxs, Span luma) + where T : unmanaged, INumber + { + int x = 0; + + for (; x + FjxlSimdVec16.Lanes <= oxs; x += FjxlSimdVec16.Lanes) + { + Vector rgb = FjxlSimdVec16.LoadG8(rgba[x..]); + StorePixels(rgb, luma[x..]); + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlSimdVec16.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlSimdVec16.cs new file mode 100644 index 0000000000..9ee046eea0 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlSimdVec16.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd; + +/// +/// Utilities for 16-bit SIMD vectors. +/// +internal static class FjxlSimdVec16 +{ + public static int Lanes { get; } = Vector.Count; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlImageBundleEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlImageBundleEncoder.cs new file mode 100644 index 0000000000..5a5bb65716 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlImageBundleEncoder.cs @@ -0,0 +1,126 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Cms; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; + +internal class JxlImageBundleEncoder +{ + public static bool TryApplyColorTransform( + Configuration configuration, + JxlColorEncoding currentEncoding, + float intensityTarget, + JxlImage3F color, + JxlImageF? black, + Rectangle rect, + JxlColorEncoding desiredEncoding, + JxlCmsInterface cms, + ref JxlImage3F output) + { + if (currentEncoding.IsGray != desiredEncoding.IsGray) + { + // both must be either grayscale or not grayscale + // can't have one that's grayscale and the other that's not + // so we return false to indicate failure + return false; + } + + JxlColorSpaceTransform transform = new(cms); + bool isGray = currentEncoding.IsGray; + + if (output.XSize < rect.Width || output.YSize < rect.Height) + { + // dimensions are within bounds + output = new(configuration, rect.Width, rect.Height); + } + else + { + // dimensions are out of bounds + // try to handle this by shrinking the image + // if shrinking fails too, then return false + if (!output.ShrinkTo(rect.Width, rect.Height)) + { + return false; + } + } + + transform.Initialize(configuration, currentEncoding, desiredEncoding, intensityTarget, rect.Width); + + // We can't use a ref variable inside of a lambda expression, + // so we store its value in a variable. The output variable + // will not be assigned. + JxlImage3F outValue = output; + + _ = Parallel.For(0, rect.Height, configuration.GetParallelOptions(), y => + { + Span sourceBuffer = transform.GetSourceBuffer(); + + if (isGray) + { + sourceBuffer = color.Plane(0).GetRow(rect, y); + } + else if (currentEncoding.Cmyk) + { + DebugGuard.NotNull(black, nameof(black)); + + Span row0 = color.Plane(0).GetRow(rect, y); + Span row1 = color.Plane(1).GetRow(rect, y); + Span row2 = color.Plane(2).GetRow(rect, y); + Span row3 = black.GetRow(rect, y); + + for (int x = 0, x4 = 0; x < rect.Width; x++, x4 += 4) + { + sourceBuffer[x4] = row0[x]; + sourceBuffer[x4 + 1] = row1[x]; + sourceBuffer[x4 + 2] = row2[x]; + sourceBuffer[x4 + 3] = row3[x]; + } + } + else + { + Span row0 = color.Plane(0).GetRow(rect, y); + Span row1 = color.Plane(1).GetRow(rect, y); + Span row2 = color.Plane(2).GetRow(rect, y); + + for (int x = 0, x3 = 0; x < rect.Width; x++, x3 += 3) + { + sourceBuffer[x3] = row0[x]; + sourceBuffer[x3 + 1] = row1[x]; + sourceBuffer[x3 + 2] = row2[x]; + } + } + + Span destBuffer = transform.GetDestinationBuffer(); + + if (!transform.Run(sourceBuffer, destBuffer, rect.Width)) + { + throw new InvalidOperationException("Could not run color space transform"); + } + + Span rowOut0 = outValue.PlaneRow(0, y); + Span rowOut1 = outValue.PlaneRow(1, y); + Span rowOut2 = outValue.PlaneRow(2, y); + + if (isGray) + { + Span bufferSlice = destBuffer[..rect.Width]; + bufferSlice.CopyTo(rowOut0); + bufferSlice.CopyTo(rowOut1); + bufferSlice.CopyTo(rowOut2); + } + else + { + for (int x = 0, x3 = 0; x < rect.Width; x++, x3 += 3) + { + rowOut0[x] = destBuffer[x3]; + rowOut1[x] = destBuffer[x3 + 1]; + rowOut2[x] = destBuffer[x3 + 2]; + } + } + }); + + return false; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77.cs new file mode 100644 index 0000000000..0feb95b989 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77.cs @@ -0,0 +1,51 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Lz77; + +/// +/// LZ77 Compression helper. +/// +internal static class JxlLz77 +{ + public static List> ApplyRunLengthEncoding( + Configuration configuration, + JxlHistogramParameters parameters, + int numContexts, + List> tokens, + JxlAnsLz77Parameters lz77) + { + List[] tokensLz77 = ArrayHelpers.CreateAndInitialize>(tokens.Count, () => []); + JxlLz77SymbolCostEstimator sce = new(numContexts, parameters.ForceHuffman, tokens, lz77); + + float bitDecrease = 0; + int totalSymbols = 0; + JxlAnsHybridUIntConfiguration uintConfig = new(); + + for (int stream = 0; stream < tokens.Count; stream++) + { + float distanceMultiplier = parameters.ImageWidths.Count > stream + ? parameters.ImageWidths[stream] + : 0; + + List input = tokens[stream]; + List output = tokensLz77[stream]; + totalSymbols += input.Count; + + using IMemoryOwner symbolCost = configuration.MemoryAllocator.Allocate(input.Count + 1); + Span symbolCostSpan = symbolCost.GetSpan(); + + for (int i = 0; i < input.Count; i++) + { + uintConfig.Encode(input[i].Value, out uint token, out uint numBits, out _); + symbolCost + } + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77Costs.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77Costs.cs new file mode 100644 index 0000000000..027f086f97 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77Costs.cs @@ -0,0 +1,94 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Lz77; + +internal static class JxlLz77Costs +{ + private static ReadOnlySpan LengthCostTable => + [ + 2.797667318563126f, 3.213177690381199f, 2.5706009246743737f, + 2.408392498667534f, 2.829649191872326f, 3.3923087753324577f, + 4.029267451554331f, 4.415576699706408f, 4.509357574741465f, + 9.21481543803004f, 10.020590190114898f, 11.858671627804766f, + 12.45853300490526f, 11.713105831990857f, 12.561996324849314f, + 13.775477692278367f, 13.174027068768641f, + ]; + + private static ReadOnlySpan DistanceCostTable => + [ + 6.368282626312716f, 5.680793277090298f, 8.347404197105247f, + 7.641619201599141f, 6.914328374119438f, 7.959808291537444f, + 8.70023120759855f, 8.71378518934703f, 9.379132523982769f, + 9.110472749092708f, 9.159029569270908f, 9.430936766731973f, + 7.278284055315169f, 7.8278514904267755f, 10.026641158289236f, + 9.976049229827066f, 9.64351607048908f, 9.563403863480442f, + 10.171474111762747f, 10.45950155077234f, 9.994813912104219f, + 10.322524683741156f, 8.465808729388186f, 8.756254166066853f, + 10.160930174662234f, 10.247329273413435f, 10.04090403724809f, + 10.129398517544082f, 9.342311691539546f, 9.07608009102374f, + 10.104799540677513f, 10.378079384990906f, 10.165828974075072f, + 10.337595322341553f, 7.940557464567944f, 10.575665823319431f, + 11.023344321751955f, 10.736144698831827f, 11.118277044595054f, + 7.468468230648442f, 10.738305230932939f, 10.906980780216568f, + 10.163468216353817f, 10.17805759656433f, 11.167283670483565f, + 11.147050200274544f, 10.517921919244333f, 10.651764778156886f, + 10.17074446448919f, 11.217636876224745f, 11.261630721139484f, + 11.403140815247259f, 10.892472096873417f, 11.1859607804481f, + 8.017346947551262f, 7.895143720278828f, 11.036577113822025f, + 11.170562110315794f, 10.326988722591086f, 10.40872184751056f, + 11.213498225466386f, 11.30580635516863f, 10.672272515665442f, + 10.768069466228063f, 11.145257364153565f, 11.64668307145549f, + 10.593156194627339f, 11.207499484844943f, 10.767517766396908f, + 10.826629811407042f, 10.737764794499988f, 10.6200448518045f, + 10.191315385198092f, 8.468384171390085f, 11.731295299170432f, + 11.824619886654398f, 10.41518844301179f, 10.16310536548649f, + 10.539423685097576f, 10.495136599328031f, 10.469112847728267f, + 11.72057686174922f, 10.910326337834674f, 11.378921834673758f, + 11.847759036098536f, 11.92071647623854f, 10.810628276345282f, + 11.008601085273893f, 11.910326337834674f, 11.949212023423133f, + 11.298614839104337f, 11.611603659010392f, 10.472930394619985f, + 11.835564720850282f, 11.523267392285337f, 12.01055816679611f, + 8.413029688994023f, 11.895784139536406f, 11.984679534970505f, + 11.220654278717394f, 11.716311684833672f, 10.61036646226114f, + 10.89849965960364f, 10.203762898863669f, 10.997560826267238f, + 11.484217379438984f, 11.792836176993665f, 12.24310468755171f, + 11.464858097919262f, 12.212747017409377f, 11.425595666074955f, + 11.572048533398757f, 12.742093965163013f, 11.381874288645637f, + 12.191870445817015f, 11.683156920035426f, 11.152442115262197f, + 11.90303691580457f, 11.653292787169159f, 11.938615382266098f, + 16.970641701570223f, 16.853602280380002f, 17.26240782594733f, + 16.644655390108507f, 17.14310889757499f, 16.910935455445955f, + 17.505678976959697f, 17.213498225466388f, 2.4162310293553024f, + 3.494587244462329f, 3.5258600986408344f, 3.4959806589517095f, + 3.098390886949687f, 3.343454654302911f, 3.588847442290287f, + 4.14614790111827f, 5.152948641990529f, 7.433696808092598f, + 9.716311684833672f, + ]; + + public static float GetLengthCost(int length) + { + new JxlAnsHybridUIntConfiguration(1, 0, 0).Encode((uint)length, out uint token, out uint bitCount, out _); + + if (token >= LengthCostTable.Length) + { + token = (uint)LengthCostTable.Length - 1; + } + + return LengthCostTable[(int)token] + bitCount; + } + + public static float GetDistanceCost(int dist) + { + new JxlAnsHybridUIntConfiguration(7, 0, 0).Encode((uint)dist, out uint token, out uint bitCount, out _); + + if (token >= DistanceCostTable.Length) + { + token = (uint)DistanceCostTable.Length - 1; + } + + return DistanceCostTable[(int)token] + bitCount; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77SymbolCostEstimator.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77SymbolCostEstimator.cs new file mode 100644 index 0000000000..7a0d84ee74 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77SymbolCostEstimator.cs @@ -0,0 +1,117 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Lz77; + +internal sealed class JxlLz77SymbolCostEstimator +{ + private readonly int maxAlphabetSize; + private readonly float[] bits; + private readonly float[] addSymbolCost; + + public JxlLz77SymbolCostEstimator(int numContexts, bool forceHuffman, List> tokens, JxlAnsLz77Parameters lz77) + { + JxlHistogram[] builder = ArrayPool.Shared.Rent(numContexts); + + for (int i = 0; i < numContexts; i++) + { + builder[i] = new(numContexts); + } + + // build histograms for estimating lz77 savings + JxlAnsHybridUIntConfiguration uintConfig = new(); + + foreach (List stream in tokens) + { + foreach (JxlToken token in stream) + { + uint tokenValue; + uint numBits; + uint bits; + + if (token.IsLz77Length) + { + lz77.LengthUintConfig.Encode(token.Value, out tokenValue, out numBits, out bits); + } + else + { + uintConfig.Encode(token.Value, out tokenValue, out numBits, out bits); + } + + tokenValue += token.IsLz77Length ? lz77.MinimumSymbol : 0; + + if ((int)token.Context >= numContexts) + { + throw new InvalidOperationException("Context is out of bounds"); + } + + builder[(int)token.Context].Add((int)tokenValue); + } + } + + this.maxAlphabetSize = 0; + + for (int i = 0; i < numContexts; i++) + { + this.maxAlphabetSize = Math.Max(this.maxAlphabetSize, builder[i].Counts.Count); + } + + this.bits = new float[numContexts * this.maxAlphabetSize]; + this.addSymbolCost = new float[numContexts]; + + for (int i = 0; i < numContexts; i++) + { + float inv_total = 1.0f / (builder[i].TotalCount + 1e-8f); + float total_cost = 0; + + for (int j = 0; j < builder[i].Counts.Count; j++) + { + int cnt = builder[i].Counts[j]; + float cost = 0; + if (cnt != 0 && cnt != builder[i].TotalCount) + { + cost = -MathF.Log2(cnt * inv_total); + if (forceHuffman) + { + cost = MathF.Ceiling(cost); + } + } + else if (cnt == 0) + { + cost = JxlAnsConstants.AnsLogTableSize; // Highest possible cost. + } + + this.bits[(i * this.maxAlphabetSize) + j] = cost; + total_cost += cost * builder[i].Counts[j]; + } + + // Penalty for adding a lz77 symbol to this contest (only used for static + // cost model). Higher penalty for contexts that have a very low + // per-symbol entropy. + this.addSymbolCost[i] = MathF.Max(0.0f, 6.0f - (total_cost * inv_total)); + } + } + + public float GetBits(int ctx, int symbol) => this.bits[(ctx * this.maxAlphabetSize) + symbol]; + + public float GetLengthCost(int context, int length, JxlAnsLz77Parameters parameters) + { + parameters.LengthUintConfig.Encode((uint)length, out uint token, out uint numBits, out uint bits); + token += parameters.MinimumSymbol; + return numBits + this.GetBits(context, (int)token); + } + + public float GetDistanceCost(int length, JxlAnsLz77Parameters parameters) + { + parameters.LengthUintConfig.Encode((uint)length, out uint token, out uint numBits, out uint bits); + return numBits + this.GetBits(parameters.NonserializedDistanceContext, (int)token); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float GetAddSymbolCost(int index) => this.addSymbolCost[index]; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Primitives/ArrayHelpers.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/ArrayHelpers.cs new file mode 100644 index 0000000000..0d466903fd --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/ArrayHelpers.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + +/// +/// Utilities for working with arrays. +/// +internal static class ArrayHelpers +{ + /// + /// Equivalent to a normal new T[length] but each item is + /// initialized with a lambda expression instead of being null. + /// + /// The array type. + /// The array length. + /// Each array item is computed with this function. + /// An array with items initialized using the function. + public static T[] CreateAndInitialize(int length, Func initializer) + { + T[] array = new T[length]; + + for (int i = 0; i < length; i++) + { + array[i] = initializer(); + } + + return array; + } +} From 0a04b34024f1c52a4226ccd493952b33c34d9920 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 16 Sep 2026 14:17:21 +0400 Subject: [PATCH 181/328] Delete fast lossless codec --- .../Encoder/FastLossless/FjxlBitWriter.cs | 88 -- .../Encoder/FastLossless/FjxlPrefixCode.cs | 462 ----------- .../FastLossless/JxlFastLosslessEncoder.cs | 758 ------------------ .../FjxlChunkEncoderExactly14Bits.cs | 64 -- .../FjxlChunkEncoderFrom9To13Bits.cs | 72 -- .../FjxlChunkEncoderMoreThan14Bits.cs | 81 -- .../FjxlChunkEncoderUpTo8Bits.cs | 69 -- .../Encoder/FastLossless/Simd/FjxlBits32.cs | 26 - .../Encoder/FastLossless/Simd/FjxlBits64.cs | 33 - .../Encoder/FastLossless/Simd/FjxlHuffman.cs | 66 -- .../Encoder/FastLossless/Simd/FjxlRowFill.cs | 23 - .../FastLossless/Simd/FjxlSimdUtils.cs | 14 - .../FastLossless/Simd/FjxlSimdVec16.cs | 14 - .../FastLossless/Simd/FjxlStoreSimd.cs | 101 --- .../FastLossless/Simd/FjxlTokenization.cs | 45 -- 15 files changed, 1916 deletions(-) delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/FjxlBitWriter.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/FjxlPrefixCode.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/JxlFastLosslessEncoder.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderExactly14Bits.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderFrom9To13Bits.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderMoreThan14Bits.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderUpTo8Bits.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlBits32.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlBits64.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlHuffman.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlRowFill.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlSimdUtils.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlSimdVec16.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlStoreSimd.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlTokenization.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/FjxlBitWriter.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/FjxlBitWriter.cs deleted file mode 100644 index 54277d41a9..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/FjxlBitWriter.cs +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless; - -/// -/// Simple MSB-first bit-stream writer implementation built on top -/// of a stream. -/// -/// Output bytes are written here. -internal sealed class FjxlBitWriter(Stream stream) : IDisposable -{ - /// - /// Temporary cache used to store pending written bits - /// before they're written to the output stream. - /// - private ulong buffer; - - /// - /// Gets the total number of bytes written to the output buffer so far. - /// - public long BytesWritten { get; private set; } - - /// - /// Gets the number of bits actively in the bit cache. - /// This is used to track how many bits were written into - /// the cache prior to sending the cache to the stream. - /// - public int BitsInBuffer { get; private set; } - - /// - /// Writes the specified bits in the Most Significant Byte (MSB) - /// order. - /// - /// Represents the number of bits to write to the bit-stream. - /// Represents the value to write to the bit-stream. - public void Write(int count, ulong bits) - { - DebugGuard.MustBeBetweenOrEqualTo(count, 0, 56, nameof(count)); - - if (count < 64) - { - bits &= (1UL << count) - 1; - } - - this.buffer |= bits << this.BitsInBuffer; - this.BitsInBuffer += count; - - this.FlushBytes(); - } - - /// - /// Internal method used to flush bytes from the cache - /// () into the output stream. - /// - private void FlushBytes() - { - int bytes = this.BitsInBuffer / 8; - - for (int i = 0; i < bytes; i++) - { - stream.WriteByte((byte)this.buffer); - this.BytesWritten++; - this.buffer >>= 8; - } - - this.BitsInBuffer -= bytes * 8; - } - - /// - /// Used by the dispose method to flush the remaining bits - /// that are not byte-aligned. F.e. if we dispose this reader - /// and we have 5 bits left, those final 5 bits are set to all 0 - /// and the byte is written to the stream. - /// - public void ZeroPadToByte() - { - if (this.BitsInBuffer != 0) - { - this.Write(8 - this.BitsInBuffer, 0); - } - } - - /// - /// Flushes out the final bytes. - /// - public void Dispose() => this.ZeroPadToByte(); -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/FjxlPrefixCode.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/FjxlPrefixCode.cs deleted file mode 100644 index f9b9c821b4..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/FjxlPrefixCode.cs +++ /dev/null @@ -1,462 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using System.Runtime.CompilerServices; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless; - -/// -/// The prefix code is used for encoding LZ77-compressed coefficients. -/// -internal sealed class FjxlPrefixCode -{ -#pragma warning disable SA1401 // Fields should be private - - /// - /// Maximum number of raw symbols for prefix coding. - /// - private const int MaxNumSymbols = JxlFastLosslessEncoder.NumRawSymbols + 1 < JxlFastLosslessEncoder.NumLz77 ? JxlFastLosslessEncoder.NumLz77 : JxlFastLosslessEncoder.NumRawSymbols + 1; - - /// - /// Gets or sets the Huffman raw bit lengths. - /// - public InlineArray19 RawLengths; - - /// - /// Gets or sets the Huffman raw code values. - /// - public InlineArray19 RawCodes; - - /// - /// Gets or sets the Huffman LZ77 bit lengths. - /// - public InlineArray33 Lz77Lengths; - - /// - /// Gets or sets the Huffman LZ77 code values. - /// - public InlineArray33 Lz77Codes; - - /// - /// Gets or sets the Huffman LZ77 cache code values. - /// - public InlineArray32 Lz77CacheBits; - - /// - /// Gets or sets the Huffman LZ77 cache bit lengths. - /// - public InlineArray32 Lz77CacheLengths; - - public FjxlPrefixCode(Span rawCounts, Span lz77Counts) - { - Span level1Counts = stackalloc ulong[JxlFastLosslessEncoder.NumRawSymbols + 1]; - rawCounts[..JxlFastLosslessEncoder.NumRawSymbols].CopyTo(level1Counts); - - this.RawCount = JxlFastLosslessEncoder.NumRawSymbols; - - while (this.RawCount > 0 && level1Counts[this.RawCount - 1] == 0) - { - this.RawCount--; - } - - level1Counts[this.RawCount] = 0; - - for (int i = 0; i < JxlFastLosslessEncoder.NumLz77; i++) - { - level1Counts[this.RawCount] += lz77Counts[i]; - } - - Span level1Lengths = stackalloc byte[JxlFastLosslessEncoder.NumRawSymbols + 1]; - level1Lengths.Clear(); - - ComputeCodeLengths(level1Counts, this.RawCount + 1, JxlFastLosslessEncoder.MinimumRawLength, JxlFastLosslessEncoder.MaximumRawLength, level1Lengths); - - Span level2Lengths = stackalloc byte[JxlFastLosslessEncoder.NumLz77]; - Span minLengths = stackalloc byte[JxlFastLosslessEncoder.NumLz77]; - - level2Lengths.Clear(); - minLengths.Clear(); - - int l = 15 - level1Lengths[this.RawCount]; - Span maxLengths = stackalloc byte[JxlFastLosslessEncoder.NumLz77]; - maxLengths.Fill((byte)l); - - int numLz77 = JxlFastLosslessEncoder.NumLz77; - while (numLz77 > 0 && lz77Counts[numLz77 - 1] == 0) - { - numLz77--; - } - - ComputeCodeLengths(lz77Counts, numLz77, minLengths, maxLengths, level2Lengths); - - level1Lengths[..this.RawCount].CopyTo(this.RawLengths); - - for (int i = 0; i < numLz77; i++) - { - this.Lz77Lengths[i] = (byte)(level2Lengths[i] != 0 ? level1Lengths[this.RawCount] + level2Lengths[i] : 0); - } - - ComputeCanonicalCode(this.RawLengths, this.RawCodes, this.Lz77Lengths, this.Lz77Codes); - - // Prepare the LZ77 cache - for (int count = 0; count < JxlFastLosslessEncoder.Lz77CacheSize; count++) - { - EncodeHybridUintLz77(count, out int token, out int nbits, out int bits); - this.Lz77CacheLengths[count] = (byte)(this.Lz77Lengths[token] + nbits + this.RawLengths[0]); - this.Lz77CacheBits[count] = - (ulong)((((bits << this.Lz77Lengths[token]) | this.Lz77Codes[token]) << this.RawLengths[0]) | - this.RawLengths[0]); - } - } - - /// - /// Gets a lookup used to reverse integers bit-wise. - /// - private static ReadOnlySpan ReverseNibbleLookup => - [ - 0b0000, 0b1000, 0b0100, 0b1100, 0b0010, 0b1010, 0b0110, 0b1110, - 0b0001, 0b1001, 0b0101, 0b1101, 0b0011, 0b1011, 0b0111, 0b1111, - ]; - -#pragma warning restore SA1401 // Fields should be private - - /// - /// Gets or sets the number of raw codes. - /// - public int RawCount { get; set; } - - /// - /// Reverses the integer bit-wise. - /// - /// Number of bits for the integer. - /// Actual bits to reverse. - /// - /// Input integer but reversed. F.e. 10010 becomes 01001. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static ushort BitReverse(int nbits, ushort bits) - { - unchecked - { - ushort rev16 = (ushort)((ReverseNibbleLookup[bits & 0xF] << 12) | - (ReverseNibbleLookup[(bits >> 4) & 0xF] << 8) | - (ReverseNibbleLookup[(bits >> 8) & 0xF] << 4) | - ReverseNibbleLookup[bits >> 12]); - return (ushort)(rev16 >> (16 - nbits)); - } - } - - private static void ComputeCanonicalCode(Span firstChunkLengths, Span firstChunkCodes, Span secondChunkLengths, Span secondChunkCodes) - { - const int maxCodeLength = 15; - - Span codeLengthCounts = stackalloc byte[maxCodeLength + 1]; - codeLengthCounts.Clear(); - - for (int i = 0; i < firstChunkCodes.Length; i++) - { - codeLengthCounts[firstChunkLengths[i]]++; - - if (firstChunkLengths[i] > 8) - { - throw new InvalidOperationException("First chunk length is too large"); - } - - if (firstChunkLengths[i] <= 0) - { - throw new InvalidOperationException("First chunk length cannot be <= 0"); - } - } - - for (int i = 0; i < secondChunkCodes.Length; i++) - { - codeLengthCounts[secondChunkLengths[i]]++; - - if (secondChunkLengths[i] > maxCodeLength) - { - throw new InvalidOperationException("Second chunk length is too large"); - } - } - - Span nextCode = stackalloc ushort[maxCodeLength + 1]; - nextCode.Clear(); - - ushort code = 0; - - for (int i = 1; i < maxCodeLength + 1; i++) - { - code = unchecked((ushort)((code + codeLengthCounts[i - 1]) << 1)); - nextCode[i] = code; - } - - unchecked - { - for (int i = 0; i < firstChunkCodes.Length; i++) - { - firstChunkCodes[i] = (byte)BitReverse(firstChunkLengths[i], nextCode[firstChunkLengths[i]]++); - } - - for (int i = 0; i < secondChunkCodes.Length; i++) - { - secondChunkCodes[i] = (byte)BitReverse(secondChunkLengths[i], nextCode[secondChunkLengths[i]]++); - } - } - } - - private static void ComputeCodeLengthsNonZeroImpl( - Span freqs, - int n, - int precision, - T infty, - Span minLimit, - Span maxLimit, - Span nbits) - where T : unmanaged, INumber - { - DebugGuard.MustBeLessThan(precision, 15, nameof(precision)); - DebugGuard.MustBeLessThanOrEqualTo(n, MaxNumSymbols, nameof(n)); - - int scale = 1 << precision; - int width = scale + 1; - - Span dynp = stackalloc T[width * (n + 1)]; - dynp.Fill(infty); - dynp[0] = T.Zero; - - for (int sym = 0; sym < n; sym++) - { - for (int bits = minLimit[sym]; bits <= maxLimit[sym]; bits++) - { - int offsetDelta = 1 << (precision - bits); - T cost = T.CreateChecked(freqs[sym]) * T.CreateChecked(bits); - - for (int off = 0; off + offsetDelta <= scale; off++) - { - int current = (sym * width) + off; - int next = ((sym + 1) * width) + off + offsetDelta; - - dynp[next] = T.Min(dynp[current] + cost, dynp[next]); - } - } - } - - int offFinal = scale; - - for (int sym = n - 1; sym >= 0; sym--) - { - if (offFinal <= 0) - { - throw new InvalidOperationException("Offset should be greater than zero"); - } - - for (int bits = minLimit[sym]; bits <= maxLimit[sym]; bits++) - { - int offsetDelta = 1 << (precision - bits); - - if (offsetDelta <= offFinal) - { - int current = (sym * width) + offFinal; - int previous = (sym * width) + offFinal - offsetDelta; - - T cost = T.CreateChecked(freqs[sym]) * T.CreateChecked(bits); - - if (dynp[current] == dynp[previous] + cost) - { - offFinal -= offsetDelta; - nbits[sym] = (byte)bits; - break; - } - } - } - } - } - - private static void ComputeCodeLengthsNonZero(Span freqs, int n, Span minLimit, Span maxLimit, Span nbits) - { - int precision = 0; - int shortestLength = 255; - ulong frequencySum = 0; - - for (int i = 0; i < n; i++) - { - frequencySum += freqs[i]; - - if (minLimit[i] < 1) - { - minLimit[i] = 1; - } - - precision = Math.Max(maxLimit[i], precision); - shortestLength = Math.Min(minLimit[i], shortestLength); - } - - precision -= shortestLength - 1; - ulong infinity = frequencySum * (ulong)precision; - - if (infinity < uint.MaxValue / 2) - { - ComputeCodeLengthsNonZeroImpl(freqs, n, precision, (uint)infinity, minLimit, maxLimit, nbits); - } - else - { - ComputeCodeLengthsNonZeroImpl(freqs, n, precision, infinity, minLimit, maxLimit, nbits); - } - } - - private static void ComputeCodeLengths(Span freqs, int n, ReadOnlySpan minLimitIn, ReadOnlySpan maxLimitIn, Span nbits) - { - DebugGuard.MustBeLessThanOrEqualTo(n, MaxNumSymbols, nameof(n)); - - Span compactFreqs = stackalloc ulong[MaxNumSymbols]; - Span minLimit = stackalloc byte[MaxNumSymbols]; - Span maxLimit = stackalloc byte[MaxNumSymbols]; - - int ni = 0; - for (int i = 0; i < n; i++) - { - if (freqs[i] != 0) - { - compactFreqs[ni] = freqs[i]; - minLimit[ni] = minLimitIn[i]; - maxLimit[ni] = maxLimitIn[i]; - ni++; - } - } - - compactFreqs[ni..].Clear(); - minLimit[ni..].Clear(); - maxLimit[ni..].Clear(); - - Span numBits = stackalloc byte[MaxNumSymbols]; - numBits.Clear(); - - ComputeCodeLengthsNonZero(compactFreqs, ni, minLimit, maxLimit, numBits); - - ni = 0; - - for (int i = 0; i < n; i++) - { - nbits[i] = 0; - if (freqs[i] != 0) - { - nbits[i] = numBits[ni++]; - } - } - } - - /// - /// Writes this LZ77 prefix code into the bit-stream. - /// - /// The bit-stream to write the prefix code into. - public void Write(FjxlBitWriter writer) - { - Span codeLengthCounts = stackalloc ulong[32].Slice(0, 18); - codeLengthCounts.Clear(); - codeLengthCounts[17] = 3 + (2 * (JxlFastLosslessEncoder.NumLz77 - 1)); - - for (int i = 0; i < 19; i++) - { - byte rawLength = this.RawLengths[i]; - - codeLengthCounts[rawLength]++; - } - - for (int i = 0; i < 33; i++) - { - byte lz77Length = this.Lz77Lengths[i]; - - codeLengthCounts[lz77Length]++; - } - - // Lengths for representing the code length - Span codeLengthsBuffer = stackalloc byte[64]; // nearest power of 2 alignment for 18*3 = 64 - Span codeLengthLengths = codeLengthsBuffer.Slice(0, 18); - Span codeLengthLengthsMinimum = codeLengthsBuffer.Slice(18, 18); - Span codeLengthLengthsMaximum = codeLengthsBuffer.Slice(18 * 2, 18); - - codeLengthLengths.Clear(); - codeLengthLengthsMinimum.Clear(); - codeLengthLengthsMaximum.Fill(5); - - ComputeCodeLengths(codeLengthCounts, 18, codeLengthLengthsMinimum, codeLengthLengthsMaximum, codeLengthLengths); - - writer.Write(2, 0b00); // HSKIP = 0 (Don't skip code lengths) - - // As per Brotli RFC - Span codeLengthOrder = [1, 2, 3, 4, 0, 5, 17, 6, 16, - 7, 8, 9, 10, 11, 12, 13, 14, 15]; - - // Lengths & codes for representing lengths of code lengths - Span codeLengthLengthLengths = [2, 4, 3, 2, 2, 4]; - Span codeLengthLengthCodes = [0, 7, 3, 2, 1, 15]; - - // Maximum number of code lengths - int numCodeLengths = 18; - while (codeLengthLengths[codeLengthOrder[numCodeLengths - 1]] == 0) - { - numCodeLengths--; - } - - // Max bits written in this loop: 18 * 4 = 72 - for (int i = 0; i < numCodeLengths; i++) - { - int symbol = codeLengthLengths[codeLengthOrder[i]]; - writer.Write(codeLengthLengthLengths[symbol], codeLengthLengthCodes[symbol]); - } - - Span codeLengthBits = stackalloc ushort[32].Slice(0, 18); - codeLengthBits.Clear(); - ComputeCanonicalCode([], [], codeLengthLengths, codeLengthBits); - - for (int i = 0; i < 19; i++) - { - byte rawLength = this.RawLengths[i]; - - writer.Write(codeLengthLengths[rawLength], codeLengthBits[rawLength]); - } - - int numLz77 = JxlFastLosslessEncoder.NumLz77; - while (this.Lz77Lengths[numLz77 - 1] == 0) - { - numLz77--; - } - - // Max bits in this block: 24 - writer.Write(codeLengthLengths[17], codeLengthBits[17]); - writer.Write(3, 0b010); // 5 - writer.Write(codeLengthLengths[17], codeLengthBits[17]); - writer.Write(3, 0b000); // (5 - 2) * 8 + 3 = 27 - writer.Write(codeLengthLengths[17], codeLengthBits[17]); - writer.Write(3, 0b010); // (27 - 2) * 8 + 5 = 205 - - // Encode LZ77 symbols with values 224 + i. - // Max. bits in this loop: 33 * 5 = 165 - for (int i = 0; i < numLz77; i++) - { - writer.Write(codeLengthLengths[this.Lz77Lengths[i]], codeLengthBits[this.Lz77Lengths[i]]); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void EncodeHybridUintLz77(int value, out int token, out int nBits, out int bits) - { - unchecked - { - int n = (int)JxlFastLosslessEncoder.FloorLog2((uint)value); - - if (value < 16) - { - token = value; - nBits = 0; - bits = 0; - } - else - { - token = 16 + n - 4; - nBits = n; - bits = value - (1 << n); - } - } - } -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/JxlFastLosslessEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/JxlFastLosslessEncoder.cs deleted file mode 100644 index e7ddf796b6..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/JxlFastLosslessEncoder.cs +++ /dev/null @@ -1,758 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; -using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd; - -// Suppress IDE0057. This is so we can stack-allocate -// a powers of 2 and then slice it to the appropriate -// length (which produces better code). -// -// Without this suppression, the analyzer produces a warning, -// recommending changing this: -// stackalloc ulong[32].Slice(0, 18) -// to: -// (stackalloc ulong[32])[..18] -// -// But then the analyzer produces a new warning, recommending -// to remove the paranthesis, changing this: -// (stackalloc ulong[32])[..18] -// to: -// stackalloc ulong[32][..18] -// -// which is invalid C# syntax. -#pragma warning disable IDE0057 // Use range operator - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless; - -/// -/// Extreme performance JPEG XL encoder which provides minimal lossless compression. -/// It also uses minimal dependencies. -/// -internal sealed class JxlFastLosslessEncoder -{ - /// - /// Specifies maximum number of bytes a frame header may use. - /// - public const int MaxFrameHeaderSize = 5; - - public const int NumRawSymbols = 19; - - public const int NumLz77 = 33; - - /// - /// Cache/dictionary size for LZ77 - /// - public const int Lz77CacheSize = 32; - - public const int Lz77Offset = 224; // ⚠️ This must always be 224. Don't change this constant. Changing this will break compression. - - public const int Lz77MinLength = 7; - - private static readonly int LogChunkSize = - JxlSimdTarget.Using512BitVectors - ? 5 - : JxlSimdTarget.Using256BitVectors - ? 4 - : 3; - - /// - /// Input frame data is stored here. - /// - private readonly FjxlFrameInputSource input; - - /// - /// Image width of the input image. - /// - private readonly int width; - - /// - /// Image height of the input image. - /// - private readonly int height; - - /// - /// Image width in groups. - /// - private readonly int numGroupsX; - - /// - /// Image height in groups. - /// - private readonly int numGroupsY; - - /// - /// Image width in groups (DC). - /// - private readonly int numDcGroupsX; - - /// - /// Image height in groups (DC). - /// - private readonly int numDcGroupsY; - - /// - /// Number of channels. (f.e. RGBA is 4, YUV is 3) - /// - private readonly int channels; - - /// - /// Number of bits represented per pixel. (f.e. 8 means pixels - /// have a 0-255 range) - /// - /// - /// Higher bit depths can represent more colors. - /// - private readonly int bitDepth; - - /// - /// Should the output image be stored in big-endian order? - /// - private readonly bool isBigEndian; - - private readonly int effort; - - private readonly bool collided; - - /// - /// Prefix codes for LZ77. - /// - private InlineArray4 hcode; - - private readonly List lookup = []; - - /// - /// Bit writer to write the JPEG XL headers. - /// - private readonly FjxlBitWriter header; - - /// - /// Bit writers for writing JPEG XL groups. - /// - private readonly List> groupData = []; - - /// - /// Sizes for each group. - /// - private readonly List groupSizes = []; - - private int acGroupDataOffset; - - private int minDcGlobalSize; - - private int currentBitWriter; - - private int bitWriterBytePos; - - private int bitsInBuffer; - - private long bitBuffer; - - private bool processDone; - - /// - /// Abstracts access to a raster frame data required for encoding. - /// - internal abstract class FjxlFrameInputSource : IDisposable - { - /// - public abstract void Dispose(); - - /// - /// Returns a span that wraps over channel color data at the - /// specified rectangular position. - /// - /// Target type of the color data. - /// Left offset - /// Right offset - /// Selection width - /// Selection height - /// The actual offset of the row in row-major order is stored here. - /// - /// A wrapper over the color data of the channel at the specified - /// position. - /// - public abstract Span GetColorChannelData(int x, int y, int width, int height, out long rowOffset) - where T : unmanaged; - } - - /// - /// Gets minimum raw lengths for prefix coding. - /// - public static ReadOnlySpan MinimumRawLength => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - - /// - /// Gets maximum raw lengths for prefix coding. - /// - public static ReadOnlySpan MaximumRawLength => [7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 10]; - - /// - /// Gets a lookup used by the method - /// to translate a bucket into a base group size. - /// - private static ReadOnlySpan GroupSizeOffset => - [ - 0, - 1024, - 17408, - 4211712 - ]; - - /// - /// Gets a lookup to determine how many bits a TOC bucket uses. - /// - private static ReadOnlySpan TocBits => [12, 16, 24, 32]; - - /// - /// Approximates Floor(Log2(v)) using integers. - /// - /// Value to retrieve Floor(Log2(v)) of. - /// Floor of second logarithm of v, or 31 if v is equal to 0. - /// This method may use CPU intrinsics provided by the .NET Runtime (e.g. BMI1 on x86). - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static uint FloorLog2(uint v) => v == 0 ? 0 : 31u - (uint)BitOperations.LeadingZeroCount(v); - - /// - /// Approximates count trailing zeros of v using integers. - /// - /// Value to retrieve number of 0 bits after last 1 bit of. - /// After the least significant 1 bit, returns the number of 0 bits. E.g. 1000 1000 00 -> 5. - /// This method may use CPU intrinsics provided by the .NET Runtime (e.g. BMI1 on x86). - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint CtzNonZero(ulong v) => (uint)BitOperations.TrailingZeroCount(v); - - /// - /// Returns a TOC bucket based on the group size. - /// - /// Specified group size. - /// TOC bucket matching the appropriate group size. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int TocBucket(int groupSize) - { - int bucket = 0; - - while (bucket < 3 && groupSize >= GroupSizeOffset[bucket + 1]) - { - bucket++; - } - - return bucket; - } - - /// - /// Returns the total number of bits required to represent - /// all given group sizes in the TOC. - /// - /// Group sizes to calculate bit sizes of. - /// Accumulated number of bits required to represent each group size. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int TocSize(Span groupSizes) - { - int tocBits = 0; - - ref int unsafeRef = ref MemoryMarshal.GetReference(groupSizes); - - for (int i = 0; i < groupSizes.Length; i++) - { - // TODO: we can try using AVX2 gather intrinsics, - // especially because TocBits can absolutely fit - // in the L1 cache - int groupSize = Unsafe.Add(ref unsafeRef, i); - int bucketForGroupSize = TocBucket(groupSize); - int bitsUsedByBucket = TocBits[bucketForGroupSize]; - - tocBits += bitsUsedByBucket; - } - - return tocBits; - } - - /// - /// Returns the number of bytes for the frame header. - /// - /// Indicates presence of the alpha channel. - /// Indicates whether this is the final frame. - /// Frame header size in bytes. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int FrameHeaderSize(bool containsAlpha, bool isLast) - { - // Original code (from libjxl): - // - // size_t nbits = 28 + (have_alpha ? 4 : 0) + (is_last ? 0 : 2); - // return (nbits + 7) / 8; - // - // In this implementation we just use constants to shave a few CPU cycles. - // The total amount of branches is reduced by one (for the !containsAlpha case), - // but we remove the arithmetic/shifting instructions. - unchecked - { - if (containsAlpha) - { - if (isLast) - { - return 5; // (34 + 7) / 8 - } - else - { - return 4; // (32 + 7) / 8 - } - } - else - { - return 4; // (30 + 7) / 8 AND (28 + 7) / 8 yield the same result - } - } - } - - private static long GetSectionSize(InlineArray4 groupData) - { - long size = 0; - - for (int j = 0; j < 4; j++) - { - FjxlBitWriter writer = groupData[j]; - - size += (writer.BytesWritten * 8) + writer.BitsInBuffer; - } - - return (size + 7) / 8; - } - - /// - /// Approximates number of bytes needed for the output image buffer. - /// - /// Bytes for the frame buffer. - private long GetOutputSize() - { - long totalSizeGroups = 0; - - Span> groups = CollectionsMarshal.AsSpan(this.groupData); - - for (int i = 0; i < groups.Length; i++) - { - InlineArray4 section = groups[i]; - - totalSizeGroups += GetSectionSize(section); - } - - return this.header.BytesWritten + totalSizeGroups; - } - - /// - /// Returns the maximum amount of bytes potentially required for the image buffer. - /// - /// Upper bound of bytes for frame buffer. - private long GetMaxRequiredOutput() => this.GetOutputSize() + 32; - - private void WriteHeader(bool addImageHeader, bool isLast) - { - FjxlBitWriter output = this.header; - bool haveAlpha = this.channels is 2 or 4; - - if (addImageHeader) - { - // File signature. This signature specifies - // a raw codestream. No container format here. - output.Write(16, 0x0AFF); - - // Handcrafted size header. - output.Write(1, 0); // Not small - - WriteSize(this.height); - output.Write(3, 0b000); // No special ratio - WriteSize(this.width); - - // Handcrafted image metadata - output.Write(1, 0); // all_default = 0 (don't assume values to be set to their defaults) - output.Write(1, 0); // extra_fields = 0 (extra fields are disabled and therefore not present) - output.Write(1, 0); // bit_depth.floating_point_sample = 0 (samples are integers) - - if (this.bitDepth == 8) - { - output.Write(2, 0b00); // bit_depth.bits_per_sample = 8 (predefined bit depth of 8 bits) - } - else if (this.bitDepth == 10) - { - output.Write(2, 0b01); // bit_depth.bits_per_sample = 10 (predefined bit depth of 10 bits) - } - else if (this.bitDepth == 12) - { - output.Write(2, 0b10); // bit_depth.bits_per_sample = 12 (predefined bit depth of 12 bits) - } - else - { - output.Write(2, 0b11); // Custom bit depth - output.Write(6, (ulong)this.bitDepth - 1); // bit depth minus 1 (so 0 becomes 1, 9 becomes 10, etc) - } - - if (this.bitDepth <= 14) - { - output.Write(1, 1); // 16-bit-buffer is sufficient - } - else - { - output.Write(1, 0); // 16-bit-buffer is NOT sufficient - } - - if (haveAlpha) - { - output.Write(2, 0b01); // Emit one extra channel (the alpha channel) - - if (this.bitDepth == 8) - { - output.Write(1, 1); // all_default = 1 (8-bit alpha is the default) - } - else - { - output.Write(1, 0); // all_default = 0 - output.Write(2, 0); // type = alpha - output.Write(1, 0); // samples are not floating point - - if (this.bitDepth == 10) - { - output.Write(2, 0b01); // bit_depth.bits_per_sample = 10 (predefined bit depth of 10 bits) - } - else if (this.bitDepth == 12) - { - output.Write(2, 0b10); // bit_depth.bits_per_sample = 12 (predefined bit depth of 12 bits) - } - else - { - output.Write(2, 0b11); // Custom bit depth - output.Write(6, (ulong)this.bitDepth - 1); // bit depth minus 1 (so 0 becomes 1, 9 becomes 10, etc) - } - - output.Write(2, 0); // dim_shift = 0 - output.Write(2, 0); // name_len = 0 - output.Write(1, 0); // alpha_associated = 0 - } - } - else - { - output.Write(2, 0b00); // 0 extra channels - } - - output.Write(1, 0); // not XYB - - if (this.channels > 2) - { - output.Write(1, 1); // color_encoding.all_default = 1 (sRGB) - } - else - { - output.Write(1, 0); // color_encoding.all_default = 0 - output.Write(1, 0); // color_encoding.want_icc = 0 - output.Write(2, 0b01); // Grayscale - output.Write(2, 0b01); // D65 - output.Write(1, 0); // No gamma transfer function - output.Write(2, 0b10); // transfer function: 2 + u(4) - output.Write(4, 11); // transfer function (specifies sRGB) - output.Write(2, 1); // relative rendering intent - } - - output.Write(2, 0b00); // No extensions - output.Write(1, 1); // all_default transform data - output.ZeroPadToByte(); // No ICC and no preview. Frame should start at byte boundary. - } - - // Handcrafted frame header - output.Write(1, 0); // all_default = 0 (non-default values) - output.Write(2, 0b00); // regular frame - output.Write(1, 1); // modular - output.Write(2, 0b00); // default flags - output.Write(1, 0); // not Y'Cb'Cr - output.Write(2, 0b00); // no upsampling - - if (haveAlpha) - { - output.Write(2, 0b00); // no alpha upsampling - } - - output.Write(2, 0b01); // default group size - output.Write(2, 0b00); // exactly one pass - output.Write(1, 0); // no custom size or origin - output.Write(2, 0b00); // Replace blending mode - - if (haveAlpha) - { - output.Write(2, 0b00); // Replace blending mode for alpha channel - } - - output.Write(2, 0b00); // a frame has no name - output.Write(1, 0); // loop filter is not all_default - output.Write(1, 0); // no Gaborish transform - output.Write(2, 0b00); // 0 EPF filters - output.Write(2, 0b00); // no LF extensions - output.Write(2, 0b00); // no FH extensions - - output.Write(1, 0); // no TOC permutation - output.ZeroPadToByte(); // TOC is byte aligned - - Span groupSizes = CollectionsMarshal.AsSpan(this.groupSizes); - - for (int i = 0; i < groupSizes.Length; i++) - { - int groupSize = groupSizes[i]; - - int bucket = TocBucket(groupSize); - output.Write(2, (ulong)bucket); - output.Write(TocBits[bucket] - 2, (ulong)(groupSize - GroupSizeOffset[bucket])); - } - - output.ZeroPadToByte(); // Groups are byte-aligned - - // Sizes are coded using a special variable-length - // kind of coding. This method does that here. - // - // It has a prefix of 2 bits, followed by the suffix of N - // bits which depend on the prefix: - // - // prefix 0b00: 9 consecutive bits - // prefix 0b01: 13 consecutive bits - // prefix 0b10: 18 consecutive bits - // prefix 0b11: 30 consecutive bits - void WriteSize(int size) - { - ulong sizeMinus1 = (ulong)size - 1uL; - - if (sizeMinus1 < (1 << 9)) - { - output.Write(2, 0b00); // 9 bits - output.Write(9, sizeMinus1); - } - else if (sizeMinus1 < (1 << 13)) - { - output.Write(2, 0b01); // 13 bits - output.Write(13, sizeMinus1); - } - else if (sizeMinus1 < (1 << 18)) - { - output.Write(2, 0b10); // 18 bits - output.Write(18, sizeMinus1); - } - else - { - output.Write(2, 0b11); // 30 bits - output.Write(30, sizeMinus1); - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int ComputeDcGlobalPadding(Span groupSizes, int acGroupDataOffset, int minDcGlobalSize, bool containsAlpha, bool isLast) - { - // Libjxl reference implements this method like this: - /* - size_t ComputeDcGlobalPadding(const std::vector& group_sizes, - size_t ac_group_data_offset, - size_t min_dc_global_size, bool have_alpha, - bool is_last) { - std::vector new_group_sizes = group_sizes; - new_group_sizes[0] = min_dc_global_size; - size_t toc_size = TOCSize(new_group_sizes); - size_t actual_offset = - FrameHeaderSize(have_alpha, is_last) + toc_size + group_sizes[0]; - return ac_group_data_offset - actual_offset; - } - */ - // The reference implementation copies the entire vector so that - // element 0 can be modified without affecting the original. - // Since TocSize() does not throw, temporarily modify element 0 - // instead, avoiding the allocation and copy. - int firstItem = groupSizes[0]; - groupSizes[0] = minDcGlobalSize; - int tocSize = TocSize(groupSizes); - int actualOffset = FrameHeaderSize(containsAlpha, isLast) + tocSize + firstItem; - groupSizes[0] = firstItem; - return acGroupDataOffset - actualOffset; - } - - private static int PredictPixels(Span pixels, Span pixelsLeft, Span pixelsTop, Span pixelsTopleft, Span residuals, T negativeOne) - where T : unmanaged - { - Vector px = Vector.Create(pixels); - Vector left = Vector.Create(pixelsLeft); - Vector top = Vector.Create(pixelsTop); - Vector topleft = Vector.Create(pixelsTopleft); - - Vector ac = left - topleft; - Vector ab = left - top; - Vector bc = top - topleft; - Vector grad = ac + top; - Vector d = ab ^ bc; - Vector zero = Vector.Zero; - Vector clamp = Vector.ConditionalSelect(Vector.GreaterThan(zero, d), top, left); - Vector s = ac ^ bc; - Vector pred = Vector.ConditionalSelect(Vector.GreaterThan(zero, s), grad, clamp); - - Vector res = px - pred; - Vector resTimes2 = res + res; - res = Vector.ConditionalSelect(Vector.GreaterThan(zero, res), Vector.Create(negativeOne) - resTimes2, resTimes2); - res.CopyTo(residuals); - - return FjxlSimdUtils.CountPrefix(Vector.Equals(res, zero)); - } - - private static void EncodeHybridUint000(uint value, out uint token, out uint nbits, out uint bits) - { - if (value == 0) - { - token = 0; - nbits = 0; - bits = 0; - return; - } - - uint n = FloorLog2(value); - token = n + 1; - nbits = n; - bits = value - (1u << (int)n); - } - - private static void GenericEncodeChunk(ReadOnlySpan residuals, int n, int skip, FjxlPrefixCode code, ref FjxlBitWriter output) - { - for (int ix = skip; ix < n; ix++) - { - EncodeHybridUint000(residuals[ix], out uint token, out uint nbits, out uint bits); - output.Write((int)(code.RawLengths[(int)token] + nbits), code.RawCodes[(int)token] | (bits << code.RawLengths[(int)token])); - } - } - - public static void PrepareDcGlobalCommon(InlineArray4 code, FjxlBitWriter output) - { - // No patches, spline, or noise. - output.Write(1, 1); // Default DC dequantization factors - output.Write(1, 1); // Use global trees and histograms - output.Write(1, 0); // Don't use LZ77 compression for the tree. - - output.Write(1, 1); // Simple code for the tree's context map - output.Write(2, 0); // All contexts clustered together - output.Write(1, 1); // Use prefix code for tree - output.Write(4, 0); // 000 hybrid unsigned integer - output.Write(6, 0b100011); // Alphabet size is 4 - output.Write(2, 1); // Simple prefix code - output.Write(2, 3); // With 4 symbols - - output.Write(2, 0); - output.Write(2, 1); - output.Write(2, 2); - output.Write(2, 3); - output.Write(1, 0); // First tree encoding option - - // Huffman table and extra bits for the tree - Span symbolBits = [0b00, 0b10, 0b001, 0b101, 0b0011, 0b0111]; - Span symbolLengths = [2, 2, 3, 3, 4, 4]; - - Span predictors = - [ - 1, 2, 1, 4, 1, 0, 0, 5, 0, 0, 0, 0, 5, - 0, 0, 0, 0, 5, 0, 0, 0, 0, 5, 0, 0, 0 - ]; - - ref byte unsafePred = ref MemoryMarshal.GetReference(predictors); - - for (int i = 0; i < predictors.Length; i++) - { - byte predictor = Unsafe.AddByteOffset(ref unsafePred, i); - output.Write(symbolLengths[predictor], symbolBits[predictor]); - } - - output.Write(1, 1); // Enable LZ77 compression for the main bit-stream - output.Write(2, 0b00); // LZ77 offset 224 - output.Write(4, 0b1010); // LZ77 minimum length 7 - - // 400 hybrid uint configuration for LZ77 - output.Write(4, 4); - output.Write(3, 0); - output.Write(3, 0); - - output.Write(1, 1); // simple code for the context map - output.Write(2, 3); // 3 bits per entry - output.Write(3, 4); // channel 3 - output.Write(3, 3); // channel 2 - output.Write(3, 2); // channel 1 - output.Write(3, 1); // channel 0 - output.Write(3, 0); // distance histogram first - - output.Write(1, 1); // use prefix codes - output.Write(4, 0); // 000 hybrid uint configuration for distances (only need 0) - - for (int i = 0; i < 4; i++) - { - output.Write(4, 0); // 000 hybrid uint configuration for symbols (only <= 10) - } - - // Distance alphabet size - output.Write(5, 0b00001); // 2: just need 1 for RLE (i.e. distance 1) - - // Symbol + LZ77 alphabet size - for (int i = 0; i < 4; i++) - { - output.Write(1, 1); // > 1 - output.Write(4, 8); // <= 512 - output.Write(8, 256); // == 512 - } - - // Distance histogram - output.Write(2, 1); // simple prefix code - output.Write(2, 0); // With 1 symbol - output.Write(1, 1); // 1 - - // write all prefix codes - for (int i = 0; i < 4; i++) - { - code[i].Write(output); - } - - // group header for global modular image - output.Write(1, 1); // Global tree - output.Write(1, 1); // All default weighted prediction - } - - public static void PrepareDcGlobal(int numChannels, bool isSingleGroup, InlineArray4 codes, FjxlBitWriter output) - { - PrepareDcGlobalCommon(codes, output); - - if (numChannels > 2) - { - output.Write(2, 0b01); // 1 transform - output.Write(2, 0b00); // RCT - output.Write(5, 0b00000); // Starting from channel 0 - output.Write(2, 0b00); // YCoCg - } - else - { - output.Write(2, 0b00); // 0 transforms - } - - if (!isSingleGroup) - { - output.ZeroPadToByte(); - } - } - - /// - /// Pair of two vectors. - /// - /// Type of the vector. - /// Low vector - /// High vector - private struct VectorPair(T lo, T hi) - where T : unmanaged - { - public T Low = lo; - public T High = hi; - } - - internal static class SimdVector32 - { - public static Vector ValueToToken(Vector vec) => Vector.Create(32) - SimdUtils.Lzcnt(vec); - - public static Vector SaturateSubtract(Vector a, Vector b) => Vector.Max(a, b) - b; - - public static Vector Pow2(Vector x) => SimdUtils.Pow2(x); - } -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderExactly14Bits.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderExactly14Bits.cs deleted file mode 100644 index f404e357d7..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderExactly14Bits.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Diagnostics; -using System.Runtime.InteropServices; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd.ChunkEncoders; - -internal readonly struct FjxlChunkEncoderExactly14Bits -{ - public const int BitDepth = 14; - public const int InputBytes = 2; - - public static ReadOnlySpan MinRawLength => - [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 8, 7 - ]; - - public static ReadOnlySpan MaxRawLength => - [ - 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 10 - ]; - - public static int MaxEncodedBitsPerSample() => 23; - - public static void PrepareForSimd(ReadOnlySpan nbits, ReadOnlySpan bits, Span nbitsSimd, Span bitsSimd) - { - CheckHuffmanBitsSimd(bits[15], nbits[15], bits[16], nbits[16]); - - nbits[..16].CopyTo(nbitsSimd); - bits[..16].CopyTo(bitsSimd); - } - - public static void EncodeChunkSimd(Span residuals, int n, int skip, ReadOnlySpan rawNBitsSimd, ReadOnlySpan rawBitsSimd, ref FjxlBitWriter output) - { - Span bits32 = stackalloc FjxlBits32[2 * ChunkSize / FjxlSimdVec16.Lanes]; - - Span buffer = stackalloc ushort[FjxlSimdVec16.Lanes * 5]; - Span bits = buffer.Slice(0, FjxlSimdVec16.Lanes); - Span nbits = buffer.Slice(FjxlSimdVec16.Lanes, FjxlSimdVec16.Lanes); - Span bitsHuff = buffer.Slice(FjxlSimdVec16.Lanes * 2, FjxlSimdVec16.Lanes); - Span nbitsHuff = buffer.Slice(FjxlSimdVec16.Lanes * 3, FjxlSimdVec16.Lanes); - Span token = buffer.Slice(FjxlSimdVec16.Lanes * 4, FjxlSimdVec16.Lanes); - - for (int i = 0; i < ChunkSize; i += FjxlSimdVec16.Lanes) - { - FjxlTokenization.TokenizeSimd(residuals[i..], token, nbits, bits); - FjxlHuffman.HuffmanSimd14(token, rawNBitsSimd, rawBitsSimd, nbitsHuff, bitsHuff); - - StoreSimdUpTo14( - nbits, - bits, - nbitsHuff, - bitsHuff, - Math.Max(n, i) - i, - Math.Max(skip, i) - i, - bits32[(2 * i / FjxlSimdVec16.Lanes)..]); - } - - StoreToWriter(bits32, ref output); - } - - public static int NumSymbols() => 17; -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderFrom9To13Bits.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderFrom9To13Bits.cs deleted file mode 100644 index 7bb349abef..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderFrom9To13Bits.cs +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Diagnostics; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd.ChunkEncoders; - -internal readonly struct FjxlChunkEncoderFrom9To13Bits -{ - public const int InputBytes = 2; - - private readonly int bitDepth; - - public FjxlChunkEncoderFrom9To13Bits(int bitDepth) - { - DebugGuard.MustBeLessThanOrEqualTo(bitDepth, 13, nameof(bitDepth)); - DebugGuard.MustBeGreaterThanOrEqualTo(bitDepth, 9, nameof(bitDepth)); - - this.bitDepth = bitDepth; - } - - public static ReadOnlySpan MinRawLength => - [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 - ]; - - public static ReadOnlySpan MaxRawLength => - [ - 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 10 - ]; - - public static int MaxEncodedBitsPerSample() => 22; - - public static void PrepareForSimd(ReadOnlySpan nbits, ReadOnlySpan bits, int n, Span nbitsSimd, Span bitsSimd) - { - DebugGuard.MustBeLessThanOrEqualTo(n, 16, nameof(n)); - - nbits[..16].CopyTo(nbitsSimd); - bits[..16].CopyTo(bitsSimd); - } - - public static void EncodeChunkSimd(Span residuals, int n, int skip, ReadOnlySpan rawNBitsSimd, ReadOnlySpan rawBitsSimd, ref FjxlBitWriter output) - { - Span bits32 = stackalloc FjxlBits32[2 * ChunkSize / FjxlSimdVec16.Lanes]; - - Span buffer = stackalloc ushort[FjxlSimdVec16.Lanes * 5]; - Span bits = buffer.Slice(0, FjxlSimdVec16.Lanes); - Span nbits = buffer.Slice(FjxlSimdVec16.Lanes, FjxlSimdVec16.Lanes); - Span bitsHuff = buffer.Slice(FjxlSimdVec16.Lanes * 2, FjxlSimdVec16.Lanes); - Span nbitsHuff = buffer.Slice(FjxlSimdVec16.Lanes * 3, FjxlSimdVec16.Lanes); - Span token = buffer.Slice(FjxlSimdVec16.Lanes * 4, FjxlSimdVec16.Lanes); - - for (int i = 0; i < ChunkSize; i += FjxlSimdVec16.Lanes) - { - TokenizeSimd(residuals[i..], token, nbits, bits); - HuffmanSimdUpTo13(token, rawNBitsSimd, rawBitsSimd, nbitsHuff, bitsHuff); - - StoreSimdUpTo14( - nbits, - bits, - nbitsHuff, - bitsHuff, - Math.Max(n, i) - i, - Math.Max(skip, i) - i, - bits32[(2 * i / FjxlSimdVec16.Lanes)..]); - } - - StoreToWriter(bits32, ref output); - } - - public int NumSymbols(bool doingYcocgOrLargePalette) => this.bitDepth + (doingYcocgOrLargePalette ? 3 : 2); -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderMoreThan14Bits.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderMoreThan14Bits.cs deleted file mode 100644 index 235f33508d..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderMoreThan14Bits.cs +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd.ChunkEncoders; - -internal readonly struct FjxlChunkEncoderMoreThan14Bits -{ - public const int InputBytes = 2; - - private readonly int bitDepth; - - public FjxlChunkEncoderMoreThan14Bits(int bitDepth) - { - DebugGuard.MustBeGreaterThan(bitDepth, 14, nameof(bitDepth)); - DebugGuard.MustBeLessThanOrEqualTo(bitDepth, 16, nameof(bitDepth)); - - this.bitDepth = bitDepth; - } - - public static ReadOnlySpan MinRawLength => - [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 8, 8, 8, 8, 8, 8, 7 - ]; - - public static ReadOnlySpan MaxRawLength => - [ - 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, - 7, 7, 7, 8, 8, 8, 8, 8, 8, 10 - ]; - - public static int MaxEncodedBitsPerSample() => 25; - - public static void PrepareForSimd(ReadOnlySpan nbits, ReadOnlySpan bits, Span nbitsSimd, Span bitsSimd) - { - CheckHuffmanBitsSimd(bits[13], nbits[13], bits[14], nbits[14]); - CheckHuffmanBitsSimd(bits[15], nbits[15], bits[16], nbits[16]); - CheckHuffmanBitsSimd(bits[17], nbits[17], bits[18], nbits[18]); - - nbits[..14].CopyTo(nbitsSimd); - bits[..14].CopyTo(bitsSimd); - - nbitsSimd[14] = nbits[15]; - bitsSimd[14] = bits[15]; - nbitsSimd[15] = nbits[17]; - bitsSimd[15] = bits[17]; - } - - public static void EncodeChunkSimd(Span residuals, int n, int skip, ReadOnlySpan rawNBitsSimd, ReadOnlySpan rawBitsSimd, ref FjxlBitWriter output) - { - Span bits32 = stackalloc FjxlBits32[2 * ChunkSize / FjxlSimdVec16.Lanes]; - - Span bufferU32 = stackalloc uint[FjxlSimdVec16.Lanes * 2]; - Span bits = bufferU32.Slice(0, FjxlSimdVec16.Lanes); - Span nbits = bufferU32.Slice(FjxlSimdVec16, FjxlSimdVec16.Lanes); - - Span bufferU16 = stackalloc uint[FjxlSimdVec16.Lanes * 3]; - Span bitsHuff = bufferU16.Slice(0, FjxlSimdVec16.Lanes); - Span nbitsHuff = bufferU16.Slice(FjxlSimdVec16.Lanes, FjxlSimdVec16.Lanes); - Span token = bufferU16.Slice(FjxlSimdVec16.Lanes * 2, FjxlSimdVec16.Lanes); - - for (int i = 0; i < ChunkSize; i += FjxlSimdVec16.Lanes) - { - TokenizeSimd(residuals[i..], token, nbits, bits); - HuffmanSimdAbove14(token, rawNBitsSimd, rawBitsSimd, nbitsHuff, bitsHuff); - - StoreSimdAbove14( - nbits, - bits, - nbitsHuff, - bitsHuff, - Math.Max(n, i) - i, - Math.Max(skip, i) - i, - bits32[(2 * i / FjxlSimdVec16.Lanes)..]); - } - - StoreToWriter(bits32, ref output); - } - - public static int NumSymbols() => 19; -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderUpTo8Bits.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderUpTo8Bits.cs deleted file mode 100644 index 89faa2f2c5..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderUpTo8Bits.cs +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd.ChunkEncoders; - -internal readonly struct FjxlChunkEncoderUpTo8Bits -{ - public const int InputBytes = 1; - - private readonly int bitDepth; - - public FjxlChunkEncoderUpTo8Bits(int bitDepth) - { - DebugGuard.MustBeLessThanOrEqualTo(bitDepth, 8, nameof(bitDepth)); - - this.bitDepth = bitDepth; - } - - public static ReadOnlySpan MinRawLength => - [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 - ]; - - public static ReadOnlySpan MaxRawLength => - [ - 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 10 - ]; - - public static int MaxEncodedBitsPerSample() => 16; - - public static void PrepareForSimd(ReadOnlySpan nbits, ReadOnlySpan bits, int n, Span nbitsSimd, Span bitsSimd) - { - DebugGuard.MustBeLessThanOrEqualTo(n, 16, nameof(n)); - - nbits[..16].CopyTo(nbitsSimd); - bits[..16].CopyTo(bitsSimd); - } - - public static void EncodeChunkSimd(Span residuals, int n, int skip, ReadOnlySpan rawNBitsSimd, ReadOnlySpan rawBitsSimd, ref FjxlBitWriter output) - { - Span bits32 = stackalloc FjxlBits32[ChunkSize / FjxlSimdVec16.Lanes]; - - Span buffer = stackalloc ushort[FjxlSimdVec16.Lanes * 5]; - Span bits = buffer.Slice(0, FjxlSimdVec16.Lanes); - Span nbits = buffer.Slice(FjxlSimdVec16.Lanes, FjxlSimdVec16.Lanes); - Span bitsHuff = buffer.Slice(FjxlSimdVec16.Lanes * 2, FjxlSimdVec16.Lanes); - Span nbitsHuff = buffer.Slice(FjxlSimdVec16.Lanes * 3, FjxlSimdVec16.Lanes); - Span token = buffer.Slice(FjxlSimdVec16.Lanes * 4, FjxlSimdVec16.Lanes); - - for (int i = 0; i < ChunkSize; i += FjxlSimdVec16.Lanes) - { - TokenizeSimd(residuals[i..], token, nbits, bits); - HuffmanSimdUpTo13(token, rawNBitsSimd, rawBitsSimd, nbitsHuff, bitsHuff); - - StoreSimdUpTo8( - nbits, - bits, - nbitsHuff, - bitsHuff, - Math.Max(n, i) - i, - Math.Max(skip, i) - i, - bits32[(int)(i / FjxlSimdVec16.Lanes)..]); - } - - StoreToWriter(bits32, ref output); - } - - public int NumSymbols(bool doingYcocgOrLargePalette) => this.bitDepth + (doingYcocgOrLargePalette ? 3 : 2); -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlBits32.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlBits32.cs deleted file mode 100644 index 2bf012e441..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlBits32.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using System.Runtime.Intrinsics; -using System.Runtime.Intrinsics.Arm; -using static SharpImmintrin.CIntrinsicArmAdvSimd; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd; - -/// -/// Contains a 32-bit pair of code lengths and code values, useful -/// for Huffman encoding. -/// -internal struct FjxlBits32(Vector counts, Vector bits) -{ - /// - /// Gets or sets the code lengths. - /// - public Vector Counts { get; set; } = counts; - - /// - /// Gets or sets the code values. - /// - public Vector Bits { get; set; } = bits; -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlBits64.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlBits64.cs deleted file mode 100644 index dca740cfbf..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlBits64.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using System.Runtime.CompilerServices; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd; - -/// -/// Contains a 64-bit pair of code lengths and code values, useful -/// for Huffman encoding. -/// -internal struct FjxlBits64(Vector counts, Vector bits) -{ - public static int Lanes { get; } = Vector.Count; - - /// - /// Gets or sets the code lengths. - /// - public Vector Counts { get; set; } = counts; - - /// - /// Gets or sets the code values. - /// - public Vector Bits { get; set; } = bits; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly void Store(ref ulong nbitsOut, ref ulong bitsOut) - { - this.Counts.StoreUnsafe(ref nbitsOut); - this.Bits.StoreUnsafe(ref bitsOut); - } -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlHuffman.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlHuffman.cs deleted file mode 100644 index 13c702f73f..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlHuffman.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd; - -internal static class FjxlHuffman -{ - public static void HuffmanSimdUpTo13(Span tokens, ReadOnlySpan rawNbitsSimd, ReadOnlySpan rawBitsSimd, Span nbitsOut, Span bitsOut) - { - Vector tok = FjxlSimdUtils.PrepareForU8Lookup(Vector.Create(tokens)); - - FjxlSimdUtils.U8Lookup(tok, rawNbitsSimd).CopyTo(nbitsOut); - FjxlSimdUtils.U8Lookup(tok, rawBitsSimd).CopyTo(bitsOut); - } - - public static void HuffmanSimd14(Span tokens, ReadOnlySpan rawNbitsSimd, ReadOnlySpan rawBitsSimd, Span nbitsOut, Span bitsOut) - { - Vector tokenCap = Vector.Create((short)15); - Vector tok = Vector.Create(tokens); - - Vector tokIndex = FjxlSimdUtils.PrepareForU8Lookup(Vector.Min(tok, tokenCap)); - - Vector huffBitsPre = FjxlSimdUtils.U8Lookup(tokIndex, rawBitsSimd); - - Vector needsHighBit = Vector.Equals(tok, Vector.Create((short)16)); - - Vector huffBits = Vector.ConditionalSelect( - needsHighBit, - huffBitsPre | Vector.Create((short)128), - huffBitsPre); - - huffBits.CopyTo(bitsOut); - - FjxlSimdUtils.U8Lookup(tokIndex, rawNbitsSimd) - .CopyTo(nbitsOut); - } - - public static void HuffmanSimdAbove14(Span tokens, ReadOnlySpan rawNbitsSimd, ReadOnlySpan rawBitsSimd, Span nbitsOut, Span bitsOut) - { - Vector tok = Vector.Create(tokens); - Vector above = Vector.GreaterThan(tok, Vector.Create((short)12)); - - Vector remapTok = Vector.ConditionalSelect( - above, - FjxlSimdUtils.HorizontalAdd(tok, Vector.Create((short)13)), - tok); - - Vector tokIndex = FjxlSimdUtils.PrepareForU8Lookup(remapTok); - Vector huffBitsPre = FjxlSimdUtils.U8Lookup(tokIndex, rawBitsSimd); - - Vector evenTok = tok & Vector.Create(unchecked((short)0xFFF)); - Vector needsHighBit = above & Vector.Equals(tok, evenTok); - - Vector huffBits = Vector.ConditionalSelect( - needsHighBit, - huffBitsPre | Vector.Create((short)128), - huffBitsPre); - - huffBits.CopyTo(bitsOut); - - FjxlSimdUtils.U8Lookup(tokIndex, rawNbitsSimd) - .CopyTo(nbitsOut); - } -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlRowFill.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlRowFill.cs deleted file mode 100644 index 2c7217befa..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlRowFill.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using System.Runtime.CompilerServices; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd; - -internal static class FjxlRowFill -{ - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void FillRowG8(Span rgba, int oxs, Span luma) - where T : unmanaged, INumber - { - int x = 0; - - for (; x + FjxlSimdVec16.Lanes <= oxs; x += FjxlSimdVec16.Lanes) - { - Vector rgb = FjxlSimdVec16.LoadG8(rgba[x..]); - StorePixels(rgb, luma[x..]); - } - } -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlSimdUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlSimdUtils.cs deleted file mode 100644 index 6cdaf7f57b..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlSimdUtils.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd; - -/// -/// SIMD utilities highly specific to the Fast Lossless JXL encoder. -/// -internal static class FjxlSimdUtils -{ - public static Vector HorizontalAdd(this Vector a, Vector b) => (a + b) >> 1; -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlSimdVec16.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlSimdVec16.cs deleted file mode 100644 index 9ee046eea0..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlSimdVec16.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd; - -/// -/// Utilities for 16-bit SIMD vectors. -/// -internal static class FjxlSimdVec16 -{ - public static int Lanes { get; } = Vector.Count; -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlStoreSimd.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlStoreSimd.cs deleted file mode 100644 index 03f78a92d8..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlStoreSimd.cs +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd; - -internal static class FjxlStoreSimd -{ - public static void StoreSimdUpTo8(Span nbitsTok, Span bitsTok, Span nbitsHuff, Span bitsHuff, int n, int skip, Span bitsOut) - { - FjxlBits16 bits = FjxlBits16.FromRaw( - Vector.Create(nbitsTok), - Vector.Create(bitsTok)); - - FjxlBits16 huffBits = FjxlBits16.FromRaw( - Vector.Create(nbitsHuff), - Vector.Create(bitsHuff)); - - bits.Interleave(huffBits); - bits.ClipTo(n); - bits.Skip(skip); - - bitsOut[0] = bits.Merge(); - } - - public static void StoreSimdUpTo14(Span nbitsTok, Span bitsTok, Span nbitsHuff, Span bitsHuff, int n, int skip, Span bitsOut) - { - VecPair> bits = Vector.Create(bitsTok).Interleave(Vector.Create(bitsHuff)); - VecPair> nbits = Vector.Create(nbitsTok).Interleave(Vector.Create(nbitsHuff)); - - Bits16 low = Bits16.FromRaw(nbits.Low, bits.Low); - Bits16 hi = Bits16.FromRaw(nbits.High, bits.High); - - int lanes = Vector.Count; - - low.ClipTo(2 * n); - low.Skip(2 * skip); - - hi.ClipTo(Math.Max(2 * n, lanes) - lanes); - hi.Skip(Math.Max(2 * skip, lanes) - lanes); - - bitsOut[0] = low.Merge(); - bitsOut[1] = hi.Merge(); - } - - public static void StoreSimdAbove14(Span nbitsTok, Span bitsTok, Span nbitsHuff, Span bitsHuff, int n, int skip, Span bitsOut) - { - Vector nbitsTokLo = Vector.Create(nbitsTok); - Vector bitsTokLo = Vector.Create(bitsTok); - - Vector nbitsTokHi = Vector.Create(nbitsTok[Vector.Count..]); - Vector bitsTokHi = Vector.Create(bitsTok[Vector.Count..]); - - Bits32 bitsLow = Bits32.FromRaw(nbitsTokLo, bitsTokLo); - Bits32 bitsHi = Bits32.FromRaw(nbitsTokHi, bitsTokHi); - - VecPair> huffBits = FjxlSimdUtils.Upcast(Vector.Create(bitsHuff)); - VecPair> huffNbits = FjxlSimdUtils.Upcast(Vector.Create(nbitsHuff)); - - Bits32 huffLow = Bits32.FromRaw(huffNbits.Low, huffBits.Low); - Bits32 huffHi = Bits32.FromRaw(huffNbits.High, huffBits.High); - - bitsLow.Interleave(huffLow); - bitsLow.ClipTo(n); - bitsLow.Skip(skip); - bitsOut[0] = bitsLow; - - int lanes = Vector.Count; - - bitsHi.Interleave(huffHi); - bitsHi.ClipTo(Math.Max(n, lanes) - lanes); - bitsHi.Skip(Math.Max(skip, lanes) - lanes); - bitsOut[1] = bitsHi; - } - - public static void StoreToWriter(ReadOnlySpan bits, FjxlBitWriter output, int n) - { - Span nbits64 = stackalloc ulong[Bits64.Lanes * n]; - Span bits64 = stackalloc ulong[Bits64.Lanes * n]; - - bits[0].Merge().Store(nbits64, bits64); - - if (n > 1) - { - bits[1].Merge().Store(nbits64[Bits64.Lanes..], bits64[Bits64.Lanes..]); - } - - if (n > 2) - { - bits[2].Merge().Store(nbits64[(2 * Bits64.Lanes)..], bits64[(2 * Bits64.Lanes)..]); - } - - if (n > 3) - { - bits[3].Merge().Store(nbits64[(3 * Bits64.Lanes)..], bits64[(3 * Bits64.Lanes)..]); - } - - output.WriteMultiple(nbits64, bits64, Bits64.Lanes * n); - } -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlTokenization.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlTokenization.cs deleted file mode 100644 index aed8a20576..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlTokenization.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd; - -internal static class FjxlTokenization -{ - public static void Tokenize16(Span residuals, Span output, Span nbitsOut, Span bitsOut) - { - Vector res = Vector.Create(residuals); - Vector token = FjxlSimdUtils.ValueToToken(res); - Vector nbits = FjxlSimdUtils.SaturateSubtract(token, Vector.One); - Vector bits = FjxlSimdUtils.SaturateSubtract(res, FjxlSimdUtils.Pow2(nbits)); - - token.CopyTo(output); - nbits.CopyTo(nbitsOut); - bits.CopyTo(bitsOut); - } - - public static void Tokenize32(Span residuals, Span tokenOut, Span nbitsOut, Span bitsOut) - { - Vector resLo = Vector.Create(residuals); - Vector resHi = Vector.Create(residuals[Vector.Count..]); - - Vector tokenLo = FjxlSimdUtils.ValueToToken(resLo); - Vector tokenHi = FjxlSimdUtils.ValueToToken(resHi); - - Vector nbitsLo = FjxlSimdUtils.SaturateSubtract(tokenLo, Vector.One); - Vector nbitsHi = FjxlSimdUtils.SaturateSubtract(tokenHi, Vector.One); - - Vector bitsLo = FjxlSimdUtils.SaturateSubtract(resLo, FjxlSimdUtils.Pow2(nbitsLo)); - Vector bitsHi = FjxlSimdUtils.SaturateSubtract(resHi, FjxlSimdUtils.Pow2(nbitsHi)); - - Vector token = FjxlSimdUtils.FromTwo32(tokenLo, tokenHi); - token.CopyTo(tokenOut); - - nbitsLo.CopyTo(nbitsOut); - nbitsHi.CopyTo(nbitsOut[Vector.Count..]); - - bitsLo.CopyTo(bitsOut); - bitsHi.CopyTo(bitsOut[Vector.Count..]); - } -} From 58c68d043f344994631059f089ad4f6ca715a354 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 16 Sep 2026 14:21:24 +0400 Subject: [PATCH 182/328] Revert "Delete fast lossless codec" This reverts commit 0a04b34024f1c52a4226ccd493952b33c34d9920. --- .../Encoder/FastLossless/FjxlBitWriter.cs | 88 ++ .../Encoder/FastLossless/FjxlPrefixCode.cs | 462 +++++++++++ .../FastLossless/JxlFastLosslessEncoder.cs | 758 ++++++++++++++++++ .../FjxlChunkEncoderExactly14Bits.cs | 64 ++ .../FjxlChunkEncoderFrom9To13Bits.cs | 72 ++ .../FjxlChunkEncoderMoreThan14Bits.cs | 81 ++ .../FjxlChunkEncoderUpTo8Bits.cs | 69 ++ .../Encoder/FastLossless/Simd/FjxlBits32.cs | 26 + .../Encoder/FastLossless/Simd/FjxlBits64.cs | 33 + .../Encoder/FastLossless/Simd/FjxlHuffman.cs | 66 ++ .../Encoder/FastLossless/Simd/FjxlRowFill.cs | 23 + .../FastLossless/Simd/FjxlSimdUtils.cs | 14 + .../FastLossless/Simd/FjxlSimdVec16.cs | 14 + .../FastLossless/Simd/FjxlStoreSimd.cs | 101 +++ .../FastLossless/Simd/FjxlTokenization.cs | 45 ++ 15 files changed, 1916 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/FjxlBitWriter.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/FjxlPrefixCode.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/JxlFastLosslessEncoder.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderExactly14Bits.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderFrom9To13Bits.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderMoreThan14Bits.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderUpTo8Bits.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlBits32.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlBits64.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlHuffman.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlRowFill.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlSimdUtils.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlSimdVec16.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlStoreSimd.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlTokenization.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/FjxlBitWriter.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/FjxlBitWriter.cs new file mode 100644 index 0000000000..54277d41a9 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/FjxlBitWriter.cs @@ -0,0 +1,88 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless; + +/// +/// Simple MSB-first bit-stream writer implementation built on top +/// of a stream. +/// +/// Output bytes are written here. +internal sealed class FjxlBitWriter(Stream stream) : IDisposable +{ + /// + /// Temporary cache used to store pending written bits + /// before they're written to the output stream. + /// + private ulong buffer; + + /// + /// Gets the total number of bytes written to the output buffer so far. + /// + public long BytesWritten { get; private set; } + + /// + /// Gets the number of bits actively in the bit cache. + /// This is used to track how many bits were written into + /// the cache prior to sending the cache to the stream. + /// + public int BitsInBuffer { get; private set; } + + /// + /// Writes the specified bits in the Most Significant Byte (MSB) + /// order. + /// + /// Represents the number of bits to write to the bit-stream. + /// Represents the value to write to the bit-stream. + public void Write(int count, ulong bits) + { + DebugGuard.MustBeBetweenOrEqualTo(count, 0, 56, nameof(count)); + + if (count < 64) + { + bits &= (1UL << count) - 1; + } + + this.buffer |= bits << this.BitsInBuffer; + this.BitsInBuffer += count; + + this.FlushBytes(); + } + + /// + /// Internal method used to flush bytes from the cache + /// () into the output stream. + /// + private void FlushBytes() + { + int bytes = this.BitsInBuffer / 8; + + for (int i = 0; i < bytes; i++) + { + stream.WriteByte((byte)this.buffer); + this.BytesWritten++; + this.buffer >>= 8; + } + + this.BitsInBuffer -= bytes * 8; + } + + /// + /// Used by the dispose method to flush the remaining bits + /// that are not byte-aligned. F.e. if we dispose this reader + /// and we have 5 bits left, those final 5 bits are set to all 0 + /// and the byte is written to the stream. + /// + public void ZeroPadToByte() + { + if (this.BitsInBuffer != 0) + { + this.Write(8 - this.BitsInBuffer, 0); + } + } + + /// + /// Flushes out the final bytes. + /// + public void Dispose() => this.ZeroPadToByte(); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/FjxlPrefixCode.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/FjxlPrefixCode.cs new file mode 100644 index 0000000000..f9b9c821b4 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/FjxlPrefixCode.cs @@ -0,0 +1,462 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless; + +/// +/// The prefix code is used for encoding LZ77-compressed coefficients. +/// +internal sealed class FjxlPrefixCode +{ +#pragma warning disable SA1401 // Fields should be private + + /// + /// Maximum number of raw symbols for prefix coding. + /// + private const int MaxNumSymbols = JxlFastLosslessEncoder.NumRawSymbols + 1 < JxlFastLosslessEncoder.NumLz77 ? JxlFastLosslessEncoder.NumLz77 : JxlFastLosslessEncoder.NumRawSymbols + 1; + + /// + /// Gets or sets the Huffman raw bit lengths. + /// + public InlineArray19 RawLengths; + + /// + /// Gets or sets the Huffman raw code values. + /// + public InlineArray19 RawCodes; + + /// + /// Gets or sets the Huffman LZ77 bit lengths. + /// + public InlineArray33 Lz77Lengths; + + /// + /// Gets or sets the Huffman LZ77 code values. + /// + public InlineArray33 Lz77Codes; + + /// + /// Gets or sets the Huffman LZ77 cache code values. + /// + public InlineArray32 Lz77CacheBits; + + /// + /// Gets or sets the Huffman LZ77 cache bit lengths. + /// + public InlineArray32 Lz77CacheLengths; + + public FjxlPrefixCode(Span rawCounts, Span lz77Counts) + { + Span level1Counts = stackalloc ulong[JxlFastLosslessEncoder.NumRawSymbols + 1]; + rawCounts[..JxlFastLosslessEncoder.NumRawSymbols].CopyTo(level1Counts); + + this.RawCount = JxlFastLosslessEncoder.NumRawSymbols; + + while (this.RawCount > 0 && level1Counts[this.RawCount - 1] == 0) + { + this.RawCount--; + } + + level1Counts[this.RawCount] = 0; + + for (int i = 0; i < JxlFastLosslessEncoder.NumLz77; i++) + { + level1Counts[this.RawCount] += lz77Counts[i]; + } + + Span level1Lengths = stackalloc byte[JxlFastLosslessEncoder.NumRawSymbols + 1]; + level1Lengths.Clear(); + + ComputeCodeLengths(level1Counts, this.RawCount + 1, JxlFastLosslessEncoder.MinimumRawLength, JxlFastLosslessEncoder.MaximumRawLength, level1Lengths); + + Span level2Lengths = stackalloc byte[JxlFastLosslessEncoder.NumLz77]; + Span minLengths = stackalloc byte[JxlFastLosslessEncoder.NumLz77]; + + level2Lengths.Clear(); + minLengths.Clear(); + + int l = 15 - level1Lengths[this.RawCount]; + Span maxLengths = stackalloc byte[JxlFastLosslessEncoder.NumLz77]; + maxLengths.Fill((byte)l); + + int numLz77 = JxlFastLosslessEncoder.NumLz77; + while (numLz77 > 0 && lz77Counts[numLz77 - 1] == 0) + { + numLz77--; + } + + ComputeCodeLengths(lz77Counts, numLz77, minLengths, maxLengths, level2Lengths); + + level1Lengths[..this.RawCount].CopyTo(this.RawLengths); + + for (int i = 0; i < numLz77; i++) + { + this.Lz77Lengths[i] = (byte)(level2Lengths[i] != 0 ? level1Lengths[this.RawCount] + level2Lengths[i] : 0); + } + + ComputeCanonicalCode(this.RawLengths, this.RawCodes, this.Lz77Lengths, this.Lz77Codes); + + // Prepare the LZ77 cache + for (int count = 0; count < JxlFastLosslessEncoder.Lz77CacheSize; count++) + { + EncodeHybridUintLz77(count, out int token, out int nbits, out int bits); + this.Lz77CacheLengths[count] = (byte)(this.Lz77Lengths[token] + nbits + this.RawLengths[0]); + this.Lz77CacheBits[count] = + (ulong)((((bits << this.Lz77Lengths[token]) | this.Lz77Codes[token]) << this.RawLengths[0]) | + this.RawLengths[0]); + } + } + + /// + /// Gets a lookup used to reverse integers bit-wise. + /// + private static ReadOnlySpan ReverseNibbleLookup => + [ + 0b0000, 0b1000, 0b0100, 0b1100, 0b0010, 0b1010, 0b0110, 0b1110, + 0b0001, 0b1001, 0b0101, 0b1101, 0b0011, 0b1011, 0b0111, 0b1111, + ]; + +#pragma warning restore SA1401 // Fields should be private + + /// + /// Gets or sets the number of raw codes. + /// + public int RawCount { get; set; } + + /// + /// Reverses the integer bit-wise. + /// + /// Number of bits for the integer. + /// Actual bits to reverse. + /// + /// Input integer but reversed. F.e. 10010 becomes 01001. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ushort BitReverse(int nbits, ushort bits) + { + unchecked + { + ushort rev16 = (ushort)((ReverseNibbleLookup[bits & 0xF] << 12) | + (ReverseNibbleLookup[(bits >> 4) & 0xF] << 8) | + (ReverseNibbleLookup[(bits >> 8) & 0xF] << 4) | + ReverseNibbleLookup[bits >> 12]); + return (ushort)(rev16 >> (16 - nbits)); + } + } + + private static void ComputeCanonicalCode(Span firstChunkLengths, Span firstChunkCodes, Span secondChunkLengths, Span secondChunkCodes) + { + const int maxCodeLength = 15; + + Span codeLengthCounts = stackalloc byte[maxCodeLength + 1]; + codeLengthCounts.Clear(); + + for (int i = 0; i < firstChunkCodes.Length; i++) + { + codeLengthCounts[firstChunkLengths[i]]++; + + if (firstChunkLengths[i] > 8) + { + throw new InvalidOperationException("First chunk length is too large"); + } + + if (firstChunkLengths[i] <= 0) + { + throw new InvalidOperationException("First chunk length cannot be <= 0"); + } + } + + for (int i = 0; i < secondChunkCodes.Length; i++) + { + codeLengthCounts[secondChunkLengths[i]]++; + + if (secondChunkLengths[i] > maxCodeLength) + { + throw new InvalidOperationException("Second chunk length is too large"); + } + } + + Span nextCode = stackalloc ushort[maxCodeLength + 1]; + nextCode.Clear(); + + ushort code = 0; + + for (int i = 1; i < maxCodeLength + 1; i++) + { + code = unchecked((ushort)((code + codeLengthCounts[i - 1]) << 1)); + nextCode[i] = code; + } + + unchecked + { + for (int i = 0; i < firstChunkCodes.Length; i++) + { + firstChunkCodes[i] = (byte)BitReverse(firstChunkLengths[i], nextCode[firstChunkLengths[i]]++); + } + + for (int i = 0; i < secondChunkCodes.Length; i++) + { + secondChunkCodes[i] = (byte)BitReverse(secondChunkLengths[i], nextCode[secondChunkLengths[i]]++); + } + } + } + + private static void ComputeCodeLengthsNonZeroImpl( + Span freqs, + int n, + int precision, + T infty, + Span minLimit, + Span maxLimit, + Span nbits) + where T : unmanaged, INumber + { + DebugGuard.MustBeLessThan(precision, 15, nameof(precision)); + DebugGuard.MustBeLessThanOrEqualTo(n, MaxNumSymbols, nameof(n)); + + int scale = 1 << precision; + int width = scale + 1; + + Span dynp = stackalloc T[width * (n + 1)]; + dynp.Fill(infty); + dynp[0] = T.Zero; + + for (int sym = 0; sym < n; sym++) + { + for (int bits = minLimit[sym]; bits <= maxLimit[sym]; bits++) + { + int offsetDelta = 1 << (precision - bits); + T cost = T.CreateChecked(freqs[sym]) * T.CreateChecked(bits); + + for (int off = 0; off + offsetDelta <= scale; off++) + { + int current = (sym * width) + off; + int next = ((sym + 1) * width) + off + offsetDelta; + + dynp[next] = T.Min(dynp[current] + cost, dynp[next]); + } + } + } + + int offFinal = scale; + + for (int sym = n - 1; sym >= 0; sym--) + { + if (offFinal <= 0) + { + throw new InvalidOperationException("Offset should be greater than zero"); + } + + for (int bits = minLimit[sym]; bits <= maxLimit[sym]; bits++) + { + int offsetDelta = 1 << (precision - bits); + + if (offsetDelta <= offFinal) + { + int current = (sym * width) + offFinal; + int previous = (sym * width) + offFinal - offsetDelta; + + T cost = T.CreateChecked(freqs[sym]) * T.CreateChecked(bits); + + if (dynp[current] == dynp[previous] + cost) + { + offFinal -= offsetDelta; + nbits[sym] = (byte)bits; + break; + } + } + } + } + } + + private static void ComputeCodeLengthsNonZero(Span freqs, int n, Span minLimit, Span maxLimit, Span nbits) + { + int precision = 0; + int shortestLength = 255; + ulong frequencySum = 0; + + for (int i = 0; i < n; i++) + { + frequencySum += freqs[i]; + + if (minLimit[i] < 1) + { + minLimit[i] = 1; + } + + precision = Math.Max(maxLimit[i], precision); + shortestLength = Math.Min(minLimit[i], shortestLength); + } + + precision -= shortestLength - 1; + ulong infinity = frequencySum * (ulong)precision; + + if (infinity < uint.MaxValue / 2) + { + ComputeCodeLengthsNonZeroImpl(freqs, n, precision, (uint)infinity, minLimit, maxLimit, nbits); + } + else + { + ComputeCodeLengthsNonZeroImpl(freqs, n, precision, infinity, minLimit, maxLimit, nbits); + } + } + + private static void ComputeCodeLengths(Span freqs, int n, ReadOnlySpan minLimitIn, ReadOnlySpan maxLimitIn, Span nbits) + { + DebugGuard.MustBeLessThanOrEqualTo(n, MaxNumSymbols, nameof(n)); + + Span compactFreqs = stackalloc ulong[MaxNumSymbols]; + Span minLimit = stackalloc byte[MaxNumSymbols]; + Span maxLimit = stackalloc byte[MaxNumSymbols]; + + int ni = 0; + for (int i = 0; i < n; i++) + { + if (freqs[i] != 0) + { + compactFreqs[ni] = freqs[i]; + minLimit[ni] = minLimitIn[i]; + maxLimit[ni] = maxLimitIn[i]; + ni++; + } + } + + compactFreqs[ni..].Clear(); + minLimit[ni..].Clear(); + maxLimit[ni..].Clear(); + + Span numBits = stackalloc byte[MaxNumSymbols]; + numBits.Clear(); + + ComputeCodeLengthsNonZero(compactFreqs, ni, minLimit, maxLimit, numBits); + + ni = 0; + + for (int i = 0; i < n; i++) + { + nbits[i] = 0; + if (freqs[i] != 0) + { + nbits[i] = numBits[ni++]; + } + } + } + + /// + /// Writes this LZ77 prefix code into the bit-stream. + /// + /// The bit-stream to write the prefix code into. + public void Write(FjxlBitWriter writer) + { + Span codeLengthCounts = stackalloc ulong[32].Slice(0, 18); + codeLengthCounts.Clear(); + codeLengthCounts[17] = 3 + (2 * (JxlFastLosslessEncoder.NumLz77 - 1)); + + for (int i = 0; i < 19; i++) + { + byte rawLength = this.RawLengths[i]; + + codeLengthCounts[rawLength]++; + } + + for (int i = 0; i < 33; i++) + { + byte lz77Length = this.Lz77Lengths[i]; + + codeLengthCounts[lz77Length]++; + } + + // Lengths for representing the code length + Span codeLengthsBuffer = stackalloc byte[64]; // nearest power of 2 alignment for 18*3 = 64 + Span codeLengthLengths = codeLengthsBuffer.Slice(0, 18); + Span codeLengthLengthsMinimum = codeLengthsBuffer.Slice(18, 18); + Span codeLengthLengthsMaximum = codeLengthsBuffer.Slice(18 * 2, 18); + + codeLengthLengths.Clear(); + codeLengthLengthsMinimum.Clear(); + codeLengthLengthsMaximum.Fill(5); + + ComputeCodeLengths(codeLengthCounts, 18, codeLengthLengthsMinimum, codeLengthLengthsMaximum, codeLengthLengths); + + writer.Write(2, 0b00); // HSKIP = 0 (Don't skip code lengths) + + // As per Brotli RFC + Span codeLengthOrder = [1, 2, 3, 4, 0, 5, 17, 6, 16, + 7, 8, 9, 10, 11, 12, 13, 14, 15]; + + // Lengths & codes for representing lengths of code lengths + Span codeLengthLengthLengths = [2, 4, 3, 2, 2, 4]; + Span codeLengthLengthCodes = [0, 7, 3, 2, 1, 15]; + + // Maximum number of code lengths + int numCodeLengths = 18; + while (codeLengthLengths[codeLengthOrder[numCodeLengths - 1]] == 0) + { + numCodeLengths--; + } + + // Max bits written in this loop: 18 * 4 = 72 + for (int i = 0; i < numCodeLengths; i++) + { + int symbol = codeLengthLengths[codeLengthOrder[i]]; + writer.Write(codeLengthLengthLengths[symbol], codeLengthLengthCodes[symbol]); + } + + Span codeLengthBits = stackalloc ushort[32].Slice(0, 18); + codeLengthBits.Clear(); + ComputeCanonicalCode([], [], codeLengthLengths, codeLengthBits); + + for (int i = 0; i < 19; i++) + { + byte rawLength = this.RawLengths[i]; + + writer.Write(codeLengthLengths[rawLength], codeLengthBits[rawLength]); + } + + int numLz77 = JxlFastLosslessEncoder.NumLz77; + while (this.Lz77Lengths[numLz77 - 1] == 0) + { + numLz77--; + } + + // Max bits in this block: 24 + writer.Write(codeLengthLengths[17], codeLengthBits[17]); + writer.Write(3, 0b010); // 5 + writer.Write(codeLengthLengths[17], codeLengthBits[17]); + writer.Write(3, 0b000); // (5 - 2) * 8 + 3 = 27 + writer.Write(codeLengthLengths[17], codeLengthBits[17]); + writer.Write(3, 0b010); // (27 - 2) * 8 + 5 = 205 + + // Encode LZ77 symbols with values 224 + i. + // Max. bits in this loop: 33 * 5 = 165 + for (int i = 0; i < numLz77; i++) + { + writer.Write(codeLengthLengths[this.Lz77Lengths[i]], codeLengthBits[this.Lz77Lengths[i]]); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void EncodeHybridUintLz77(int value, out int token, out int nBits, out int bits) + { + unchecked + { + int n = (int)JxlFastLosslessEncoder.FloorLog2((uint)value); + + if (value < 16) + { + token = value; + nBits = 0; + bits = 0; + } + else + { + token = 16 + n - 4; + nBits = n; + bits = value - (1 << n); + } + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/JxlFastLosslessEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/JxlFastLosslessEncoder.cs new file mode 100644 index 0000000000..e7ddf796b6 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/JxlFastLosslessEncoder.cs @@ -0,0 +1,758 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd; + +// Suppress IDE0057. This is so we can stack-allocate +// a powers of 2 and then slice it to the appropriate +// length (which produces better code). +// +// Without this suppression, the analyzer produces a warning, +// recommending changing this: +// stackalloc ulong[32].Slice(0, 18) +// to: +// (stackalloc ulong[32])[..18] +// +// But then the analyzer produces a new warning, recommending +// to remove the paranthesis, changing this: +// (stackalloc ulong[32])[..18] +// to: +// stackalloc ulong[32][..18] +// +// which is invalid C# syntax. +#pragma warning disable IDE0057 // Use range operator + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless; + +/// +/// Extreme performance JPEG XL encoder which provides minimal lossless compression. +/// It also uses minimal dependencies. +/// +internal sealed class JxlFastLosslessEncoder +{ + /// + /// Specifies maximum number of bytes a frame header may use. + /// + public const int MaxFrameHeaderSize = 5; + + public const int NumRawSymbols = 19; + + public const int NumLz77 = 33; + + /// + /// Cache/dictionary size for LZ77 + /// + public const int Lz77CacheSize = 32; + + public const int Lz77Offset = 224; // ⚠️ This must always be 224. Don't change this constant. Changing this will break compression. + + public const int Lz77MinLength = 7; + + private static readonly int LogChunkSize = + JxlSimdTarget.Using512BitVectors + ? 5 + : JxlSimdTarget.Using256BitVectors + ? 4 + : 3; + + /// + /// Input frame data is stored here. + /// + private readonly FjxlFrameInputSource input; + + /// + /// Image width of the input image. + /// + private readonly int width; + + /// + /// Image height of the input image. + /// + private readonly int height; + + /// + /// Image width in groups. + /// + private readonly int numGroupsX; + + /// + /// Image height in groups. + /// + private readonly int numGroupsY; + + /// + /// Image width in groups (DC). + /// + private readonly int numDcGroupsX; + + /// + /// Image height in groups (DC). + /// + private readonly int numDcGroupsY; + + /// + /// Number of channels. (f.e. RGBA is 4, YUV is 3) + /// + private readonly int channels; + + /// + /// Number of bits represented per pixel. (f.e. 8 means pixels + /// have a 0-255 range) + /// + /// + /// Higher bit depths can represent more colors. + /// + private readonly int bitDepth; + + /// + /// Should the output image be stored in big-endian order? + /// + private readonly bool isBigEndian; + + private readonly int effort; + + private readonly bool collided; + + /// + /// Prefix codes for LZ77. + /// + private InlineArray4 hcode; + + private readonly List lookup = []; + + /// + /// Bit writer to write the JPEG XL headers. + /// + private readonly FjxlBitWriter header; + + /// + /// Bit writers for writing JPEG XL groups. + /// + private readonly List> groupData = []; + + /// + /// Sizes for each group. + /// + private readonly List groupSizes = []; + + private int acGroupDataOffset; + + private int minDcGlobalSize; + + private int currentBitWriter; + + private int bitWriterBytePos; + + private int bitsInBuffer; + + private long bitBuffer; + + private bool processDone; + + /// + /// Abstracts access to a raster frame data required for encoding. + /// + internal abstract class FjxlFrameInputSource : IDisposable + { + /// + public abstract void Dispose(); + + /// + /// Returns a span that wraps over channel color data at the + /// specified rectangular position. + /// + /// Target type of the color data. + /// Left offset + /// Right offset + /// Selection width + /// Selection height + /// The actual offset of the row in row-major order is stored here. + /// + /// A wrapper over the color data of the channel at the specified + /// position. + /// + public abstract Span GetColorChannelData(int x, int y, int width, int height, out long rowOffset) + where T : unmanaged; + } + + /// + /// Gets minimum raw lengths for prefix coding. + /// + public static ReadOnlySpan MinimumRawLength => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + + /// + /// Gets maximum raw lengths for prefix coding. + /// + public static ReadOnlySpan MaximumRawLength => [7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 10]; + + /// + /// Gets a lookup used by the method + /// to translate a bucket into a base group size. + /// + private static ReadOnlySpan GroupSizeOffset => + [ + 0, + 1024, + 17408, + 4211712 + ]; + + /// + /// Gets a lookup to determine how many bits a TOC bucket uses. + /// + private static ReadOnlySpan TocBits => [12, 16, 24, 32]; + + /// + /// Approximates Floor(Log2(v)) using integers. + /// + /// Value to retrieve Floor(Log2(v)) of. + /// Floor of second logarithm of v, or 31 if v is equal to 0. + /// This method may use CPU intrinsics provided by the .NET Runtime (e.g. BMI1 on x86). + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint FloorLog2(uint v) => v == 0 ? 0 : 31u - (uint)BitOperations.LeadingZeroCount(v); + + /// + /// Approximates count trailing zeros of v using integers. + /// + /// Value to retrieve number of 0 bits after last 1 bit of. + /// After the least significant 1 bit, returns the number of 0 bits. E.g. 1000 1000 00 -> 5. + /// This method may use CPU intrinsics provided by the .NET Runtime (e.g. BMI1 on x86). + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint CtzNonZero(ulong v) => (uint)BitOperations.TrailingZeroCount(v); + + /// + /// Returns a TOC bucket based on the group size. + /// + /// Specified group size. + /// TOC bucket matching the appropriate group size. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int TocBucket(int groupSize) + { + int bucket = 0; + + while (bucket < 3 && groupSize >= GroupSizeOffset[bucket + 1]) + { + bucket++; + } + + return bucket; + } + + /// + /// Returns the total number of bits required to represent + /// all given group sizes in the TOC. + /// + /// Group sizes to calculate bit sizes of. + /// Accumulated number of bits required to represent each group size. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int TocSize(Span groupSizes) + { + int tocBits = 0; + + ref int unsafeRef = ref MemoryMarshal.GetReference(groupSizes); + + for (int i = 0; i < groupSizes.Length; i++) + { + // TODO: we can try using AVX2 gather intrinsics, + // especially because TocBits can absolutely fit + // in the L1 cache + int groupSize = Unsafe.Add(ref unsafeRef, i); + int bucketForGroupSize = TocBucket(groupSize); + int bitsUsedByBucket = TocBits[bucketForGroupSize]; + + tocBits += bitsUsedByBucket; + } + + return tocBits; + } + + /// + /// Returns the number of bytes for the frame header. + /// + /// Indicates presence of the alpha channel. + /// Indicates whether this is the final frame. + /// Frame header size in bytes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int FrameHeaderSize(bool containsAlpha, bool isLast) + { + // Original code (from libjxl): + // + // size_t nbits = 28 + (have_alpha ? 4 : 0) + (is_last ? 0 : 2); + // return (nbits + 7) / 8; + // + // In this implementation we just use constants to shave a few CPU cycles. + // The total amount of branches is reduced by one (for the !containsAlpha case), + // but we remove the arithmetic/shifting instructions. + unchecked + { + if (containsAlpha) + { + if (isLast) + { + return 5; // (34 + 7) / 8 + } + else + { + return 4; // (32 + 7) / 8 + } + } + else + { + return 4; // (30 + 7) / 8 AND (28 + 7) / 8 yield the same result + } + } + } + + private static long GetSectionSize(InlineArray4 groupData) + { + long size = 0; + + for (int j = 0; j < 4; j++) + { + FjxlBitWriter writer = groupData[j]; + + size += (writer.BytesWritten * 8) + writer.BitsInBuffer; + } + + return (size + 7) / 8; + } + + /// + /// Approximates number of bytes needed for the output image buffer. + /// + /// Bytes for the frame buffer. + private long GetOutputSize() + { + long totalSizeGroups = 0; + + Span> groups = CollectionsMarshal.AsSpan(this.groupData); + + for (int i = 0; i < groups.Length; i++) + { + InlineArray4 section = groups[i]; + + totalSizeGroups += GetSectionSize(section); + } + + return this.header.BytesWritten + totalSizeGroups; + } + + /// + /// Returns the maximum amount of bytes potentially required for the image buffer. + /// + /// Upper bound of bytes for frame buffer. + private long GetMaxRequiredOutput() => this.GetOutputSize() + 32; + + private void WriteHeader(bool addImageHeader, bool isLast) + { + FjxlBitWriter output = this.header; + bool haveAlpha = this.channels is 2 or 4; + + if (addImageHeader) + { + // File signature. This signature specifies + // a raw codestream. No container format here. + output.Write(16, 0x0AFF); + + // Handcrafted size header. + output.Write(1, 0); // Not small + + WriteSize(this.height); + output.Write(3, 0b000); // No special ratio + WriteSize(this.width); + + // Handcrafted image metadata + output.Write(1, 0); // all_default = 0 (don't assume values to be set to their defaults) + output.Write(1, 0); // extra_fields = 0 (extra fields are disabled and therefore not present) + output.Write(1, 0); // bit_depth.floating_point_sample = 0 (samples are integers) + + if (this.bitDepth == 8) + { + output.Write(2, 0b00); // bit_depth.bits_per_sample = 8 (predefined bit depth of 8 bits) + } + else if (this.bitDepth == 10) + { + output.Write(2, 0b01); // bit_depth.bits_per_sample = 10 (predefined bit depth of 10 bits) + } + else if (this.bitDepth == 12) + { + output.Write(2, 0b10); // bit_depth.bits_per_sample = 12 (predefined bit depth of 12 bits) + } + else + { + output.Write(2, 0b11); // Custom bit depth + output.Write(6, (ulong)this.bitDepth - 1); // bit depth minus 1 (so 0 becomes 1, 9 becomes 10, etc) + } + + if (this.bitDepth <= 14) + { + output.Write(1, 1); // 16-bit-buffer is sufficient + } + else + { + output.Write(1, 0); // 16-bit-buffer is NOT sufficient + } + + if (haveAlpha) + { + output.Write(2, 0b01); // Emit one extra channel (the alpha channel) + + if (this.bitDepth == 8) + { + output.Write(1, 1); // all_default = 1 (8-bit alpha is the default) + } + else + { + output.Write(1, 0); // all_default = 0 + output.Write(2, 0); // type = alpha + output.Write(1, 0); // samples are not floating point + + if (this.bitDepth == 10) + { + output.Write(2, 0b01); // bit_depth.bits_per_sample = 10 (predefined bit depth of 10 bits) + } + else if (this.bitDepth == 12) + { + output.Write(2, 0b10); // bit_depth.bits_per_sample = 12 (predefined bit depth of 12 bits) + } + else + { + output.Write(2, 0b11); // Custom bit depth + output.Write(6, (ulong)this.bitDepth - 1); // bit depth minus 1 (so 0 becomes 1, 9 becomes 10, etc) + } + + output.Write(2, 0); // dim_shift = 0 + output.Write(2, 0); // name_len = 0 + output.Write(1, 0); // alpha_associated = 0 + } + } + else + { + output.Write(2, 0b00); // 0 extra channels + } + + output.Write(1, 0); // not XYB + + if (this.channels > 2) + { + output.Write(1, 1); // color_encoding.all_default = 1 (sRGB) + } + else + { + output.Write(1, 0); // color_encoding.all_default = 0 + output.Write(1, 0); // color_encoding.want_icc = 0 + output.Write(2, 0b01); // Grayscale + output.Write(2, 0b01); // D65 + output.Write(1, 0); // No gamma transfer function + output.Write(2, 0b10); // transfer function: 2 + u(4) + output.Write(4, 11); // transfer function (specifies sRGB) + output.Write(2, 1); // relative rendering intent + } + + output.Write(2, 0b00); // No extensions + output.Write(1, 1); // all_default transform data + output.ZeroPadToByte(); // No ICC and no preview. Frame should start at byte boundary. + } + + // Handcrafted frame header + output.Write(1, 0); // all_default = 0 (non-default values) + output.Write(2, 0b00); // regular frame + output.Write(1, 1); // modular + output.Write(2, 0b00); // default flags + output.Write(1, 0); // not Y'Cb'Cr + output.Write(2, 0b00); // no upsampling + + if (haveAlpha) + { + output.Write(2, 0b00); // no alpha upsampling + } + + output.Write(2, 0b01); // default group size + output.Write(2, 0b00); // exactly one pass + output.Write(1, 0); // no custom size or origin + output.Write(2, 0b00); // Replace blending mode + + if (haveAlpha) + { + output.Write(2, 0b00); // Replace blending mode for alpha channel + } + + output.Write(2, 0b00); // a frame has no name + output.Write(1, 0); // loop filter is not all_default + output.Write(1, 0); // no Gaborish transform + output.Write(2, 0b00); // 0 EPF filters + output.Write(2, 0b00); // no LF extensions + output.Write(2, 0b00); // no FH extensions + + output.Write(1, 0); // no TOC permutation + output.ZeroPadToByte(); // TOC is byte aligned + + Span groupSizes = CollectionsMarshal.AsSpan(this.groupSizes); + + for (int i = 0; i < groupSizes.Length; i++) + { + int groupSize = groupSizes[i]; + + int bucket = TocBucket(groupSize); + output.Write(2, (ulong)bucket); + output.Write(TocBits[bucket] - 2, (ulong)(groupSize - GroupSizeOffset[bucket])); + } + + output.ZeroPadToByte(); // Groups are byte-aligned + + // Sizes are coded using a special variable-length + // kind of coding. This method does that here. + // + // It has a prefix of 2 bits, followed by the suffix of N + // bits which depend on the prefix: + // + // prefix 0b00: 9 consecutive bits + // prefix 0b01: 13 consecutive bits + // prefix 0b10: 18 consecutive bits + // prefix 0b11: 30 consecutive bits + void WriteSize(int size) + { + ulong sizeMinus1 = (ulong)size - 1uL; + + if (sizeMinus1 < (1 << 9)) + { + output.Write(2, 0b00); // 9 bits + output.Write(9, sizeMinus1); + } + else if (sizeMinus1 < (1 << 13)) + { + output.Write(2, 0b01); // 13 bits + output.Write(13, sizeMinus1); + } + else if (sizeMinus1 < (1 << 18)) + { + output.Write(2, 0b10); // 18 bits + output.Write(18, sizeMinus1); + } + else + { + output.Write(2, 0b11); // 30 bits + output.Write(30, sizeMinus1); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int ComputeDcGlobalPadding(Span groupSizes, int acGroupDataOffset, int minDcGlobalSize, bool containsAlpha, bool isLast) + { + // Libjxl reference implements this method like this: + /* + size_t ComputeDcGlobalPadding(const std::vector& group_sizes, + size_t ac_group_data_offset, + size_t min_dc_global_size, bool have_alpha, + bool is_last) { + std::vector new_group_sizes = group_sizes; + new_group_sizes[0] = min_dc_global_size; + size_t toc_size = TOCSize(new_group_sizes); + size_t actual_offset = + FrameHeaderSize(have_alpha, is_last) + toc_size + group_sizes[0]; + return ac_group_data_offset - actual_offset; + } + */ + // The reference implementation copies the entire vector so that + // element 0 can be modified without affecting the original. + // Since TocSize() does not throw, temporarily modify element 0 + // instead, avoiding the allocation and copy. + int firstItem = groupSizes[0]; + groupSizes[0] = minDcGlobalSize; + int tocSize = TocSize(groupSizes); + int actualOffset = FrameHeaderSize(containsAlpha, isLast) + tocSize + firstItem; + groupSizes[0] = firstItem; + return acGroupDataOffset - actualOffset; + } + + private static int PredictPixels(Span pixels, Span pixelsLeft, Span pixelsTop, Span pixelsTopleft, Span residuals, T negativeOne) + where T : unmanaged + { + Vector px = Vector.Create(pixels); + Vector left = Vector.Create(pixelsLeft); + Vector top = Vector.Create(pixelsTop); + Vector topleft = Vector.Create(pixelsTopleft); + + Vector ac = left - topleft; + Vector ab = left - top; + Vector bc = top - topleft; + Vector grad = ac + top; + Vector d = ab ^ bc; + Vector zero = Vector.Zero; + Vector clamp = Vector.ConditionalSelect(Vector.GreaterThan(zero, d), top, left); + Vector s = ac ^ bc; + Vector pred = Vector.ConditionalSelect(Vector.GreaterThan(zero, s), grad, clamp); + + Vector res = px - pred; + Vector resTimes2 = res + res; + res = Vector.ConditionalSelect(Vector.GreaterThan(zero, res), Vector.Create(negativeOne) - resTimes2, resTimes2); + res.CopyTo(residuals); + + return FjxlSimdUtils.CountPrefix(Vector.Equals(res, zero)); + } + + private static void EncodeHybridUint000(uint value, out uint token, out uint nbits, out uint bits) + { + if (value == 0) + { + token = 0; + nbits = 0; + bits = 0; + return; + } + + uint n = FloorLog2(value); + token = n + 1; + nbits = n; + bits = value - (1u << (int)n); + } + + private static void GenericEncodeChunk(ReadOnlySpan residuals, int n, int skip, FjxlPrefixCode code, ref FjxlBitWriter output) + { + for (int ix = skip; ix < n; ix++) + { + EncodeHybridUint000(residuals[ix], out uint token, out uint nbits, out uint bits); + output.Write((int)(code.RawLengths[(int)token] + nbits), code.RawCodes[(int)token] | (bits << code.RawLengths[(int)token])); + } + } + + public static void PrepareDcGlobalCommon(InlineArray4 code, FjxlBitWriter output) + { + // No patches, spline, or noise. + output.Write(1, 1); // Default DC dequantization factors + output.Write(1, 1); // Use global trees and histograms + output.Write(1, 0); // Don't use LZ77 compression for the tree. + + output.Write(1, 1); // Simple code for the tree's context map + output.Write(2, 0); // All contexts clustered together + output.Write(1, 1); // Use prefix code for tree + output.Write(4, 0); // 000 hybrid unsigned integer + output.Write(6, 0b100011); // Alphabet size is 4 + output.Write(2, 1); // Simple prefix code + output.Write(2, 3); // With 4 symbols + + output.Write(2, 0); + output.Write(2, 1); + output.Write(2, 2); + output.Write(2, 3); + output.Write(1, 0); // First tree encoding option + + // Huffman table and extra bits for the tree + Span symbolBits = [0b00, 0b10, 0b001, 0b101, 0b0011, 0b0111]; + Span symbolLengths = [2, 2, 3, 3, 4, 4]; + + Span predictors = + [ + 1, 2, 1, 4, 1, 0, 0, 5, 0, 0, 0, 0, 5, + 0, 0, 0, 0, 5, 0, 0, 0, 0, 5, 0, 0, 0 + ]; + + ref byte unsafePred = ref MemoryMarshal.GetReference(predictors); + + for (int i = 0; i < predictors.Length; i++) + { + byte predictor = Unsafe.AddByteOffset(ref unsafePred, i); + output.Write(symbolLengths[predictor], symbolBits[predictor]); + } + + output.Write(1, 1); // Enable LZ77 compression for the main bit-stream + output.Write(2, 0b00); // LZ77 offset 224 + output.Write(4, 0b1010); // LZ77 minimum length 7 + + // 400 hybrid uint configuration for LZ77 + output.Write(4, 4); + output.Write(3, 0); + output.Write(3, 0); + + output.Write(1, 1); // simple code for the context map + output.Write(2, 3); // 3 bits per entry + output.Write(3, 4); // channel 3 + output.Write(3, 3); // channel 2 + output.Write(3, 2); // channel 1 + output.Write(3, 1); // channel 0 + output.Write(3, 0); // distance histogram first + + output.Write(1, 1); // use prefix codes + output.Write(4, 0); // 000 hybrid uint configuration for distances (only need 0) + + for (int i = 0; i < 4; i++) + { + output.Write(4, 0); // 000 hybrid uint configuration for symbols (only <= 10) + } + + // Distance alphabet size + output.Write(5, 0b00001); // 2: just need 1 for RLE (i.e. distance 1) + + // Symbol + LZ77 alphabet size + for (int i = 0; i < 4; i++) + { + output.Write(1, 1); // > 1 + output.Write(4, 8); // <= 512 + output.Write(8, 256); // == 512 + } + + // Distance histogram + output.Write(2, 1); // simple prefix code + output.Write(2, 0); // With 1 symbol + output.Write(1, 1); // 1 + + // write all prefix codes + for (int i = 0; i < 4; i++) + { + code[i].Write(output); + } + + // group header for global modular image + output.Write(1, 1); // Global tree + output.Write(1, 1); // All default weighted prediction + } + + public static void PrepareDcGlobal(int numChannels, bool isSingleGroup, InlineArray4 codes, FjxlBitWriter output) + { + PrepareDcGlobalCommon(codes, output); + + if (numChannels > 2) + { + output.Write(2, 0b01); // 1 transform + output.Write(2, 0b00); // RCT + output.Write(5, 0b00000); // Starting from channel 0 + output.Write(2, 0b00); // YCoCg + } + else + { + output.Write(2, 0b00); // 0 transforms + } + + if (!isSingleGroup) + { + output.ZeroPadToByte(); + } + } + + /// + /// Pair of two vectors. + /// + /// Type of the vector. + /// Low vector + /// High vector + private struct VectorPair(T lo, T hi) + where T : unmanaged + { + public T Low = lo; + public T High = hi; + } + + internal static class SimdVector32 + { + public static Vector ValueToToken(Vector vec) => Vector.Create(32) - SimdUtils.Lzcnt(vec); + + public static Vector SaturateSubtract(Vector a, Vector b) => Vector.Max(a, b) - b; + + public static Vector Pow2(Vector x) => SimdUtils.Pow2(x); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderExactly14Bits.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderExactly14Bits.cs new file mode 100644 index 0000000000..f404e357d7 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderExactly14Bits.cs @@ -0,0 +1,64 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd.ChunkEncoders; + +internal readonly struct FjxlChunkEncoderExactly14Bits +{ + public const int BitDepth = 14; + public const int InputBytes = 2; + + public static ReadOnlySpan MinRawLength => + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 8, 7 + ]; + + public static ReadOnlySpan MaxRawLength => + [ + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 10 + ]; + + public static int MaxEncodedBitsPerSample() => 23; + + public static void PrepareForSimd(ReadOnlySpan nbits, ReadOnlySpan bits, Span nbitsSimd, Span bitsSimd) + { + CheckHuffmanBitsSimd(bits[15], nbits[15], bits[16], nbits[16]); + + nbits[..16].CopyTo(nbitsSimd); + bits[..16].CopyTo(bitsSimd); + } + + public static void EncodeChunkSimd(Span residuals, int n, int skip, ReadOnlySpan rawNBitsSimd, ReadOnlySpan rawBitsSimd, ref FjxlBitWriter output) + { + Span bits32 = stackalloc FjxlBits32[2 * ChunkSize / FjxlSimdVec16.Lanes]; + + Span buffer = stackalloc ushort[FjxlSimdVec16.Lanes * 5]; + Span bits = buffer.Slice(0, FjxlSimdVec16.Lanes); + Span nbits = buffer.Slice(FjxlSimdVec16.Lanes, FjxlSimdVec16.Lanes); + Span bitsHuff = buffer.Slice(FjxlSimdVec16.Lanes * 2, FjxlSimdVec16.Lanes); + Span nbitsHuff = buffer.Slice(FjxlSimdVec16.Lanes * 3, FjxlSimdVec16.Lanes); + Span token = buffer.Slice(FjxlSimdVec16.Lanes * 4, FjxlSimdVec16.Lanes); + + for (int i = 0; i < ChunkSize; i += FjxlSimdVec16.Lanes) + { + FjxlTokenization.TokenizeSimd(residuals[i..], token, nbits, bits); + FjxlHuffman.HuffmanSimd14(token, rawNBitsSimd, rawBitsSimd, nbitsHuff, bitsHuff); + + StoreSimdUpTo14( + nbits, + bits, + nbitsHuff, + bitsHuff, + Math.Max(n, i) - i, + Math.Max(skip, i) - i, + bits32[(2 * i / FjxlSimdVec16.Lanes)..]); + } + + StoreToWriter(bits32, ref output); + } + + public static int NumSymbols() => 17; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderFrom9To13Bits.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderFrom9To13Bits.cs new file mode 100644 index 0000000000..7bb349abef --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderFrom9To13Bits.cs @@ -0,0 +1,72 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd.ChunkEncoders; + +internal readonly struct FjxlChunkEncoderFrom9To13Bits +{ + public const int InputBytes = 2; + + private readonly int bitDepth; + + public FjxlChunkEncoderFrom9To13Bits(int bitDepth) + { + DebugGuard.MustBeLessThanOrEqualTo(bitDepth, 13, nameof(bitDepth)); + DebugGuard.MustBeGreaterThanOrEqualTo(bitDepth, 9, nameof(bitDepth)); + + this.bitDepth = bitDepth; + } + + public static ReadOnlySpan MinRawLength => + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ]; + + public static ReadOnlySpan MaxRawLength => + [ + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 10 + ]; + + public static int MaxEncodedBitsPerSample() => 22; + + public static void PrepareForSimd(ReadOnlySpan nbits, ReadOnlySpan bits, int n, Span nbitsSimd, Span bitsSimd) + { + DebugGuard.MustBeLessThanOrEqualTo(n, 16, nameof(n)); + + nbits[..16].CopyTo(nbitsSimd); + bits[..16].CopyTo(bitsSimd); + } + + public static void EncodeChunkSimd(Span residuals, int n, int skip, ReadOnlySpan rawNBitsSimd, ReadOnlySpan rawBitsSimd, ref FjxlBitWriter output) + { + Span bits32 = stackalloc FjxlBits32[2 * ChunkSize / FjxlSimdVec16.Lanes]; + + Span buffer = stackalloc ushort[FjxlSimdVec16.Lanes * 5]; + Span bits = buffer.Slice(0, FjxlSimdVec16.Lanes); + Span nbits = buffer.Slice(FjxlSimdVec16.Lanes, FjxlSimdVec16.Lanes); + Span bitsHuff = buffer.Slice(FjxlSimdVec16.Lanes * 2, FjxlSimdVec16.Lanes); + Span nbitsHuff = buffer.Slice(FjxlSimdVec16.Lanes * 3, FjxlSimdVec16.Lanes); + Span token = buffer.Slice(FjxlSimdVec16.Lanes * 4, FjxlSimdVec16.Lanes); + + for (int i = 0; i < ChunkSize; i += FjxlSimdVec16.Lanes) + { + TokenizeSimd(residuals[i..], token, nbits, bits); + HuffmanSimdUpTo13(token, rawNBitsSimd, rawBitsSimd, nbitsHuff, bitsHuff); + + StoreSimdUpTo14( + nbits, + bits, + nbitsHuff, + bitsHuff, + Math.Max(n, i) - i, + Math.Max(skip, i) - i, + bits32[(2 * i / FjxlSimdVec16.Lanes)..]); + } + + StoreToWriter(bits32, ref output); + } + + public int NumSymbols(bool doingYcocgOrLargePalette) => this.bitDepth + (doingYcocgOrLargePalette ? 3 : 2); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderMoreThan14Bits.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderMoreThan14Bits.cs new file mode 100644 index 0000000000..235f33508d --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderMoreThan14Bits.cs @@ -0,0 +1,81 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd.ChunkEncoders; + +internal readonly struct FjxlChunkEncoderMoreThan14Bits +{ + public const int InputBytes = 2; + + private readonly int bitDepth; + + public FjxlChunkEncoderMoreThan14Bits(int bitDepth) + { + DebugGuard.MustBeGreaterThan(bitDepth, 14, nameof(bitDepth)); + DebugGuard.MustBeLessThanOrEqualTo(bitDepth, 16, nameof(bitDepth)); + + this.bitDepth = bitDepth; + } + + public static ReadOnlySpan MinRawLength => + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 8, 8, 8, 8, 8, 8, 7 + ]; + + public static ReadOnlySpan MaxRawLength => + [ + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 8, 8, 8, 8, 8, 8, 10 + ]; + + public static int MaxEncodedBitsPerSample() => 25; + + public static void PrepareForSimd(ReadOnlySpan nbits, ReadOnlySpan bits, Span nbitsSimd, Span bitsSimd) + { + CheckHuffmanBitsSimd(bits[13], nbits[13], bits[14], nbits[14]); + CheckHuffmanBitsSimd(bits[15], nbits[15], bits[16], nbits[16]); + CheckHuffmanBitsSimd(bits[17], nbits[17], bits[18], nbits[18]); + + nbits[..14].CopyTo(nbitsSimd); + bits[..14].CopyTo(bitsSimd); + + nbitsSimd[14] = nbits[15]; + bitsSimd[14] = bits[15]; + nbitsSimd[15] = nbits[17]; + bitsSimd[15] = bits[17]; + } + + public static void EncodeChunkSimd(Span residuals, int n, int skip, ReadOnlySpan rawNBitsSimd, ReadOnlySpan rawBitsSimd, ref FjxlBitWriter output) + { + Span bits32 = stackalloc FjxlBits32[2 * ChunkSize / FjxlSimdVec16.Lanes]; + + Span bufferU32 = stackalloc uint[FjxlSimdVec16.Lanes * 2]; + Span bits = bufferU32.Slice(0, FjxlSimdVec16.Lanes); + Span nbits = bufferU32.Slice(FjxlSimdVec16, FjxlSimdVec16.Lanes); + + Span bufferU16 = stackalloc uint[FjxlSimdVec16.Lanes * 3]; + Span bitsHuff = bufferU16.Slice(0, FjxlSimdVec16.Lanes); + Span nbitsHuff = bufferU16.Slice(FjxlSimdVec16.Lanes, FjxlSimdVec16.Lanes); + Span token = bufferU16.Slice(FjxlSimdVec16.Lanes * 2, FjxlSimdVec16.Lanes); + + for (int i = 0; i < ChunkSize; i += FjxlSimdVec16.Lanes) + { + TokenizeSimd(residuals[i..], token, nbits, bits); + HuffmanSimdAbove14(token, rawNBitsSimd, rawBitsSimd, nbitsHuff, bitsHuff); + + StoreSimdAbove14( + nbits, + bits, + nbitsHuff, + bitsHuff, + Math.Max(n, i) - i, + Math.Max(skip, i) - i, + bits32[(2 * i / FjxlSimdVec16.Lanes)..]); + } + + StoreToWriter(bits32, ref output); + } + + public static int NumSymbols() => 19; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderUpTo8Bits.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderUpTo8Bits.cs new file mode 100644 index 0000000000..89faa2f2c5 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderUpTo8Bits.cs @@ -0,0 +1,69 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd.ChunkEncoders; + +internal readonly struct FjxlChunkEncoderUpTo8Bits +{ + public const int InputBytes = 1; + + private readonly int bitDepth; + + public FjxlChunkEncoderUpTo8Bits(int bitDepth) + { + DebugGuard.MustBeLessThanOrEqualTo(bitDepth, 8, nameof(bitDepth)); + + this.bitDepth = bitDepth; + } + + public static ReadOnlySpan MinRawLength => + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ]; + + public static ReadOnlySpan MaxRawLength => + [ + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 10 + ]; + + public static int MaxEncodedBitsPerSample() => 16; + + public static void PrepareForSimd(ReadOnlySpan nbits, ReadOnlySpan bits, int n, Span nbitsSimd, Span bitsSimd) + { + DebugGuard.MustBeLessThanOrEqualTo(n, 16, nameof(n)); + + nbits[..16].CopyTo(nbitsSimd); + bits[..16].CopyTo(bitsSimd); + } + + public static void EncodeChunkSimd(Span residuals, int n, int skip, ReadOnlySpan rawNBitsSimd, ReadOnlySpan rawBitsSimd, ref FjxlBitWriter output) + { + Span bits32 = stackalloc FjxlBits32[ChunkSize / FjxlSimdVec16.Lanes]; + + Span buffer = stackalloc ushort[FjxlSimdVec16.Lanes * 5]; + Span bits = buffer.Slice(0, FjxlSimdVec16.Lanes); + Span nbits = buffer.Slice(FjxlSimdVec16.Lanes, FjxlSimdVec16.Lanes); + Span bitsHuff = buffer.Slice(FjxlSimdVec16.Lanes * 2, FjxlSimdVec16.Lanes); + Span nbitsHuff = buffer.Slice(FjxlSimdVec16.Lanes * 3, FjxlSimdVec16.Lanes); + Span token = buffer.Slice(FjxlSimdVec16.Lanes * 4, FjxlSimdVec16.Lanes); + + for (int i = 0; i < ChunkSize; i += FjxlSimdVec16.Lanes) + { + TokenizeSimd(residuals[i..], token, nbits, bits); + HuffmanSimdUpTo13(token, rawNBitsSimd, rawBitsSimd, nbitsHuff, bitsHuff); + + StoreSimdUpTo8( + nbits, + bits, + nbitsHuff, + bitsHuff, + Math.Max(n, i) - i, + Math.Max(skip, i) - i, + bits32[(int)(i / FjxlSimdVec16.Lanes)..]); + } + + StoreToWriter(bits32, ref output); + } + + public int NumSymbols(bool doingYcocgOrLargePalette) => this.bitDepth + (doingYcocgOrLargePalette ? 3 : 2); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlBits32.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlBits32.cs new file mode 100644 index 0000000000..2bf012e441 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlBits32.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.Arm; +using static SharpImmintrin.CIntrinsicArmAdvSimd; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd; + +/// +/// Contains a 32-bit pair of code lengths and code values, useful +/// for Huffman encoding. +/// +internal struct FjxlBits32(Vector counts, Vector bits) +{ + /// + /// Gets or sets the code lengths. + /// + public Vector Counts { get; set; } = counts; + + /// + /// Gets or sets the code values. + /// + public Vector Bits { get; set; } = bits; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlBits64.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlBits64.cs new file mode 100644 index 0000000000..dca740cfbf --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlBits64.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd; + +/// +/// Contains a 64-bit pair of code lengths and code values, useful +/// for Huffman encoding. +/// +internal struct FjxlBits64(Vector counts, Vector bits) +{ + public static int Lanes { get; } = Vector.Count; + + /// + /// Gets or sets the code lengths. + /// + public Vector Counts { get; set; } = counts; + + /// + /// Gets or sets the code values. + /// + public Vector Bits { get; set; } = bits; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly void Store(ref ulong nbitsOut, ref ulong bitsOut) + { + this.Counts.StoreUnsafe(ref nbitsOut); + this.Bits.StoreUnsafe(ref bitsOut); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlHuffman.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlHuffman.cs new file mode 100644 index 0000000000..13c702f73f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlHuffman.cs @@ -0,0 +1,66 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd; + +internal static class FjxlHuffman +{ + public static void HuffmanSimdUpTo13(Span tokens, ReadOnlySpan rawNbitsSimd, ReadOnlySpan rawBitsSimd, Span nbitsOut, Span bitsOut) + { + Vector tok = FjxlSimdUtils.PrepareForU8Lookup(Vector.Create(tokens)); + + FjxlSimdUtils.U8Lookup(tok, rawNbitsSimd).CopyTo(nbitsOut); + FjxlSimdUtils.U8Lookup(tok, rawBitsSimd).CopyTo(bitsOut); + } + + public static void HuffmanSimd14(Span tokens, ReadOnlySpan rawNbitsSimd, ReadOnlySpan rawBitsSimd, Span nbitsOut, Span bitsOut) + { + Vector tokenCap = Vector.Create((short)15); + Vector tok = Vector.Create(tokens); + + Vector tokIndex = FjxlSimdUtils.PrepareForU8Lookup(Vector.Min(tok, tokenCap)); + + Vector huffBitsPre = FjxlSimdUtils.U8Lookup(tokIndex, rawBitsSimd); + + Vector needsHighBit = Vector.Equals(tok, Vector.Create((short)16)); + + Vector huffBits = Vector.ConditionalSelect( + needsHighBit, + huffBitsPre | Vector.Create((short)128), + huffBitsPre); + + huffBits.CopyTo(bitsOut); + + FjxlSimdUtils.U8Lookup(tokIndex, rawNbitsSimd) + .CopyTo(nbitsOut); + } + + public static void HuffmanSimdAbove14(Span tokens, ReadOnlySpan rawNbitsSimd, ReadOnlySpan rawBitsSimd, Span nbitsOut, Span bitsOut) + { + Vector tok = Vector.Create(tokens); + Vector above = Vector.GreaterThan(tok, Vector.Create((short)12)); + + Vector remapTok = Vector.ConditionalSelect( + above, + FjxlSimdUtils.HorizontalAdd(tok, Vector.Create((short)13)), + tok); + + Vector tokIndex = FjxlSimdUtils.PrepareForU8Lookup(remapTok); + Vector huffBitsPre = FjxlSimdUtils.U8Lookup(tokIndex, rawBitsSimd); + + Vector evenTok = tok & Vector.Create(unchecked((short)0xFFF)); + Vector needsHighBit = above & Vector.Equals(tok, evenTok); + + Vector huffBits = Vector.ConditionalSelect( + needsHighBit, + huffBitsPre | Vector.Create((short)128), + huffBitsPre); + + huffBits.CopyTo(bitsOut); + + FjxlSimdUtils.U8Lookup(tokIndex, rawNbitsSimd) + .CopyTo(nbitsOut); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlRowFill.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlRowFill.cs new file mode 100644 index 0000000000..2c7217befa --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlRowFill.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd; + +internal static class FjxlRowFill +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void FillRowG8(Span rgba, int oxs, Span luma) + where T : unmanaged, INumber + { + int x = 0; + + for (; x + FjxlSimdVec16.Lanes <= oxs; x += FjxlSimdVec16.Lanes) + { + Vector rgb = FjxlSimdVec16.LoadG8(rgba[x..]); + StorePixels(rgb, luma[x..]); + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlSimdUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlSimdUtils.cs new file mode 100644 index 0000000000..6cdaf7f57b --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlSimdUtils.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd; + +/// +/// SIMD utilities highly specific to the Fast Lossless JXL encoder. +/// +internal static class FjxlSimdUtils +{ + public static Vector HorizontalAdd(this Vector a, Vector b) => (a + b) >> 1; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlSimdVec16.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlSimdVec16.cs new file mode 100644 index 0000000000..9ee046eea0 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlSimdVec16.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd; + +/// +/// Utilities for 16-bit SIMD vectors. +/// +internal static class FjxlSimdVec16 +{ + public static int Lanes { get; } = Vector.Count; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlStoreSimd.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlStoreSimd.cs new file mode 100644 index 0000000000..03f78a92d8 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlStoreSimd.cs @@ -0,0 +1,101 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd; + +internal static class FjxlStoreSimd +{ + public static void StoreSimdUpTo8(Span nbitsTok, Span bitsTok, Span nbitsHuff, Span bitsHuff, int n, int skip, Span bitsOut) + { + FjxlBits16 bits = FjxlBits16.FromRaw( + Vector.Create(nbitsTok), + Vector.Create(bitsTok)); + + FjxlBits16 huffBits = FjxlBits16.FromRaw( + Vector.Create(nbitsHuff), + Vector.Create(bitsHuff)); + + bits.Interleave(huffBits); + bits.ClipTo(n); + bits.Skip(skip); + + bitsOut[0] = bits.Merge(); + } + + public static void StoreSimdUpTo14(Span nbitsTok, Span bitsTok, Span nbitsHuff, Span bitsHuff, int n, int skip, Span bitsOut) + { + VecPair> bits = Vector.Create(bitsTok).Interleave(Vector.Create(bitsHuff)); + VecPair> nbits = Vector.Create(nbitsTok).Interleave(Vector.Create(nbitsHuff)); + + Bits16 low = Bits16.FromRaw(nbits.Low, bits.Low); + Bits16 hi = Bits16.FromRaw(nbits.High, bits.High); + + int lanes = Vector.Count; + + low.ClipTo(2 * n); + low.Skip(2 * skip); + + hi.ClipTo(Math.Max(2 * n, lanes) - lanes); + hi.Skip(Math.Max(2 * skip, lanes) - lanes); + + bitsOut[0] = low.Merge(); + bitsOut[1] = hi.Merge(); + } + + public static void StoreSimdAbove14(Span nbitsTok, Span bitsTok, Span nbitsHuff, Span bitsHuff, int n, int skip, Span bitsOut) + { + Vector nbitsTokLo = Vector.Create(nbitsTok); + Vector bitsTokLo = Vector.Create(bitsTok); + + Vector nbitsTokHi = Vector.Create(nbitsTok[Vector.Count..]); + Vector bitsTokHi = Vector.Create(bitsTok[Vector.Count..]); + + Bits32 bitsLow = Bits32.FromRaw(nbitsTokLo, bitsTokLo); + Bits32 bitsHi = Bits32.FromRaw(nbitsTokHi, bitsTokHi); + + VecPair> huffBits = FjxlSimdUtils.Upcast(Vector.Create(bitsHuff)); + VecPair> huffNbits = FjxlSimdUtils.Upcast(Vector.Create(nbitsHuff)); + + Bits32 huffLow = Bits32.FromRaw(huffNbits.Low, huffBits.Low); + Bits32 huffHi = Bits32.FromRaw(huffNbits.High, huffBits.High); + + bitsLow.Interleave(huffLow); + bitsLow.ClipTo(n); + bitsLow.Skip(skip); + bitsOut[0] = bitsLow; + + int lanes = Vector.Count; + + bitsHi.Interleave(huffHi); + bitsHi.ClipTo(Math.Max(n, lanes) - lanes); + bitsHi.Skip(Math.Max(skip, lanes) - lanes); + bitsOut[1] = bitsHi; + } + + public static void StoreToWriter(ReadOnlySpan bits, FjxlBitWriter output, int n) + { + Span nbits64 = stackalloc ulong[Bits64.Lanes * n]; + Span bits64 = stackalloc ulong[Bits64.Lanes * n]; + + bits[0].Merge().Store(nbits64, bits64); + + if (n > 1) + { + bits[1].Merge().Store(nbits64[Bits64.Lanes..], bits64[Bits64.Lanes..]); + } + + if (n > 2) + { + bits[2].Merge().Store(nbits64[(2 * Bits64.Lanes)..], bits64[(2 * Bits64.Lanes)..]); + } + + if (n > 3) + { + bits[3].Merge().Store(nbits64[(3 * Bits64.Lanes)..], bits64[(3 * Bits64.Lanes)..]); + } + + output.WriteMultiple(nbits64, bits64, Bits64.Lanes * n); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlTokenization.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlTokenization.cs new file mode 100644 index 0000000000..aed8a20576 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlTokenization.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd; + +internal static class FjxlTokenization +{ + public static void Tokenize16(Span residuals, Span output, Span nbitsOut, Span bitsOut) + { + Vector res = Vector.Create(residuals); + Vector token = FjxlSimdUtils.ValueToToken(res); + Vector nbits = FjxlSimdUtils.SaturateSubtract(token, Vector.One); + Vector bits = FjxlSimdUtils.SaturateSubtract(res, FjxlSimdUtils.Pow2(nbits)); + + token.CopyTo(output); + nbits.CopyTo(nbitsOut); + bits.CopyTo(bitsOut); + } + + public static void Tokenize32(Span residuals, Span tokenOut, Span nbitsOut, Span bitsOut) + { + Vector resLo = Vector.Create(residuals); + Vector resHi = Vector.Create(residuals[Vector.Count..]); + + Vector tokenLo = FjxlSimdUtils.ValueToToken(resLo); + Vector tokenHi = FjxlSimdUtils.ValueToToken(resHi); + + Vector nbitsLo = FjxlSimdUtils.SaturateSubtract(tokenLo, Vector.One); + Vector nbitsHi = FjxlSimdUtils.SaturateSubtract(tokenHi, Vector.One); + + Vector bitsLo = FjxlSimdUtils.SaturateSubtract(resLo, FjxlSimdUtils.Pow2(nbitsLo)); + Vector bitsHi = FjxlSimdUtils.SaturateSubtract(resHi, FjxlSimdUtils.Pow2(nbitsHi)); + + Vector token = FjxlSimdUtils.FromTwo32(tokenLo, tokenHi); + token.CopyTo(tokenOut); + + nbitsLo.CopyTo(nbitsOut); + nbitsHi.CopyTo(nbitsOut[Vector.Count..]); + + bitsLo.CopyTo(bitsOut); + bitsHi.CopyTo(bitsOut[Vector.Count..]); + } +} From 25eae36501a634adc7bd9ff47cec5c8b846eefeb Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 16 Sep 2026 14:24:30 +0400 Subject: [PATCH 183/328] Remove lossless codec implementation --- .../Encoder/FastLossless/FjxlBitWriter.cs | 88 -- .../Encoder/FastLossless/FjxlPrefixCode.cs | 462 ----------- .../FastLossless/JxlFastLosslessEncoder.cs | 758 ------------------ .../FjxlChunkEncoderExactly14Bits.cs | 64 -- .../FjxlChunkEncoderFrom9To13Bits.cs | 72 -- .../FjxlChunkEncoderMoreThan14Bits.cs | 81 -- .../FjxlChunkEncoderUpTo8Bits.cs | 69 -- .../Encoder/FastLossless/Simd/FjxlBits32.cs | 26 - .../Encoder/FastLossless/Simd/FjxlBits64.cs | 33 - .../Encoder/FastLossless/Simd/FjxlHuffman.cs | 66 -- .../Encoder/FastLossless/Simd/FjxlRowFill.cs | 23 - .../FastLossless/Simd/FjxlSimdUtils.cs | 14 - .../FastLossless/Simd/FjxlSimdVec16.cs | 14 - .../FastLossless/Simd/FjxlStoreSimd.cs | 101 --- .../FastLossless/Simd/FjxlTokenization.cs | 45 -- 15 files changed, 1916 deletions(-) delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/FjxlBitWriter.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/FjxlPrefixCode.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/JxlFastLosslessEncoder.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderExactly14Bits.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderFrom9To13Bits.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderMoreThan14Bits.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderUpTo8Bits.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlBits32.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlBits64.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlHuffman.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlRowFill.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlSimdUtils.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlSimdVec16.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlStoreSimd.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlTokenization.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/FjxlBitWriter.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/FjxlBitWriter.cs deleted file mode 100644 index 54277d41a9..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/FjxlBitWriter.cs +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless; - -/// -/// Simple MSB-first bit-stream writer implementation built on top -/// of a stream. -/// -/// Output bytes are written here. -internal sealed class FjxlBitWriter(Stream stream) : IDisposable -{ - /// - /// Temporary cache used to store pending written bits - /// before they're written to the output stream. - /// - private ulong buffer; - - /// - /// Gets the total number of bytes written to the output buffer so far. - /// - public long BytesWritten { get; private set; } - - /// - /// Gets the number of bits actively in the bit cache. - /// This is used to track how many bits were written into - /// the cache prior to sending the cache to the stream. - /// - public int BitsInBuffer { get; private set; } - - /// - /// Writes the specified bits in the Most Significant Byte (MSB) - /// order. - /// - /// Represents the number of bits to write to the bit-stream. - /// Represents the value to write to the bit-stream. - public void Write(int count, ulong bits) - { - DebugGuard.MustBeBetweenOrEqualTo(count, 0, 56, nameof(count)); - - if (count < 64) - { - bits &= (1UL << count) - 1; - } - - this.buffer |= bits << this.BitsInBuffer; - this.BitsInBuffer += count; - - this.FlushBytes(); - } - - /// - /// Internal method used to flush bytes from the cache - /// () into the output stream. - /// - private void FlushBytes() - { - int bytes = this.BitsInBuffer / 8; - - for (int i = 0; i < bytes; i++) - { - stream.WriteByte((byte)this.buffer); - this.BytesWritten++; - this.buffer >>= 8; - } - - this.BitsInBuffer -= bytes * 8; - } - - /// - /// Used by the dispose method to flush the remaining bits - /// that are not byte-aligned. F.e. if we dispose this reader - /// and we have 5 bits left, those final 5 bits are set to all 0 - /// and the byte is written to the stream. - /// - public void ZeroPadToByte() - { - if (this.BitsInBuffer != 0) - { - this.Write(8 - this.BitsInBuffer, 0); - } - } - - /// - /// Flushes out the final bytes. - /// - public void Dispose() => this.ZeroPadToByte(); -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/FjxlPrefixCode.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/FjxlPrefixCode.cs deleted file mode 100644 index f9b9c821b4..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/FjxlPrefixCode.cs +++ /dev/null @@ -1,462 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using System.Runtime.CompilerServices; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless; - -/// -/// The prefix code is used for encoding LZ77-compressed coefficients. -/// -internal sealed class FjxlPrefixCode -{ -#pragma warning disable SA1401 // Fields should be private - - /// - /// Maximum number of raw symbols for prefix coding. - /// - private const int MaxNumSymbols = JxlFastLosslessEncoder.NumRawSymbols + 1 < JxlFastLosslessEncoder.NumLz77 ? JxlFastLosslessEncoder.NumLz77 : JxlFastLosslessEncoder.NumRawSymbols + 1; - - /// - /// Gets or sets the Huffman raw bit lengths. - /// - public InlineArray19 RawLengths; - - /// - /// Gets or sets the Huffman raw code values. - /// - public InlineArray19 RawCodes; - - /// - /// Gets or sets the Huffman LZ77 bit lengths. - /// - public InlineArray33 Lz77Lengths; - - /// - /// Gets or sets the Huffman LZ77 code values. - /// - public InlineArray33 Lz77Codes; - - /// - /// Gets or sets the Huffman LZ77 cache code values. - /// - public InlineArray32 Lz77CacheBits; - - /// - /// Gets or sets the Huffman LZ77 cache bit lengths. - /// - public InlineArray32 Lz77CacheLengths; - - public FjxlPrefixCode(Span rawCounts, Span lz77Counts) - { - Span level1Counts = stackalloc ulong[JxlFastLosslessEncoder.NumRawSymbols + 1]; - rawCounts[..JxlFastLosslessEncoder.NumRawSymbols].CopyTo(level1Counts); - - this.RawCount = JxlFastLosslessEncoder.NumRawSymbols; - - while (this.RawCount > 0 && level1Counts[this.RawCount - 1] == 0) - { - this.RawCount--; - } - - level1Counts[this.RawCount] = 0; - - for (int i = 0; i < JxlFastLosslessEncoder.NumLz77; i++) - { - level1Counts[this.RawCount] += lz77Counts[i]; - } - - Span level1Lengths = stackalloc byte[JxlFastLosslessEncoder.NumRawSymbols + 1]; - level1Lengths.Clear(); - - ComputeCodeLengths(level1Counts, this.RawCount + 1, JxlFastLosslessEncoder.MinimumRawLength, JxlFastLosslessEncoder.MaximumRawLength, level1Lengths); - - Span level2Lengths = stackalloc byte[JxlFastLosslessEncoder.NumLz77]; - Span minLengths = stackalloc byte[JxlFastLosslessEncoder.NumLz77]; - - level2Lengths.Clear(); - minLengths.Clear(); - - int l = 15 - level1Lengths[this.RawCount]; - Span maxLengths = stackalloc byte[JxlFastLosslessEncoder.NumLz77]; - maxLengths.Fill((byte)l); - - int numLz77 = JxlFastLosslessEncoder.NumLz77; - while (numLz77 > 0 && lz77Counts[numLz77 - 1] == 0) - { - numLz77--; - } - - ComputeCodeLengths(lz77Counts, numLz77, minLengths, maxLengths, level2Lengths); - - level1Lengths[..this.RawCount].CopyTo(this.RawLengths); - - for (int i = 0; i < numLz77; i++) - { - this.Lz77Lengths[i] = (byte)(level2Lengths[i] != 0 ? level1Lengths[this.RawCount] + level2Lengths[i] : 0); - } - - ComputeCanonicalCode(this.RawLengths, this.RawCodes, this.Lz77Lengths, this.Lz77Codes); - - // Prepare the LZ77 cache - for (int count = 0; count < JxlFastLosslessEncoder.Lz77CacheSize; count++) - { - EncodeHybridUintLz77(count, out int token, out int nbits, out int bits); - this.Lz77CacheLengths[count] = (byte)(this.Lz77Lengths[token] + nbits + this.RawLengths[0]); - this.Lz77CacheBits[count] = - (ulong)((((bits << this.Lz77Lengths[token]) | this.Lz77Codes[token]) << this.RawLengths[0]) | - this.RawLengths[0]); - } - } - - /// - /// Gets a lookup used to reverse integers bit-wise. - /// - private static ReadOnlySpan ReverseNibbleLookup => - [ - 0b0000, 0b1000, 0b0100, 0b1100, 0b0010, 0b1010, 0b0110, 0b1110, - 0b0001, 0b1001, 0b0101, 0b1101, 0b0011, 0b1011, 0b0111, 0b1111, - ]; - -#pragma warning restore SA1401 // Fields should be private - - /// - /// Gets or sets the number of raw codes. - /// - public int RawCount { get; set; } - - /// - /// Reverses the integer bit-wise. - /// - /// Number of bits for the integer. - /// Actual bits to reverse. - /// - /// Input integer but reversed. F.e. 10010 becomes 01001. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static ushort BitReverse(int nbits, ushort bits) - { - unchecked - { - ushort rev16 = (ushort)((ReverseNibbleLookup[bits & 0xF] << 12) | - (ReverseNibbleLookup[(bits >> 4) & 0xF] << 8) | - (ReverseNibbleLookup[(bits >> 8) & 0xF] << 4) | - ReverseNibbleLookup[bits >> 12]); - return (ushort)(rev16 >> (16 - nbits)); - } - } - - private static void ComputeCanonicalCode(Span firstChunkLengths, Span firstChunkCodes, Span secondChunkLengths, Span secondChunkCodes) - { - const int maxCodeLength = 15; - - Span codeLengthCounts = stackalloc byte[maxCodeLength + 1]; - codeLengthCounts.Clear(); - - for (int i = 0; i < firstChunkCodes.Length; i++) - { - codeLengthCounts[firstChunkLengths[i]]++; - - if (firstChunkLengths[i] > 8) - { - throw new InvalidOperationException("First chunk length is too large"); - } - - if (firstChunkLengths[i] <= 0) - { - throw new InvalidOperationException("First chunk length cannot be <= 0"); - } - } - - for (int i = 0; i < secondChunkCodes.Length; i++) - { - codeLengthCounts[secondChunkLengths[i]]++; - - if (secondChunkLengths[i] > maxCodeLength) - { - throw new InvalidOperationException("Second chunk length is too large"); - } - } - - Span nextCode = stackalloc ushort[maxCodeLength + 1]; - nextCode.Clear(); - - ushort code = 0; - - for (int i = 1; i < maxCodeLength + 1; i++) - { - code = unchecked((ushort)((code + codeLengthCounts[i - 1]) << 1)); - nextCode[i] = code; - } - - unchecked - { - for (int i = 0; i < firstChunkCodes.Length; i++) - { - firstChunkCodes[i] = (byte)BitReverse(firstChunkLengths[i], nextCode[firstChunkLengths[i]]++); - } - - for (int i = 0; i < secondChunkCodes.Length; i++) - { - secondChunkCodes[i] = (byte)BitReverse(secondChunkLengths[i], nextCode[secondChunkLengths[i]]++); - } - } - } - - private static void ComputeCodeLengthsNonZeroImpl( - Span freqs, - int n, - int precision, - T infty, - Span minLimit, - Span maxLimit, - Span nbits) - where T : unmanaged, INumber - { - DebugGuard.MustBeLessThan(precision, 15, nameof(precision)); - DebugGuard.MustBeLessThanOrEqualTo(n, MaxNumSymbols, nameof(n)); - - int scale = 1 << precision; - int width = scale + 1; - - Span dynp = stackalloc T[width * (n + 1)]; - dynp.Fill(infty); - dynp[0] = T.Zero; - - for (int sym = 0; sym < n; sym++) - { - for (int bits = minLimit[sym]; bits <= maxLimit[sym]; bits++) - { - int offsetDelta = 1 << (precision - bits); - T cost = T.CreateChecked(freqs[sym]) * T.CreateChecked(bits); - - for (int off = 0; off + offsetDelta <= scale; off++) - { - int current = (sym * width) + off; - int next = ((sym + 1) * width) + off + offsetDelta; - - dynp[next] = T.Min(dynp[current] + cost, dynp[next]); - } - } - } - - int offFinal = scale; - - for (int sym = n - 1; sym >= 0; sym--) - { - if (offFinal <= 0) - { - throw new InvalidOperationException("Offset should be greater than zero"); - } - - for (int bits = minLimit[sym]; bits <= maxLimit[sym]; bits++) - { - int offsetDelta = 1 << (precision - bits); - - if (offsetDelta <= offFinal) - { - int current = (sym * width) + offFinal; - int previous = (sym * width) + offFinal - offsetDelta; - - T cost = T.CreateChecked(freqs[sym]) * T.CreateChecked(bits); - - if (dynp[current] == dynp[previous] + cost) - { - offFinal -= offsetDelta; - nbits[sym] = (byte)bits; - break; - } - } - } - } - } - - private static void ComputeCodeLengthsNonZero(Span freqs, int n, Span minLimit, Span maxLimit, Span nbits) - { - int precision = 0; - int shortestLength = 255; - ulong frequencySum = 0; - - for (int i = 0; i < n; i++) - { - frequencySum += freqs[i]; - - if (minLimit[i] < 1) - { - minLimit[i] = 1; - } - - precision = Math.Max(maxLimit[i], precision); - shortestLength = Math.Min(minLimit[i], shortestLength); - } - - precision -= shortestLength - 1; - ulong infinity = frequencySum * (ulong)precision; - - if (infinity < uint.MaxValue / 2) - { - ComputeCodeLengthsNonZeroImpl(freqs, n, precision, (uint)infinity, minLimit, maxLimit, nbits); - } - else - { - ComputeCodeLengthsNonZeroImpl(freqs, n, precision, infinity, minLimit, maxLimit, nbits); - } - } - - private static void ComputeCodeLengths(Span freqs, int n, ReadOnlySpan minLimitIn, ReadOnlySpan maxLimitIn, Span nbits) - { - DebugGuard.MustBeLessThanOrEqualTo(n, MaxNumSymbols, nameof(n)); - - Span compactFreqs = stackalloc ulong[MaxNumSymbols]; - Span minLimit = stackalloc byte[MaxNumSymbols]; - Span maxLimit = stackalloc byte[MaxNumSymbols]; - - int ni = 0; - for (int i = 0; i < n; i++) - { - if (freqs[i] != 0) - { - compactFreqs[ni] = freqs[i]; - minLimit[ni] = minLimitIn[i]; - maxLimit[ni] = maxLimitIn[i]; - ni++; - } - } - - compactFreqs[ni..].Clear(); - minLimit[ni..].Clear(); - maxLimit[ni..].Clear(); - - Span numBits = stackalloc byte[MaxNumSymbols]; - numBits.Clear(); - - ComputeCodeLengthsNonZero(compactFreqs, ni, minLimit, maxLimit, numBits); - - ni = 0; - - for (int i = 0; i < n; i++) - { - nbits[i] = 0; - if (freqs[i] != 0) - { - nbits[i] = numBits[ni++]; - } - } - } - - /// - /// Writes this LZ77 prefix code into the bit-stream. - /// - /// The bit-stream to write the prefix code into. - public void Write(FjxlBitWriter writer) - { - Span codeLengthCounts = stackalloc ulong[32].Slice(0, 18); - codeLengthCounts.Clear(); - codeLengthCounts[17] = 3 + (2 * (JxlFastLosslessEncoder.NumLz77 - 1)); - - for (int i = 0; i < 19; i++) - { - byte rawLength = this.RawLengths[i]; - - codeLengthCounts[rawLength]++; - } - - for (int i = 0; i < 33; i++) - { - byte lz77Length = this.Lz77Lengths[i]; - - codeLengthCounts[lz77Length]++; - } - - // Lengths for representing the code length - Span codeLengthsBuffer = stackalloc byte[64]; // nearest power of 2 alignment for 18*3 = 64 - Span codeLengthLengths = codeLengthsBuffer.Slice(0, 18); - Span codeLengthLengthsMinimum = codeLengthsBuffer.Slice(18, 18); - Span codeLengthLengthsMaximum = codeLengthsBuffer.Slice(18 * 2, 18); - - codeLengthLengths.Clear(); - codeLengthLengthsMinimum.Clear(); - codeLengthLengthsMaximum.Fill(5); - - ComputeCodeLengths(codeLengthCounts, 18, codeLengthLengthsMinimum, codeLengthLengthsMaximum, codeLengthLengths); - - writer.Write(2, 0b00); // HSKIP = 0 (Don't skip code lengths) - - // As per Brotli RFC - Span codeLengthOrder = [1, 2, 3, 4, 0, 5, 17, 6, 16, - 7, 8, 9, 10, 11, 12, 13, 14, 15]; - - // Lengths & codes for representing lengths of code lengths - Span codeLengthLengthLengths = [2, 4, 3, 2, 2, 4]; - Span codeLengthLengthCodes = [0, 7, 3, 2, 1, 15]; - - // Maximum number of code lengths - int numCodeLengths = 18; - while (codeLengthLengths[codeLengthOrder[numCodeLengths - 1]] == 0) - { - numCodeLengths--; - } - - // Max bits written in this loop: 18 * 4 = 72 - for (int i = 0; i < numCodeLengths; i++) - { - int symbol = codeLengthLengths[codeLengthOrder[i]]; - writer.Write(codeLengthLengthLengths[symbol], codeLengthLengthCodes[symbol]); - } - - Span codeLengthBits = stackalloc ushort[32].Slice(0, 18); - codeLengthBits.Clear(); - ComputeCanonicalCode([], [], codeLengthLengths, codeLengthBits); - - for (int i = 0; i < 19; i++) - { - byte rawLength = this.RawLengths[i]; - - writer.Write(codeLengthLengths[rawLength], codeLengthBits[rawLength]); - } - - int numLz77 = JxlFastLosslessEncoder.NumLz77; - while (this.Lz77Lengths[numLz77 - 1] == 0) - { - numLz77--; - } - - // Max bits in this block: 24 - writer.Write(codeLengthLengths[17], codeLengthBits[17]); - writer.Write(3, 0b010); // 5 - writer.Write(codeLengthLengths[17], codeLengthBits[17]); - writer.Write(3, 0b000); // (5 - 2) * 8 + 3 = 27 - writer.Write(codeLengthLengths[17], codeLengthBits[17]); - writer.Write(3, 0b010); // (27 - 2) * 8 + 5 = 205 - - // Encode LZ77 symbols with values 224 + i. - // Max. bits in this loop: 33 * 5 = 165 - for (int i = 0; i < numLz77; i++) - { - writer.Write(codeLengthLengths[this.Lz77Lengths[i]], codeLengthBits[this.Lz77Lengths[i]]); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void EncodeHybridUintLz77(int value, out int token, out int nBits, out int bits) - { - unchecked - { - int n = (int)JxlFastLosslessEncoder.FloorLog2((uint)value); - - if (value < 16) - { - token = value; - nBits = 0; - bits = 0; - } - else - { - token = 16 + n - 4; - nBits = n; - bits = value - (1 << n); - } - } - } -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/JxlFastLosslessEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/JxlFastLosslessEncoder.cs deleted file mode 100644 index e7ddf796b6..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/JxlFastLosslessEncoder.cs +++ /dev/null @@ -1,758 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; -using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd; - -// Suppress IDE0057. This is so we can stack-allocate -// a powers of 2 and then slice it to the appropriate -// length (which produces better code). -// -// Without this suppression, the analyzer produces a warning, -// recommending changing this: -// stackalloc ulong[32].Slice(0, 18) -// to: -// (stackalloc ulong[32])[..18] -// -// But then the analyzer produces a new warning, recommending -// to remove the paranthesis, changing this: -// (stackalloc ulong[32])[..18] -// to: -// stackalloc ulong[32][..18] -// -// which is invalid C# syntax. -#pragma warning disable IDE0057 // Use range operator - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless; - -/// -/// Extreme performance JPEG XL encoder which provides minimal lossless compression. -/// It also uses minimal dependencies. -/// -internal sealed class JxlFastLosslessEncoder -{ - /// - /// Specifies maximum number of bytes a frame header may use. - /// - public const int MaxFrameHeaderSize = 5; - - public const int NumRawSymbols = 19; - - public const int NumLz77 = 33; - - /// - /// Cache/dictionary size for LZ77 - /// - public const int Lz77CacheSize = 32; - - public const int Lz77Offset = 224; // ⚠️ This must always be 224. Don't change this constant. Changing this will break compression. - - public const int Lz77MinLength = 7; - - private static readonly int LogChunkSize = - JxlSimdTarget.Using512BitVectors - ? 5 - : JxlSimdTarget.Using256BitVectors - ? 4 - : 3; - - /// - /// Input frame data is stored here. - /// - private readonly FjxlFrameInputSource input; - - /// - /// Image width of the input image. - /// - private readonly int width; - - /// - /// Image height of the input image. - /// - private readonly int height; - - /// - /// Image width in groups. - /// - private readonly int numGroupsX; - - /// - /// Image height in groups. - /// - private readonly int numGroupsY; - - /// - /// Image width in groups (DC). - /// - private readonly int numDcGroupsX; - - /// - /// Image height in groups (DC). - /// - private readonly int numDcGroupsY; - - /// - /// Number of channels. (f.e. RGBA is 4, YUV is 3) - /// - private readonly int channels; - - /// - /// Number of bits represented per pixel. (f.e. 8 means pixels - /// have a 0-255 range) - /// - /// - /// Higher bit depths can represent more colors. - /// - private readonly int bitDepth; - - /// - /// Should the output image be stored in big-endian order? - /// - private readonly bool isBigEndian; - - private readonly int effort; - - private readonly bool collided; - - /// - /// Prefix codes for LZ77. - /// - private InlineArray4 hcode; - - private readonly List lookup = []; - - /// - /// Bit writer to write the JPEG XL headers. - /// - private readonly FjxlBitWriter header; - - /// - /// Bit writers for writing JPEG XL groups. - /// - private readonly List> groupData = []; - - /// - /// Sizes for each group. - /// - private readonly List groupSizes = []; - - private int acGroupDataOffset; - - private int minDcGlobalSize; - - private int currentBitWriter; - - private int bitWriterBytePos; - - private int bitsInBuffer; - - private long bitBuffer; - - private bool processDone; - - /// - /// Abstracts access to a raster frame data required for encoding. - /// - internal abstract class FjxlFrameInputSource : IDisposable - { - /// - public abstract void Dispose(); - - /// - /// Returns a span that wraps over channel color data at the - /// specified rectangular position. - /// - /// Target type of the color data. - /// Left offset - /// Right offset - /// Selection width - /// Selection height - /// The actual offset of the row in row-major order is stored here. - /// - /// A wrapper over the color data of the channel at the specified - /// position. - /// - public abstract Span GetColorChannelData(int x, int y, int width, int height, out long rowOffset) - where T : unmanaged; - } - - /// - /// Gets minimum raw lengths for prefix coding. - /// - public static ReadOnlySpan MinimumRawLength => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - - /// - /// Gets maximum raw lengths for prefix coding. - /// - public static ReadOnlySpan MaximumRawLength => [7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 10]; - - /// - /// Gets a lookup used by the method - /// to translate a bucket into a base group size. - /// - private static ReadOnlySpan GroupSizeOffset => - [ - 0, - 1024, - 17408, - 4211712 - ]; - - /// - /// Gets a lookup to determine how many bits a TOC bucket uses. - /// - private static ReadOnlySpan TocBits => [12, 16, 24, 32]; - - /// - /// Approximates Floor(Log2(v)) using integers. - /// - /// Value to retrieve Floor(Log2(v)) of. - /// Floor of second logarithm of v, or 31 if v is equal to 0. - /// This method may use CPU intrinsics provided by the .NET Runtime (e.g. BMI1 on x86). - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static uint FloorLog2(uint v) => v == 0 ? 0 : 31u - (uint)BitOperations.LeadingZeroCount(v); - - /// - /// Approximates count trailing zeros of v using integers. - /// - /// Value to retrieve number of 0 bits after last 1 bit of. - /// After the least significant 1 bit, returns the number of 0 bits. E.g. 1000 1000 00 -> 5. - /// This method may use CPU intrinsics provided by the .NET Runtime (e.g. BMI1 on x86). - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint CtzNonZero(ulong v) => (uint)BitOperations.TrailingZeroCount(v); - - /// - /// Returns a TOC bucket based on the group size. - /// - /// Specified group size. - /// TOC bucket matching the appropriate group size. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int TocBucket(int groupSize) - { - int bucket = 0; - - while (bucket < 3 && groupSize >= GroupSizeOffset[bucket + 1]) - { - bucket++; - } - - return bucket; - } - - /// - /// Returns the total number of bits required to represent - /// all given group sizes in the TOC. - /// - /// Group sizes to calculate bit sizes of. - /// Accumulated number of bits required to represent each group size. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int TocSize(Span groupSizes) - { - int tocBits = 0; - - ref int unsafeRef = ref MemoryMarshal.GetReference(groupSizes); - - for (int i = 0; i < groupSizes.Length; i++) - { - // TODO: we can try using AVX2 gather intrinsics, - // especially because TocBits can absolutely fit - // in the L1 cache - int groupSize = Unsafe.Add(ref unsafeRef, i); - int bucketForGroupSize = TocBucket(groupSize); - int bitsUsedByBucket = TocBits[bucketForGroupSize]; - - tocBits += bitsUsedByBucket; - } - - return tocBits; - } - - /// - /// Returns the number of bytes for the frame header. - /// - /// Indicates presence of the alpha channel. - /// Indicates whether this is the final frame. - /// Frame header size in bytes. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int FrameHeaderSize(bool containsAlpha, bool isLast) - { - // Original code (from libjxl): - // - // size_t nbits = 28 + (have_alpha ? 4 : 0) + (is_last ? 0 : 2); - // return (nbits + 7) / 8; - // - // In this implementation we just use constants to shave a few CPU cycles. - // The total amount of branches is reduced by one (for the !containsAlpha case), - // but we remove the arithmetic/shifting instructions. - unchecked - { - if (containsAlpha) - { - if (isLast) - { - return 5; // (34 + 7) / 8 - } - else - { - return 4; // (32 + 7) / 8 - } - } - else - { - return 4; // (30 + 7) / 8 AND (28 + 7) / 8 yield the same result - } - } - } - - private static long GetSectionSize(InlineArray4 groupData) - { - long size = 0; - - for (int j = 0; j < 4; j++) - { - FjxlBitWriter writer = groupData[j]; - - size += (writer.BytesWritten * 8) + writer.BitsInBuffer; - } - - return (size + 7) / 8; - } - - /// - /// Approximates number of bytes needed for the output image buffer. - /// - /// Bytes for the frame buffer. - private long GetOutputSize() - { - long totalSizeGroups = 0; - - Span> groups = CollectionsMarshal.AsSpan(this.groupData); - - for (int i = 0; i < groups.Length; i++) - { - InlineArray4 section = groups[i]; - - totalSizeGroups += GetSectionSize(section); - } - - return this.header.BytesWritten + totalSizeGroups; - } - - /// - /// Returns the maximum amount of bytes potentially required for the image buffer. - /// - /// Upper bound of bytes for frame buffer. - private long GetMaxRequiredOutput() => this.GetOutputSize() + 32; - - private void WriteHeader(bool addImageHeader, bool isLast) - { - FjxlBitWriter output = this.header; - bool haveAlpha = this.channels is 2 or 4; - - if (addImageHeader) - { - // File signature. This signature specifies - // a raw codestream. No container format here. - output.Write(16, 0x0AFF); - - // Handcrafted size header. - output.Write(1, 0); // Not small - - WriteSize(this.height); - output.Write(3, 0b000); // No special ratio - WriteSize(this.width); - - // Handcrafted image metadata - output.Write(1, 0); // all_default = 0 (don't assume values to be set to their defaults) - output.Write(1, 0); // extra_fields = 0 (extra fields are disabled and therefore not present) - output.Write(1, 0); // bit_depth.floating_point_sample = 0 (samples are integers) - - if (this.bitDepth == 8) - { - output.Write(2, 0b00); // bit_depth.bits_per_sample = 8 (predefined bit depth of 8 bits) - } - else if (this.bitDepth == 10) - { - output.Write(2, 0b01); // bit_depth.bits_per_sample = 10 (predefined bit depth of 10 bits) - } - else if (this.bitDepth == 12) - { - output.Write(2, 0b10); // bit_depth.bits_per_sample = 12 (predefined bit depth of 12 bits) - } - else - { - output.Write(2, 0b11); // Custom bit depth - output.Write(6, (ulong)this.bitDepth - 1); // bit depth minus 1 (so 0 becomes 1, 9 becomes 10, etc) - } - - if (this.bitDepth <= 14) - { - output.Write(1, 1); // 16-bit-buffer is sufficient - } - else - { - output.Write(1, 0); // 16-bit-buffer is NOT sufficient - } - - if (haveAlpha) - { - output.Write(2, 0b01); // Emit one extra channel (the alpha channel) - - if (this.bitDepth == 8) - { - output.Write(1, 1); // all_default = 1 (8-bit alpha is the default) - } - else - { - output.Write(1, 0); // all_default = 0 - output.Write(2, 0); // type = alpha - output.Write(1, 0); // samples are not floating point - - if (this.bitDepth == 10) - { - output.Write(2, 0b01); // bit_depth.bits_per_sample = 10 (predefined bit depth of 10 bits) - } - else if (this.bitDepth == 12) - { - output.Write(2, 0b10); // bit_depth.bits_per_sample = 12 (predefined bit depth of 12 bits) - } - else - { - output.Write(2, 0b11); // Custom bit depth - output.Write(6, (ulong)this.bitDepth - 1); // bit depth minus 1 (so 0 becomes 1, 9 becomes 10, etc) - } - - output.Write(2, 0); // dim_shift = 0 - output.Write(2, 0); // name_len = 0 - output.Write(1, 0); // alpha_associated = 0 - } - } - else - { - output.Write(2, 0b00); // 0 extra channels - } - - output.Write(1, 0); // not XYB - - if (this.channels > 2) - { - output.Write(1, 1); // color_encoding.all_default = 1 (sRGB) - } - else - { - output.Write(1, 0); // color_encoding.all_default = 0 - output.Write(1, 0); // color_encoding.want_icc = 0 - output.Write(2, 0b01); // Grayscale - output.Write(2, 0b01); // D65 - output.Write(1, 0); // No gamma transfer function - output.Write(2, 0b10); // transfer function: 2 + u(4) - output.Write(4, 11); // transfer function (specifies sRGB) - output.Write(2, 1); // relative rendering intent - } - - output.Write(2, 0b00); // No extensions - output.Write(1, 1); // all_default transform data - output.ZeroPadToByte(); // No ICC and no preview. Frame should start at byte boundary. - } - - // Handcrafted frame header - output.Write(1, 0); // all_default = 0 (non-default values) - output.Write(2, 0b00); // regular frame - output.Write(1, 1); // modular - output.Write(2, 0b00); // default flags - output.Write(1, 0); // not Y'Cb'Cr - output.Write(2, 0b00); // no upsampling - - if (haveAlpha) - { - output.Write(2, 0b00); // no alpha upsampling - } - - output.Write(2, 0b01); // default group size - output.Write(2, 0b00); // exactly one pass - output.Write(1, 0); // no custom size or origin - output.Write(2, 0b00); // Replace blending mode - - if (haveAlpha) - { - output.Write(2, 0b00); // Replace blending mode for alpha channel - } - - output.Write(2, 0b00); // a frame has no name - output.Write(1, 0); // loop filter is not all_default - output.Write(1, 0); // no Gaborish transform - output.Write(2, 0b00); // 0 EPF filters - output.Write(2, 0b00); // no LF extensions - output.Write(2, 0b00); // no FH extensions - - output.Write(1, 0); // no TOC permutation - output.ZeroPadToByte(); // TOC is byte aligned - - Span groupSizes = CollectionsMarshal.AsSpan(this.groupSizes); - - for (int i = 0; i < groupSizes.Length; i++) - { - int groupSize = groupSizes[i]; - - int bucket = TocBucket(groupSize); - output.Write(2, (ulong)bucket); - output.Write(TocBits[bucket] - 2, (ulong)(groupSize - GroupSizeOffset[bucket])); - } - - output.ZeroPadToByte(); // Groups are byte-aligned - - // Sizes are coded using a special variable-length - // kind of coding. This method does that here. - // - // It has a prefix of 2 bits, followed by the suffix of N - // bits which depend on the prefix: - // - // prefix 0b00: 9 consecutive bits - // prefix 0b01: 13 consecutive bits - // prefix 0b10: 18 consecutive bits - // prefix 0b11: 30 consecutive bits - void WriteSize(int size) - { - ulong sizeMinus1 = (ulong)size - 1uL; - - if (sizeMinus1 < (1 << 9)) - { - output.Write(2, 0b00); // 9 bits - output.Write(9, sizeMinus1); - } - else if (sizeMinus1 < (1 << 13)) - { - output.Write(2, 0b01); // 13 bits - output.Write(13, sizeMinus1); - } - else if (sizeMinus1 < (1 << 18)) - { - output.Write(2, 0b10); // 18 bits - output.Write(18, sizeMinus1); - } - else - { - output.Write(2, 0b11); // 30 bits - output.Write(30, sizeMinus1); - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int ComputeDcGlobalPadding(Span groupSizes, int acGroupDataOffset, int minDcGlobalSize, bool containsAlpha, bool isLast) - { - // Libjxl reference implements this method like this: - /* - size_t ComputeDcGlobalPadding(const std::vector& group_sizes, - size_t ac_group_data_offset, - size_t min_dc_global_size, bool have_alpha, - bool is_last) { - std::vector new_group_sizes = group_sizes; - new_group_sizes[0] = min_dc_global_size; - size_t toc_size = TOCSize(new_group_sizes); - size_t actual_offset = - FrameHeaderSize(have_alpha, is_last) + toc_size + group_sizes[0]; - return ac_group_data_offset - actual_offset; - } - */ - // The reference implementation copies the entire vector so that - // element 0 can be modified without affecting the original. - // Since TocSize() does not throw, temporarily modify element 0 - // instead, avoiding the allocation and copy. - int firstItem = groupSizes[0]; - groupSizes[0] = minDcGlobalSize; - int tocSize = TocSize(groupSizes); - int actualOffset = FrameHeaderSize(containsAlpha, isLast) + tocSize + firstItem; - groupSizes[0] = firstItem; - return acGroupDataOffset - actualOffset; - } - - private static int PredictPixels(Span pixels, Span pixelsLeft, Span pixelsTop, Span pixelsTopleft, Span residuals, T negativeOne) - where T : unmanaged - { - Vector px = Vector.Create(pixels); - Vector left = Vector.Create(pixelsLeft); - Vector top = Vector.Create(pixelsTop); - Vector topleft = Vector.Create(pixelsTopleft); - - Vector ac = left - topleft; - Vector ab = left - top; - Vector bc = top - topleft; - Vector grad = ac + top; - Vector d = ab ^ bc; - Vector zero = Vector.Zero; - Vector clamp = Vector.ConditionalSelect(Vector.GreaterThan(zero, d), top, left); - Vector s = ac ^ bc; - Vector pred = Vector.ConditionalSelect(Vector.GreaterThan(zero, s), grad, clamp); - - Vector res = px - pred; - Vector resTimes2 = res + res; - res = Vector.ConditionalSelect(Vector.GreaterThan(zero, res), Vector.Create(negativeOne) - resTimes2, resTimes2); - res.CopyTo(residuals); - - return FjxlSimdUtils.CountPrefix(Vector.Equals(res, zero)); - } - - private static void EncodeHybridUint000(uint value, out uint token, out uint nbits, out uint bits) - { - if (value == 0) - { - token = 0; - nbits = 0; - bits = 0; - return; - } - - uint n = FloorLog2(value); - token = n + 1; - nbits = n; - bits = value - (1u << (int)n); - } - - private static void GenericEncodeChunk(ReadOnlySpan residuals, int n, int skip, FjxlPrefixCode code, ref FjxlBitWriter output) - { - for (int ix = skip; ix < n; ix++) - { - EncodeHybridUint000(residuals[ix], out uint token, out uint nbits, out uint bits); - output.Write((int)(code.RawLengths[(int)token] + nbits), code.RawCodes[(int)token] | (bits << code.RawLengths[(int)token])); - } - } - - public static void PrepareDcGlobalCommon(InlineArray4 code, FjxlBitWriter output) - { - // No patches, spline, or noise. - output.Write(1, 1); // Default DC dequantization factors - output.Write(1, 1); // Use global trees and histograms - output.Write(1, 0); // Don't use LZ77 compression for the tree. - - output.Write(1, 1); // Simple code for the tree's context map - output.Write(2, 0); // All contexts clustered together - output.Write(1, 1); // Use prefix code for tree - output.Write(4, 0); // 000 hybrid unsigned integer - output.Write(6, 0b100011); // Alphabet size is 4 - output.Write(2, 1); // Simple prefix code - output.Write(2, 3); // With 4 symbols - - output.Write(2, 0); - output.Write(2, 1); - output.Write(2, 2); - output.Write(2, 3); - output.Write(1, 0); // First tree encoding option - - // Huffman table and extra bits for the tree - Span symbolBits = [0b00, 0b10, 0b001, 0b101, 0b0011, 0b0111]; - Span symbolLengths = [2, 2, 3, 3, 4, 4]; - - Span predictors = - [ - 1, 2, 1, 4, 1, 0, 0, 5, 0, 0, 0, 0, 5, - 0, 0, 0, 0, 5, 0, 0, 0, 0, 5, 0, 0, 0 - ]; - - ref byte unsafePred = ref MemoryMarshal.GetReference(predictors); - - for (int i = 0; i < predictors.Length; i++) - { - byte predictor = Unsafe.AddByteOffset(ref unsafePred, i); - output.Write(symbolLengths[predictor], symbolBits[predictor]); - } - - output.Write(1, 1); // Enable LZ77 compression for the main bit-stream - output.Write(2, 0b00); // LZ77 offset 224 - output.Write(4, 0b1010); // LZ77 minimum length 7 - - // 400 hybrid uint configuration for LZ77 - output.Write(4, 4); - output.Write(3, 0); - output.Write(3, 0); - - output.Write(1, 1); // simple code for the context map - output.Write(2, 3); // 3 bits per entry - output.Write(3, 4); // channel 3 - output.Write(3, 3); // channel 2 - output.Write(3, 2); // channel 1 - output.Write(3, 1); // channel 0 - output.Write(3, 0); // distance histogram first - - output.Write(1, 1); // use prefix codes - output.Write(4, 0); // 000 hybrid uint configuration for distances (only need 0) - - for (int i = 0; i < 4; i++) - { - output.Write(4, 0); // 000 hybrid uint configuration for symbols (only <= 10) - } - - // Distance alphabet size - output.Write(5, 0b00001); // 2: just need 1 for RLE (i.e. distance 1) - - // Symbol + LZ77 alphabet size - for (int i = 0; i < 4; i++) - { - output.Write(1, 1); // > 1 - output.Write(4, 8); // <= 512 - output.Write(8, 256); // == 512 - } - - // Distance histogram - output.Write(2, 1); // simple prefix code - output.Write(2, 0); // With 1 symbol - output.Write(1, 1); // 1 - - // write all prefix codes - for (int i = 0; i < 4; i++) - { - code[i].Write(output); - } - - // group header for global modular image - output.Write(1, 1); // Global tree - output.Write(1, 1); // All default weighted prediction - } - - public static void PrepareDcGlobal(int numChannels, bool isSingleGroup, InlineArray4 codes, FjxlBitWriter output) - { - PrepareDcGlobalCommon(codes, output); - - if (numChannels > 2) - { - output.Write(2, 0b01); // 1 transform - output.Write(2, 0b00); // RCT - output.Write(5, 0b00000); // Starting from channel 0 - output.Write(2, 0b00); // YCoCg - } - else - { - output.Write(2, 0b00); // 0 transforms - } - - if (!isSingleGroup) - { - output.ZeroPadToByte(); - } - } - - /// - /// Pair of two vectors. - /// - /// Type of the vector. - /// Low vector - /// High vector - private struct VectorPair(T lo, T hi) - where T : unmanaged - { - public T Low = lo; - public T High = hi; - } - - internal static class SimdVector32 - { - public static Vector ValueToToken(Vector vec) => Vector.Create(32) - SimdUtils.Lzcnt(vec); - - public static Vector SaturateSubtract(Vector a, Vector b) => Vector.Max(a, b) - b; - - public static Vector Pow2(Vector x) => SimdUtils.Pow2(x); - } -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderExactly14Bits.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderExactly14Bits.cs deleted file mode 100644 index f404e357d7..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderExactly14Bits.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Diagnostics; -using System.Runtime.InteropServices; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd.ChunkEncoders; - -internal readonly struct FjxlChunkEncoderExactly14Bits -{ - public const int BitDepth = 14; - public const int InputBytes = 2; - - public static ReadOnlySpan MinRawLength => - [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 8, 7 - ]; - - public static ReadOnlySpan MaxRawLength => - [ - 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 10 - ]; - - public static int MaxEncodedBitsPerSample() => 23; - - public static void PrepareForSimd(ReadOnlySpan nbits, ReadOnlySpan bits, Span nbitsSimd, Span bitsSimd) - { - CheckHuffmanBitsSimd(bits[15], nbits[15], bits[16], nbits[16]); - - nbits[..16].CopyTo(nbitsSimd); - bits[..16].CopyTo(bitsSimd); - } - - public static void EncodeChunkSimd(Span residuals, int n, int skip, ReadOnlySpan rawNBitsSimd, ReadOnlySpan rawBitsSimd, ref FjxlBitWriter output) - { - Span bits32 = stackalloc FjxlBits32[2 * ChunkSize / FjxlSimdVec16.Lanes]; - - Span buffer = stackalloc ushort[FjxlSimdVec16.Lanes * 5]; - Span bits = buffer.Slice(0, FjxlSimdVec16.Lanes); - Span nbits = buffer.Slice(FjxlSimdVec16.Lanes, FjxlSimdVec16.Lanes); - Span bitsHuff = buffer.Slice(FjxlSimdVec16.Lanes * 2, FjxlSimdVec16.Lanes); - Span nbitsHuff = buffer.Slice(FjxlSimdVec16.Lanes * 3, FjxlSimdVec16.Lanes); - Span token = buffer.Slice(FjxlSimdVec16.Lanes * 4, FjxlSimdVec16.Lanes); - - for (int i = 0; i < ChunkSize; i += FjxlSimdVec16.Lanes) - { - FjxlTokenization.TokenizeSimd(residuals[i..], token, nbits, bits); - FjxlHuffman.HuffmanSimd14(token, rawNBitsSimd, rawBitsSimd, nbitsHuff, bitsHuff); - - StoreSimdUpTo14( - nbits, - bits, - nbitsHuff, - bitsHuff, - Math.Max(n, i) - i, - Math.Max(skip, i) - i, - bits32[(2 * i / FjxlSimdVec16.Lanes)..]); - } - - StoreToWriter(bits32, ref output); - } - - public static int NumSymbols() => 17; -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderFrom9To13Bits.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderFrom9To13Bits.cs deleted file mode 100644 index 7bb349abef..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderFrom9To13Bits.cs +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Diagnostics; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd.ChunkEncoders; - -internal readonly struct FjxlChunkEncoderFrom9To13Bits -{ - public const int InputBytes = 2; - - private readonly int bitDepth; - - public FjxlChunkEncoderFrom9To13Bits(int bitDepth) - { - DebugGuard.MustBeLessThanOrEqualTo(bitDepth, 13, nameof(bitDepth)); - DebugGuard.MustBeGreaterThanOrEqualTo(bitDepth, 9, nameof(bitDepth)); - - this.bitDepth = bitDepth; - } - - public static ReadOnlySpan MinRawLength => - [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 - ]; - - public static ReadOnlySpan MaxRawLength => - [ - 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 10 - ]; - - public static int MaxEncodedBitsPerSample() => 22; - - public static void PrepareForSimd(ReadOnlySpan nbits, ReadOnlySpan bits, int n, Span nbitsSimd, Span bitsSimd) - { - DebugGuard.MustBeLessThanOrEqualTo(n, 16, nameof(n)); - - nbits[..16].CopyTo(nbitsSimd); - bits[..16].CopyTo(bitsSimd); - } - - public static void EncodeChunkSimd(Span residuals, int n, int skip, ReadOnlySpan rawNBitsSimd, ReadOnlySpan rawBitsSimd, ref FjxlBitWriter output) - { - Span bits32 = stackalloc FjxlBits32[2 * ChunkSize / FjxlSimdVec16.Lanes]; - - Span buffer = stackalloc ushort[FjxlSimdVec16.Lanes * 5]; - Span bits = buffer.Slice(0, FjxlSimdVec16.Lanes); - Span nbits = buffer.Slice(FjxlSimdVec16.Lanes, FjxlSimdVec16.Lanes); - Span bitsHuff = buffer.Slice(FjxlSimdVec16.Lanes * 2, FjxlSimdVec16.Lanes); - Span nbitsHuff = buffer.Slice(FjxlSimdVec16.Lanes * 3, FjxlSimdVec16.Lanes); - Span token = buffer.Slice(FjxlSimdVec16.Lanes * 4, FjxlSimdVec16.Lanes); - - for (int i = 0; i < ChunkSize; i += FjxlSimdVec16.Lanes) - { - TokenizeSimd(residuals[i..], token, nbits, bits); - HuffmanSimdUpTo13(token, rawNBitsSimd, rawBitsSimd, nbitsHuff, bitsHuff); - - StoreSimdUpTo14( - nbits, - bits, - nbitsHuff, - bitsHuff, - Math.Max(n, i) - i, - Math.Max(skip, i) - i, - bits32[(2 * i / FjxlSimdVec16.Lanes)..]); - } - - StoreToWriter(bits32, ref output); - } - - public int NumSymbols(bool doingYcocgOrLargePalette) => this.bitDepth + (doingYcocgOrLargePalette ? 3 : 2); -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderMoreThan14Bits.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderMoreThan14Bits.cs deleted file mode 100644 index 235f33508d..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderMoreThan14Bits.cs +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd.ChunkEncoders; - -internal readonly struct FjxlChunkEncoderMoreThan14Bits -{ - public const int InputBytes = 2; - - private readonly int bitDepth; - - public FjxlChunkEncoderMoreThan14Bits(int bitDepth) - { - DebugGuard.MustBeGreaterThan(bitDepth, 14, nameof(bitDepth)); - DebugGuard.MustBeLessThanOrEqualTo(bitDepth, 16, nameof(bitDepth)); - - this.bitDepth = bitDepth; - } - - public static ReadOnlySpan MinRawLength => - [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 8, 8, 8, 8, 8, 8, 7 - ]; - - public static ReadOnlySpan MaxRawLength => - [ - 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, - 7, 7, 7, 8, 8, 8, 8, 8, 8, 10 - ]; - - public static int MaxEncodedBitsPerSample() => 25; - - public static void PrepareForSimd(ReadOnlySpan nbits, ReadOnlySpan bits, Span nbitsSimd, Span bitsSimd) - { - CheckHuffmanBitsSimd(bits[13], nbits[13], bits[14], nbits[14]); - CheckHuffmanBitsSimd(bits[15], nbits[15], bits[16], nbits[16]); - CheckHuffmanBitsSimd(bits[17], nbits[17], bits[18], nbits[18]); - - nbits[..14].CopyTo(nbitsSimd); - bits[..14].CopyTo(bitsSimd); - - nbitsSimd[14] = nbits[15]; - bitsSimd[14] = bits[15]; - nbitsSimd[15] = nbits[17]; - bitsSimd[15] = bits[17]; - } - - public static void EncodeChunkSimd(Span residuals, int n, int skip, ReadOnlySpan rawNBitsSimd, ReadOnlySpan rawBitsSimd, ref FjxlBitWriter output) - { - Span bits32 = stackalloc FjxlBits32[2 * ChunkSize / FjxlSimdVec16.Lanes]; - - Span bufferU32 = stackalloc uint[FjxlSimdVec16.Lanes * 2]; - Span bits = bufferU32.Slice(0, FjxlSimdVec16.Lanes); - Span nbits = bufferU32.Slice(FjxlSimdVec16, FjxlSimdVec16.Lanes); - - Span bufferU16 = stackalloc uint[FjxlSimdVec16.Lanes * 3]; - Span bitsHuff = bufferU16.Slice(0, FjxlSimdVec16.Lanes); - Span nbitsHuff = bufferU16.Slice(FjxlSimdVec16.Lanes, FjxlSimdVec16.Lanes); - Span token = bufferU16.Slice(FjxlSimdVec16.Lanes * 2, FjxlSimdVec16.Lanes); - - for (int i = 0; i < ChunkSize; i += FjxlSimdVec16.Lanes) - { - TokenizeSimd(residuals[i..], token, nbits, bits); - HuffmanSimdAbove14(token, rawNBitsSimd, rawBitsSimd, nbitsHuff, bitsHuff); - - StoreSimdAbove14( - nbits, - bits, - nbitsHuff, - bitsHuff, - Math.Max(n, i) - i, - Math.Max(skip, i) - i, - bits32[(2 * i / FjxlSimdVec16.Lanes)..]); - } - - StoreToWriter(bits32, ref output); - } - - public static int NumSymbols() => 19; -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderUpTo8Bits.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderUpTo8Bits.cs deleted file mode 100644 index 89faa2f2c5..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/ChunkEncoders/FjxlChunkEncoderUpTo8Bits.cs +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd.ChunkEncoders; - -internal readonly struct FjxlChunkEncoderUpTo8Bits -{ - public const int InputBytes = 1; - - private readonly int bitDepth; - - public FjxlChunkEncoderUpTo8Bits(int bitDepth) - { - DebugGuard.MustBeLessThanOrEqualTo(bitDepth, 8, nameof(bitDepth)); - - this.bitDepth = bitDepth; - } - - public static ReadOnlySpan MinRawLength => - [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 - ]; - - public static ReadOnlySpan MaxRawLength => - [ - 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 10 - ]; - - public static int MaxEncodedBitsPerSample() => 16; - - public static void PrepareForSimd(ReadOnlySpan nbits, ReadOnlySpan bits, int n, Span nbitsSimd, Span bitsSimd) - { - DebugGuard.MustBeLessThanOrEqualTo(n, 16, nameof(n)); - - nbits[..16].CopyTo(nbitsSimd); - bits[..16].CopyTo(bitsSimd); - } - - public static void EncodeChunkSimd(Span residuals, int n, int skip, ReadOnlySpan rawNBitsSimd, ReadOnlySpan rawBitsSimd, ref FjxlBitWriter output) - { - Span bits32 = stackalloc FjxlBits32[ChunkSize / FjxlSimdVec16.Lanes]; - - Span buffer = stackalloc ushort[FjxlSimdVec16.Lanes * 5]; - Span bits = buffer.Slice(0, FjxlSimdVec16.Lanes); - Span nbits = buffer.Slice(FjxlSimdVec16.Lanes, FjxlSimdVec16.Lanes); - Span bitsHuff = buffer.Slice(FjxlSimdVec16.Lanes * 2, FjxlSimdVec16.Lanes); - Span nbitsHuff = buffer.Slice(FjxlSimdVec16.Lanes * 3, FjxlSimdVec16.Lanes); - Span token = buffer.Slice(FjxlSimdVec16.Lanes * 4, FjxlSimdVec16.Lanes); - - for (int i = 0; i < ChunkSize; i += FjxlSimdVec16.Lanes) - { - TokenizeSimd(residuals[i..], token, nbits, bits); - HuffmanSimdUpTo13(token, rawNBitsSimd, rawBitsSimd, nbitsHuff, bitsHuff); - - StoreSimdUpTo8( - nbits, - bits, - nbitsHuff, - bitsHuff, - Math.Max(n, i) - i, - Math.Max(skip, i) - i, - bits32[(int)(i / FjxlSimdVec16.Lanes)..]); - } - - StoreToWriter(bits32, ref output); - } - - public int NumSymbols(bool doingYcocgOrLargePalette) => this.bitDepth + (doingYcocgOrLargePalette ? 3 : 2); -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlBits32.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlBits32.cs deleted file mode 100644 index 2bf012e441..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlBits32.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using System.Runtime.Intrinsics; -using System.Runtime.Intrinsics.Arm; -using static SharpImmintrin.CIntrinsicArmAdvSimd; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd; - -/// -/// Contains a 32-bit pair of code lengths and code values, useful -/// for Huffman encoding. -/// -internal struct FjxlBits32(Vector counts, Vector bits) -{ - /// - /// Gets or sets the code lengths. - /// - public Vector Counts { get; set; } = counts; - - /// - /// Gets or sets the code values. - /// - public Vector Bits { get; set; } = bits; -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlBits64.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlBits64.cs deleted file mode 100644 index dca740cfbf..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlBits64.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using System.Runtime.CompilerServices; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd; - -/// -/// Contains a 64-bit pair of code lengths and code values, useful -/// for Huffman encoding. -/// -internal struct FjxlBits64(Vector counts, Vector bits) -{ - public static int Lanes { get; } = Vector.Count; - - /// - /// Gets or sets the code lengths. - /// - public Vector Counts { get; set; } = counts; - - /// - /// Gets or sets the code values. - /// - public Vector Bits { get; set; } = bits; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly void Store(ref ulong nbitsOut, ref ulong bitsOut) - { - this.Counts.StoreUnsafe(ref nbitsOut); - this.Bits.StoreUnsafe(ref bitsOut); - } -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlHuffman.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlHuffman.cs deleted file mode 100644 index 13c702f73f..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlHuffman.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd; - -internal static class FjxlHuffman -{ - public static void HuffmanSimdUpTo13(Span tokens, ReadOnlySpan rawNbitsSimd, ReadOnlySpan rawBitsSimd, Span nbitsOut, Span bitsOut) - { - Vector tok = FjxlSimdUtils.PrepareForU8Lookup(Vector.Create(tokens)); - - FjxlSimdUtils.U8Lookup(tok, rawNbitsSimd).CopyTo(nbitsOut); - FjxlSimdUtils.U8Lookup(tok, rawBitsSimd).CopyTo(bitsOut); - } - - public static void HuffmanSimd14(Span tokens, ReadOnlySpan rawNbitsSimd, ReadOnlySpan rawBitsSimd, Span nbitsOut, Span bitsOut) - { - Vector tokenCap = Vector.Create((short)15); - Vector tok = Vector.Create(tokens); - - Vector tokIndex = FjxlSimdUtils.PrepareForU8Lookup(Vector.Min(tok, tokenCap)); - - Vector huffBitsPre = FjxlSimdUtils.U8Lookup(tokIndex, rawBitsSimd); - - Vector needsHighBit = Vector.Equals(tok, Vector.Create((short)16)); - - Vector huffBits = Vector.ConditionalSelect( - needsHighBit, - huffBitsPre | Vector.Create((short)128), - huffBitsPre); - - huffBits.CopyTo(bitsOut); - - FjxlSimdUtils.U8Lookup(tokIndex, rawNbitsSimd) - .CopyTo(nbitsOut); - } - - public static void HuffmanSimdAbove14(Span tokens, ReadOnlySpan rawNbitsSimd, ReadOnlySpan rawBitsSimd, Span nbitsOut, Span bitsOut) - { - Vector tok = Vector.Create(tokens); - Vector above = Vector.GreaterThan(tok, Vector.Create((short)12)); - - Vector remapTok = Vector.ConditionalSelect( - above, - FjxlSimdUtils.HorizontalAdd(tok, Vector.Create((short)13)), - tok); - - Vector tokIndex = FjxlSimdUtils.PrepareForU8Lookup(remapTok); - Vector huffBitsPre = FjxlSimdUtils.U8Lookup(tokIndex, rawBitsSimd); - - Vector evenTok = tok & Vector.Create(unchecked((short)0xFFF)); - Vector needsHighBit = above & Vector.Equals(tok, evenTok); - - Vector huffBits = Vector.ConditionalSelect( - needsHighBit, - huffBitsPre | Vector.Create((short)128), - huffBitsPre); - - huffBits.CopyTo(bitsOut); - - FjxlSimdUtils.U8Lookup(tokIndex, rawNbitsSimd) - .CopyTo(nbitsOut); - } -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlRowFill.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlRowFill.cs deleted file mode 100644 index 2c7217befa..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlRowFill.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using System.Runtime.CompilerServices; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd; - -internal static class FjxlRowFill -{ - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void FillRowG8(Span rgba, int oxs, Span luma) - where T : unmanaged, INumber - { - int x = 0; - - for (; x + FjxlSimdVec16.Lanes <= oxs; x += FjxlSimdVec16.Lanes) - { - Vector rgb = FjxlSimdVec16.LoadG8(rgba[x..]); - StorePixels(rgb, luma[x..]); - } - } -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlSimdUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlSimdUtils.cs deleted file mode 100644 index 6cdaf7f57b..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlSimdUtils.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd; - -/// -/// SIMD utilities highly specific to the Fast Lossless JXL encoder. -/// -internal static class FjxlSimdUtils -{ - public static Vector HorizontalAdd(this Vector a, Vector b) => (a + b) >> 1; -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlSimdVec16.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlSimdVec16.cs deleted file mode 100644 index 9ee046eea0..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlSimdVec16.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd; - -/// -/// Utilities for 16-bit SIMD vectors. -/// -internal static class FjxlSimdVec16 -{ - public static int Lanes { get; } = Vector.Count; -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlStoreSimd.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlStoreSimd.cs deleted file mode 100644 index 03f78a92d8..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlStoreSimd.cs +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd; - -internal static class FjxlStoreSimd -{ - public static void StoreSimdUpTo8(Span nbitsTok, Span bitsTok, Span nbitsHuff, Span bitsHuff, int n, int skip, Span bitsOut) - { - FjxlBits16 bits = FjxlBits16.FromRaw( - Vector.Create(nbitsTok), - Vector.Create(bitsTok)); - - FjxlBits16 huffBits = FjxlBits16.FromRaw( - Vector.Create(nbitsHuff), - Vector.Create(bitsHuff)); - - bits.Interleave(huffBits); - bits.ClipTo(n); - bits.Skip(skip); - - bitsOut[0] = bits.Merge(); - } - - public static void StoreSimdUpTo14(Span nbitsTok, Span bitsTok, Span nbitsHuff, Span bitsHuff, int n, int skip, Span bitsOut) - { - VecPair> bits = Vector.Create(bitsTok).Interleave(Vector.Create(bitsHuff)); - VecPair> nbits = Vector.Create(nbitsTok).Interleave(Vector.Create(nbitsHuff)); - - Bits16 low = Bits16.FromRaw(nbits.Low, bits.Low); - Bits16 hi = Bits16.FromRaw(nbits.High, bits.High); - - int lanes = Vector.Count; - - low.ClipTo(2 * n); - low.Skip(2 * skip); - - hi.ClipTo(Math.Max(2 * n, lanes) - lanes); - hi.Skip(Math.Max(2 * skip, lanes) - lanes); - - bitsOut[0] = low.Merge(); - bitsOut[1] = hi.Merge(); - } - - public static void StoreSimdAbove14(Span nbitsTok, Span bitsTok, Span nbitsHuff, Span bitsHuff, int n, int skip, Span bitsOut) - { - Vector nbitsTokLo = Vector.Create(nbitsTok); - Vector bitsTokLo = Vector.Create(bitsTok); - - Vector nbitsTokHi = Vector.Create(nbitsTok[Vector.Count..]); - Vector bitsTokHi = Vector.Create(bitsTok[Vector.Count..]); - - Bits32 bitsLow = Bits32.FromRaw(nbitsTokLo, bitsTokLo); - Bits32 bitsHi = Bits32.FromRaw(nbitsTokHi, bitsTokHi); - - VecPair> huffBits = FjxlSimdUtils.Upcast(Vector.Create(bitsHuff)); - VecPair> huffNbits = FjxlSimdUtils.Upcast(Vector.Create(nbitsHuff)); - - Bits32 huffLow = Bits32.FromRaw(huffNbits.Low, huffBits.Low); - Bits32 huffHi = Bits32.FromRaw(huffNbits.High, huffBits.High); - - bitsLow.Interleave(huffLow); - bitsLow.ClipTo(n); - bitsLow.Skip(skip); - bitsOut[0] = bitsLow; - - int lanes = Vector.Count; - - bitsHi.Interleave(huffHi); - bitsHi.ClipTo(Math.Max(n, lanes) - lanes); - bitsHi.Skip(Math.Max(skip, lanes) - lanes); - bitsOut[1] = bitsHi; - } - - public static void StoreToWriter(ReadOnlySpan bits, FjxlBitWriter output, int n) - { - Span nbits64 = stackalloc ulong[Bits64.Lanes * n]; - Span bits64 = stackalloc ulong[Bits64.Lanes * n]; - - bits[0].Merge().Store(nbits64, bits64); - - if (n > 1) - { - bits[1].Merge().Store(nbits64[Bits64.Lanes..], bits64[Bits64.Lanes..]); - } - - if (n > 2) - { - bits[2].Merge().Store(nbits64[(2 * Bits64.Lanes)..], bits64[(2 * Bits64.Lanes)..]); - } - - if (n > 3) - { - bits[3].Merge().Store(nbits64[(3 * Bits64.Lanes)..], bits64[(3 * Bits64.Lanes)..]); - } - - output.WriteMultiple(nbits64, bits64, Bits64.Lanes * n); - } -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlTokenization.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlTokenization.cs deleted file mode 100644 index aed8a20576..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/FastLossless/Simd/FjxlTokenization.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.FastLossless.Simd; - -internal static class FjxlTokenization -{ - public static void Tokenize16(Span residuals, Span output, Span nbitsOut, Span bitsOut) - { - Vector res = Vector.Create(residuals); - Vector token = FjxlSimdUtils.ValueToToken(res); - Vector nbits = FjxlSimdUtils.SaturateSubtract(token, Vector.One); - Vector bits = FjxlSimdUtils.SaturateSubtract(res, FjxlSimdUtils.Pow2(nbits)); - - token.CopyTo(output); - nbits.CopyTo(nbitsOut); - bits.CopyTo(bitsOut); - } - - public static void Tokenize32(Span residuals, Span tokenOut, Span nbitsOut, Span bitsOut) - { - Vector resLo = Vector.Create(residuals); - Vector resHi = Vector.Create(residuals[Vector.Count..]); - - Vector tokenLo = FjxlSimdUtils.ValueToToken(resLo); - Vector tokenHi = FjxlSimdUtils.ValueToToken(resHi); - - Vector nbitsLo = FjxlSimdUtils.SaturateSubtract(tokenLo, Vector.One); - Vector nbitsHi = FjxlSimdUtils.SaturateSubtract(tokenHi, Vector.One); - - Vector bitsLo = FjxlSimdUtils.SaturateSubtract(resLo, FjxlSimdUtils.Pow2(nbitsLo)); - Vector bitsHi = FjxlSimdUtils.SaturateSubtract(resHi, FjxlSimdUtils.Pow2(nbitsHi)); - - Vector token = FjxlSimdUtils.FromTwo32(tokenLo, tokenHi); - token.CopyTo(tokenOut); - - nbitsLo.CopyTo(nbitsOut); - nbitsHi.CopyTo(nbitsOut[Vector.Count..]); - - bitsLo.CopyTo(bitsOut); - bitsHi.CopyTo(bitsOut[Vector.Count..]); - } -} From afbed06f72fc38982e9aacd515f5995d55d7db2c Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 16 Sep 2026 21:08:56 +0400 Subject: [PATCH 184/328] Add adaptive quantization progress --- .../Encoder/JxlAdaptiveQuantization.cs | 1060 +++++++++++++++++ 1 file changed, 1060 insertions(+) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs index 00150b3dd3..56303ac96f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs @@ -1,7 +1,19 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; +using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Memory; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Group; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Modular; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; @@ -18,6 +30,10 @@ internal static class JxlAdaptiveQuantization private const float SGRetMul = SGMul2 * 18.6580932135f * JxlMath.InverseLog2E; private const float SGVOffset = 7.7825991679894591f; + private const float DcQuantPow = 0.83f; + private const float DcQuant = 1.095924047623553f; + private const float AcQuant = 0.765f; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static float ComputeMaskForAcStrategyUse(float outputValue) { @@ -27,6 +43,25 @@ public static float ComputeMaskForAcStrategyUse(float outputValue) return multiplier / (outputValue + offset); } + public static Vector ComputeMask(Vector outputValue) + { + Vector @base = Vector.Create(-0.7647f); + Vector mul4 = Vector.Create(9.4708735624378946f); + Vector mul2 = Vector.Create(17.35036561631863f); + Vector offset2 = Vector.Create(302.59587815579727f); + Vector mul3 = Vector.Create(6.7943250517376494f); + Vector offset3 = Vector.Create(3.7179635626140772f); + Vector offset4 = Vector.Create(0.25f) * offset3; + Vector mul0 = Vector.Create(0.80061762862741759f); + + Vector v1 = Vector.Max(outputValue * mul0, Vector.Create(1e-3f)); + Vector v2 = Vector.One / (v1 + offset2); + Vector v3 = Vector.One / ((v1 * v1) + offset3); + Vector v4 = Vector.One / ((v1 * v1) + offset4); + + return @base + ((mul4 * v4) + ((mul2 * v2) + (mul3 * v3))); + } + public static float RatioOfDerivativesOfCubicRootToSimpleGamma(float v, bool invert = false) { float epsilon = 1e-2f; @@ -41,4 +76,1029 @@ public static float RatioOfDerivativesOfCubicRootToSimpleGamma(float v, bool inv return invert ? num / den : den / num; } + + public static Vector RatioOfDerivativesOfCubicRootToSimpleGamma(Vector v, bool invert) + { + float epsilon = 1e-2f; + v = Vector.Max(Vector.Zero, v); + + Vector numMul = Vector.Create(SGRetMul * 3 * SGMul); + Vector vOffset = Vector.Create((SGVOffset * JxlMath.InverseLog2E) + epsilon); + Vector denMul = Vector.Create(JxlMath.InverseLog2E * SGMul); + + Vector v2 = v * v; + Vector num = (numMul * v2) + Vector.Create(epsilon); + Vector den = ((denMul * v) * v2) + vOffset; + + return invert ? num / den : den / num; + } + + public static float GammaModulation(int x, int y, JxlPlane xybX, JxlPlane xybY, Rectangle rect, float outVal) + { + const float biasConst = 0.16f; + const float gamma = 0.1005613337192697f; + + Vector overallRatio = Vector.Zero; + Vector bias = new(biasConst); + + for (int dy = 0; dy < 8; dy++) + { + ReadOnlySpan rowInX = xybX.GetRow(rect, y + dy); + ReadOnlySpan rowInY = xybY.GetRow(rect, y + dy); + + for (int dx = 0; dx < 8; dx += Vector.Count) + { + Vector iny = new(rowInY[(x + dx)..]); + iny += bias; + + Vector inx = new(rowInX[(x + dx)..]); + + Vector r = iny - inx; + Vector ratioR = RatioOfDerivativesOfCubicRootToSimpleGamma(r, invert: true); + overallRatio += ratioR; + + Vector g = iny + inx; + Vector ratioG = RatioOfDerivativesOfCubicRootToSimpleGamma(g, invert: true); + overallRatio += ratioG; + } + } + + float overallRatioScalar = Vector.Sum(overallRatio) * (0.5f / 64.0f); + + return (gamma * MathF.Log2(overallRatioScalar)) + outVal; + } + + // Change precision in 8x8 blocks that have significant amounts of blue + // content (but are not close to solid blue). + // This is based on the idea that M and L cone activations saturate the + // S (blue) receptors, and the S reception becomes more important when + // both M and L levels are low. In that case M and L receptors may be + // observing S-spectra instead and viewing them with higher spatial + // accuracy, justifying spending more bits here. + public static Vector BlueModulation(int x, int y, JxlPlane planex, JxlPlane planey, JxlPlane planeb, Rectangle rect, Vector outVal) + { + const float limitConst = 0.010474084867598155f; + const float offsetConst = 0.0031994768654636393f; + const float multiplier = 0.90590804735610064f; + const float maxLimit = 15.463398341612438f; + + Vector sum = Vector.Zero; + Vector limit = new(limitConst); + Vector offset = new(offsetConst); + + for (int dy = 0; dy < 8; dy++) + { + ReadOnlySpan rowInX = planex.GetRow(rect, y + dy); + ReadOnlySpan rowInY = planey.GetRow(rect, y + dy); + ReadOnlySpan rowInB = planeb.GetRow(rect, y + dy); + + for (int dx = 0; dx < 8; dx += Vector.Count) + { + Vector pX = Vector.Create(rowInX[(x + dx)..]); + Vector pB = Vector.Create(rowInB[(x + dx)..]); + Vector pYRaw = Vector.Create(rowInY[(x + dx)..]) + offset; + + Vector pYEffective = pYRaw + Vector.Abs(pX); + Vector excess = pB - pYEffective; + + Vector contribution = Vector.Min(excess, limit); + contribution = Vector.ConditionalSelect(Vector.GreaterThan(pB, pYEffective), contribution, Vector.Zero); + + sum += contribution; + } + } + + float scalarSum = Vector.Sum(sum); + + if (scalarSum >= 32 * limitConst) + { + scalarSum = (64 * limitConst) - scalarSum; + } + + if (scalarSum >= maxLimit * limitConst) + { + scalarSum = maxLimit * limitConst; + } + + scalarSum *= multiplier; + + return new Vector(scalarSum) + outVal; + } + + private static float HfModulationScalar(int x, int y, JxlPlane xybY, Rectangle rect, float outVal) + { + const float valueMinY = 0.0206f; + const float mulY = -0.38f; + const float offset = 0.42f; + + float sumY = 0; + + for (int dy = 0; dy < 8; dy++) + { + ReadOnlySpan rowInY = xybY.GetRow(rect, y + dy)[x..]; + ReadOnlySpan rowInYNext = dy == 7 + ? rowInY + : xybY.GetRow(rect, y + dy + 1)[x..]; + + for (int dx = 0; dx < 7; dx++) + { + sumY += MathF.Min(valueMinY, MathF.Abs(rowInY[dx] - rowInY[dx + 1])); + } + + for (int dx = 0; dx < 8; dx++) + { + sumY += MathF.Min(valueMinY, MathF.Abs(rowInY[dx] - rowInYNext[dx])); + } + } + + float scalarSumY = sumY * mulY; + scalarSumY += offset; + + return scalarSumY + outVal; + } + + private static Vector HfModulationVectorized(int x, int y, JxlImageF xybY, Rectangle rect, Vector outVal) + { + const float valueMinY = 0.0206f; + const float mulY = -0.38f; + const float offset = 0.42f; + + Vector sumY = Vector.Zero; + Vector valueMinVY = new(valueMinY); + + ReadOnlySpan maskRight = [uint.MaxValue, uint.MaxValue, uint.MaxValue, uint.MaxValue, uint.MaxValue, uint.MaxValue, uint.MaxValue, 0]; + + for (int dy = 0; dy < 8; dy++) + { + ReadOnlySpan rowInY = xybY.GetRow(rect, y + dy)[x..]; + ReadOnlySpan rowInYNext = dy == 7 + ? rowInY + : xybY.GetRow(rect, y + dy + 1)[x..]; + + for (int dx = 0; dx < 8; dx += Vector.Count) + { + Vector mask = Vector.Create(maskRight[dx..]); + Vector maskFloat = Vector.AsVectorSingle(mask); + + Vector pY = Vector.Create(rowInY[dx..]); + Vector prY = Vector.Create(rowInY[(dx + 1)..]); + Vector pdY = Vector.Create(rowInYNext[dx..]); + + Vector horizontal = Vector.Min(valueMinVY, Vector.Abs(pY - prY)); + Vector vertical = Vector.Min(valueMinVY, Vector.Abs(pY - pdY)); + + sumY += Vector.BitwiseAnd(horizontal, maskFloat); + sumY += vertical; + } + } + + float scalarSumY = Vector.Sum(sumY); + scalarSumY *= mulY; + scalarSumY += offset; + + return new Vector(scalarSumY) + outVal; + } + + public static Vector HfModulation(int x, int y, JxlImageF xybY, Rectangle rect, Vector outVal) + { + if (Vector.Count == 1) + { + // We don't have SIMD support, everything is scalar. + // Loop iterations are different in scalar HF modulation, + // so it's more efficient to go with a separate duplicated + // routine. + float result = HfModulationScalar(x, y, xybY, rect, outVal.ToScalar()); + return Vector.Create(result); + } + else + { + // We have SIMD support + return HfModulationVectorized(x, y, xybY, rect, outVal); + } + } + + private static void PerBlockModulations(float butteraugliTarget, JxlPlane xybX, JxlPlane xybY, JxlPlane xybB, Rectangle rectIn, float scale, Rectangle rectOut, JxlImageF output) + { + float baseLevel = 0.48f * scale; + + const float dampenRampStart = 2.0f; + const float dampenRampEnd = 14.0f; + + float dampen = 1.0f; + + if (butteraugliTarget >= dampenRampStart) + { + dampen = 1.0f - ((butteraugliTarget - dampenRampStart) / (dampenRampEnd - dampenRampStart)); + + if (dampen < 0) + { + dampen = 0; + } + } + + float mul = scale * dampen; + float add = (1.0f - dampen) * baseLevel; + + for (int iy = rectOut.Y0(); iy < rectOut.Y1(); iy++) + { + int y = iy * 8; + Span rowOut = output.GetRow(iy); + + for (int ix = rectOut.X0(); ix < rectOut.X1(); ix++) + { + int x = ix * 8; + + Vector maskVal = ComputeMask(Vector.Create(rowOut[ix])); + + maskVal = GammaModulation(x, y, xybX, xybY, rectIn, maskVal); + float outVal = HfModulationScalar(x, y, xybY, rectIn, maskVal); + outVal = MathF.Min(outVal, BlueModulation(x, y, xybX, xybY, xybB, rectIn, maskVal)); + + rowOut[ix] = (MathF.Pow(2.0f, outVal * 1.442695041f) * mul) + add; + } + } + } + + private static float MaskingSqrt(float v) + { + const float kLogOffset = 27.505837037000106f; + const float kMul = 211.66567973503678f; + + return 0.25f * MathF.Sqrt((v * MathF.Sqrt(kMul * 1e8f)) + kLogOffset); + } + + private static Vector MaskingSqrt(Vector v) + { + Vector kLogOffset = Vector.Create(27.505837037000106f); + Vector kMul = Vector.Create(211.66567973503678f); + + return Vector.Create(0.25f) * Vector.SquareRoot((v * Vector.SquareRoot(kMul * 1e8f)) + kLogOffset); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void StoreMin4(float v, ref float min0, ref float min1, ref float min2, ref float min3) + { + if (v < min3) + { + if (v < min0) + { + min3 = min2; + min2 = min1; + min1 = min0; + min0 = v; + } + else if (v < min1) + { + min3 = min2; + min2 = min1; + min1 = v; + } + else if (v < min2) + { + min3 = min2; + min2 = v; + } + else + { + min3 = v; + } + } + } + + // Look for smooth areas near the area of degradation. + // If the areas are generally smooth, don't do masking. + // Output is downsampled by 2x. + public static bool FuzzyErosion(float butteraugliTarget, Rectangle fromRect, JxlImageF from, Rectangle toRect, JxlImageF to) + { + int width = from.XSize; + int height = from.YSize; + const int step = 1; + + if (toRect.Width != fromRect.Width || toRect.Height != fromRect.Height) + { + return false; + } + + ReadOnlySpan mulBase = [0.125f, 0.1f, 0.09f, 0.06f]; + ReadOnlySpan mulAdd = [0.0f, -0.1f, -0.09f, -0.06f]; + float mul = 0f; + + if (butteraugliTarget < 2.0f) + { + mul = (2.0f - butteraugliTarget) * (1.0f / 2.0f); + } + + InlineArray4 multipliers = default; + float normSum = 0; + + for (int ii = 0; ii < 4; ii++) + { + multipliers[ii] = mulBase[ii] + (mul * mulAdd[ii]); + normSum += multipliers[ii]; + } + + const float total = 0.29959705784054957f; + + for (int ii = 0; ii < 4; ii++) + { + multipliers[ii] *= total / normSum; + } + + for (int fy = 0; fy < fromRect.Height; fy++) + { + int y = fy + fromRect.Y0(); + int ym1 = y >= step ? y - step : y; + int yp1 = y + step < height ? y + step : y; + + Span rowt = from.GetRow(ym1); + Span row = from.GetRow(y); + Span rowb = from.GetRow(yp1); + Span rowOut = to.GetRow(toRect, fy / 2); + + for (int fx = 0; fx < fromRect.Height; fx++) + { + int x = fx + fromRect.X0(); + int xm1 = x >= step ? x - step : x; + int xp1 = x + step < width ? x + step : x; + + // float min[4] = { row[x], row[xm1], row[xp1], rowt[xm1] }; + InlineArray4 min = default; + min[0] = row[x]; + min[1] = row[xm1]; + min[2] = row[xp1]; + min[3] = rowt[xm1]; + + if (min[0] > min[1]) + { + RuntimeUtility.Swap(ref min[0], ref min[1]); + } + + if (min[0] > min[2]) + { + RuntimeUtility.Swap(ref min[0], ref min[2]); + } + + if (min[0] > min[3]) + { + RuntimeUtility.Swap(ref min[0], ref min[3]); + } + + if (min[1] > min[2]) + { + RuntimeUtility.Swap(ref min[1], ref min[2]); + } + + if (min[1] > min[3]) + { + RuntimeUtility.Swap(ref min[1], ref min[3]); + } + + if (min[2] > min[3]) + { + RuntimeUtility.Swap(ref min[2], ref min[3]); + } + + // remaining 5 values of a 3x3 neighborhood + ref float rmin0 = ref min[0]; + ref float rmin1 = ref min[1]; + ref float rmin2 = ref min[2]; + ref float rmin3 = ref min[3]; + StoreMin4(rowt[x], ref rmin0, ref rmin1, ref rmin2, ref rmin3); + StoreMin4(rowt[xp1], ref rmin0, ref rmin1, ref rmin2, ref rmin3); + StoreMin4(rowb[xm1], ref rmin0, ref rmin1, ref rmin2, ref rmin3); + StoreMin4(rowb[x], ref rmin0, ref rmin1, ref rmin2, ref rmin3); + StoreMin4(rowb[xp1], ref rmin0, ref rmin1, ref rmin2, ref rmin3); + + float v = (multipliers[0] * min[0]) + (multipliers[1] * min[1]) + + (multipliers[2] * min[2]) + (multipliers[3] * min[3]); + + if (fx % 2 == 0 && fy % 2 == 0) + { + rowOut[fx / 2] = v; + } + else + { + rowOut[fx / 2] += v; + } + } + } + + return true; + } + + private static void Blur1x1Masking(Configuration configuration, ref JxlImageF mask1x1, Rectangle rect) + { + const float filterMask1x1_0 = 0.364911248f; + const float filterMask1x1_1 = 0.05f; + const float filterMask1x1_2 = 0.1688888021f; + const float filterMask1x1_3 = 0.221069183f; + const float filterMask1x1_4 = 0.306563504f; + + double sum = 1.0 + + (4.0 * ((filterMask1x1_0 + filterMask1x1_1) + + (filterMask1x1_2 + filterMask1x1_4) + + (2.0 * filterMask1x1_3))); + + if (sum < 1e-5) + { + sum = 1e-5; + } + + float normalize = (float)(1.0 / sum); + + JxlWeightsSymmetric5 weights = default; + weights.C = JxlWeightsSymmetric5.CreateVector4(normalize); + weights.R = JxlWeightsSymmetric5.CreateVector4(normalize * filterMask1x1_0); + weights.R2 = JxlWeightsSymmetric5.CreateVector4(normalize * filterMask1x1_2); + weights.D = JxlWeightsSymmetric5.CreateVector4(normalize * filterMask1x1_1); + weights.D2 = JxlWeightsSymmetric5.CreateVector4(normalize * filterMask1x1_4); + weights.L = JxlWeightsSymmetric5.CreateVector4(normalize * filterMask1x1_3); + + JxlImageF temp = new(configuration, rect.Width, rect.Height); + + if (!JxlConvolve.Symmetric5(mask1x1, in rect, ref weights, temp, temp.GetRectangle())) + { + throw new InvalidOperationException("Symmetric5 convolution failed"); + } + + mask1x1 = temp; + } + + public static JxlImageF AdaptiveQuantizationMap(Configuration configuration, float butteraugliTarget, JxlImageF xyb, Rectangle rect, float scale, out JxlImageF mask, out JxlImageF mask1x1) + { + if ((rect.Width & 7) != 0) + { + throw new ArgumentException("Rectangle width must be divisible by 8", nameof(rect)); + } + + if ((rect.Height & 7) != 0) + { + throw new ArgumentException("Rectangle height must be divisible by 8", nameof(rect)); + } + + AdaptiveQuantizationInternal impl = new(); + + int xSizeBlocks = rect.Width / 8; + int ySizeBlocks = rect.Height / 8; + + impl.AdaptiveQuantizationMap = new JxlImageF(configuration, xSizeBlocks, ySizeBlocks); + mask = new JxlImageF(configuration, xSizeBlocks, ySizeBlocks); + mask1x1 = new JxlImageF(configuration, xyb.XSize, xyb.YSize); + + // out variables cannot be captured in a local function + JxlImageF maskRef = mask; + JxlImageF mask1x1Ref = mask1x1; + + impl.InitializeBuffer(configuration); + + void ProcessTile(int idx) + { + int numTilesX = JxlMath.DivCeil(xSizeBlocks, JxlEncoderParameters.TileDimensionsInBlocks); + + int tx = (int)(idx % (uint)numTilesX); + int ty = (int)(idx / (uint)numTilesX); + + int by0 = ty * JxlEncoderParameters.TileDimensionsInBlocks; + int by1 = Math.Min((ty + 1) * JxlEncoderParameters.TileDimensionsInBlocks, ySizeBlocks); + + int bx0 = tx * JxlEncoderParameters.TileDimensionsInBlocks; + int bx1 = Math.Min((tx + 1) * JxlEncoderParameters.TileDimensionsInBlocks, xSizeBlocks); + + Rectangle rectOut = new(bx0, by0, bx1 - bx0, by1 - by0); + + if (!impl.TryComputeTile(butteraugliTarget, scale, xyb, rect, rectOut, idx, maskRef, mask1x1Ref)) + { + throw new InvalidOperationException("Couldn't process tile"); + } + } + + int numTiles = JxlMath.DivCeil(xSizeBlocks, JxlEncoderParameters.TileDimensionsInBlocks) * + JxlMath.DivCeil(ySizeBlocks, JxlEncoderParameters.TileDimensionsInBlocks); + + _ = Parallel.For(0, numTiles, configuration.GetParallelOptions(), ProcessTile); + Blur1x1Masking(configuration, ref mask1x1, rect); + + return impl.AdaptiveQuantizationMap!; + } + + private static JxlImageF TileDistMap(Configuration configuration, JxlImageF distMap, int tileSize, int margin, JxlAcStrategyImage acStrategy) + { + int tileXSize = (distMap.XSize + tileSize - 1) / tileSize; + int tileYSize = (distMap.YSize + tileSize - 1) / tileSize; + + JxlImageF tileDistMap = new(configuration, tileXSize, tileYSize); + + const float borderMul = 0.98f; + const float cornerMul = 0.7f; + const float tileNorm = 1.2f; + + for (int tileY = 0; tileY < tileYSize; tileY++) + { + JxlAcStrategyRow acStrategyRow = acStrategy.GetRow(tileY); + Span distRow = tileDistMap.GetRow(tileY); + + for (int tileX = 0; tileX < tileXSize; tileX++) + { + JxlAcStrategy acs = acStrategyRow[tileX]; + + if (!acs.IsFirstBlock) + { + continue; + } + + int thisTileXSize = acs.CoveredBlocksX * tileSize; + int thisTileYSize = acs.CoveredBlocksY * tileSize; + + int yBegin = Math.Max(0, (tileSize * tileY) - margin); + int yEnd = Math.Min(distMap.YSize, (tileSize * tileY) + thisTileYSize + margin); + + int xBegin = Math.Max(0, (tileSize * tileX) - margin); + int xEnd = Math.Min(distMap.XSize, (tileSize * tileX) + thisTileXSize + margin); + + float distNorm = 0.0f; + double pixels = 0.0; + + for (int y = yBegin; y < yEnd; y++) + { + float yMul = 1.0f; + + if (margin != 0 && (y == yBegin || y == yEnd - 1)) + { + yMul = borderMul; + } + + ReadOnlySpan row = distMap.GetRow(y); + + for (int x = xBegin; x < xEnd; x++) + { + float xMul = yMul; + + if (margin != 0 && (x == xBegin || x == xEnd - 1)) + { + if (xMul == 1.0f) + { + xMul = borderMul; + } + else + { + xMul = cornerMul; + } + } + + float v = row[x]; + + v *= v; + v *= v; + v *= v; + v *= v; + + distNorm += xMul * v; + pixels += xMul; + } + } + + if (pixels == 0) + { + pixels = 1; + } + + float tileDist = tileNorm * MathF.Pow((float)(distNorm / pixels), 1.0f / 16.0f); + + distRow[tileX] = tileDist; + + for (int iy = 0; iy < acs.CoveredBlocksY; iy++) + { + for (int ix = 0; ix < acs.CoveredBlocksX; ix++) + { + tileDistMap.GetRow(tileY + iy)[tileX + ix] = tileDist; + } + } + } + } + + return tileDistMap; + } + + private static JxlImageBundle RoundtripImage(Configuration configuration, JxlFrameHeader frameHeader, JxlImage3F opsin, JxlPassesEncoderState encState, JxlCmsInterface cms) + { + JxlPassesDecoderState decState = new(frameHeader, configuration); + + decState.OutputEncodingInfo.SetFromMetadata(encState.Shared.Metadata); + decState.Shared = encState.Shared; + + if (opsin.YSize % JxlFrameDimensions.BlockDimensions != 0) + { + throw new InvalidOperationException("The opsin height must be divisible by the block dimension."); + } + + int xSizeGroups = JxlMath.DivCeil(opsin.XSize, JxlFrameDimensions.GroupDimensions); + int ySizeGroups = JxlMath.DivCeil(opsin.YSize, JxlFrameDimensions.GroupDimensions); + int numGroups = xSizeGroups * ySizeGroups; + + int numSpecialFrames = encState.SpecialFrames.Count; + int numPasses = encState.ProgressiveSplitter.GetNumPasses(); + + JxlModularFrameEncoder modularFrameEncoder = JxlModularFrameEncoder.Create(configuration, frameHeader, encState.CParams, false); + + InitializePassesEncoder(configuration, frameHeader, opsin, opsin.GetRectangle(), cms, encState, modularFrameEncoder, null); + + decState.Initialize(frameHeader); + decState.InitializeForAc(configuration, numPasses); + + JxlImageBundle decoded = new(configuration, encState.Shared.Metadata.M) + { + Origin = frameHeader.FrameOrigin + }; + + JxlImage3F tmp = new(configuration, opsin.XSize, opsin.YSize); + + decoded.SetFromImage(tmp, decState.OutputEncodingInfo.ColorEncoding); + + JxlPassesDecoderState.PipelineOptions options = new() + { + UseSlowRenderPipeline = false, + Coalescing = false, + RenderSpotcolors = false, + RenderNoise = false + }; + + JxlImageMetadata metadata = decoded.Metadata!; + + decState.PreparePipeline(frameHeader, encState.Shared.Metadata.ImageMetadata!, decoded, options); + + JxlGroupDecoderCache[] groupDecCaches = new JxlGroupDecoderCache[numThreads]; + + decState.RenderPipeline.PrepareForThreads(numThreads, useGroupIds: false); + + void ProcessGroup(int groupIndex) + { + if (frameHeader.LoopFilter!.EpfIterations > 0) + { + JxlLoopFilter.ComputeSigma( + decState.Shared.FrameDimensions.BlockGroupRect(groupIndex), + decState); + } + + RenderPipelineInput input = decState.RenderPipeline.GetInputBuffers(groupIndex, groupIndex); + + JxlGroupDecoder.DecodeGroupForRoundtrip( + frameHeader, + encState.Coeffs, + groupIndex, + decState, + groupDecCaches[groupIndex], + groupIndex, + input, + null, + null); + + for (int c = 0; c < metadata.ExtraChannelCount; c++) + { + (JxlImageF plane, Rectangle rect) = input.GetBuffer(3 + c); + JxlImageOperations.FillPlane(0.0f, plane, rect); + } + + input.Done(); + } + + _ = Parallel.For(0, numGroups, configuration.GetParallelOptions(), ProcsesGr); + encState.SpecialFrames.RemoveRange(numSpecialFrames, encState.SpecialFrames.Count - numSpecialFrames); + + return decoded; + } + + public static float InitialQuantDc(float butteraugliTarget) + { + const float dcMul = 0.3f; // Butteraugli target where non-linearity kicks in + float butteraugliTargetDc = MathF.Max( + 0.5f * butteraugliTarget, + MathF.Min(butteraugliTarget, dcMul * MathF.Pow((1.0f / dcMul) * butteraugliTarget, DcQuantPow))); + + return MathF.Min(DcQuant / butteraugliTargetDc, 50f); + } + + public static bool AdjustQuantField(JxlAcStrategyImage acStrategy, Rectangle rect, float butteraugliTarget, JxlImageF quantField) + { + int stride = quantField.PixelsPerRow; + float meanMaxMixer = 1.0f; + { + const float limit = 1.54138f; + const float mul = 0.56391f; + const float min = 0f; + + if (butteraugliTarget > limit) + { + meanMaxMixer -= (butteraugliTarget - limit) * mul; + + if (meanMaxMixer < min) + { + meanMaxMixer = min; + } + } + } + + for (int y = 0; y < rect.Height; y++) + { + JxlAcStrategyRow acStrategyRow = acStrategy.GetRow(in rect, y); + Span quantRow = quantField.GetRow(rect, y); + + for (int x = 0; x < rect.Width; x++) + { + JxlAcStrategy acs = acStrategyRow[x]; + + if (!acs.IsFirstBlock) + { + continue; + } + + if (x + acs.CoveredBlocksX > quantField.XSize || y + acs.CoveredBlocksY > quantField.YSize) + { + return false; + } + + float max = quantRow[x]; + float mean = 0f; + + for (int iy = 0; iy < acs.CoveredBlocksY; y++) + { + for (int ix = 0; ix < acs.CoveredBlocksX; x++) + { + mean += quantRow[x + ix + (iy * stride)]; + max = MathF.Max(quantRow[x + ix + (iy * stride)], max); + } + } + + mean /= acs.CoveredBlocksY * acs.CoveredBlocksX; + + if (acs.CoveredBlocksY * acs.CoveredBlocksX >= 4) + { + max *= meanMaxMixer; + max += (1.0f - meanMaxMixer) * mean; + } + + for (int iy = 0; iy < acs.CoveredBlocksY; iy++) + { + for (int ix = 0; ix < acs.CoveredBlocksX; ix++) + { + quantRow[x + ix + (iy * stride)] = max; + } + } + } + } + + return true; + } + + private sealed class AdaptiveQuantizationInternal + { + public List PreErosion { get; set; } = []; + + public JxlImageF? AdaptiveQuantizationMap { get; set; } + + public JxlImageF? DiffBuffer { get; set; } + + public void InitializeBuffer(Configuration configuration) + { + int numThreads = configuration.MaxDegreeOfParallelism == -1 ? Environment.ProcessorCount : configuration.MaxDegreeOfParallelism; + + this.DiffBuffer = new(configuration, JxlEncoderParameters.TileDimensions + 8, numThreads); + + for (int i = this.PreErosion.Count; i < numThreads; i++) + { + this.PreErosion.Add( + new JxlImageF( + configuration, + (JxlEncoderParameters.TileDimensionsInBlocks * 2) + 2, + (JxlEncoderParameters.TileDimensionsInBlocks * 2) + 2)); + } + } + + public bool TryComputeTile(float butteraugliTarget, float scale, JxlImage3F xyb, Rectangle rectIn, Rectangle rectOut, int parallelIndex, JxlImageF mask, JxlImageF mask1x1) + { + const float matchGammaOffset = 0.019f; + + if (this.DiffBuffer is null) + { + return false; + } + + if (rectIn.X0() % JxlFrameDimensions.BlockDimensions != 0 || rectIn.Y0() % JxlFrameDimensions.BlockDimensions != 0) + { + return false; + } + + (int width, int height) = (xyb.XSize, xyb.YSize); + + int yStart1x1 = rectIn.Y0() + (rectOut.Y0() * 8); + int yEnd1x1 = yStart1x1 + (rectOut.Height * 8); + + int xStart1x1 = rectIn.X0() + (rectOut.X0() * 8); + int xEnd1x1 = xStart1x1 + (rectOut.Width * 8); + + if (rectIn.X0() != 0 && rectOut.X0() == 0) + { + xStart1x1 -= 2; + } + + if (rectIn.X1() < width && rectOut.X1() * 8 == rectIn.Width) + { + xEnd1x1 += 2; + } + + if (rectIn.Y0() != 0 && rectOut.Y0() == 0) + { + yStart1x1 -= 2; + } + + if (rectIn.Y1() < height && rectOut.Y1() * 8 == rectIn.Height) + { + yEnd1x1 += 2; + } + + for (int y = yStart1x1; y < yEnd1x1; y++) + { + int y2 = y + 1 < height ? y + 1 : y; + int y1 = y > 0 ? y - 1 : y; + + Span rowIn = xyb.PlaneRow(1, y); + Span rowIn1 = xyb.PlaneRow(1, y1); + Span rowIn2 = xyb.PlaneRow(1, y2); + Span mask1x1Out = mask1x1.GetRow(y); + + for (int x = xStart1x1; x < xEnd1x1; x++) + { + const float multiplier = 1.0f; + const float scaler = 1.0f; + const float offset = 0.01f; + + int x2 = x + 1 < width ? x + 1 : x; + int x1 = x > 0 ? x - 1 : x; + + float @base = 0.25f * ((rowIn2[x] + rowIn1[x]) + (rowIn[x1] + rowIn[x2])); + float gammac = RatioOfDerivativesOfCubicRootToSimpleGamma(rowIn[x] + matchGammaOffset); + float diff = MathF.Abs(gammac * (rowIn[x] - @base)); + + diff *= scaler; + diff = MathF.Log(1.0f + diff); // log1p + mask1x1Out[x] = multiplier / (diff + offset); + } + } + + int yStart = rectIn.Y0() + (rectOut.Y0() * 8); + int yEnd = yStart + (rectOut.Height * 8); + + int xStart = rectIn.X0() + (rectOut.X0() * 8); + int xEnd = xStart + (rectOut.Width * 8); + + if (xStart != 0) + { + xStart -= 4; + } + + if (xEnd != width) + { + xEnd += 4; + } + + if (yStart != 0) + { + yStart -= 4; + } + + if (yEnd != height) + { + yEnd += 4; + } + + if (!this.PreErosion[parallelIndex].ShrinkTo((xEnd - xStart) / 4, (yEnd - yStart) / 4)) + { + return false; + } + + const float limit = 0.2f; + + for (int y = yStart; y < yEnd; y++) + { + int y2 = y + 1 < height ? y + 1 : y; + int y1 = y > 0 ? y - 1 : y; + + Span rowIn = xyb.PlaneRow(1, y); + Span rowIn1 = xyb.PlaneRow(1, y1); + Span rowIn2 = xyb.PlaneRow(1, y2); + Span rowOut = this.DiffBuffer!.GetRow(parallelIndex); + + // Pass Span variables instead of capturing them as a ref struct + // variable cannot be captured. + void ScalarPixel(int x, Span rowIn, Span rowIn1, Span rowIn2, Span rowOut) + { + int x2 = x + 1 < width ? x + 1 : x; + int x1 = x > 0 ? x - 1 : x; + + float @base = 0.25f * ((rowIn2[x] + rowIn1[x]) + (rowIn[x1] + rowIn[x2])); + float gammac = RatioOfDerivativesOfCubicRootToSimpleGamma(rowIn[x] + matchGammaOffset); + float diff = gammac * (rowIn[x] - @base); + + // diff = Min(diff², limit) + diff *= diff; + if (diff >= limit) + { + diff = limit; + } + + diff = MaskingSqrt(diff); + if ((y % 4) != 0) + { + rowOut[x - xStart] += diff; + } + else + { + rowOut[x - xStart] = diff; + } + } + + int x = xStart; + + if (xStart == 0) + { + ScalarPixel(xStart, rowIn, rowIn1, rowIn2, rowOut); + x++; + } + + // SIMD + Vector matchGammaOffsetV = Vector.Create(matchGammaOffset); + Vector quarter = Vector.Create(0.25f); + + for (; x + 1 + Vector.Count < xEnd; x += Vector.Count) + { + Vector input = Vector.Create(rowIn[x..]); + + // Neighbors + Vector inputRight = Vector.Create(rowIn[(x + 1)..]); + Vector inputLeft = Vector.Create(rowIn[(x - 1)..]); + Vector inputTop = Vector.Create(rowIn2[x..]); + Vector inputBottom = Vector.Create(rowIn1[x..]); + + Vector @base = quarter * ((inputRight + inputLeft) + (inputTop + inputBottom)); + Vector gammacv = RatioOfDerivativesOfCubicRootToSimpleGamma(input + matchGammaOffsetV, invert: false); + Vector diff = gammacv * (input - @base); + diff *= diff; + diff = Vector.Min(diff, Vector.Create(limit)); + diff = MaskingSqrt(diff); + + if ((y & 3) != 0) + { + diff += Vector.Create(rowOut[(x - xStart)..]); + } + + diff.CopyTo(rowOut[(x - xStart)..]); + } + + // Scalar + for (; x < xEnd; x++) + { + ScalarPixel(x, rowIn, rowIn1, rowIn2, rowOut); + } + + if (y % 4 == 3) + { + Span rowDOut = this.PreErosion[parallelIndex].GetRow((y - yStart) / 4); + + for (int qx = 0; qx < (xEnd - xStart) / 4; qx++) + { + rowDOut[qx] = ((rowOut[qx * 4] + rowOut[(qx * 4) + 1]) + (rowOut[(qx * 4) + 2] + rowOut[(qx * 4) + 3])) * 0.25f; + } + } + } + + if (xStart % (JxlFrameDimensions.BlockDimensions / 2) != 0 || yStart % (JxlFrameDimensions.BlockDimensions / 2) != 0) + { + return false; + } + + Rectangle fromRect = new( + x: xStart % 8 == 0 ? 0 : 1, + y: yStart % 8 == 0 ? 0 : 1, + width: rectOut.Width * 2, + height: rectOut.Height * 2); + + if (!FuzzyErosion(butteraugliTarget, fromRect, this.PreErosion[parallelIndex], rectOut, this.AdaptiveQuantizationMap!)) + { + return false; + } + + for (int y = 0; y < rectOut.Height; y++) + { + Span aqMapRow = this.AdaptiveQuantizationMap!.GetRow(rectOut, y); + Span maskRow = mask.GetRow(rectOut, y); + + for (int x = 0; x < rectOut.Width; x++) + { + maskRow[x] = ComputeMaskForAcStrategyUse(aqMapRow[x]); + } + } + + PerBlockModulations(butteraugliTarget, xyb.Plane(0), xyb.Plane(1), xyb.Plane(2), rectIn, scale, rectOut, this.AdaptiveQuantizationMap!); + return true; + } + } } From 7fda9060798b7fbeacbf8f706ddc56da976f8102 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 17 Sep 2026 15:50:35 +0400 Subject: [PATCH 185/328] Add adaptive quantization encoder --- .../Encoder/JxlAdaptiveQuantization.cs | 374 +++++++++++++++++- 1 file changed, 372 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs index 56303ac96f..8af29b2665 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs @@ -4,16 +4,20 @@ using System.Numerics; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Formats.Jxl.Cms; using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Group; -using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Modular; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Comparator; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; @@ -34,6 +38,9 @@ internal static class JxlAdaptiveQuantization private const float DcQuant = 1.095924047623553f; private const float AcQuant = 0.765f; + private const int DefaultButteraugliIterations = 2; + private const int MaxButteraugliIterations = 4; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static float ComputeMaskForAcStrategyUse(float outputValue) { @@ -712,7 +719,10 @@ private static JxlImageBundle RoundtripImage(Configuration configuration, JxlFra JxlImage3F tmp = new(configuration, opsin.XSize, opsin.YSize); - decoded.SetFromImage(tmp, decState.OutputEncodingInfo.ColorEncoding); + if (!decoded.SetFromImage(tmp, decState.OutputEncodingInfo.ColorEncoding)) + { + throw new InvalidOperationException("Could not set image bundle"); + } JxlPassesDecoderState.PipelineOptions options = new() { @@ -849,6 +859,366 @@ public static bool AdjustQuantField(JxlAcStrategyImage acStrategy, Rectangle rec return true; } + public static bool FindBestQuantization(Configuration configuration, JxlFrameHeader frameHeader, JxlImage3F linear, JxlImage3F opsin, JxlImageF quantField, JxlPassesEncoderState encState, JxlCmsInterface cms, JxlAuxiliaryOutput auxOut) + { + JxlCompressionParameters cparams = encState.CompressionParameters; + + if (cparams.Resampling > 1 && cparams.OriginalButteraugliDistance <= 4.0f * cparams.Resampling) + { + // For downsampled opsin image, the butteraugli based adaptive quantization + // loop would only make the size bigger without improving the distance much, + // so in this case we enable it only for very high butteraugli targets. + return true; + } + + JxlQuantizer quantizer = encState.Shared.Quantizer; + JxlImageI rawQuantField = encState.Shared.RawQuantField; + float butteraugliTarget = cparams.ButteraugliDistance; + float originalButteraugli = cparams.OriginalButteraugliDistance; + + ButteraugliParameters parameters = new(); + JxlCustomTransferFunction tf = frameHeader.Metadata!.ImageMetadata!.ColorEncoding!.TransferFunction; + + parameters.IntensityTarget = tf.IsPq || tf.IsHlg ? frameHeader.Metadata.ImageMetadata.IntensityTarget : 0; + + ButteraugliJxlComparator comparator = new(parameters, cms); + comparator.SetReferenceImage(configuration, linear); + + bool lowerIsBetter = comparator.GoodQualityScore < comparator.BadQualityScore; + float initialQuantDc = InitialQuantDc(butteraugliTarget); + + if (!AdjustQuantField(encState.Shared.AcStrategy, quantField.GetRectangle(), originalButteraugli, quantField)) + { + return false; + } + + using JxlImageF initialQuantField = new(configuration, quantField.XSize, quantField.YSize); + + if (!JxlImageOperations.CopyImage(quantField, initialQuantField)) + { + return false; + } + + JxlImageOperations.ImageMinMax(initialQuantField, out float initialQfMin, out float initialQfMax); + + float initialQfRatio = initialQfMax / initialQfMin; + float qfMaxDeviationLow = MathF.Sqrt(250f / initialQfRatio); + float asymmetry = 2; + + if (qfMaxDeviationLow < asymmetry) + { + asymmetry = qfMaxDeviationLow; + } + + float qfLower = initialQfMin / (asymmetry * qfMaxDeviationLow); + float qfHigher = initialQfMax * (qfMaxDeviationLow / asymmetry); + + if (qfHigher / qfLower < 253) + { + return false; + } + + const int originalComparisonRound = 1; + int iters = DefaultButteraugliIterations; + + if (cparams.SpeedTier <= JxlSpeedTier.Tortoise) + { + iters = MaxButteraugliIterations; + } + + JxlImageF tileDistmap = new(); + + for (int i = 0; i < iters + 1; i++) + { + if (!quantizer.SetQuantField(configuration, initialQuantDc, quantField, rawQuantField)) + { + return false; + } + + JxlImageBundle decLinear = RoundtripImage(configuration, frameHeader, opsin, encState, cms); + + JxlImageF diffmap = new(); + comparator.CompareWith(configuration, decLinear, diffmap, out float score); + + if (!lowerIsBetter) + { + score = -score; + ScoreImage(-1.0f, diffmap); + } + + tileDistmap = TileDistMap(configuration, diffmap, 8 * cparams.Resampling, 0, encState.Shared.AcStrategy); + + auxOut.NumberOfButteraugliIterations++; + if (i == iters) + { + break; + } + + Span pow = [0.2, 0.2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]; + Span powMod = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]; + + if (i == originalComparisonRound) + { + double initMul = 0.6; + double oneMinusInitMul = 1.0 - initMul; + + for (int y = 0; y < quantField.YSize; y++) + { + Span rowQ = quantField.GetRow(y); + Span rowInit = initialQuantField.GetRow(y); + + for (int x = 0; x < quantField.XSize; x++) + { + double clamp = (oneMinusInitMul * rowQ[x]) + (initMul * rowInit[x]); + + if (rowQ[x] < clamp) + { + rowQ[x] = (float)clamp; + + if (rowQ[x] > qfHigher) + { + rowQ[x] = qfHigher; + } + + if (rowQ[x] < qfLower) + { + rowQ[x] = qfLower; + } + } + } + } + } + + double curPow = 0; + + if (i < 7) + { + curPow = pow[i] + ((originalButteraugli - 1.0) * powMod[i]); + + if (curPow < 0) + { + curPow = 0; + } + } + + if (curPow == 0.0) + { + for (int y = 0; y < quantField.YSize; y++) + { + Span rowDist = tileDistmap.GetRow(y); + Span rowQ = quantField.GetRow(y); + + for (int x = 0; x < quantField.XSize; x++) + { + float diff = rowDist[x] / originalButteraugli; + + if (diff > 1.0f) + { + float old = rowQ[x]; + int qfOld = (int)MathF.Round(old * quantizer.InverseGlobalScale, MidpointRounding.AwayFromZero); + int qfNew = (int)MathF.Round(rowQ[x] * quantizer.InverseGlobalScale, MidpointRounding.AwayFromZero); + + if (qfOld == qfNew) + { + rowQ[x] = old + quantizer.Scale; + } + } + + if (rowQ[x] > qfHigher) + { + rowQ[x] = qfHigher; + } + + if (rowQ[x] < qfLower) + { + rowQ[x] = qfLower; + } + } + } + } + else + { + for (int y = 0; y < quantField.YSize; y++) + { + Span rowDist = tileDistmap.GetRow(y); + Span rowQ = quantField.GetRow(y); + + for (int x = 0; x < quantField.XSize; x++) + { + float diff = rowDist[x] / originalButteraugli; + + if (diff <= 1.0f) + { + rowQ[x] *= (float)Math.Pow(diff, curPow); + } + else + { + float old = rowQ[x]; + rowQ[x] *= diff; + + int qfOld = (int)MathF.Round(old * quantizer.InverseGlobalScale, MidpointRounding.AwayFromZero); + int qfNew = (int)MathF.Round(rowQ[x] * quantizer.InverseGlobalScale, MidpointRounding.AwayFromZero); + + if (qfOld == qfNew) + { + rowQ[x] = old + quantizer.Scale; + } + } + + if (rowQ[x] < qfHigher) + { + rowQ[x] = qfHigher; + } + + if (rowQ[x] < qfLower) + { + rowQ[x] = qfLower; + } + } + } + } + } + + if (!quantizer.SetQuantField(configuration, initialQuantDc, quantField, rawQuantField)) + { + return false; + } + + return true; + } + + public static bool FindBestQuantizationMaxError(Configuration configuration, JxlFrameHeader frameHeader, JxlImage3F opsin, JxlImageF quantField, JxlPassesEncoderState encState, JxlCmsInterface cms, JxlAuxiliaryOutput auxOut) + { + JxlCompressionParameters cparams = encState.CompressionParameters; + JxlQuantizer quantizer = encState.Shared.Quantizer; + JxlImageI rawQuantField = encState.Shared.RawQuantField; + + float initialQuantDc = 16f * MathF.Sqrt(0.1f / cparams.ButteraugliDistance); + + if (!AdjustQuantField(encState.Shared.AcStrategy, quantField.GetRectangle(), cparams.OriginalButteraugliDistance, quantField)) + { + return false; + } + + InlineArray3 inverseMaxError = default; + inverseMaxError[0] = 1.0f / cparams.MaxError[0]; + inverseMaxError[1] = 1.0f / cparams.MaxError[1]; + inverseMaxError[2] = 1.0f / cparams.MaxError[2]; + + for (int i = 0; i < MaxButteraugliIterations + 1; i++) + { + if (!quantizer.SetQuantField(configuration, initialQuantDc, quantField, rawQuantField)) + { + return false; + } + + JxlImageBundle decoded = RoundtripImage(configuration, frameHeader, opsin, encState, cms); + + for (int by = 0; by < encState.Shared.FrameDimensions.YSizeBlocks; by++) + { + JxlAcStrategyRow acStrategyRow = encState.Shared.AcStrategy.GetRow(by); + + for (int bx = 0; bx < encState.Shared.FrameDimensions.XSizeBlocks; bx++) + { + JxlAcStrategy acs = acStrategyRow[bx]; + + if (!acs.IsFirstBlock) + { + continue; + } + + float maxError = 0; + + for (int c = 0; c < 3; c++) + { + for (int y = by * JxlFrameDimensions.BlockDimensions; y < (by + acs.CoveredBlocksY) * JxlFrameDimensions.BlockDimensions; y++) + { + if (y >= decoded.YSize) + { + continue; + } + + Span inRow = opsin.PlaneRow(c, y); + Span decRow = decoded.Color!.PlaneRow(c, y); + + for (int x = bx * JxlFrameDimensions.BlockDimensions; x < (bx + acs.CoveredBlocksX) * JxlFrameDimensions.BlockDimensions; x++) + { + if (x >= decoded.XSize) + { + continue; + } + + maxError = MathF.Max(MathF.Abs(inRow[x] - decRow[x]) * inverseMaxError[c], maxError); + } + } + } + + // Target an error between max_error/2 and max_error. + // If the error in the varblock is above the target, increase the qf to + // compensate. If the error is below the target, decrease the qf. + // However, to avoid an excessive increase of the qf, only do so if the + // error is less than half the maximum allowed error. + float qfMul = + maxError < 0.5f + ? maxError * 2.0f + : maxError > 1.0f + ? maxError + : 1.0f; + + for (int qy = by; qy < by + acs.CoveredBlocksY; qy++) + { + Span qfRow = quantField.GetRow(qy); + + for (int qx = bx; qx < bx + acs.CoveredBlocksX; qx++) + { + qfRow[qx] *= qfMul; + } + } + } + } + } + + if (!quantizer.SetQuantField(configuration, initialQuantDc, quantField, rawQuantField)) + { + return false; + } + + return true; + } + + public static JxlImageF InitialQuantField(Configuration configuration, float butteraugliTarget, JxlImage3F opsin, Rectangle rect, float rescale, JxlImageF mask, JxlImageF mask1x1) + { + float quantAc = AcQuant / butteraugliTarget; + + return AdaptiveQuantizationMap(configuration, butteraugliTarget, opsin, rect, quantAc * rescale, mask, mask1x1); + } + + public static bool FindBestQuantizer( + Configuration configuration, + JxlFrameHeader frameHeader, + JxlImage3F linear, + JxlImage3F opsin, + JxlImageF quantField, + JxlPassesEncoderState encState, + JxlCmsInterface cms, + JxlAuxiliaryOutput auxOut, + double rescale) + { + _ = rescale; // This parameter is unused but is present in reference + JxlCompressionParameters cparams = encState.CompressionParameters; + + if (cparams.MaxErrorMode) + { + return FindBestQuantizationMaxError(configuration, frameHeader, opsin, quantField, encState, cms, auxOut); + } + else if (linear && cparams.SpeedTier <= JxlSpeedTier.Kitten) + { + return FindBestQuantization(configuration, frameHeader, linear, opsin, quantField, encState, cms, auxOut); + } + + return true; + } + private sealed class AdaptiveQuantizationInternal { public List PreErosion { get; set; } = []; From ca008380107f4a3dd2b9bd140c36f1d37ba9cdd4 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 17 Sep 2026 19:38:19 +0400 Subject: [PATCH 186/328] Add scalar tone mapping --- .../Cms/ToneMapping/JxlHlgOotfToneMapper.cs | 46 ++++++++ .../Cms/ToneMapping/JxlRec2048ToneMapper.cs | 102 ++++++++++++++++++ .../Cms/ToneMapping/JxlToneMappingUtils.cs | 60 +++++++++++ 3 files changed, 208 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Cms/ToneMapping/JxlHlgOotfToneMapper.cs create mode 100644 src/ImageSharp/Formats/Jxl/Cms/ToneMapping/JxlRec2048ToneMapper.cs create mode 100644 src/ImageSharp/Formats/Jxl/Cms/ToneMapping/JxlToneMappingUtils.cs diff --git a/src/ImageSharp/Formats/Jxl/Cms/ToneMapping/JxlHlgOotfToneMapper.cs b/src/ImageSharp/Formats/Jxl/Cms/ToneMapping/JxlHlgOotfToneMapper.cs new file mode 100644 index 0000000000..32fdb1b430 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/ToneMapping/JxlHlgOotfToneMapper.cs @@ -0,0 +1,46 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms.ToneMapping; + +internal struct JxlHlgOotfToneMapper +{ + private readonly float exponent; + private readonly bool applyOotf; + private readonly float redY; + private readonly float greenY; + private readonly float blueY; + + public JxlHlgOotfToneMapper(float gamma, Vector3 luminances) + { + this.exponent = gamma - 1; + this.redY = luminances.X; + this.greenY = luminances.Y; + this.blueY = luminances.Z; + this.applyOotf = this.exponent is < -0.01f or > 0.01f; + } + + public JxlHlgOotfToneMapper(float sourceLuminance, float targetLuminance, Vector3 primariesLuminances) + : this( + gamma: MathF.Pow(1.111f, MathF.Log2(targetLuminance / sourceLuminance)), + luminances: primariesLuminances) + { + } + + public readonly void Apply(Span rgb) + { + if (!this.applyOotf) + { + return; + } + + float luminance = (this.redY * rgb[0]) + (this.greenY * rgb[1]) + (this.blueY * rgb[2]); + float ratio = MathF.Min(MathF.Pow(luminance, this.exponent), 1e9f); + + rgb[0] *= ratio; + rgb[1] *= ratio; + rgb[2] *= ratio; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Cms/ToneMapping/JxlRec2048ToneMapper.cs b/src/ImageSharp/Formats/Jxl/Cms/ToneMapping/JxlRec2048ToneMapper.cs new file mode 100644 index 0000000000..6409946833 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/ToneMapping/JxlRec2048ToneMapper.cs @@ -0,0 +1,102 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Jxl.Cms.TransferFunctions; + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms.ToneMapping; + +internal readonly struct JxlRec2048ToneMapper +{ + private readonly float redY; + private readonly float greenY; + private readonly float blueY; + + private readonly float pqMasteringMin; + private readonly float pqMasteringMax; + private readonly float pqMasteringRange; + private readonly float inversePqMasteringRange; + + private readonly float minLum; + private readonly float maxLum; + private readonly float ks; + private readonly float inverseOneMinusKs; + private readonly float normalizer; + private readonly float inverseTargetPeak; + + private readonly InlineArray2 sourceRange; + private readonly InlineArray2 targetRange; + + public JxlRec2048ToneMapper(InlineArray2 sourceRange, InlineArray2 targetRange, Vector3 primariesLuminances) + { + this.redY = primariesLuminances.X; + this.greenY = primariesLuminances.Y; + this.blueY = primariesLuminances.Z; + + this.pqMasteringMin = InverseEotf(sourceRange[0]); + this.pqMasteringMax = InverseEotf(sourceRange[1]); + this.pqMasteringRange = this.pqMasteringMax - this.pqMasteringMin; + this.inversePqMasteringRange = 1.0f / this.pqMasteringRange; + + this.minLum = (InverseEotf(targetRange[0]) - this.pqMasteringMin) * this.inversePqMasteringRange; + this.maxLum = (InverseEotf(targetRange[1]) - this.pqMasteringMin) * this.inversePqMasteringRange; + this.ks = (1.5f * this.maxLum) - 0.5f; + + this.inverseOneMinusKs = 1.0f / MathF.Max(1e-6f, 1.0f - this.ks); + this.normalizer = sourceRange[1] / targetRange[1]; + this.inverseTargetPeak = 1.0f / targetRange[1]; + + this.sourceRange = sourceRange; + this.targetRange = targetRange; + } + + private static float InverseEotf(float luminance) => (float)JxlPqTransferFunction.EncodedFromDisplay(1.0f, luminance); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private float T(float a) => (a - this.ks) * this.inverseOneMinusKs; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private float P(float b) + { + float tb = this.T(b); // tb = T(b) + float tb2 = tb * tb; // tb² + float tb3 = tb2 * tb2; // tb³ + + return (((2 * tb3) - (3 * tb2) + 1) * this.ks) + + ((tb3 - (2 * tb2) + tb) * (1 - this.ks)) + + (((-2 * tb3) + (3 * tb2)) * this.maxLum); + } + + public void ToneMap(Span rgb) + { + float luminance = this.sourceRange[1] * ((this.redY * rgb[0]) + (this.greenY * rgb[1]) + (this.blueY * rgb[2])); + float normalizedPq = MathF.Min(1.0f, (InverseEotf(luminance) - this.pqMasteringMin) * this.inversePqMasteringRange); + float e2 = (normalizedPq < this.ks) ? normalizedPq : this.P(normalizedPq); + float oneMinusE2 = 1 - e2; + float oneMinusE2_2 = oneMinusE2 * oneMinusE2; + float oneMinusE2_4 = oneMinusE2_2 * oneMinusE2_2; + float e3 = (this.minLum * oneMinusE2_4) + e2; + float e4 = (e3 * this.pqMasteringRange) + this.pqMasteringMin; + float d4 = (float)JxlPqTransferFunction.DisplayFromEncoded(1.0f, e4); + float newLuminance = Math.Clamp(d4, 0, this.targetRange[1]); + float minLuminance = 1e-6f; + bool useCap = luminance <= minLuminance; + float ratio = newLuminance / MathF.Max(luminance, minLuminance); + float cap = newLuminance * this.inverseTargetPeak; + float multiplier = ratio * this.normalizer; + + if (useCap) + { + rgb[0] = cap; + rgb[1] = cap; + rgb[2] = cap; + } + else + { + rgb[0] *= multiplier; + rgb[1] *= multiplier; + rgb[2] *= multiplier; + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Cms/ToneMapping/JxlToneMappingUtils.cs b/src/ImageSharp/Formats/Jxl/Cms/ToneMapping/JxlToneMappingUtils.cs new file mode 100644 index 0000000000..485a0e20a5 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/ToneMapping/JxlToneMappingUtils.cs @@ -0,0 +1,60 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms.ToneMapping; + +internal static class JxlToneMappingUtils +{ + public static void GamutMapScalar(Span rgb, Vector3 primariesLuminances, float preserveSaturation = 0.1f) + { + float luminance = (primariesLuminances.X * rgb[0]) + + (primariesLuminances.Y * rgb[1]) + + (primariesLuminances.Z * rgb[2]); + + // Desaturate out-of-gamut pixels. This is done by mixing each pixel + // with just enough gray of the target luminance to make all + // components non-negative. + // + // - For saturation preservation, if a component is still larger than + // 1 then the pixel is normalized to have a maximum component of 1. + // That will reduce its luminance. + // + // - For luminance preservation, getting all components below 1 is + // done by mixing in yet more gray. That will desaturate it further. + float grayMixSaturation = 0f; + float grayMixLuminance = 0f; + + for (int idx = 0; idx < 3; idx++) + { + float value = rgb[idx]; + float valMinusGray = value - luminance; + float invValMinusGray = 1.0f / ((valMinusGray == 0.0f) ? 1.0f : valMinusGray); + float valOverValMinusGray = valMinusGray * invValMinusGray; + + grayMixSaturation = valMinusGray >= 0.0f + ? grayMixSaturation + : MathF.Max(grayMixSaturation, valOverValMinusGray); + grayMixLuminance = MathF.Max(grayMixLuminance, (valMinusGray <= 0.0f) ? grayMixSaturation : valOverValMinusGray - invValMinusGray); + } + + float grayMix = Math.Clamp( + (preserveSaturation * (grayMixSaturation - grayMixLuminance)) + grayMixLuminance, + 0.0f, + 1.0f); + + for (int idx = 0; idx < 3; idx++) + { + ref float val = ref rgb[idx]; + val = (grayMix * (luminance - val)) + val; + } + + float maxColor = MathF.Max(1.0f, MathF.Max(rgb[0], MathF.Max(rgb[1], rgb[2]))); + float normalizer = 1.0f / maxColor; + + rgb[0] *= normalizer; + rgb[1] *= normalizer; + rgb[2] *= normalizer; + } +} From 8def552efbae9a4701143cd532e1ac514565069e Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 17 Sep 2026 19:54:14 +0400 Subject: [PATCH 187/328] Complete CMS tone mapping --- .../Cms/ToneMapping/JxlHlgOotfToneMapper.cs | 28 ++++++- .../Cms/ToneMapping/JxlRec2048ToneMapper.cs | 78 +++++++++++++++++++ .../Cms/ToneMapping/JxlToneMappingUtils.cs | 62 +++++++++++++++ 3 files changed, 167 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jxl/Cms/ToneMapping/JxlHlgOotfToneMapper.cs b/src/ImageSharp/Formats/Jxl/Cms/ToneMapping/JxlHlgOotfToneMapper.cs index 32fdb1b430..a62e639823 100644 --- a/src/ImageSharp/Formats/Jxl/Cms/ToneMapping/JxlHlgOotfToneMapper.cs +++ b/src/ImageSharp/Formats/Jxl/Cms/ToneMapping/JxlHlgOotfToneMapper.cs @@ -2,10 +2,11 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using SixLabors.ImageSharp.Formats.Jxl.Processing; namespace SixLabors.ImageSharp.Formats.Jxl.Cms.ToneMapping; -internal struct JxlHlgOotfToneMapper +internal readonly struct JxlHlgOotfToneMapper { private readonly float exponent; private readonly bool applyOotf; @@ -29,6 +30,16 @@ public JxlHlgOotfToneMapper(float sourceLuminance, float targetLuminance, Vector { } + public readonly bool WarrantsGamutMapping => this.applyOotf && this.exponent < 0; + + public static JxlHlgOotfToneMapper FromSceneLight(float displayLuminance, Vector3 primariesLuminances) => new( + gamma: 1.2f * MathF.Pow(1.111f, MathF.Log2(displayLuminance / 1000.0f)), + luminances: primariesLuminances); + + public static JxlHlgOotfToneMapper ToSceneLight(float displayLuminance, Vector3 primariesLuminances) => new( + gamma: (1 / 1.2f) * MathF.Pow(1.111f, -MathF.Log2(displayLuminance / 1000.0f)), + luminances: primariesLuminances); + public readonly void Apply(Span rgb) { if (!this.applyOotf) @@ -43,4 +54,19 @@ public readonly void Apply(Span rgb) rgb[1] *= ratio; rgb[2] *= ratio; } + + public readonly void Apply(ref Vector red, ref Vector green, ref Vector blue) + { + if (!this.applyOotf) + { + return; + } + + Vector luminance = (this.redY * red) + (this.greenY * green) + (this.blueY * blue); + Vector ratio = Vector.Min(JxlSimdUtils.FastPowf(luminance, Vector.Create(this.exponent)), Vector.Create(1e9f)); + + red *= ratio; + green *= ratio; + blue *= ratio; + } } diff --git a/src/ImageSharp/Formats/Jxl/Cms/ToneMapping/JxlRec2048ToneMapper.cs b/src/ImageSharp/Formats/Jxl/Cms/ToneMapping/JxlRec2048ToneMapper.cs index 6409946833..b63368773a 100644 --- a/src/ImageSharp/Formats/Jxl/Cms/ToneMapping/JxlRec2048ToneMapper.cs +++ b/src/ImageSharp/Formats/Jxl/Cms/ToneMapping/JxlRec2048ToneMapper.cs @@ -28,6 +28,8 @@ internal readonly struct JxlRec2048ToneMapper private readonly InlineArray2 sourceRange; private readonly InlineArray2 targetRange; + private readonly JxlPqTransferFunction tfPq = new(1.0f); + public JxlRec2048ToneMapper(InlineArray2 sourceRange, InlineArray2 targetRange, Vector3 primariesLuminances) { this.redY = primariesLuminances.X; @@ -53,9 +55,19 @@ public JxlRec2048ToneMapper(InlineArray2 sourceRange, InlineArray2 private static float InverseEotf(float luminance) => (float)JxlPqTransferFunction.EncodedFromDisplay(1.0f, luminance); + private Vector InverseEotf(Vector luminance) => this.tfPq.EncodedFromDisplay(luminance); + [MethodImpl(MethodImplOptions.AggressiveInlining)] private float T(float a) => (a - this.ks) * this.inverseOneMinusKs; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Vector T(Vector a) + { + Vector ks = Vector.Create(this.ks); + Vector inverseOneMinusKs = Vector.Create(this.inverseOneMinusKs); + return (a - ks) * inverseOneMinusKs; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private float P(float b) { @@ -68,6 +80,24 @@ private float P(float b) (((-2 * tb3) + (3 * tb2)) * this.maxLum); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Vector P(Vector b) + { + Vector tb = this.T(b); // tb = T(b) + Vector tb2 = tb * tb; // tb² + Vector tb3 = tb2 * tb2; // tb³ + + Vector v2 = Vector.Create(2.0f); + Vector v3 = Vector.Create(3.0f); + Vector v1 = Vector.Create(1.0f); + Vector vm2 = Vector.Create(-2.0f); + Vector vks = Vector.Create(this.ks); + + return (((v2 * tb3) - (v3 * tb2) + v1) * this.ks) + + ((tb3 - (v2 * tb2) + tb) * (v1 - vks)) + + (((vm2 * tb3) + (v3 * tb2)) * this.maxLum); + } + public void ToneMap(Span rgb) { float luminance = this.sourceRange[1] * ((this.redY * rgb[0]) + (this.greenY * rgb[1]) + (this.blueY * rgb[2])); @@ -99,4 +129,52 @@ public void ToneMap(Span rgb) rgb[2] *= multiplier; } } + + public void ToneMap(ref Vector red, ref Vector green, ref Vector blue) + { + Vector luminance = + this.sourceRange[1] * + ((this.redY * red) + (this.greenY * green) + (this.blueY * blue)); + + Vector pqMasteringMin = new(this.pqMasteringMin); + Vector invPqMasteringRange = new(this.inversePqMasteringRange); + + Vector normalizedPq = Vector.Min( + Vector.One, + (this.InverseEotf(luminance) - pqMasteringMin) * invPqMasteringRange); + + Vector ks = new(this.ks); + Vector e2 = Vector.ConditionalSelect( + Vector.LessThan(normalizedPq, ks), + normalizedPq, + this.P(normalizedPq)); + + Vector oneMinusE2 = Vector.One - e2; + Vector oneMinusE2_2 = oneMinusE2 * oneMinusE2; + Vector oneMinusE2_4 = oneMinusE2_2 * oneMinusE2_2; + + Vector b = new(this.minLum); + Vector e3 = (b * oneMinusE2_4) + e2; + + Vector pqMasteringRange = new(this.pqMasteringRange); + Vector e4 = (e3 * pqMasteringRange) + pqMasteringMin; + + Vector newLuminance = Vector.Min( + new Vector(this.targetRange[1]), + Vector.Max( + this.tfPq.DisplayFromEncoded(e4), + Vector.Zero)); + + Vector minLuminance = new(1e-6f); + Vector useCap = Vector.LessThanOrEqual(luminance, minLuminance); + + Vector ratio = newLuminance / Vector.Max(luminance, minLuminance); + Vector cap = newLuminance * new Vector(this.inverseTargetPeak); + Vector normalizer = new(this.normalizer); + Vector multiplier = ratio * normalizer; + + red = Vector.ConditionalSelect(useCap, cap, red * multiplier); + green = Vector.ConditionalSelect(useCap, cap, green * multiplier); + blue = Vector.ConditionalSelect(useCap, cap, blue * multiplier); + } } diff --git a/src/ImageSharp/Formats/Jxl/Cms/ToneMapping/JxlToneMappingUtils.cs b/src/ImageSharp/Formats/Jxl/Cms/ToneMapping/JxlToneMappingUtils.cs index 485a0e20a5..75980c4ca6 100644 --- a/src/ImageSharp/Formats/Jxl/Cms/ToneMapping/JxlToneMappingUtils.cs +++ b/src/ImageSharp/Formats/Jxl/Cms/ToneMapping/JxlToneMappingUtils.cs @@ -57,4 +57,66 @@ public static void GamutMapScalar(Span rgb, Vector3 primariesLuminances, rgb[1] *= normalizer; rgb[2] *= normalizer; } + + public static void GamutMap(ref Vector red, ref Vector green, ref Vector blue, Vector3 primariesLuminances, float preserveSaturation = 0.1f) + { + Vector luminance = + (new Vector(primariesLuminances.X) * red) + + (new Vector(primariesLuminances.Y) * green) + + (new Vector(primariesLuminances.Z) * blue); + + // Desaturate out-of-gamut pixels by mixing each pixel with gray + // of the target luminance until all components are non-negative. + Vector zero = Vector.Zero; + Vector one = Vector.One; + + Vector grayMixSaturation = zero; + Vector grayMixLuminance = zero; + + Span> channels = [red, green, blue]; + + foreach (Vector val in channels) + { + Vector valMinusGray = val - luminance; + + Vector invValMinusGray = one / Vector.ConditionalSelect( + Vector.Equals(valMinusGray, zero), + one, + valMinusGray); + + Vector valOverValMinusGray = val * invValMinusGray; + + grayMixSaturation = Vector.ConditionalSelect( + Vector.GreaterThanOrEqual(valMinusGray, zero), + grayMixSaturation, + Vector.Max(grayMixSaturation, valOverValMinusGray)); + + grayMixLuminance = Vector.Max( + grayMixLuminance, + Vector.ConditionalSelect( + Vector.LessThanOrEqual(valMinusGray, zero), + grayMixSaturation, + valOverValMinusGray - invValMinusGray)); + } + + Vector grayMix = Vector.Min( + one, + Vector.Max( + zero, + (new Vector(preserveSaturation) * (grayMixSaturation - grayMixLuminance)) + grayMixLuminance)); + + red = (grayMix * (luminance - red)) + red; + green = (grayMix * (luminance - green)) + green; + blue = (grayMix * (luminance - blue)) + blue; + + Vector maxClr = Vector.Max( + Vector.Max(one, red), + Vector.Max(green, blue)); + + Vector normalizer = one / maxClr; + + red *= normalizer; + green *= normalizer; + blue *= normalizer; + } } From 36749c5de21bf524b43f589befdc4aab35b4f0c8 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 17 Sep 2026 20:17:19 +0400 Subject: [PATCH 188/328] Complete compressed DC --- .../Formats/Jxl/Processing/JxlCompressedDc.cs | 307 +++++++++++++++++- 1 file changed, 306 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlCompressedDc.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlCompressedDc.cs index 4dc25c082c..3bede20ba8 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlCompressedDc.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlCompressedDc.cs @@ -4,6 +4,10 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; @@ -13,7 +17,7 @@ internal static class JxlCompressedDc private const float W2 = 0.0334829185968739f; private const float W0 = 1.0f - (4.0f * (W1 + W2)); - private static unsafe void ComputePixelChannelPacked(float dcFactor, Span rowTop, Span row, Span rowBottom, out Vector mc, out Vector sm, ref Vector gap, int x) + private static unsafe void ComputePixelChannelPacked(float dcFactor, ReadOnlySpan rowTop, ReadOnlySpan row, ReadOnlySpan rowBottom, out Vector mc, out Vector sm, ref Vector gap, int x) { // Use ptr for aligned vector loads fixed (float* pRowTop = rowTop) @@ -52,4 +56,305 @@ private static unsafe void ComputePixelChannelPacked(float dcFactor, Span } } } + + private static void ComputePixelChannel(float dcFactor, ReadOnlySpan rowTop, ReadOnlySpan row, ReadOnlySpan rowBottom, out float mc, out float sm, ref float gap, int x) + { + float tl = rowTop[x - 1]; + float tc = rowTop[x]; + float tr = rowTop[x + 1]; + + float ml = row[x - 1]; + mc = row[x]; + float mr = row[x + 1]; + + float bl = rowBottom[x - 1]; + float bc = rowBottom[x]; + float br = rowBottom[x + 1]; + + float corner = (tl + tr) + (bl + br); + float side = (ml + mr) + (tc + bc); + + sm = (corner * W2) + ((side * W1) + (mc * W0)); + + gap = MathF.Max(gap, MathF.Abs((mc - sm) / dcFactor)); + } + + private static void ComputePixel( + InlineArray3 dcFactors, + ReadOnlySpan rowsTop0, + ReadOnlySpan rowsTop1, + ReadOnlySpan rowsTop2, + ReadOnlySpan rows0, + ReadOnlySpan rows1, + ReadOnlySpan rows2, + ReadOnlySpan rowsBottom0, + ReadOnlySpan rowsBottom1, + ReadOnlySpan rowsBottom2, + Span outRows0, + Span outRows1, + Span outRows2, + int x) + { + Vector gap = new(0.5f); + + ComputePixelChannelPacked(dcFactors[0], rowsTop0, rows0, rowsBottom0, out Vector mcX, out Vector smX, ref gap, x); + ComputePixelChannelPacked(dcFactors[1], rowsTop1, rows1, rowsBottom1, out Vector mcY, out Vector smY, ref gap, x); + ComputePixelChannelPacked(dcFactors[2], rowsTop2, rows2, rowsBottom2, out Vector mcB, out Vector smB, ref gap, x); + + Vector factor = Vector.Create(3.0f) - (4.0f * gap); + factor = Vector.Max(factor, Vector.Zero); + + Vector output = ((smX - mcX) * factor) + mcX; + output.CopyTo(outRows0[x..]); + + output = ((smY - mcY) * factor) + mcY; + output.CopyTo(outRows1[x..]); + + output = ((smB - mcB) * factor) + mcB; + output.CopyTo(outRows2[x..]); + } + + private static void ComputePixelScalar( + InlineArray3 dcFactors, + ReadOnlySpan rowsTop0, + ReadOnlySpan rowsTop1, + ReadOnlySpan rowsTop2, + ReadOnlySpan rows0, + ReadOnlySpan rows1, + ReadOnlySpan rows2, + ReadOnlySpan rowsBottom0, + ReadOnlySpan rowsBottom1, + ReadOnlySpan rowsBottom2, + Span outRows0, + Span outRows1, + Span outRows2, + int x) + { + float gap = 0.5f; + + ComputePixelChannel(dcFactors[0], rowsTop0, rows0, rowsBottom0, out float mcX, out float smX, ref gap, x); + ComputePixelChannel(dcFactors[1], rowsTop1, rows1, rowsBottom1, out float mcY, out float smY, ref gap, x); + ComputePixelChannel(dcFactors[2], rowsTop2, rows2, rowsBottom2, out float mcB, out float smB, ref gap, x); + + float factor = MathF.Max(3.0f - (4.0f * gap), 0.0f); + + outRows0[x] = ((smX - mcX) * factor) + mcX; + outRows1[x] = ((smY - mcY) * factor) + mcY; + outRows2[x] = ((smB - mcB) * factor) + mcB; + } + + public static bool AdaptiveDCSmoothing(Configuration configuration, InlineArray3 dcFactors, JxlImage3F dc) + { + int xSize = dc.XSize; + int ySize = dc.YSize; + + if (ySize <= 2 || xSize <= 2) + { + return true; + } + + // Don't dispose. + JxlImage3F smoothed = new(configuration, xSize, ySize); + + // Fill borders that the loop below will not. + // First and last rows are unused by the processing loop. + for (int c = 0; c < 3; c++) + { + dc.PlaneRow(c, 0).CopyTo(smoothed.PlaneRow(c, 0)); + dc.PlaneRow(c, ySize - 1).CopyTo(smoothed.PlaneRow(c, ySize - 1)); + } + + _ = Parallel.For(1, ySize, configuration.GetParallelOptions(), y => + { + ReadOnlySpan rowTopX = dc.PlaneRow(0, y - 1); + ReadOnlySpan rowTopY = dc.PlaneRow(1, y - 1); + ReadOnlySpan rowTopB = dc.PlaneRow(2, y - 1); + + ReadOnlySpan rowX = dc.PlaneRow(0, y); + ReadOnlySpan rowY = dc.PlaneRow(1, y); + ReadOnlySpan rowB = dc.PlaneRow(2, y); + + ReadOnlySpan rowBottomX = dc.PlaneRow(0, y + 1); + ReadOnlySpan rowBottomY = dc.PlaneRow(1, y + 1); + ReadOnlySpan rowBottomB = dc.PlaneRow(2, y + 1); + + Span rowOutX = smoothed.PlaneRow(0, y); + Span rowOutY = smoothed.PlaneRow(1, y); + Span rowOutB = smoothed.PlaneRow(2, y); + + // Preserve the left and right borders. + rowOutX[0] = rowX[0]; + rowOutY[0] = rowY[0]; + rowOutB[0] = rowB[0]; + + rowOutX[xSize - 1] = rowX[xSize - 1]; + rowOutY[xSize - 1] = rowY[xSize - 1]; + rowOutB[xSize - 1] = rowB[xSize - 1]; + + int x = 1; + + // Scalar pixels before the first complete SIMD vector. + int vectorLength = Vector.Count; + for (; x < Math.Min(vectorLength, xSize - 1); x++) + { + ComputePixelScalar(dcFactors, rowTopX, rowTopY, rowTopB, rowX, rowY, rowB, rowBottomX, rowBottomY, rowBottomB, rowOutX, rowOutY, rowOutB, x); + } + + // Full SIMD vectors. + for (; x + vectorLength <= xSize - 1; x += vectorLength) + { + ComputePixel(dcFactors, rowTopX, rowTopY, rowTopB, rowX, rowY, rowB, rowBottomX, rowBottomY, rowBottomB, rowOutX, rowOutY, rowOutB, x); + } + + // Remaining scalar pixels. + for (; x < xSize - 1; x++) + { + ComputePixelScalar(dcFactors, rowTopX, rowTopY, rowTopB, rowX, rowY, rowB, rowBottomX, rowBottomY, rowBottomB, rowOutX, rowOutY, rowOutB, x); + } + }); + + dc.Swap(smoothed); + return true; + } + + public static void DequantDC(Rectangle r, JxlImage3F dc, JxlImageB quantDc, JxlModularImage input, InlineArray3 dcFactors, float mul, InlineArray3 cflFactors, JxlYCbCrChromaSubsampling chromaSubsampling, JxlBlockContextMap bctx) + { + if (chromaSubsampling.Is444) + { + Vector facX = new(dcFactors[0] * mul); + Vector facY = new(dcFactors[1] * mul); + Vector facB = new(dcFactors[2] * mul); + Vector cflFacX = new(cflFactors[0]); + Vector cflFacB = new(cflFactors[2]); + + for (int y = 0; y < r.Height; y++) + { + Span decRowX = dc.Plane(0).GetRow(r, y); + Span decRowY = dc.Plane(1).GetRow(r, y); + Span decRowB = dc.Plane(2).GetRow(r, y); + + ReadOnlySpan quantRowX = input.Channels[1].Plane.GetRow(y); + ReadOnlySpan quantRowY = input.Channels[0].Plane.GetRow(y); + ReadOnlySpan quantRowB = input.Channels[2].Plane.GetRow(y); + + int x = 0; + int vectorCount = Vector.Count; + + for (; x + vectorCount <= r.Width; x += vectorCount) + { + Vector inX = Vector.ConvertToSingle(Vector.Create(quantRowX[x..])) * facX; + Vector inY = Vector.ConvertToSingle(Vector.Create(quantRowY[x..])) * facY; + Vector inB = Vector.ConvertToSingle(Vector.Create(quantRowB[x..])) * facB; + + inY.CopyTo(decRowY[x..]); + ((inY * cflFacX) + inX).CopyTo(decRowX[x..]); + ((inY * cflFacB) + inB).CopyTo(decRowB[x..]); + } + + for (; x < r.Width; x++) + { + float inX = quantRowX[x] * dcFactors[0] * mul; + float inY = quantRowY[x] * dcFactors[1] * mul; + float inB = quantRowB[x] * dcFactors[2] * mul; + + decRowY[x] = inY; + decRowX[x] = (inY * cflFactors[0]) + inX; + decRowB[x] = (inY * cflFactors[2]) + inB; + } + } + } + else + { + foreach (int c in (ReadOnlySpan)[1, 0, 2]) + { + Rectangle rect = new( + r.X0() >> chromaSubsampling.HShift(c), + r.Y0() >> chromaSubsampling.VShift(c), + r.Width >> chromaSubsampling.HShift(c), + r.Height >> chromaSubsampling.VShift(c)); + + Vector fac = new(dcFactors[c] * mul); + JxlModularChannel channel = input.Channels[c < 2 ? c ^ 1 : c]; + + for (int y = 0; y < rect.Height; y++) + { + ReadOnlySpan quantRow = channel.Plane.GetRow(y); + Span row = dc.Plane(c).GetRow(rect, y); + + int x = 0; + int vectorCount = Vector.Count; + + for (; x + vectorCount <= rect.Width; x += vectorCount) + { + Vector output = Vector.ConvertToSingle(Vector.Create(quantRow[x..])) * fac; + output.CopyTo(row[x..]); + } + + for (; x < rect.Width; x++) + { + row[x] = quantRow[x] * dcFactors[c] * mul; + } + } + } + } + + if (bctx.DcContextCount <= 1) + { + for (int y = 0; y < r.Height; y++) + { + Span qdcRow = quantDc.GetRow(r, y); + qdcRow.Clear(); + } + } + else + { + for (int y = 0; y < r.Height; y++) + { + Span qdcRow = quantDc.GetRow(r, y); + + ReadOnlySpan quantRowX = input.Channels[1].Plane.GetRow(y >> chromaSubsampling.VShift(0)); + ReadOnlySpan quantRowY = input.Channels[0].Plane.GetRow(y >> chromaSubsampling.VShift(1)); + ReadOnlySpan quantRowB = input.Channels[2].Plane.GetRow(y >> chromaSubsampling.VShift(2)); + + for (int x = 0; x < r.Width; x++) + { + int bucketX = CountThresholds( + quantRowX[x >> chromaSubsampling.HShift(0)], + CollectionsMarshal.AsSpan(bctx.DcThresholds[0])); + + int bucketY = CountThresholds( + quantRowY[x >> chromaSubsampling.HShift(1)], + CollectionsMarshal.AsSpan(bctx.DcThresholds[1])); + + int bucketB = CountThresholds( + quantRowB[x >> chromaSubsampling.HShift(2)], + CollectionsMarshal.AsSpan(bctx.DcThresholds[2])); + + int bucket = bucketX; + + bucket *= bctx.DcThresholds[2].Count + 1; + bucket += bucketB; + bucket *= bctx.DcThresholds[1].Count + 1; + bucket += bucketY; + + qdcRow[x] = (byte)bucket; + } + } + } + } + + private static int CountThresholds(int value, ReadOnlySpan thresholds) + { + int bucket = 0; + + foreach (int threshold in thresholds) + { + if (value > threshold) + { + bucket++; + } + } + + return bucket; + } } From f109219580225d417205e9176f08cc711a2889b8 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 17 Sep 2026 20:30:54 +0400 Subject: [PATCH 189/328] Add optimizations --- .../Jxl/IO/FrameHeader/JxlFrameHeader.cs | 13 +++--- .../Decoder/Group/JxlGroupDecoder.cs | 4 +- .../Processing/Decoder/JxlToJpegDecoder.cs | 3 +- .../Decoder/JxlTransformsDecoder.cs | 40 +++++++++++++------ .../Encoder/JxlTransformsEncoder.cs | 24 +++++------ 5 files changed, 49 insertions(+), 35 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs index e39739db8b..d02b283eaf 100644 --- a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs +++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs @@ -348,11 +348,12 @@ public JxlFrameDimensions FrameDimensions (this.animationFrame?.Duration == 0 || this.saveAsReference != 0); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static InlineArray3 JpegOrder(JxlColorTransform transform, bool isGray) + public static void JpegOrder(JxlColorTransform transform, bool isGray, out InlineArray3 result) { if (isGray) { - return default; // {0, 0, 0} + result = default; // {0, 0, 0} + return; } switch (transform) @@ -360,19 +361,19 @@ public static InlineArray3 JpegOrder(JxlColorTransform transform, bool isGr case JxlColorTransform.YCbCr: { // {1, 0, 2} - InlineArray3 result = default; + result = default; result[0] = 1; result[2] = 2; - return result; + return; } case JxlColorTransform.None: { // {0, 1, 2} - InlineArray3 result = default; + result = default; result[1] = 1; result[2] = 2; - return result; + return; } case JxlColorTransform.Xyb: diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs index 26f47a6646..fcc2cbcf16 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs @@ -182,7 +182,7 @@ private static bool DecodeGroupCore( return false; } - jpegCMap = JxlFrameHeader.JpegOrder(frameHeader.ColorTransform, jpegIsGray); + JxlFrameHeader.JpegOrder(frameHeader.ColorTransform, jpegIsGray, out jpegCMap); Span qe = decState.Shared.Matrices.GetEncodings(); @@ -415,7 +415,7 @@ private static bool DecodeGroupCore( return false; } - foreach (int c in new[] { 1, 0, 2 }) + foreach (int c in (ReadOnlySpan)[1, 0, 2]) { // Propagate only Y for grayscale. if (jpegIsGray && c != 1) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlToJpegDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlToJpegDecoder.cs index cf15210deb..cd7a56da76 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlToJpegDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlToJpegDecoder.cs @@ -55,8 +55,7 @@ internal class JxlToJpegDecoder public void StartBox(bool boxUntilEof, long contentsSize) { - this.buffer.Dispose(); - this.buffer = new MemoryStream(); + this.buffer.Position = 0; this.IsParsingBox = true; if (boxUntilEof) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlTransformsDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlTransformsDecoder.cs index d52e009ade..61cc3a6e53 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlTransformsDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlTransformsDecoder.cs @@ -371,10 +371,10 @@ public static void Idct2TopBlock(int s, Span block, int strideOut, Span coefficients, S Span coeff = stackalloc float[4 * 4]; // No need to zero-initialize coeff[0] = dcs[0]; - for (int iy = 0, iy4 = 0, iy28 = 0; iy < 4; iy++, iy4 += 4, iy28 += 16) + int iy28 = 0; // iy * 2 * 8 + for (int iy4 = 0; iy4 < 16; iy4 += 4) { for (int ix = 0; ix < 4; ix++) { - if (ix == 0 && iy == 0) + if (ix == 0 && iy4 == 0) { continue; } coeff[iy4 + ix] = coefficients[iy28 + (ix * 2)]; } + + iy28 += 16; } + iy28 = 0; + Span block = stackalloc float[4 * 8]; // Don't zero-init AfvIdct4x4(coeff, block); @@ -484,19 +489,23 @@ public static void AfvTransformToPixels(int afvKind, Span coefficients, S // IDCT4x4 in (odd, even) positions. block[0] = dcs[1]; - for (int iy = 0, iy4 = 0, iy28 = 0; iy < 4; iy++, iy4 += 4, iy28 += 16) + for (int iy4 = 0; iy4 < 16; iy4 += 4) { for (int ix = 0; ix < 4; ix++) { - if (ix == 0 && iy == 0) + if (ix == 0 && iy4 == 0) { continue; } block[iy4 + ix] = coefficients[iy28 + (ix * 2) + 1]; } + + iy28 += 16; } + iy28 = 0; + JxlDct.ComputeScaledInverseDct( 4, 4, @@ -506,7 +515,8 @@ public static void AfvTransformToPixels(int afvKind, Span coefficients, S block[0] = dcs[2]; - for (int iy = 0, iy8 = 0; iy < 4; iy++, iy8 += 8) + int iy8 = 0; + for (int iy = 0; iy < 4; iy++) { for (int ix = 0; ix < 8; ix++) { @@ -517,8 +527,12 @@ public static void AfvTransformToPixels(int afvKind, Span coefficients, S block[iy8 + ix] = coefficients[((1 + (iy * 2)) * 8) + ix]; } + + iy8 += 8; } + iy8 = 0; + JxlDct.ComputeScaledInverseDct( 4, 8, @@ -541,10 +555,10 @@ public static void TransformToPixels(JxlAcStrategyType strategy, Span coe float block10 = coefficients[8]; float block11 = coefficients[9]; - dcs[0] = block00 + block01 + block10 + block11; - dcs[1] = block00 + block01 - block10 - block11; - dcs[2] = block00 - block01 + block10 - block11; - dcs[3] = block00 - block01 - block10 + block11; + dcs[0] = (block00 + block01) + (block10 + block11); + dcs[1] = (block00 + block01) - (block10 - block11); + dcs[2] = (block00 - block01) + (block10 - block11); + dcs[3] = (block00 - block01) - (block10 + block11); for (int y = 0; y < 2; y++) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlTransformsEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlTransformsEncoder.cs index 85c69cb80c..a915b1618e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlTransformsEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlTransformsEncoder.cs @@ -373,10 +373,10 @@ public static void Dct2TopBlock(int s, Span block, int stride, Span p float block10 = coefficients[8]; float block11 = coefficients[9]; - coefficients[0] = (block00 + block01 + block10 + block11) * 0.25f; - coefficients[1] = (block00 + block01 - block10 - block11) * 0.25f; - coefficients[8] = (block00 - block01 + block10 - block11) * 0.25f; - coefficients[9] = (block00 - block01 - block10 + block11) * 0.25f; + coefficients[0] = ((block00 + block01) + (block10 + block11)) * 0.25f; + coefficients[1] = ((block00 + block01) - (block10 - block11)) * 0.25f; + coefficients[8] = ((block00 - block01) + (block10 - block11)) * 0.25f; + coefficients[9] = ((block00 - block01) - (block10 + block11)) * 0.25f; break; } @@ -653,10 +653,10 @@ public static void TransformFromPixels(JxlAcStrategyType strategy, Span p float block10 = coefficients[8]; float block11 = coefficients[9]; - coefficients[0] = (block00 + block01 + block10 + block11) * 0.25f; - coefficients[1] = (block00 + block01 - block10 - block11) * 0.25f; - coefficients[8] = (block00 - block01 + block10 - block11) * 0.25f; - coefficients[9] = (block00 - block01 - block10 + block11) * 0.25f; + coefficients[0] = ((block00 + block01) + (block10 + block11)) * 0.25f; + coefficients[1] = ((block00 + block01) - (block10 - block11)) * 0.25f; + coefficients[8] = ((block00 - block01) + (block10 - block11)) * 0.25f; + coefficients[9] = ((block00 - block01) - (block10 + block11)) * 0.25f; break; } From 9804b7c42877b16994f0e8cd46044e2f078ed73d Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 17 Sep 2026 21:06:11 +0400 Subject: [PATCH 190/328] Add external image encoder --- .../Encoder/JxlExternalImageEncoder.cs | 214 ++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlExternalImageEncoder.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlExternalImageEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlExternalImageEncoder.cs new file mode 100644 index 0000000000..ac2651d71a --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlExternalImageEncoder.cs @@ -0,0 +1,214 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Cms; +using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.Memory; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; + +internal class JxlExternalImageEncoder +{ + private static int JxlDataTypeBytes(JxlDataType dataType) => dataType switch + { + JxlDataType.Byte => 1, + JxlDataType.UInt16 => 2, + JxlDataType.Half => 2, + JxlDataType.Single => 4, + _ => 0 + }; + + private static void ConvertFromExternalPlaneNoSizeCheck(Configuration configuration, Memory data, int xSize, int ySize, int stride, int bitsPerSample, JxlPixelFormat format, int c, JxlImageF channel) + { + if (format.DataType == JxlDataType.Byte) + { + if (bitsPerSample is 0 or > 8) + { + throw new InvalidOperationException("Invalid bits per sample."); + } + } + else if (format.DataType == JxlDataType.UInt16) + { + if (bitsPerSample is <= 8 or > 16) + { + throw new InvalidOperationException("Invalid bits per sample."); + } + } + else if (format.DataType is not JxlDataType.Half and not JxlDataType.Single) + { + throw new NotSupportedException($"Unsupported pixel format data type {format.DataType}."); + } + + if (channel.XSize != xSize || channel.YSize != ySize) + { + throw new InvalidOperationException("Channel dimensions do not match."); + } + + int bytesPerChannel = JxlDataTypeBytes(format.DataType); + int bytesPerPixel = format.Channels * bytesPerChannel; + + if (xSize > int.MaxValue / bytesPerPixel) + { + throw new InvalidOperationException("Image dimensions are too large."); + } + + int bytesPerRow = xSize * bytesPerPixel; + + if (bytesPerRow > stride) + { + throw new InvalidOperationException("Row stride is too small."); + } + + int pixelOffset = c * bytesPerChannel; + float scale = 1.0F / ((1 << bitsPerSample) - 1); + + bool littleEndian = format.Endianness == ByteOrder.LittleEndian; + + void ConvertRow(int y) + { + int offset = (y * stride) + pixelOffset; + Memory rowOut = channel.GetRowMemory(y); + + bool SaveValue(int index, float value) + { + rowOut.Span[index] = value; + return true; + } + + LoadFloatRow(data[offset..], xSize, bytesPerPixel, format.DataType, littleEndian, scale, SaveValue); + } + + _ = Parallel.For(0, ySize, configuration.GetParallelOptions(), ConvertRow); + } + + private static bool ConvertFromExternal(Configuration configuration, Memory data, int xSize, int ySize, int bitsPerSample, JxlPixelFormat format, int c, JxlPlane channel) + { + int bytesPerChannel = JxlDataTypeBytes(format.DataType); + + if ((ulong)format.Channels > ulong.MaxValue / (ulong)bytesPerChannel) + { + throw new InvalidOperationException("Invalid format."); + } + + ulong bytesPerPixel = (ulong)format.Channels * (ulong)bytesPerChannel; + + if ((ulong)xSize > ulong.MaxValue / bytesPerPixel) + { + throw new InvalidOperationException("Image dimensions are too large."); + } + + ulong lastRowSize = (ulong)xSize * bytesPerPixel; + + if (!JxlMath.SafeRoundUpTo(lastRowSize, format.Align, out ulong rowSize)) + { + throw new InvalidOperationException("Image dimensions are too large."); + } + + if (xSize == 0 || ySize == 0) + { + throw new InvalidOperationException("Empty image."); + } + + if (!JxlMath.SafeMultiply((int)rowSize, ySize - 1, out int bytesToRead) || + !JxlMath.SafeAdd(bytesToRead, (int)lastRowSize, out bytesToRead)) + { + throw new InvalidOperationException("Image dimensions are too large."); + } + + if (data.Length < bytesToRead) + { + throw new ArgumentException($"Buffer size is too small, expected: {bytesToRead} got: {data.Length}."); + } + + if ((ulong)data.Length > rowSize * (ulong)ySize) + { + throw new ArgumentException("Buffer size is too large."); + } + + return ConvertFromExternalPlaneNoSizeCheck(configuration, data, (ulong)xSize, ySize, rowSize, bitsPerSample, format, c, channel); + } + + public static bool ConvertFromExternal(Configuration configuration, Memory bytes, int xSize, int ySize, JxlColorEncoding currentColorEncoding, int bitsPerSample, JxlPixelFormat format, JxlImageBundle imageBundle, bool setAlpha) + { + int colorChannels = currentColorEncoding.Channels; + bool hasAlpha = format.Channels is 2 or 4; + + if (format.Channels < colorChannels) + { + throw new InvalidOperationException($"Expected {colorChannels} color channels, received only {format.Channels} channels."); + } + + JxlImage3F color = new(configuration, xSize, ySize); + + for (int c = 0; c < colorChannels; c++) + { + if (!ConvertFromExternal(configuration, bytes, xSize, ySize, bitsPerSample, format, c, color.Plane(c))) + { + return false; + } + } + + if (colorChannels == 1) + { + if (!JxlImageOperations.CopyImage(color.Plane(0), color.Plane(1))) + { + throw new InvalidOperationException("Failed to copy plane"); + } + + if (!JxlImageOperations.CopyImage(color.Plane(0), color.Plane(2))) + { + throw new InvalidOperationException("Failed to copy plane"); + } + } + + if (!imageBundle.SetFromImage(color, currentColorEncoding)) + { + throw new InvalidOperationException("Could not set image bundle"); + } + + if (setAlpha) + { + JxlImageF alpha = new(configuration, xSize, ySize); + + if (hasAlpha) + { + if (!ConvertFromExternal(configuration, bytes, xSize, ySize, bitsPerSample, format, format.Channels - 1, alpha)) + { + return false; + } + } + else + { + JxlImageOperations.FillImage(1.0F, alpha); + } + + if (!imageBundle.TrySetAlpha(alpha)) + { + throw new InvalidOperationException("Failed to set alpha channel"); + } + } + + return true; + } + + public static bool BufferToImageF(Configuration configuration, JxlPixelFormat pixelFormat, int xSize, int ySize, Memory buffer, JxlImageF channel) + { + int bitDepth = JxlDataTypeBytes(pixelFormat.DataType) * 8; + return ConvertFromExternal(configuration, buffer, xSize, ySize, bitDepth, pixelFormat, 0, channel); + } + + public static bool BufferToImageBundle(Configuration configuration, JxlPixelFormat pixelFormat, int xSize, int ySize, Memory buffer, JxlColorEncoding currentColorEncoding, JxlImageBundle imageBundle, bool setAlpha) + { + int bitDepth = JxlDataTypeBytes(pixelFormat.DataType) * 8; + + if (!ConvertFromExternal(configuration, buffer, xSize, ySize, currentColorEncoding, bitDepth, pixelFormat, imageBundle, setAlpha)) + { + return false; + } + + return imageBundle.VerifyMetadata(); + } +} From 55b8da89e993b09d56b71a0e0977c00a4be3d3bc Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 17 Sep 2026 21:23:41 +0400 Subject: [PATCH 191/328] Complete image bundle encoder --- .../Encoder/JxlImageBundleEncoder.cs | 80 +++++++++++++++++++ .../Jxl/Processing/Image/JxlImageBundle.cs | 30 +++++++ 2 files changed, 110 insertions(+) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlImageBundleEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlImageBundleEncoder.cs index 5a5bb65716..067ef0bee7 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlImageBundleEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlImageBundleEncoder.cs @@ -2,7 +2,9 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Jxl.Cms; +using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; @@ -123,4 +125,82 @@ public static bool TryApplyColorTransform( return false; } + + // Copies ib:rect, converts, and copies into out. + public static bool CopyToT( + Configuration configuration, + JxlImageMetadata metadata, + JxlImageBundle ib, + Rectangle rectangle, + JxlColorEncoding desiredEncoding, + JxlCmsInterface cms, + JxlImage3F output) + => TryApplyColorTransform( + configuration, + ib.CurrentColorEncoding!, + metadata.IntensityTarget, + ib.Color!, + ib.Black!, + rectangle, + desiredEncoding, + cms, + output); + + public static bool TransformIfNeeded( + Configuration configuration, + JxlImageBundle input, + JxlColorEncoding desiredEncoding, + JxlCmsInterface cms, + JxlImageBundle store, + ref JxlImageBundle output) + { + if (input.CurrentColorEncoding!.IsSameColorEncoding(desiredEncoding) && !input.ContainsBlack) + { + output = input; + return true; + } + + using JxlImage3F color = new(configuration, input.Color!.XSize, input.Color.YSize); + + if (!JxlImageOperations.CopyImage(input.Color!, color)) + { + return false; + } + + if (!store.SetFromImage(color, input.CurrentColorEncoding!)) + { + return false; + } + + if (input.ContainsExtraChannels()) + { + List extraChannels = []; + + foreach (JxlImageF extraChannel in input.EnumerateExtraChannels()) + { + JxlImageF ec = new(configuration, extraChannel.XSize, extraChannel.YSize); + + if (!JxlImageOperations.CopyImage(extraChannel, ec)) + { + return false; + } + + extraChannels.Add(ec); + extraChannel.Dispose(); + } + + if (!store.TrySetExtraChannels(extraChannels)) + { + return false; + } + } + + if (!store.TransformTo(configuration, desiredEncoding, cms)) + { + return false; + } + + output = store; + return true; + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs index b4ca42d788..8971482812 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs @@ -6,6 +6,7 @@ using SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Image; @@ -504,4 +505,33 @@ public JxlImageBundle Copy(Configuration configuration) return copy; } + + public bool CopyTo( + Configuration configuration, + Rectangle rectangle, + JxlColorEncoding desiredEncoding, + JxlCmsInterface cms, + JxlImage3F output) + => JxlImageBundleEncoder.CopyToT( + configuration, + this.Metadata!, + this, + rectangle, + desiredEncoding, + cms, + output); + + public bool TransformTo( + Configuration configuration, + JxlColorEncoding desiredEncoding, + JxlCmsInterface cms) + { + if (!this.CopyTo(configuration, this.Color!.GetRectangle(), desiredEncoding, cms, this.Color!)) + { + return false; + } + + this.CurrentColorEncoding = desiredEncoding; + return true; + } } From 255c8926c3f4b5c204df3553537bb505d69e2f44 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Fri, 18 Sep 2026 16:14:34 +0400 Subject: [PATCH 192/328] Add chroma upsampling stages --- .../HorizontalChromaUpsamplingStage.cs | 51 +++++++++++++++++ .../VerticalChromaUpsamplingStage.cs | 56 +++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/HorizontalChromaUpsamplingStage.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/VerticalChromaUpsamplingStage.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/HorizontalChromaUpsamplingStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/HorizontalChromaUpsamplingStage.cs new file mode 100644 index 0000000000..7a4796370d --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/HorizontalChromaUpsamplingStage.cs @@ -0,0 +1,51 @@ +// 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.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +internal class HorizontalChromaUpsamplingStage : RenderPipelineStageBase +{ + private readonly int channel; + + public HorizontalChromaUpsamplingStage(Configuration configuration, int channel) + : base(configuration) + { + this.Settings = RenderPipelineStageConfiguration.CreateShiftX(shift: 1, border: 1); + this.channel = channel; + } + + public override string Name => "HChromaUps"; + + public override void ProcessRow(Buffer2D> inputRows, Buffer2D> outputRows, int xExtraLeft, int xExtraRight, int width, int xPos, int yPos) + { + int xStart = -JxlMath.RoundUpTo(xExtraLeft, Vector.Count); + int xEnd = width + xExtraRight; + + Vector threeFour = Vector.Create(0.75f); // 3/4 + Vector oneFour = Vector.Create(0.25f); // 1/4 + Span rowIn = this.GetInputRow(inputRows, this.channel, 0); + Span rowOut = GetOutputRow(outputRows, this.channel, 0); + + ref float rRowIn = ref MemoryMarshal.GetReference(rowIn); + ref float rRowOut = ref MemoryMarshal.GetReference(rowOut); + + for (int x = xStart; x < xEnd; x += Vector.Count) + { + Vector current = Vector.LoadUnsafe(ref Unsafe.Add(ref rRowIn, x)) * threeFour; + Vector prev = Vector.LoadUnsafe(ref Unsafe.Add(ref rRowIn, x - 1)); + Vector next = Vector.LoadUnsafe(ref Unsafe.Add(ref rRowIn, x + 1)); + + Vector left = (oneFour * prev) + current; + Vector right = (oneFour * next) + current; + + JxlSimdUtils.StoreInterleaved(left, right, ref Unsafe.Add(ref rRowOut, x * 2)); + } + } + + public override RenderPipelineChannelMode GetChannelMode(int channel) => channel == this.channel ? RenderPipelineChannelMode.InOut : RenderPipelineChannelMode.Ignored; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/VerticalChromaUpsamplingStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/VerticalChromaUpsamplingStage.cs new file mode 100644 index 0000000000..db39b802a7 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/VerticalChromaUpsamplingStage.cs @@ -0,0 +1,56 @@ +// 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.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +internal class VerticalChromaUpsamplingStage : RenderPipelineStageBase +{ + private readonly int channel; + + public VerticalChromaUpsamplingStage(Configuration configuration, int channel) + : base(configuration) + { + this.Settings = RenderPipelineStageConfiguration.CreateShiftY(shift: 1, border: 1); + this.channel = channel; + } + + public override string Name => "VChromaUps"; + + public override void ProcessRow(Buffer2D> inputRows, Buffer2D> outputRows, int xExtraLeft, int xExtraRight, int width, int xPos, int yPos) + { + int xStart = -JxlMath.RoundUpTo(xExtraLeft, Vector.Count); + int xEnd = width + xExtraRight; + + Vector threeFour = Vector.Create(0.75f); // 3/4 + Vector oneFour = Vector.Create(0.25f); // 1/4 + + Span rowTop = this.GetInputRow(inputRows, this.channel, -1); + Span rowMid = this.GetInputRow(inputRows, this.channel, 0); + Span rowBot = this.GetInputRow(inputRows, this.channel, 1); + + Span rowOut0 = GetOutputRow(outputRows, this.channel, 0); + Span rowOut1 = GetOutputRow(outputRows, this.channel, 1); + + ref float rRowTop = ref MemoryMarshal.GetReference(rowTop); + ref float rRowMid = ref MemoryMarshal.GetReference(rowMid); + ref float rRowBot = ref MemoryMarshal.GetReference(rowBot); + + for (int x = xStart; x < xEnd; x += Vector.Count) + { + Vector vtop = Vector.LoadUnsafe(ref Unsafe.Add(ref rRowTop, x)); + Vector vmid = Vector.LoadUnsafe(ref Unsafe.Add(ref rRowMid, x)); + Vector vbot = Vector.LoadUnsafe(ref Unsafe.Add(ref rRowBot, x)); + Vector vmidscale = vmid * threeFour; + + ((vtop * oneFour) + vmidscale).CopyTo(rowOut0[x..]); + ((vbot * oneFour) + vmidscale).CopyTo(rowOut1[x..]); + } + } + + public override RenderPipelineChannelMode GetChannelMode(int channel) => channel == this.channel ? RenderPipelineChannelMode.InOut : RenderPipelineChannelMode.Ignored; +} From d23651f5f8b2bf28c751f3e4d6e594730172e719 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Fri, 18 Sep 2026 16:55:42 +0400 Subject: [PATCH 193/328] Add from linear stage --- .../RenderPipeline/FromLinearStage.cs | 50 +++++ .../FromLinearStageOperators.cs | 189 ++++++++++++++++++ 2 files changed, 239 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/FromLinearStage.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/FromLinearStageOperators.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/FromLinearStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/FromLinearStage.cs new file mode 100644 index 0000000000..ddc5c1e878 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/FromLinearStage.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +internal class FromLinearStage : RenderPipelineStageBase + where TOperator : FromLinearStageOperators.IOperator +{ + private readonly FromLinearStageOperators.IOperator op; + + public FromLinearStage(Configuration configuration, JxlOutputEncodingInfo outputEncodingInfo) + : base(configuration) + { + this.Settings = default; + this.op = TOperator.Create(outputEncodingInfo); + } + + public override string Name => "FromLinear"; + + public override void ProcessRow(Buffer2D> inputRows, Buffer2D> outputRows, int xExtraLeft, int xExtraRight, int width, int xPos, int yPos) + { + if (xExtraLeft != 0 || xExtraRight != 0) + { + throw new InvalidOperationException("xExtraLeft, xExtraRight must both = 0"); + } + + Span row0 = this.GetInputRow(inputRows, 0, 0); + Span row1 = this.GetInputRow(inputRows, 1, 0); + Span row2 = this.GetInputRow(inputRows, 2, 0); + + for (int x = 0; x < width; x += Vector.Count) + { + Vector r = Vector.Create(row0[x..]); + Vector g = Vector.Create(row1[x..]); + Vector b = Vector.Create(row2[x..]); + + this.op.Transform(ref r, ref g, ref b); + + r.CopyTo(row0[x..]); + g.CopyTo(row1[x..]); + b.CopyTo(row2[x..]); + } + } + + public override RenderPipelineChannelMode GetChannelMode(int channel) => channel < 3 ? RenderPipelineChannelMode.InPlace : RenderPipelineChannelMode.Ignored; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/FromLinearStageOperators.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/FromLinearStageOperators.cs new file mode 100644 index 0000000000..f097cf76c0 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/FromLinearStageOperators.cs @@ -0,0 +1,189 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Formats.Jxl.Cms.ToneMapping; +using SixLabors.ImageSharp.Formats.Jxl.Cms.TransferFunctions; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +internal static class FromLinearStageOperators +{ + /// + /// Abstracts applying transfer functions on the RGB channel. + /// + public interface IOperator + { + /// + /// Applies the transfer function on three RGB channels. + /// + /// Red + /// Green + /// Blue + public void Transform( + ref Vector r, + ref Vector g, + ref Vector b); + + /// + /// Creates a new instance of this operator, or uses a shared + /// instance if the operator doesn't depend on instance data. + /// + /// + /// The output encoding info may contain settings required for + /// specific transforms, such as inverse gamma or intensity target. + /// + /// + /// The created operator. + /// + public static abstract IOperator Create(JxlOutputEncodingInfo outputEncodingInfo); + } + + /// + /// Preserves initial vector values as-is. + /// + public sealed class LinearOperator : IOperator + { + /// + /// Shared instance of this operator. + /// + public static readonly LinearOperator Instance = new(); + + /// + static IOperator IOperator.Create(JxlOutputEncodingInfo outputEncodingInfo) => Instance; + + /// + public void Transform(ref Vector r, ref Vector g, ref Vector b) + { + } + } + + /// + /// Applies the sRGB transfer function on RGB channels. + /// + public sealed class RgbOperator : IOperator + { + /// + /// Shared instance of this operator. + /// + public static readonly RgbOperator Instance = new(); + + /// + static IOperator IOperator.Create(JxlOutputEncodingInfo outputEncodingInfo) => Instance; + + /// + public void Transform(ref Vector r, ref Vector g, ref Vector b) + { + r = JxlSRgbTransferFunction.EncodedFromDisplay(r); + g = JxlSRgbTransferFunction.EncodedFromDisplay(g); + b = JxlSRgbTransferFunction.EncodedFromDisplay(b); + } + } + + /// + /// Uses the Perceptual Quantization (PQ) transfer function. + /// + /// Contains the intensity target for the transform. + public sealed class PqOperator(JxlOutputEncodingInfo outputEncodingInfo) : IOperator + { + /// + /// The actual PQ transform is handled by this field. + /// + private readonly JxlPqTransferFunction pq = new(outputEncodingInfo.OriginalIntensityTarget); + + /// + static IOperator IOperator.Create(JxlOutputEncodingInfo outputEncodingInfo) => new PqOperator(outputEncodingInfo); + + /// + public void Transform(ref Vector r, ref Vector g, ref Vector b) + { + r = this.pq.EncodedFromDisplay(r); + g = this.pq.EncodedFromDisplay(g); + b = this.pq.EncodedFromDisplay(b); + } + } + + /// + /// Uses the Hybrid Log Gamma (HLG) tone mapper and transfer function. + /// + /// Contains luminances and the intensity target for the tone mapping. + public sealed class HlgOperator(JxlOutputEncodingInfo outputEncodingInfo) : IOperator + { + /// + /// The actual HLG tone mapping is handled by this field. + /// + private readonly JxlHlgOotfToneMapper hlgOotf = JxlHlgOotfToneMapper.ToSceneLight( + outputEncodingInfo.DesiredIntensityTarget, + outputEncodingInfo.Luminances); + + /// + static IOperator IOperator.Create(JxlOutputEncodingInfo outputEncodingInfo) => new HlgOperator(outputEncodingInfo); + + /// + public void Transform(ref Vector r, ref Vector g, ref Vector b) + { + // Tone map first + this.hlgOotf.Apply(ref r, ref g, ref b); + + // Then apply transfer function + r = JxlHlgTransferFunction.EncodedFromDisplay(r); + g = JxlHlgTransferFunction.EncodedFromDisplay(g); + b = JxlHlgTransferFunction.EncodedFromDisplay(b); + } + } + + /// + /// Uses the transfer function described in the ITU-R BT.709 standard. + /// + public sealed class Bt709Operator : IOperator + { + /// + /// Shared instance of this operator. + /// + public static readonly Bt709Operator Instance = new(); + + /// + static IOperator IOperator.Create(JxlOutputEncodingInfo outputEncodingInfo) => Instance; + + /// + public void Transform(ref Vector r, ref Vector g, ref Vector b) + { + r = JxlBt709TransferFunction.EncodedFromDisplay(r); + g = JxlBt709TransferFunction.EncodedFromDisplay(g); + b = JxlBt709TransferFunction.EncodedFromDisplay(b); + } + } + + /// + /// Applies a simple gamma/power-law transfer operation independently to the red, green and + /// blue channels. + /// + /// Contains the inverse gamma. + public sealed class GammaOperator(JxlOutputEncodingInfo outputEncodingInfo) : IOperator + { + /// + /// The inverse gamma from output encoding info. + /// + private readonly float inverseGamma = outputEncodingInfo.InverseGamma; + + static IOperator IOperator.Create(JxlOutputEncodingInfo outputEncodingInfo) => new GammaOperator(outputEncodingInfo); + + /// + public void Transform(ref Vector r, ref Vector g, ref Vector b) + { + r = Vector.ConditionalSelect( + Vector.LessThan(r, Vector.Create(1e-5f)), + Vector.Zero, + JxlSimdUtils.FastPowf(r, Vector.Create(this.inverseGamma))); + g = Vector.ConditionalSelect( + Vector.LessThan(g, Vector.Create(1e-5f)), + Vector.Zero, + JxlSimdUtils.FastPowf(g, Vector.Create(this.inverseGamma))); + b = Vector.ConditionalSelect( + Vector.LessThan(b, Vector.Create(1e-5f)), + Vector.Zero, + JxlSimdUtils.FastPowf(b, Vector.Create(this.inverseGamma))); + } + } +} From d75b412ae98958eca27907d13926cc26718afa4f Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Fri, 18 Sep 2026 17:04:19 +0400 Subject: [PATCH 194/328] Add to linear stage This one was very simple, one can simply copy the from linear stage and just swap out a few things to turn it into a "to linear" stage --- .../RenderPipeline/ToLinearStage.cs | 50 +++++ .../RenderPipeline/ToLinearStageOperators.cs | 189 ++++++++++++++++++ 2 files changed, 239 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/ToLinearStage.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/ToLinearStageOperators.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/ToLinearStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/ToLinearStage.cs new file mode 100644 index 0000000000..8689dc3eaf --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/ToLinearStage.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +internal class ToLinearStage : RenderPipelineStageBase + where TOperator : ToLinearStageOperators.IOperator +{ + private readonly ToLinearStageOperators.IOperator op; + + public ToLinearStage(Configuration configuration, JxlOutputEncodingInfo outputEncodingInfo) + : base(configuration) + { + this.Settings = default; + this.op = TOperator.Create(outputEncodingInfo); + } + + public override string Name => "ToLinear"; + + public override void ProcessRow(Buffer2D> inputRows, Buffer2D> outputRows, int xExtraLeft, int xExtraRight, int width, int xPos, int yPos) + { + if (xExtraLeft != 0 || xExtraRight != 0) + { + throw new InvalidOperationException("xExtraLeft, xExtraRight must both = 0"); + } + + Span row0 = this.GetInputRow(inputRows, 0, 0); + Span row1 = this.GetInputRow(inputRows, 1, 0); + Span row2 = this.GetInputRow(inputRows, 2, 0); + + for (int x = 0; x < width; x += Vector.Count) + { + Vector r = Vector.Create(row0[x..]); + Vector g = Vector.Create(row1[x..]); + Vector b = Vector.Create(row2[x..]); + + this.op.Transform(ref r, ref g, ref b); + + r.CopyTo(row0[x..]); + g.CopyTo(row1[x..]); + b.CopyTo(row2[x..]); + } + } + + public override RenderPipelineChannelMode GetChannelMode(int channel) => channel < 3 ? RenderPipelineChannelMode.InPlace : RenderPipelineChannelMode.Ignored; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/ToLinearStageOperators.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/ToLinearStageOperators.cs new file mode 100644 index 0000000000..161c2f5d33 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/ToLinearStageOperators.cs @@ -0,0 +1,189 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Formats.Jxl.Cms.ToneMapping; +using SixLabors.ImageSharp.Formats.Jxl.Cms.TransferFunctions; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +internal static class ToLinearStageOperators +{ + /// + /// Abstracts applying inverse transfer functions on the RGB channel. + /// + public interface IOperator + { + /// + /// Applies the inverse transfer function on three RGB channels. + /// + /// Red + /// Green + /// Blue + public void Transform( + ref Vector r, + ref Vector g, + ref Vector b); + + /// + /// Creates a new instance of this operator, or uses a shared + /// instance if the operator doesn't depend on instance data. + /// + /// + /// The output encoding info may contain settings required for + /// specific transforms, such as inverse gamma or intensity target. + /// + /// + /// The created operator. + /// + public static abstract IOperator Create(JxlOutputEncodingInfo outputEncodingInfo); + } + + /// + /// Preserves initial vector values as-is. + /// + public sealed class LinearOperator : IOperator + { + /// + /// Shared instance of this operator. + /// + public static readonly LinearOperator Instance = new(); + + /// + static IOperator IOperator.Create(JxlOutputEncodingInfo outputEncodingInfo) => Instance; + + /// + public void Transform(ref Vector r, ref Vector g, ref Vector b) + { + } + } + + /// + /// Applies the sRGB transfer function on RGB channels. + /// + public sealed class RgbOperator : IOperator + { + /// + /// Shared instance of this operator. + /// + public static readonly RgbOperator Instance = new(); + + /// + static IOperator IOperator.Create(JxlOutputEncodingInfo outputEncodingInfo) => Instance; + + /// + public void Transform(ref Vector r, ref Vector g, ref Vector b) + { + r = JxlSRgbTransferFunction.DisplayFromEncoded(r); + g = JxlSRgbTransferFunction.DisplayFromEncoded(g); + b = JxlSRgbTransferFunction.DisplayFromEncoded(b); + } + } + + /// + /// Uses the Perceptual Quantization (PQ) transfer function. + /// + /// Contains the intensity target for the transform. + public sealed class PqOperator(JxlOutputEncodingInfo outputEncodingInfo) : IOperator + { + /// + /// The actual PQ transform is handled by this field. + /// + private readonly JxlPqTransferFunction pq = new(outputEncodingInfo.OriginalIntensityTarget); + + /// + static IOperator IOperator.Create(JxlOutputEncodingInfo outputEncodingInfo) => new PqOperator(outputEncodingInfo); + + /// + public void Transform(ref Vector r, ref Vector g, ref Vector b) + { + r = this.pq.DisplayFromEncoded(r); + g = this.pq.DisplayFromEncoded(g); + b = this.pq.DisplayFromEncoded(b); + } + } + + /// + /// Uses the Hybrid Log Gamma (HLG) tone mapper and transfer function. + /// + /// Contains luminances and the intensity target for the tone mapping. + public sealed class HlgOperator(JxlOutputEncodingInfo outputEncodingInfo) : IOperator + { + /// + /// The actual HLG tone mapping is handled by this field. + /// + private readonly JxlHlgOotfToneMapper hlgOotf = JxlHlgOotfToneMapper.ToSceneLight( + outputEncodingInfo.OriginalIntensityTarget, + outputEncodingInfo.Luminances); + + /// + static IOperator IOperator.Create(JxlOutputEncodingInfo outputEncodingInfo) => new HlgOperator(outputEncodingInfo); + + /// + public void Transform(ref Vector r, ref Vector g, ref Vector b) + { + // Apply inverse transfer function first + r = JxlHlgTransferFunction.DisplayFromEncoded(r); + g = JxlHlgTransferFunction.DisplayFromEncoded(g); + b = JxlHlgTransferFunction.DisplayFromEncoded(b); + + // Then tone map + this.hlgOotf.Apply(ref r, ref g, ref b); + } + } + + /// + /// Uses the transfer function described in the ITU-R BT.709 standard. + /// + public sealed class Bt709Operator : IOperator + { + /// + /// Shared instance of this operator. + /// + public static readonly Bt709Operator Instance = new(); + + /// + static IOperator IOperator.Create(JxlOutputEncodingInfo outputEncodingInfo) => Instance; + + /// + public void Transform(ref Vector r, ref Vector g, ref Vector b) + { + r = JxlBt709TransferFunction.DisplayFromEncoded(r); + g = JxlBt709TransferFunction.DisplayFromEncoded(g); + b = JxlBt709TransferFunction.DisplayFromEncoded(b); + } + } + + /// + /// Applies a simple gamma/power-law transfer operation independently to the red, green and + /// blue channels. + /// + /// Contains the inverse gamma. + public sealed class GammaOperator(JxlOutputEncodingInfo outputEncodingInfo) : IOperator + { + /// + /// The gamma from output encoding info. + /// + private readonly float gamma = 1.0f / outputEncodingInfo.InverseGamma; + + static IOperator IOperator.Create(JxlOutputEncodingInfo outputEncodingInfo) => new GammaOperator(outputEncodingInfo); + + /// + public void Transform(ref Vector r, ref Vector g, ref Vector b) + { + r = Vector.ConditionalSelect( + Vector.LessThan(r, Vector.Create(1e-5f)), + Vector.Zero, + JxlSimdUtils.FastPowf(r, Vector.Create(this.gamma))); + g = Vector.ConditionalSelect( + Vector.LessThan(g, Vector.Create(1e-5f)), + Vector.Zero, + JxlSimdUtils.FastPowf(g, Vector.Create(this.gamma))); + b = Vector.ConditionalSelect( + Vector.LessThan(b, Vector.Create(1e-5f)), + Vector.Zero, + JxlSimdUtils.FastPowf(b, Vector.Create(this.gamma))); + } + } +} From 5953b9ead36bfa7f10fe2d5977c16d984cb50b82 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Fri, 18 Sep 2026 19:38:35 +0400 Subject: [PATCH 195/328] Add noise stages --- src/ImageSharp/Common/Helpers/SimdUtils.cs | 18 ++++ ...8ToneMapper.cs => JxlRec2408ToneMapper.cs} | 4 +- .../Formats/Jxl/Processing/JxlConvolve.cs | 43 ++------- .../Formats/Jxl/Processing/JxlSimdUtils.cs | 32 +++++++ .../RenderPipeline/AddNoiseStage.cs | 93 +++++++++++++++++++ .../RenderPipeline/ConvolveNoiseStage.cs | 65 +++++++++++++ .../RenderPipeline/NoiseStageUtils.cs | 56 +++++++++++ .../RenderPipeline/StrengthEvaluationLut.cs | 80 ++++++++++++++++ 8 files changed, 352 insertions(+), 39 deletions(-) rename src/ImageSharp/Formats/Jxl/Cms/ToneMapping/{JxlRec2048ToneMapper.cs => JxlRec2408ToneMapper.cs} (98%) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/AddNoiseStage.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/ConvolveNoiseStage.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/NoiseStageUtils.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/StrengthEvaluationLut.cs diff --git a/src/ImageSharp/Common/Helpers/SimdUtils.cs b/src/ImageSharp/Common/Helpers/SimdUtils.cs index 7e78581c33..c346e9e6bc 100644 --- a/src/ImageSharp/Common/Helpers/SimdUtils.cs +++ b/src/ImageSharp/Common/Helpers/SimdUtils.cs @@ -173,6 +173,24 @@ internal static Vector Pow2(Vector vector) } } + internal static Vector GatherBytes(Vector indices, Vector table) + { + Span indexValues = stackalloc byte[Vector.Count]; + Span tableValues = stackalloc int[Vector.Count]; + Span resultValues = stackalloc int[Vector.Count]; + + indices.CopyTo(indexValues); + table.CopyTo(tableValues); + + for (int i = 0; i < resultValues.Length; i++) + { + byte index = indexValues[i]; + resultValues[i] = index < tableValues.Length ? tableValues[index] : 0; + } + + return Vector.Create(resultValues); + } + [Conditional("DEBUG")] private static void DebugVerifySpanInput(ReadOnlySpan source, ReadOnlySpan dest, int shouldBeDivisibleBy) { diff --git a/src/ImageSharp/Formats/Jxl/Cms/ToneMapping/JxlRec2048ToneMapper.cs b/src/ImageSharp/Formats/Jxl/Cms/ToneMapping/JxlRec2408ToneMapper.cs similarity index 98% rename from src/ImageSharp/Formats/Jxl/Cms/ToneMapping/JxlRec2048ToneMapper.cs rename to src/ImageSharp/Formats/Jxl/Cms/ToneMapping/JxlRec2408ToneMapper.cs index b63368773a..097490eeb2 100644 --- a/src/ImageSharp/Formats/Jxl/Cms/ToneMapping/JxlRec2048ToneMapper.cs +++ b/src/ImageSharp/Formats/Jxl/Cms/ToneMapping/JxlRec2408ToneMapper.cs @@ -7,7 +7,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Cms.ToneMapping; -internal readonly struct JxlRec2048ToneMapper +internal readonly struct JxlRec2408ToneMapper { private readonly float redY; private readonly float greenY; @@ -30,7 +30,7 @@ internal readonly struct JxlRec2048ToneMapper private readonly JxlPqTransferFunction tfPq = new(1.0f); - public JxlRec2048ToneMapper(InlineArray2 sourceRange, InlineArray2 targetRange, Vector3 primariesLuminances) + public JxlRec2408ToneMapper(InlineArray2 sourceRange, InlineArray2 targetRange, Vector3 primariesLuminances) { this.redY = primariesLuminances.X; this.greenY = primariesLuminances.Y; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs index 233a98549d..8e60721a6f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs @@ -103,12 +103,12 @@ public static void Symmetric5Interior( ref JxlWeightsSymmetric5 weights, Span rowOut) { - Vector w0 = LoadDuplicate128(weights.GetCVector()); // c - Vector w1 = LoadDuplicate128(weights.GetRVector()); // r - Vector w2 = LoadDuplicate128(weights.GetR2Vector()); // R - Vector w4 = LoadDuplicate128(weights.GetDVector()); // d - Vector w5 = LoadDuplicate128(weights.GetLVector()); // L - Vector w8 = LoadDuplicate128(weights.GetD2Vector()); // D + Vector w0 = JxlSimdUtils.LoadDuplicate128(weights.GetCVector()); // c + Vector w1 = JxlSimdUtils.LoadDuplicate128(weights.GetRVector()); // r + Vector w2 = JxlSimdUtils.LoadDuplicate128(weights.GetR2Vector()); // R + Vector w4 = JxlSimdUtils.LoadDuplicate128(weights.GetDVector()); // d + Vector w5 = JxlSimdUtils.LoadDuplicate128(weights.GetLVector()); // L + Vector w8 = JxlSimdUtils.LoadDuplicate128(weights.GetD2Vector()); // D int height = image.YSize; Vector sum0 = WeightedSum(image, wrapY, ix, iy, height, w0, w1, w2) @@ -390,37 +390,6 @@ public static void FirstL2(ReadOnlySpan c, Span dst) } } - /// - /// A SIMD utility method which takes in the 128 bit vector - /// and duplicates its values to fit in the CPU vector size. - /// For example, - /// - /// 128 bit vectors: A B C D (as-is) - /// 256 bit vectors: A B C D A B C D (duplicate once) - /// 512 bit vectors: A B C D A B C D A B C D A B C D (duplicate three times) - /// - /// Vector<T> has support for arbitrarily large - /// vector sizes. For example, some ARM CPUs support 2048-bit - /// vectors through Vector<T>. In that specific case, this - /// method can be used for future-proofing. - /// - /// Note that this method, albeit future-proof, may be considered - /// slow for smaller vector sizes (think CPUs with 256bit vectors). - /// - /// Vector to duplicate. - /// New vector that is duplicated across the width. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector LoadDuplicate128(Vector128 vec) - { - Span value = stackalloc float[Vector.Count]; - for (int i = 0; i < Vector.Count; i += 4) - { - vec.CopyTo(value[i..]); - } - - return new(value); - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int RoundUpTo(int value, int multiple) => ((value + multiple - 1) / multiple) * multiple; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs index 750f910383..261f7bc668 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs @@ -541,6 +541,38 @@ public static uint MaxValue(Span data) return TensorPrimitives.Max(copy); } + /// + /// A SIMD utility method which takes in the 128 bit vector + /// and duplicates its values to fit in the CPU vector size. + /// For example, + /// + /// 128 bit vectors: A B C D (as-is) + /// 256 bit vectors: A B C D A B C D (duplicate once) + /// 512 bit vectors: A B C D A B C D A B C D A B C D (duplicate three times) + /// + /// Vector<T> has support for arbitrarily large + /// vector sizes. For example, some ARM CPUs support 2048-bit + /// vectors through Vector<T>. In that specific case, this + /// method can be used for future-proofing. + /// + /// Note that this method, albeit future-proof, may be considered + /// slow for smaller vector sizes (think CPUs with 256bit vectors). + /// + /// Vector to duplicate. + /// New vector that is duplicated across the width. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector LoadDuplicate128(Vector128 vec) + where T : unmanaged + { + Span value = stackalloc T[Vector.Count]; + for (int i = 0; i < Vector.Count; i += 4) + { + vec.CopyTo(value[i..]); + } + + return new(value); + } + /// /// Incrementing values to compute the Iota function. /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/AddNoiseStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/AddNoiseStage.cs new file mode 100644 index 0000000000..8c9b58e037 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/AddNoiseStage.cs @@ -0,0 +1,93 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Noise; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +internal class AddNoiseStage( + Configuration configuration, + JxlNoiseParameters noiseParams, + JxlColorCorrelation colorCorrelation, + int firstC) + : RenderPipelineStageBase(configuration) +{ + public override string Name => "AddNoise"; + + public override void ProcessRow(Buffer2D> inputRows, Buffer2D> outputRows, int xExtraLeft, int xExtraRight, int width, int xPos, int yPos) + { + if (!noiseParams.ContainsAny) + { + return; + } + + if (xExtraLeft != 0 || xExtraRight != 0) + { + throw new InvalidOperationException("xExtraLeft and xExtraRight must both be 0"); + } + + StrengthEvaluationLut noiseModel = new(noiseParams); + Vector half = Vector.Create(0.5f); + + // With the prior subtract-random Laplacian approximation, rnd_* ranges were + // about [-1.5, 1.6]; Laplacian3 about doubles this to [-3.6, 3.6], so the + // normalizer is half of what it was before (0.5). + Vector normConst = Vector.Create(0.22f); + + float yToX = colorCorrelation.YToXRatio(0); + float yToB = colorCorrelation.YToBRatio(0); + + Span rowX = this.GetInputRow(inputRows, 0, 0); + Span rowY = this.GetInputRow(inputRows, 1, 0); + Span rowB = this.GetInputRow(inputRows, 2, 0); + + Span rowRndR = this.GetInputRow(inputRows, firstC + 0, 0); + Span rowRndG = this.GetInputRow(inputRows, firstC + 1, 0); + Span rowRndC = this.GetInputRow(inputRows, firstC + 2, 0); + + for (int x = 0; x < width; x += Vector.Count) + { + Vector vx = Vector.Create(rowX[x..]); + Vector vy = Vector.Create(rowY[x..]); + + Vector inputG = vy - vx; + Vector inputR = vy + vx; + + Vector noiseStrengthG = NoiseStageUtils.NoiseStrength(ref noiseModel, inputG * half); + Vector noiseStrengthR = NoiseStageUtils.NoiseStrength(ref noiseModel, inputR * half); + + Vector additionalRandomNoiseRed = Vector.Create(rowRndR[x..]) * normConst; + Vector additionalRandomNoiseGreen = Vector.Create(rowRndG[x..]) * normConst; + Vector additionalRandomNoiseCorrelated = Vector.Create(rowRndC[x..]) * normConst; + + NoiseStageUtils.AddNoiseToRgb( + additionalRandomNoiseRed, + additionalRandomNoiseGreen, + additionalRandomNoiseCorrelated, + noiseStrengthG, + noiseStrengthR, + yToX, + yToB, + rowX[x..], + rowY[x..], + rowB[x..]); + } + } + + public override RenderPipelineChannelMode GetChannelMode(int channel) + { + if (channel >= firstC) + { + return RenderPipelineChannelMode.Input; + } + + if (channel < 3) + { + return RenderPipelineChannelMode.InPlace; + } + + return RenderPipelineChannelMode.Ignored; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/ConvolveNoiseStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/ConvolveNoiseStage.cs new file mode 100644 index 0000000000..72ec94c67e --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/ConvolveNoiseStage.cs @@ -0,0 +1,65 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +internal class ConvolveNoiseStage : RenderPipelineStageBase +{ + /// + /// Offset of the first channel. (3 channels in total) + /// + private readonly int firstC; + + public ConvolveNoiseStage(Configuration configuration, int firstC) + : base(configuration) + { + this.Settings = RenderPipelineStageConfiguration.CreateSymmetricBorderOnly(2); + this.firstC = firstC; + } + + public override string Name => "ConvNoise"; + + public override void ProcessRow(Buffer2D> inputRows, Buffer2D> outputRows, int xExtraLeft, int xExtraRight, int width, int xPos, int yPos) + { + int xStart = -xExtraLeft; + int xEnd = width + xExtraRight; + + for (int c = this.firstC; c < this.firstC + 3; c++) + { + Span row0 = this.GetInputRow(inputRows, c, -2); + Span row1 = this.GetInputRow(inputRows, c, 1); + Span row2 = this.GetInputRow(inputRows, c, 0); + Span row3 = this.GetInputRow(inputRows, c, 1); + Span row4 = this.GetInputRow(inputRows, c, 2); + + Span rowOut = GetOutputRow(outputRows, c, 0); + + for (int x = xStart; x < xEnd; x += Vector.Count) + { + Vector p00 = Vector.Create(row2[x..]); + Vector others = Vector.Zero; + + for (int i = -2; i <= 2; i++) + { + others += Vector.Create(row0[(x + i)..]); + others += Vector.Create(row1[(x + i)..]); + others += Vector.Create(row3[(x + i)..]); + others += Vector.Create(row4[(x + i)..]); + } + + others += Vector.Create(row2[(x - 2)..]); + others += Vector.Create(row2[(x - 1)..]); + others += Vector.Create(row2[(x + 1)..]); + others += Vector.Create(row2[(x + 2)..]); + + Vector pixels = Vector.FusedMultiplyAdd(others, Vector.Create(0.16f), p00 * Vector.Create(-3.84f)); + pixels.CopyTo(rowOut[x..]); + } + } + } + + public override RenderPipelineChannelMode GetChannelMode(int channel) => channel >= this.firstC ? RenderPipelineChannelMode.InOut : RenderPipelineChannelMode.Ignored; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/NoiseStageUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/NoiseStageUtils.cs new file mode 100644 index 0000000000..247daa64dd --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/NoiseStageUtils.cs @@ -0,0 +1,56 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +/// +/// Utilities specific to noise stages. +/// +internal static class NoiseStageUtils +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector Clamp0ToMax(Vector x, Vector maxValue) + { + Vector clamped = Vector.Min(x, maxValue); + return Vector.Max(Vector.Zero, clamped); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector NoiseStrength(ref StrengthEvaluationLut eval, Vector x) => Clamp0ToMax(eval.Compute(x), Vector.One); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void AddNoiseToRgb( + Vector rndNoiseR, + Vector rndNoiseG, + Vector rndNoiseCor, + Vector noiseStrengthG, + Vector noiseStrengthR, + float yToX, + float yToB, + Span outX, + Span outY, + Span outB) + { + Vector rgCorr = Vector.Create(0.9921875f); // 127/128 + Vector rgnCorr = Vector.Create(0.0078125f); // 1/128 + + Vector redNoise = noiseStrengthR * Vector.FusedMultiplyAdd(rgnCorr, rndNoiseR, rgCorr * rndNoiseCor); + Vector greenNoise = noiseStrengthG * Vector.FusedMultiplyAdd(rgnCorr, rndNoiseG, rgCorr * rndNoiseCor); + + Vector vx = Vector.Create(outX); + Vector vy = Vector.Create(outY); + Vector vb = Vector.Create(outB); + + Vector rgNoise = redNoise + greenNoise; + vx = Vector.FusedMultiplyAdd(Vector.Create(yToX), rgNoise, redNoise - greenNoise) + vx; + vy += rgNoise; + vb = Vector.FusedMultiplyAdd(Vector.Create(yToB), rgNoise, vb); + + vx.CopyTo(outX); + vy.CopyTo(outY); + vb.CopyTo(outB); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/StrengthEvaluationLut.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/StrengthEvaluationLut.cs new file mode 100644 index 0000000000..42ddc4e055 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/StrengthEvaluationLut.cs @@ -0,0 +1,80 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Noise; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +internal unsafe struct StrengthEvaluationLut +{ + private fixed byte high16Lut[16]; + private fixed byte low16Lut[16]; + + public StrengthEvaluationLut(JxlNoiseParameters noise) + { + Span lut = stackalloc uint[JxlNoiseParameters.NoisePoints]; + + // We do a direct reinterpret from float to uint + noise.Lookup.AsSpan().CopyTo(MemoryMarshal.Cast(lut)); + + int i2 = 0; // i * 2 + int i21 = 1; // (i * 2) + 1 + for (int i = 0; i < JxlNoiseParameters.NoisePoints; i++) + { + uint currLut = lut[i]; + + this.low16Lut[i2] = (byte)((currLut >> 0) & 0xFF); + this.low16Lut[i21] = (byte)((currLut >> 8) & 0xFF); + this.high16Lut[i2] = (byte)((currLut >> 16) & 0xFF); + this.high16Lut[i21] = (byte)((currLut >> 24) & 0xFF); + + i2 += 2; + i21 += 2; + } + } + + public Vector Compute(Vector vx) + { + fixed (byte* pHigh16Lut = this.high16Lut) + { + fixed (byte* pLow16Lut = this.low16Lut) + { + const int scale = JxlNoiseParameters.NoisePoints - 2; + + Vector scaledVx = Vector.Max(Vector.Zero, vx * Vector.Create((float)scale)); + Vector floorX = Vector.Floor(scaledVx); + Vector fracX = scaledVx - floorX; + + floorX = Vector.ConditionalSelect( + Vector.GreaterThan(scaledVx, Vector.Create((float)scale + 1)), + Vector.Create((float)scale), + floorX); + fracX = Vector.ConditionalSelect( + Vector.GreaterThan(scaledVx, Vector.Create((float)scale + 1)), + Vector.One, + fracX); + + Vector floorXInt = Vector.ConvertToInt32Native(floorX); + Vector floorXIndicesLow = (floorXInt * Vector.Create(0x0202)) + Vector.Create(0x100); + Vector floorXIndicesHigh = (floorXInt * Vector.Create(0x2020000)) + Vector.Create(0x01000000); + + Vector low16 = JxlSimdUtils.LoadDuplicate128(Vector128.Load(pLow16Lut)); + Vector hi16 = JxlSimdUtils.LoadDuplicate128(Vector128.Load(pHigh16Lut)); + Vector lowm = Vector.Create(0xFFFF); // Low mask + Vector him = Vector.Create(unchecked((int)0xFFFF0000)); // High mask + + Vector low = ((SimdUtils.GatherBytes(low16, floorXIndicesLow) & lowm) | (SimdUtils.GatherBytes(hi16, floorXIndicesHigh) & him)).As(); + + floorXIndicesLow += Vector.Create(0x202); + floorXIndicesHigh += Vector.Create(0x20200000); + + Vector high = ((SimdUtils.GatherBytes(low16, floorXIndicesLow) & lowm) | (SimdUtils.GatherBytes(hi16, floorXIndicesHigh) & him)).As(); + + return ((high - low) * fracX) + low; + } + } + } +} From dcb3a3a9b3ba22dcc087d48d045486ad5e2f31b2 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Fri, 18 Sep 2026 21:21:20 +0400 Subject: [PATCH 196/328] Complete write to output stages --- .../Formats/Jxl/Memory/JxlMemoryHelpers.cs | 25 + .../Jxl/Processing/Image/IJxlImageOutput.cs | 11 + .../Jxl/Processing/Image/JxlImageBundle.cs | 6 + .../RenderPipeline/WriteToImage3FStage.cs | 47 ++ .../RenderPipeline/WriteToImageBundleStage.cs | 83 +++ .../RenderPipeline/WriteToOutputStage.cs | 481 +++++++++++++++++- 6 files changed, 634 insertions(+), 19 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToImage3FStage.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToImageBundleStage.cs diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlMemoryHelpers.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlMemoryHelpers.cs index 17f2119487..54c7bd7560 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/JxlMemoryHelpers.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlMemoryHelpers.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Runtime.InteropServices; +using System.Security.AccessControl; namespace SixLabors.ImageSharp.Formats.Jxl.Memory; @@ -19,6 +20,13 @@ public static Memory CastMemory(Memory MemoryFromList(List list) + where T : unmanaged + { + ListFactoryMemoryManager lf = new(list); + return lf.Memory; + } + private sealed class CastingMemoryManager(Memory memory) : MemoryManager where TSourceType : unmanaged @@ -36,4 +44,21 @@ protected override void Dispose(bool disposing) // blank } } + + private sealed class ListFactoryMemoryManager(List list) + : MemoryManager + where T : unmanaged + { + public override Span GetSpan() => CollectionsMarshal.AsSpan(list); + + // We don't use these. + public override MemoryHandle Pin(int elementIndex = 0) => throw new NotImplementedException(); + + public override void Unpin() => throw new NotImplementedException(); + + protected override void Dispose(bool disposing) + { + // blank + } + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Image/IJxlImageOutput.cs b/src/ImageSharp/Formats/Jxl/Processing/Image/IJxlImageOutput.cs index 72d631ff3b..2bc74ef17b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Image/IJxlImageOutput.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Image/IJxlImageOutput.cs @@ -10,6 +10,11 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Image; /// internal interface IJxlImageOutput : IDisposable { + /// + /// Gets or sets the index of this channel. + /// + public int ChannelIndex { get; set; } + /// /// Gets or sets the pixel format for the output pixels. /// @@ -30,6 +35,12 @@ internal interface IJxlImageOutput : IDisposable /// public int Stride { get; set; } + /// + /// Gets or sets a value indicating whether endianness must be + /// swapped. + /// + public bool SwapEndianness { get; set; } + /// /// Outputs the image. /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs index 8971482812..aa6d8c6a5d 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs @@ -312,6 +312,12 @@ public bool TryGetColor(out JxlImage3F? color) /// Extra channels enumerable. public IEnumerable EnumerateExtraChannels() => this.extraChannels; + /// + /// Adds a specified extra channel. + /// + /// The extra channel to be added to the image bundle. + public void AddExtraChannel(JxlImageF channel) => this.extraChannels.Add(channel); + /// /// Sets the extra channels. /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToImage3FStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToImage3FStage.cs new file mode 100644 index 0000000000..a7ededa877 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToImage3FStage.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +internal class WriteToImage3FStage(Configuration configuration, JxlImage3F image) + : RenderPipelineStageBase(configuration) +{ + public override string Name => "WriteI3F"; + + public override void SetInputSizes(Span inputSizes) + { + DebugGuard.MustBeGreaterThanOrEqualTo(inputSizes.Length, 3, nameof(inputSizes)); + + for (int c = 1; c < 3; c++) + { + if (inputSizes[c].Width != inputSizes[0].Width || + inputSizes[c].Height != inputSizes[0].Height) + { + throw new InvalidOperationException("All input sizes must be equal to the first input size"); + } + } + + image.Dispose(); + image = new(this.GetConfiguration(), inputSizes[0].Width, inputSizes[0].Height); + } + + public override void ProcessRow(Buffer2D> inputRows, Buffer2D> outputRows, int xExtraLeft, int xExtraRight, int width, int xPos, int yPos) + { + if (xExtraLeft != 0 || xExtraRight != 0) + { + throw new InvalidOperationException("xExtraLeft and xExtraRight must be 0"); + } + + for (int c = 0; c < 3; c++) + { + this.GetInputRow(inputRows, c, 0)[..width] + .CopyTo(image.PlaneRow(c, yPos)[xPos..]); + } + } + + public override RenderPipelineChannelMode GetChannelMode(int channel) + => channel < 3 ? RenderPipelineChannelMode.Input : RenderPipelineChannelMode.Ignored; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToImageBundleStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToImageBundleStage.cs new file mode 100644 index 0000000000..7d4d163ad8 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToImageBundleStage.cs @@ -0,0 +1,83 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Cms; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +internal class WriteToImageBundleStage(Configuration configuration, JxlImageBundle bundle, JxlColorEncoding colorEncoding) + : RenderPipelineStageBase(configuration) +{ + public override string Name => "WriteIB"; + + public override void SetInputSizes(Span inputSizes) + { + if (inputSizes.Length < 3) + { + throw new InvalidOperationException("Not enough input sizes"); + } + + for (int c = 1; c < inputSizes.Length; c++) + { + if (inputSizes[c].Width != inputSizes[0].Width || + inputSizes[c].Height != inputSizes[0].Height) + { + throw new InvalidOperationException("Input sizes must be equal to the first input size"); + } + } + + JxlImage3F tmp = new(this.GetConfiguration(), inputSizes[0].Width, inputSizes[0].Height); + + if (!bundle.SetFromImage(tmp, colorEncoding)) + { + throw new InvalidOperationException("Could not set the image of an image bundle"); + } + + foreach (JxlImageF ec in bundle.EnumerateExtraChannels()) + { + // We don't want a memory leak here, as we're + // removing old extra channels and inserting + // new ones + ec.Dispose(); + } + + bundle.ClearExtraChannels(); + + for (int c = 3; c < inputSizes.Length; c++) + { + JxlImageF ch = new(this.GetConfiguration(), inputSizes[c].Width, inputSizes[c].Height); + bundle.AddExtraChannel(ch); + } + } + + public override void ProcessRow(Buffer2D> inputRows, Buffer2D> outputRows, int xExtraLeft, int xExtraRight, int width, int xPos, int yPos) + { + if (xExtraLeft != 0 || xExtraRight != 0) + { + throw new InvalidOperationException("xExtraLeft and xExtraRight must be 0"); + } + + for (int c = 0; c < 3; c++) + { + this.GetInputRow(inputRows, c, 0)[..width] + .CopyTo(bundle.Color!.PlaneRow(c, yPos)[xPos..]); + } + + int ecIndex = 0; + foreach (JxlImageF ec in bundle.EnumerateExtraChannels()) + { + if (ec.XSize < xPos + width) + { + throw new InvalidOperationException("Width is too small"); + } + + this.GetInputRow(inputRows, ecIndex++ + 3, 0)[..width] + .CopyTo(ec.GetRow(yPos)[(xPos - xExtraLeft)..]); + } + } + + public override RenderPipelineChannelMode GetChannelMode(int channel) => RenderPipelineChannelMode.Input; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs index 62ae1a00fa..476beace38 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs @@ -1,34 +1,72 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; +using System.Buffers.Binary; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; -internal sealed class WriteToOutputStage +internal sealed class WriteToOutputStage : RenderPipelineStageBase, IDisposable { private const int ChunkSize = 1024; - private int width; - private int height; - private IJxlImageOutput main; - private int numColors; - private bool wantAlpha; - private bool hasAlpha; - private bool unpremultiplyAlpha; - private int alphaC; - private bool flipX; - private bool flipY; - private bool transpose; + private readonly int width; + private readonly int height; + private readonly IJxlImageOutput main; + private readonly int numColors; + private readonly bool wantAlpha; + private readonly bool hasAlpha; + private readonly bool unpremultiplyAlpha; + private readonly int alphaC; + private readonly bool flipX; + private readonly bool flipY; + private readonly bool transpose; private readonly List extraChannels = []; private readonly List opaqueAlpha = []; - private readonly Configuration configuration; - private List> tempIn; - private List> tempOut; + private readonly List> tempIn = []; + private readonly List> tempOut = []; + + // For Dispose() + private readonly List> disposables = []; + + public WriteToOutputStage(Configuration configuration, IJxlImageOutput output, int width, int height, bool hasAlpha, bool unpremultiplyAlpha, int alphaC, JxlExifOrientation undoOrientation, List extraChannel, Func extraFactory) + : base(configuration) + { + this.width = width; + this.height = height; + this.main = output; + this.numColors = this.main.PixelFormat.Channels < 3 ? 1 : 3; + this.wantAlpha = this.main.PixelFormat.Channels is 2 or 4; + this.hasAlpha = hasAlpha; + this.unpremultiplyAlpha = unpremultiplyAlpha; + this.alphaC = alphaC; + this.flipX = ShouldFlipX(undoOrientation); + this.flipY = ShouldFlipY(undoOrientation); + this.transpose = ShouldTranspose(undoOrientation); + this.opaqueAlpha = new(ChunkSize); + CollectionsMarshal.AsSpan(this.opaqueAlpha).Fill(1.0f); + + for (int ec = 0; ec < this.extraChannels.Count; ec++) + { + if (extraChannel[ec].IsPresent) + { + IJxlImageOutput extra = extraFactory(extraChannel[ec]); + extra.ChannelIndex = 3 + ec; + this.extraChannels.Add(extra); + } + } + } + + public override string Name => "WritePixelCB"; /// /// Gets the 32x32 blue noise dithering pattern lookup @@ -251,7 +289,118 @@ JxlExifOrientation.Rotate90 or JxlExifOrientation.Rotate270 or JxlExifOrientation.AntiTranspose; - private unsafe void UnpremultiplyAlpha(int threadId, int len, float** lineBuffers) + public override RenderPipelineChannelMode GetChannelMode(int channel) + { + if (channel < this.numColors || (this.hasAlpha && channel == this.alphaC)) + { + return RenderPipelineChannelMode.Input; + } + + foreach (IJxlImageOutput ec in this.extraChannels) + { + if (channel == ec.ChannelIndex) + { + return RenderPipelineChannelMode.Input; + } + } + + return RenderPipelineChannelMode.Ignored; + } + + public override void Dispose() + { + base.Dispose(); + + foreach (IDisposable disposables in this.disposables) + { + disposables.Dispose(); + } + } + + public override void ProcessRow(Buffer2D> inputRows, Buffer2D> outputRows, int xExtraLeft, int xExtraRight, int width, int xPos, int yPos) + { + // HACK: to pass thread ID we use the mask in xExtraLeft, which shall be 0 anyway + int threadId = xExtraLeft & ~7; + + if (yPos >= this.height) + { + return; + } + + if (xPos >= this.width) + { + return; + } + + if (this.flipY) + { + yPos = this.height - 1 - yPos; + } + + int limit = Math.Min(width, this.width - xPos); + + for (int x0 = 0; x0 < limit; x0 += ChunkSize) + { + int xStart = xPos + x0; + int length = Math.Min(ChunkSize, limit - x0); + + Span> lineBuffers = [Memory.Empty, Memory.Empty, Memory.Empty, Memory.Empty]; + + for (int c = 0; c < this.numColors; c++) + { + lineBuffers[c] = this.GetInputRowMemory(inputRows, c, 0)[x0..]; + } + + if (this.hasAlpha) + { + lineBuffers[this.numColors] = this.GetInputRowMemory(inputRows, this.alphaC, 0)[x0..]; + } + else + { + lineBuffers[this.numColors] = JxlMemoryHelpers.MemoryFromList(this.opaqueAlpha); + } + + if (this.hasAlpha && this.wantAlpha && this.unpremultiplyAlpha) + { + this.UnpremultiplyAlpha(threadId, length, lineBuffers); + } + + this.OutputBuffers(this.main, threadId, yPos, ref xStart, length, lineBuffers[0], lineBuffers[1], lineBuffers[2], lineBuffers[3]); + + foreach (IJxlImageOutput extra in this.extraChannels) + { + lineBuffers[0] = this.GetInputRowMemory(inputRows, extra.ChannelIndex, 0)[x0..]; + this.OutputBuffers(extra, threadId, yPos, ref xStart, length, lineBuffers[0], lineBuffers[1], lineBuffers[2], lineBuffers[3]); + } + } + } + + public void PrepareForThreads(Configuration configuration, int numThreads) + { + this.tempOut.Resize(numThreads); + int allocSize = sizeof(float) * ChunkSize; + + for (int i = 0; i < this.tempOut.Count; i++) + { + IMemoryOwner owner = configuration.MemoryAllocator.Allocate(allocSize * this.main.PixelFormat.Channels); + this.disposables.Add(owner); + this.tempOut[i] = owner.Memory; + } + + if ((this.hasAlpha && this.wantAlpha && this.unpremultiplyAlpha) || this.flipX) + { + this.tempIn.Resize(numThreads * this.main.PixelFormat.Channels); + + for (int i = 0; i < this.tempIn.Count; i++) + { + IMemoryOwner owner = configuration.MemoryAllocator.Allocate(allocSize); + this.disposables.Add(owner); + this.tempIn[i] = owner.Memory; + } + } + } + + private unsafe void UnpremultiplyAlpha(int threadId, int len, Span> lineBuffers) { // Highly unsafe code! ⚠️ float** tempIn = stackalloc float*[4]; @@ -266,7 +415,7 @@ private unsafe void UnpremultiplyAlpha(int threadId, int len, float** lineBuffer tempIn[c] = (float*)Unsafe.AsPointer(ref MemoryMarshal.Cast(this.tempIn[tix].Span)[0]); - MemoryMarshal.CreateSpan(ref Unsafe.AsRef(lineBuffers[c]), len) + lineBuffers[c].Span[..len] .CopyTo(MemoryMarshal.CreateSpan(ref Unsafe.AsRef(tempIn[c]), len)); } @@ -325,10 +474,304 @@ private unsafe void UnpremultiplyAlpha(int threadId, int len, float** lineBuffer for (int c = 0; c < this.main.PixelFormat.Channels; c++) { - fixed (byte* ptr = this.tempIn[c].Span) + lineBuffers[c] = JxlMemoryHelpers.CastMemory(this.tempIn[c]); + } + } + + private static void StoreFloatRow(IJxlImageOutput output, Span input0, Span input1, Span input2, Span input3, int length, Span result) + { + Span bufferSpan = MemoryMarshal.Cast(output.Buffer.Span); + + if (output.PixelFormat.Channels == 1) + { + input0.CopyTo(result); + } + else if (output.PixelFormat.Channels == 2) + { + for (int i = 0; i < length; i += Vector.Count) + { + JxlSimdUtils.StoreInterleaved( + Vector.Create(input0[i..]), + Vector.Create(input1[i..]), + ref bufferSpan[2 * i]); + } + } + else if (output.PixelFormat.Channels == 3) + { + for (int i = 0; i < length; i += Vector.Count) + { + JxlSimdUtils.StoreInterleaved( + Vector.Create(input0[i..]), + Vector.Create(input1[i..]), + Vector.Create(input2[i..]), + ref bufferSpan[3 * i]); + } + } + else if (output.PixelFormat.Channels == 4) + { + for (int i = 0; i < length; i += Vector.Count) + { + JxlSimdUtils.StoreInterleaved( + Vector.Create(input0[i..]), + Vector.Create(input1[i..]), + Vector.Create(input2[i..]), + Vector.Create(input3[i..]), + ref bufferSpan[4 * i]); + } + } + } + + private static void StoreFloat16Row(IJxlImageOutput output, Span input0, Span input1, Span input2, Span input3, int length, Span result) + { + if (output.PixelFormat.Channels == 1) + { + for (int i = 0; i < length; i += Vector.Count) + { + Vector v0 = Vector.Create(input0[i..]); + JxlHalfUtils.ConvertSingleToHalf(v0).CopyTo(result[i..]); + } + } + else if (output.PixelFormat.Channels == 2) + { + for (int i = 0; i < length; i += Vector.Count) + { + Vector v0 = Vector.Create(input0[i..]); + Vector v1 = Vector.Create(input1[i..]); + JxlSimdUtils.StoreInterleaved( + JxlHalfUtils.ConvertSingleToHalf(v0), + JxlHalfUtils.ConvertSingleToHalf(v1), + ref result[i * 2]); + } + } + else if (output.PixelFormat.Channels == 3) + { + for (int i = 0; i < length; i += Vector.Count) + { + Vector v0 = Vector.Create(input0[i..]); + Vector v1 = Vector.Create(input1[i..]); + Vector v2 = Vector.Create(input2[i..]); + JxlSimdUtils.StoreInterleaved( + JxlHalfUtils.ConvertSingleToHalf(v0), + JxlHalfUtils.ConvertSingleToHalf(v1), + JxlHalfUtils.ConvertSingleToHalf(v2), + ref result[i * 3]); + } + } + else if (output.PixelFormat.Channels == 4) + { + for (int i = 0; i < length; i += Vector.Count) + { + Vector v0 = Vector.Create(input0[i..]); + Vector v1 = Vector.Create(input1[i..]); + Vector v2 = Vector.Create(input2[i..]); + Vector v3 = Vector.Create(input3[i..]); + JxlSimdUtils.StoreInterleaved( + JxlHalfUtils.ConvertSingleToHalf(v0), + JxlHalfUtils.ConvertSingleToHalf(v1), + JxlHalfUtils.ConvertSingleToHalf(v2), + JxlHalfUtils.ConvertSingleToHalf(v3), + ref result[i * 4]); + } + } + } + + private static void StoreUnsignedRow(IJxlImageOutput output, Span input0, Span input1, Span input2, Span input3, int length, Span result, int xStart, int yPos) + where TUnsigned : unmanaged, INumber + { + Vector mul = Vector.Create((1 << output.BitsPerSample) - 1.0f); + + if (output.PixelFormat.Channels == 1) + { + for (int i = 0; i < length; i += Vector.Count) + { + MakeUnsigned( + Vector.Create(input0[i..]), + xStart + i, + yPos, + mul, + 0) + .CopyTo(result[i..]); + } + } + else if (output.PixelFormat.Channels == 2) + { + for (int i = 0; i < length; i += Vector.Count) + { + JxlSimdUtils.StoreInterleaved( + MakeUnsigned(Vector.Create(input0[i..]), xStart + i, yPos, mul, 0), + MakeUnsigned(Vector.Create(input1[i..]), xStart + i, yPos, mul, 1), + ref result[i * 2]); + } + } + else if (output.PixelFormat.Channels == 3) + { + for (int i = 0; i < length; i += Vector.Count) + { + JxlSimdUtils.StoreInterleaved( + MakeUnsigned(Vector.Create(input0[i..]), xStart + i, yPos, mul, 0), + MakeUnsigned(Vector.Create(input1[i..]), xStart + i, yPos, mul, 1), + MakeUnsigned(Vector.Create(input2[i..]), xStart + i, yPos, mul, 2), + ref result[i * 3]); + } + } + else if (output.PixelFormat.Channels == 4) + { + for (int i = 0; i < length; i += Vector.Count) { - lineBuffers[c] = (float*)ptr; + JxlSimdUtils.StoreInterleaved( + MakeUnsigned(Vector.Create(input0[i..]), xStart + i, yPos, mul, 0), + MakeUnsigned(Vector.Create(input1[i..]), xStart + i, yPos, mul, 1), + MakeUnsigned(Vector.Create(input2[i..]), xStart + i, yPos, mul, 2), + MakeUnsigned(Vector.Create(input3[i..]), xStart + i, yPos, mul, 3), + ref result[i * 4]); } } } + + private static Vector MakeUnsigned(Vector v, int x0, int y0, Vector mul, int c) + where T : unmanaged + { + v *= mul; + + if (typeof(T) == typeof(byte)) + { + int xOff = (x0 + (c * 23)) % 32; + int yOff = (y0 + (c * 13)) % 32; + int pos = (yOff * 48) + xOff; + + Vector dither = Vector.Create(DitheringPattern[pos..]); + v += dither; + } + + v = Vector.Min(Vector.Max(Vector.Zero, v), mul); + + Vector ni = Vector.ConvertToInt32(v); + Vector nu = Vector.AsVectorUInt32(ni); + + if (typeof(T) == typeof(uint)) + { + return (Vector)(object)nu; + } + + return nu.As(); + } + + private void FlipX(IJxlImageOutput output, int threadId, int length, ref int xStart, Span> lineBuffers) + { + InlineArray4> temporaryInput = default; + + for (int c = 0; c < output.PixelFormat.Channels; c++) + { + int tix = (threadId * this.main.PixelFormat.Channels) + c; + temporaryInput[c] = JxlMemoryHelpers.CastMemory(this.tempIn[tix]); + + lineBuffers[c][..length].Span.CopyTo(temporaryInput[c].Span); + } + + int last = length - 1; + int num = length / 2; + + for (int i = 0; i < num; i++) + { + for (int c = 0; c < output.PixelFormat.Channels; c++) + { + RuntimeUtility.Swap(ref temporaryInput[c].Span[i], ref temporaryInput[c].Span[last - i]); + } + } + + for (int c = 0; c < output.PixelFormat.Channels; c++) + { + lineBuffers[c] = temporaryInput[c]; + } + + xStart = this.width - xStart - length; + } + + private unsafe void WriteToOutput(IJxlImageOutput output, int yPos, int xStart, int length, Span result) + where T : unmanaged + { + if (this.transpose) + { + int stride = output.PixelFormat.Channels * sizeof(T); + int offset = (xStart * output.Stride) + (yPos * stride); + + for (int i = 0, j = 0; i < length; i++, j += output.PixelFormat.Channels) + { + int ix = offset + (i * output.Stride); + MemoryMarshal.Cast(result).Slice(j, stride).CopyTo(output.Buffer.Span[ix..]); + } + } + else + { + int stride = output.PixelFormat.Channels * sizeof(T); + int offset = (yPos * output.Stride) + (xStart * stride); + MemoryMarshal.Cast(result)[..(length * stride)].CopyTo(output.Buffer.Span[offset..]); + } + } + + private void OutputBuffers(IJxlImageOutput output, int threadId, int ypos, ref int xstart, int length, Memory input0, Memory input1, Memory input2, Memory input3) + { + if (this.flipX) + { + Span> buffers = [input0, input1, input2, input3]; + this.FlipX(output, threadId, length, ref xstart, buffers); + } + + if (output.PixelFormat.DataType == IO.JxlDataType.Byte) + { + Span temp = this.tempOut[threadId].Span; + StoreUnsignedRow(output, input0.Span, input1.Span, input2.Span, input3.Span, length, temp, xstart, ypos); + this.WriteToOutput(output, ypos, xstart, length, temp); + } + else if (output.PixelFormat.DataType is IO.JxlDataType.UInt16 or IO.JxlDataType.Single) + { + Span temp = MemoryMarshal.Cast(this.tempOut[threadId].Span); + + if (output.PixelFormat.DataType == IO.JxlDataType.UInt16) + { + StoreUnsignedRow(output, input0.Span, input1.Span, input2.Span, input3.Span, length, temp, xstart, ypos); + } + else + { + StoreFloat16Row(output, input0.Span, input1.Span, input2.Span, input3.Span, length, temp); + } + + if (output.SwapEndianness) + { + int outputLength = length * output.PixelFormat.Channels; + for (int j = 0; j < outputLength; j += Vector.Count) + { + Vector v = Vector.Create(temp[j..]); + Vector vswap = Vector.ShiftRightLogical(v, 8) | (v << 8); + vswap.CopyTo(temp[j..]); + } + } + + this.WriteToOutput(output, ypos, xstart, length, temp); + } + else if (output.PixelFormat.DataType == IO.JxlDataType.Single) + { + Span temp = MemoryMarshal.Cast(this.tempOut[threadId].Span); + StoreFloatRow(output, input0.Span, input1.Span, input2.Span, input3.Span, length, temp); + + if (output.SwapEndianness) + { + int outputLength = length * output.PixelFormat.Channels; + + for (int j = 0; j < outputLength; j++) + { + temp[j] = ReverseEndianness(temp[j]); + } + } + + this.WriteToOutput(output, ypos, xstart, length, temp); + } + } + + private static float ReverseEndianness(float value) + { + int bits = BitConverter.SingleToInt32Bits(value); + bits = BinaryPrimitives.ReverseEndianness(bits); + return BitConverter.Int32BitsToSingle(bits); + } } From 684cb90485d90f1bb2df4e42ac10d90f645cb528 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 23 Sep 2026 06:35:23 +0400 Subject: [PATCH 197/328] Add dot dictionary, dot detection, incomplete modular frame decoder, blank upsampling stage, new tests --- .../Decoder/Modular/JxlModularFrameDecoder.cs | 240 ++++++- .../Decoder/Modular/JxlModularKind.cs | 14 + .../Decoder/Modular/JxlModularStreamId.cs | 42 ++ .../DotDetection/JxlConnectedComponent.cs | 93 +++ .../DotDetection/JxlDotDetectionUtils.cs | 679 ++++++++++++++++++ .../Encoder/DotDetection/JxlDotDictionary.cs | 104 +++ .../DotDetection/JxlEllipseQuantParameters.cs | 40 ++ .../JxlGaussianDetectParameters.cs | 50 ++ .../DotDetection/JxlGaussianEllipse.cs | 75 ++ .../Formats/Jxl/Processing/JxlSimdUtils.cs | 7 +- .../Processing/Primitives/JxlMatrix3x3F.cs | 31 + .../RenderPipeline/UpsamplingStage.cs | 26 + .../Jxl/Processing/CoefficientOrderTests.cs | 67 ++ .../Formats/Jxl/Processing/OpsinImageTests.cs | 153 ++++ 14 files changed, 1610 insertions(+), 11 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/Modular/JxlModularKind.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/Modular/JxlModularStreamId.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlConnectedComponent.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlDotDetectionUtils.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlDotDictionary.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlEllipseQuantParameters.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlGaussianDetectParameters.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlGaussianEllipse.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/UpsamplingStage.cs create mode 100644 tests/ImageSharp.Tests/Formats/Jxl/Processing/CoefficientOrderTests.cs create mode 100644 tests/ImageSharp.Tests/Formats/Jxl/Processing/OpsinImageTests.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Modular/JxlModularFrameDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Modular/JxlModularFrameDecoder.cs index 416a14b49a..ec6c02d0d6 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Modular/JxlModularFrameDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Modular/JxlModularFrameDecoder.cs @@ -1,25 +1,249 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; +using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular; using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding; using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; +using Tree = System.Collections.Generic.List; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Modular; -internal sealed class JxlModularFrameDecoder +internal sealed class JxlModularFrameDecoder(Configuration configuration, JxlFrameDimensions fd) { - private readonly Configuration configuration; - private readonly JxlModularImage fullImage; - private readonly List globalTransform; - private JxlFrameDimensions frameDimensions; + private const float AlmostZero = 1e-8f; + + private readonly Configuration configuration = configuration; + private readonly JxlModularImage fullImage = new(); + private readonly List globalTransform = []; + private readonly JxlFrameDimensions frameDimensions = fd; private bool doColor; private bool haveSomething; private bool useFullImage = true; private bool allSameShift; - private JxlAnsCode code; - private readonly List contextMap; - private JxlGroupHeader groupHeader; + private readonly Tree tree = []; + private JxlAnsCode code = new(); + private readonly List contextMap = []; + private JxlGroupHeader groupHeader = new(); + + public Configuration Configuration => this.configuration; + + public bool UsesFullImage => this.useFullImage; + + public bool ContainsDc => this.haveSomething; + + private static void MultiplySum(int xSize, ReadOnlySpan rowIn, ReadOnlySpan rowInY, float factor, Span rowOut) + { + Vector factorV = new(factor); + + int lanes = Vector.Count; + + for (int x = 0; x < xSize; x += lanes) + { + Vector inV = Vector.ConvertToSingle(Vector.Create(rowIn[x..])); + inV += Vector.ConvertToSingle(Vector.Create(rowInY[x..])); + + Vector outV = inV * factorV; + outV.CopyTo(rowOut[x..]); + } + } + + private static void RgbFromSingle(int xSize, ReadOnlySpan rowIn, float factor, Span outR, Span outG, Span outB) + { + Vector factorV = new(factor); + + int lanes = Vector.Count; + + for (int x = 0; x < xSize; x += lanes) + { + Vector inV = Vector.ConvertToSingle(Vector.Create(rowIn[x..])); + Vector outV = inV * factorV; + + outV.CopyTo(outR[x..]); + outV.CopyTo(outG[x..]); + outV.CopyTo(outB[x..]); + } + } + + private static void SingleFromSingle(int xSize, ReadOnlySpan rowIn, float factor, Span rowOut) + { + Vector factorV = new(factor); + + int lanes = Vector.Count; + + for (int x = 0; x < xSize; x += lanes) + { + Vector inV = Vector.ConvertToSingle(Vector.Create(rowIn[x..])); + Vector outV = inV * factorV; + + outV.CopyTo(rowOut[x..]); + } + } + + private static void SingleFromSingleAccurate(int xSize, Span rowIn, float factor, Span rowOut) + { + // TensorPrimitives doesn't have a .ConvertToSingle + // with Span as input. + for (int x = 0; x < xSize; x++) + { + rowOut[x] = rowIn[x] * factor; + } + } + + private static void IntToFloat(ReadOnlySpan rowIn, Span rowOut, int xSize, int bits, int expBits) + { + if (bits == 32) + { + if (expBits != 8) + { + throw new InvalidOperationException("Exponent bits must be equal to 8"); + } + + rowIn[..xSize].CopyTo(rowOut); + return; + } + + int expBias = (1 << (expBits - 1)) - 1; + int signShift = bits - 1; + int mantBits = bits - expBits - 1; + int mantShift = 23 - mantBits; + + for (int x = 0; x < xSize; x++) + { + uint f = unchecked((uint)BitConverter.SingleToInt32Bits(rowIn[x])); + uint signBit = f >> signShift; + + f &= (1u << signShift) - 1; + + if (f == 0) + { + rowOut[x] = signBit != 0 ? -0f : 0f; + continue; + } + + int exp = (int)(f >> mantBits); + int mantissa = (int)(f & ((1u << mantBits) - 1)); + + if (exp == (1 << expBits) - 1) + { + // NaN or infinity. + uint result = signBit != 0 ? 0x80000000u : 0u; + result |= 0xFFu << 23; + result |= (uint)(mantissa << mantShift); + + rowOut[x] = BitConverter.Int32BitsToSingle(unchecked((int)result)); + continue; + } + + mantissa <<= mantShift; + + // Try to normalize only if there is space for maneuver. + if (exp == 0 && expBits < 8) + { + // Subnormal number. + while ((mantissa & 0x800000) == 0) + { + mantissa <<= 1; + exp--; + } + + exp++; + + // Remove leading 1 because it is implicit now. + mantissa &= 0x7FFFFF; + } + + exp -= expBias; + + // Break up the arbitrary float into its parts, then reassemble + // into binary32. + exp += 127; + + if (exp < 0) + { + throw new InvalidOperationException("Exponent cannot be negative"); + } + + uint resultBits = signBit != 0 ? 0x80000000u : 0u; + resultBits |= (uint)(exp << 23); + resultBits |= (uint)mantissa; + + rowOut[x] = BitConverter.Int32BitsToSingle(unchecked((int)resultBits)); + } + } + + public void DecodeQuantizerTable(int requiredSizeX, int requiredSizeY, JxlBitReader reader, JxlQuantizerEncoding encoding, int index, JxlModularFrameDecoder? decoder) + { + float qtableDen = 0; + if (!JxlF16Coder.Read(reader, ref qtableDen)) + { + throw new InvalidOperationException("Quantizer table denominator is too small"); + } + + encoding.QuantizationTableDenominator = qtableDen; + + using JxlModularImage img = JxlModularImage.Create(this.configuration, requiredSizeX, requiredSizeY, 8, 3); + JxlModularOptions options = new(); + + if (decoder is not null) + { + JxlModularStreamId qt = JxlModularStreamId.CreateQuantizerTable(index); + ModularGenericDecompress( + reader, + img, + null, + qt.GetId(decoder.frameDimensions), + options, + true, + decoder.tree, + decoder.code, + decoder.contextMap); + } + else + { + ModularGenericDecompress( + reader, + img, + null, + 0, + options, + true); + } + + if (encoding.QuantizationTable is null) + { + encoding.QuantizationTable = new int[requiredSizeX * requiredSizeY * 3]; + } + else + { + if (encoding.QuantizationTable.Length != requiredSizeX * requiredSizeY * 3) + { + throw new InvalidOperationException("The length of the quantization table doesn't match"); + } + } + + Span qtable = encoding.QuantizationTable.AsSpan(); + + for (int c = 0; c < 3; c++) + { + for (int y = 0; y < requiredSizeY; y++) + { + Span row = img.Channels[c].GetRow(y); + + for (int x = 0; x < requiredSizeX; x++) + { + qtable[(c * requiredSizeX * requiredSizeY) + (y * requiredSizeX) + x] = row[x]; + + if (row[x] <= 0) + { + throw new InvalidOperationException("Invalid raw quantization table"); + } + } + } + } + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Modular/JxlModularKind.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Modular/JxlModularKind.cs new file mode 100644 index 0000000000..2f944f6110 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Modular/JxlModularKind.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Modular; + +internal enum JxlModularKind : byte +{ + GlobalData, + VarDctDc, + ModularDc, + AcMetadata, + QuantizerTable, + ModularAc +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Modular/JxlModularStreamId.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Modular/JxlModularStreamId.cs new file mode 100644 index 0000000000..580b6cc875 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Modular/JxlModularStreamId.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Modular; + +internal readonly record struct JxlModularStreamId( + JxlModularKind Kind, + int QuantTableId, + int GroupId, + int PassId) +{ + public static readonly JxlModularStreamId Global = new(JxlModularKind.GlobalData, 0, 0, 0); + + public int GetId(JxlFrameDimensions dimensions) => this.Kind switch + { + JxlModularKind.GlobalData => 0, + JxlModularKind.VarDctDc => 1 + this.GroupId, + JxlModularKind.ModularDc => 1 + dimensions.NumDcGroups + this.GroupId, + JxlModularKind.AcMetadata => 1 + (2 * dimensions.NumDcGroups) + this.GroupId, + JxlModularKind.QuantizerTable => 1 + (3 * dimensions.NumDcGroups) + this.QuantTableId, + JxlModularKind.ModularAc or _ => 1 + (3 * dimensions.NumDcGroups) + JxlQuantizerConstants.NumberOfQuantizerTables + dimensions.NumGroups + this.PassId + this.GroupId + }; + + public static JxlModularStreamId CreateVarDctDc(int groupId) => new(JxlModularKind.VarDctDc, 0, groupId, 0); + + public static JxlModularStreamId CreateModularDc(int groupId) => new(JxlModularKind.ModularDc, 0, groupId, 0); + + public static JxlModularStreamId CreateAcMetadata(int groupId) => new(JxlModularKind.AcMetadata, 0, groupId, 0); + + public static JxlModularStreamId CreateModularAc(int groupId, int passId) => new(JxlModularKind.ModularAc, 0, groupId, passId); + + public static JxlModularStreamId CreateQuantizerTable(int quantTableId) + { + DebugGuard.MustBeLessThan(quantTableId, JxlQuantizerConstants.NumberOfQuantizerTables, nameof(quantTableId)); + return new(JxlModularKind.QuantizerTable, quantTableId, 0, 0); + } + + public static int Num(JxlFrameDimensions frameDimensions, int passes) => CreateModularAc(0, passes).GetId(frameDimensions); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlConnectedComponent.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlConnectedComponent.cs new file mode 100644 index 0000000000..78fd91806a --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlConnectedComponent.cs @@ -0,0 +1,93 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.DotDetection; + +internal sealed class JxlConnectedComponent +{ + private Rectangle bounds; + private readonly List points = []; + private float maxEnergy; + private float meanEnergy; + private float varEnergy; + private float meanBg; + private float varBg; + private Point mode; + + public JxlConnectedComponent(Rectangle bounds, List points) + { + this.bounds = bounds; + this.points = points; + } + + public Point Mode => this.mode; + + public Rectangle Bounds => this.bounds; + + public float Score { get; private set; } + + public void ComputeStats(JxlImageF energy, in Rectangle rect, int extra) + { + this.maxEnergy = 0.0F; + this.meanEnergy = 0.0F; + this.varEnergy = 0.0F; + this.meanBg = 0.0F; + this.varBg = 0.0F; + int nIn = 0; + int nOut = 0; + this.mode.X = 0; + this.mode.Y = 0; + + for (int sy = -extra; sy < (this.bounds.Height + extra); sy++) + { + int y = sy + this.bounds.Y0(); + + if (y < 0 || (uint)y >= rect.Height) + { + continue; + } + + Span erow = energy.GetRow(rect, y); + + for (int sx = -extra; sx < (this.bounds.Width + extra); sx++) + { + int x = sx + this.bounds.X0(); + + if (x < 0 || (uint)x >= rect.Width) + { + continue; + } + + if (erow[x] > this.maxEnergy) + { + this.maxEnergy = erow[x]; + this.mode.X = x; + this.mode.Y = y; + } + + if (JxlDotDetectionUtils.PointInRect(this.bounds, new Point(x, y))) + { + this.meanEnergy += erow[x]; + this.varEnergy += erow[x] * erow[x]; + nIn++; + } + else + { + this.meanBg += erow[x]; + this.varBg += erow[x] * erow[x]; + nOut++; + } + } + } + + this.meanEnergy /= nIn; + this.meanBg /= nOut; + this.varEnergy = (this.varEnergy / nIn) - (this.meanEnergy * this.meanEnergy); + + this.varBg = (this.varBg / nOut) - (this.meanBg * this.meanBg); + this.Score = (this.meanEnergy - this.meanBg) / MathF.Sqrt(this.varBg); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlDotDetectionUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlDotDetectionUtils.cs new file mode 100644 index 0000000000..bbccda5211 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlDotDetectionUtils.cs @@ -0,0 +1,679 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using Matrix2x2 = System.Runtime.CompilerServices.InlineArray2>; +using Vector2 = System.Runtime.CompilerServices.InlineArray2; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.DotDetection; + +internal static class JxlDotDetectionUtils +{ + public const int EllipseWindowSize = 5; + + /// + /// Maximum area in pixels of an ellipse. + /// + public const int MaxCCSize = 1000; + + public const bool OptimizeBackground = true; + + /// + /// Sum of Square Differences + /// + /// Contains the memory allocator and parallel options. + /// Original image + /// Smooth image + /// Sum of Square Differences coefficients + public static JxlImageF Ssd(Configuration configuration, JxlImage3F original, JxlImage3F smooth) + { + Vector colorCoef0 = Vector.Zero; + Vector colorCoef1 = Vector.Create(10.0f); + Vector colorCoef2 = Vector.Zero; + + JxlImageF sumOfSquares = new(configuration, original.XSize, original.YSize); + + _ = Parallel.For(0, original.YSize, configuration.GetParallelOptions(), y => + { + Span originalRow0 = original.Plane(0).GetRow(y); + Span originalRow1 = original.Plane(1).GetRow(y); + Span originalRow2 = original.Plane(2).GetRow(y); + + Span smoothRow0 = smooth.Plane(0).GetRow(y); + Span smoothRow1 = smooth.Plane(1).GetRow(y); + Span smoothRow2 = smooth.Plane(2).GetRow(y); + + Span sumOfSquaresRow = sumOfSquares.GetRow(y); + + for (int x = 0; x < original.XSize; x += Vector.Count) + { + Vector v0 = Vector.Create(originalRow0[x..]) - Vector.Create(smoothRow0[x..]); + Vector v1 = Vector.Create(originalRow1[x..]) - Vector.Create(smoothRow1[x..]); + Vector v2 = Vector.Create(originalRow2[x..]) - Vector.Create(smoothRow2[x..]); + + v0 = (v0 * v0) * colorCoef0; + v1 = (v1 * v1) * colorCoef1; + v2 = (v2 * v2) * colorCoef2; + + Vector weightedSumOfSquareDiffs = v0 + v1 + v2; + weightedSumOfSquareDiffs.CopyTo(sumOfSquaresRow[x..]); + } + }); + + return sumOfSquares; + } + + /// + /// Creates Gaussian that smooths noise but preserves dots. + /// + /// Gaussian + public static JxlWeightsSeparable5 WeightsSeparable5Gaussian0_65() + { + const float w0 = 0.558311f; + const float w1 = 0.210395f; + const float w2 = 0.010449f; + + JxlWeightsSeparable5 weights = default; + + // HWY_REP4(w0), HWY_REP4(w1), HWY_REP4(w2) + weights.Horizontal[0] = w0; + weights.Horizontal[1] = w0; + weights.Horizontal[2] = w0; + weights.Horizontal[3] = w0; + + weights.Horizontal[4] = w1; + weights.Horizontal[5] = w1; + weights.Horizontal[6] = w1; + weights.Horizontal[7] = w1; + + weights.Horizontal[8] = w2; + weights.Horizontal[9] = w2; + weights.Horizontal[10] = w2; + weights.Horizontal[11] = w2; + + // Same weights for vertical + weights.Vertical = weights.Horizontal; + + return weights; + } + + /// + /// Creates Gaussian that removes dots. + /// + /// Gaussian + public static JxlWeightsSeparable5 WeightsSeparable5Gaussian3() + { + const float w0 = 0.222338f; + const float w1 = 0.210431f; + const float w2 = 0.1784f; + + JxlWeightsSeparable5 weights = default; + + // HWY_REP4(w0), HWY_REP4(w1), HWY_REP4(w2) + weights.Horizontal[0] = w0; + weights.Horizontal[1] = w0; + weights.Horizontal[2] = w0; + weights.Horizontal[3] = w0; + + weights.Horizontal[4] = w1; + weights.Horizontal[5] = w1; + weights.Horizontal[6] = w1; + weights.Horizontal[7] = w1; + + weights.Horizontal[8] = w2; + weights.Horizontal[9] = w2; + weights.Horizontal[10] = w2; + weights.Horizontal[11] = w2; + + // Same weights for vertical + weights.Vertical = weights.Horizontal; + + return weights; + } + + public static double DotGaussianModel(double dx, double dy, double ct, double st, double sigmaX, double sigmaY, double intensity) + { + double rx = (ct * dx) + (st * dy); + double ry = (-st * dx) + (ct * dy); + double md = (rx * rx / sigmaX) + (ry * ry / sigmaY); + double value = intensity * Math.Exp(-0.5 * md); + + return value; + } + + public static bool PointInRect(in Rectangle r, in Point p) + => (uint)p.X >= r.X0() && + (uint)p.X < (r.X0() + r.Width) + && (uint)p.Y >= r.Y0() + && (uint)p.Y < (r.Y0() + r.Height); + + public static Rectangle BoundingRectangle(Span pixels) + { + DebugGuard.MustBeGreaterThan(pixels.Length, 0, nameof(pixels)); + + int lowX = pixels[0].X; + int highX = pixels[0].X; + int lowY = pixels[0].Y; + int highY = pixels[0].Y; + + foreach (Point p in pixels) + { + lowX = Math.Min(lowX, p.X); + highX = Math.Max(highX, p.X); + lowY = Math.Min(lowY, p.Y); + highY = Math.Max(highY, p.Y); + } + + return new Rectangle( + lowX, + lowY, + highX - lowX + 1, + highY - lowY + 1); + } + + public static JxlGaussianEllipse? FitGaussianFast(JxlConnectedComponent cc, Rectangle rect, JxlImage3F img, JxlImage3F background) + { + const bool leastSqIntensity = true; + const double epsilon = 1e-6; + const int rectBounds = EllipseWindowSize >> 1; + + JxlGaussianEllipse ans = default; + double sum = 0; + int n = 0; + + // All values are initialized to 0 by default. + InlineArray3 m1 = default; + InlineArray3 m2 = default; + InlineArray3 color = default; + InlineArray3 bgColor = default; + + for (int c = 0; c < 3; c++) + { + color[c] = img.PlaneRow(rect, c, cc.Mode.Y)[cc.Mode.X] - + background.PlaneRow(rect, c, cc.Mode.Y)[cc.Mode.X]; + } + + double sign = (color[1] > 0) ? 1 : -1; + + for (int sy = -rectBounds; sy <= rectBounds; sy++) + { + int y = sy + cc.Mode.Y; + + // Bounds check + if (y < 0 || y >= rect.Height) + { + continue; + } + + Span row = img.PlaneRow(rect, 1, y); + Span bgrow = background.PlaneRow(rect, 1, y); + + for (int sx = -rectBounds; sx <= rectBounds; sx++) + { + int x = sx + cc.Mode.X; + + // Bounds check + if (x < 0 || x >= rect.Width) + { + continue; + } + + double w = Math.Max(epsilon, sign * (row[x] - bgrow[x])); + sum += w; + + m1[0] += w * x; + m1[1] += w * y; + m2[0] += w * x * x; + m2[1] += w * x * y; + m2[2] += w * y * y; + + for (int c = 0; c < 3; c++) + { + bgColor[c] += background.PlaneRow(rect, c, y)[x]; + } + + n++; + } + } + + if (n <= 0) + { + // n must be at least 1 + return null; + } + + for (int i = 0; i < 3; i++) + { + m1[i] /= sum; + m2[i] /= sum; + bgColor[i] /= n; + } + + double sigmaMult = 1.0; + + // This inline array is read-only. It contains scale multipliers. + InlineArray3 scaleMult = default; + scaleMult[0] = 1.1; + scaleMult[1] = 1.1; + scaleMult[2] = 1.1; + + ans.X = m1[0]; + ans.Y = m1[1]; + + ans.Intensity[0] = scaleMult[0] * color[0]; + ans.Intensity[1] = scaleMult[1] * color[1]; + ans.Intensity[2] = scaleMult[2] * color[2]; + + Matrix2x2 sigma = default; + Vector2 d = default; + Matrix2x2 uMatrix = default; + + sigma[0][0] = m2[0] - (m1[0] * m1[0]); + sigma[1][1] = m2[2] - (m1[1] * m1[1]); + sigma[0][1] = sigma[1][0] = m2[1] - (m1[0] * m1[1]); + + JxlLinearAlgebra.ConvertToDiagonal(sigma, ref d, ref uMatrix); + + Vector2 u = default; + u[0] = uMatrix[1][0]; + u[1] = uMatrix[1][1]; + + int p1 = 0; + int p2 = 1; + + if (d[0] < d[1]) + { + RuntimeUtility.Swap(ref p1, ref p2); + } + + ans.SigmaX = sigmaMult * d[p1]; + ans.SigmaY = sigmaMult * d[p2]; + ans.Angle = Math.Atan2(u[p1], u[p2]); + ans.L2Loss = 0; + ans.BackgroundColor = bgColor; + + if (leastSqIntensity) + { + ref JxlGaussianEllipse ellipse = ref ans; + + (double ct, double st) = Math.SinCos(ans.Angle); + + // Estimate intensity with least squares (fixed background) + for (int c = 0; c < 3; c++) + { + double gg = 0.0; + double gd = 0.0; + + int yc = cc.Mode.Y; + int xc = cc.Mode.X; + + for (int y = yc - rectBounds; y <= yc + rectBounds; y++) + { + if (y < 0 || y >= rect.Height) + { + continue; + } + + Span row = img.PlaneRow(rect, c, y); + Span bgrow = background.PlaneRow(rect, c, y); + + for (int x = xc - rectBounds; x <= xc + rectBounds; x++) + { + if (x < 0 || x >= rect.Width) + { + continue; + } + + double target = row[x] - bgrow[x]; + double gaussian = DotGaussianModel(x - ellipse.X, y - ellipse.Y, ct, st, ellipse.SigmaX, ellipse.SigmaY, 1.0); + + gg += gaussian * gaussian; + gd += gaussian * target; + } + } + + ans.Intensity[c] = gd / (gg + 1e-6); + } + } + + ComputeDotLossless(ref ans, cc, rect, img, background); + return ans; + } + + public static void ComputeDotLossless(ref JxlGaussianEllipse ellipse, JxlConnectedComponent cc, Rectangle rect, JxlImage3F img, JxlImage3F background) + { + const int rectBounds = 2; + const double intensityR = 1.0; + const double sigmaR = 0.0; + const double zeroEpsilon = 0.1; + + (double ct, double st) = Math.SinCos(ellipse.Angle); + + // Read-only + InlineArray3 channelGains = default; + channelGains[0] = 1.0; + channelGains[1] = 1.0; + channelGains[2] = 1.0; + + int n = 0; + + ellipse.L1Loss = 0; + ellipse.L2Loss = 0; + ellipse.NegativePixels = 0; + ellipse.NegativeValue[0] = 0; + ellipse.NegativeValue[1] = 0; + ellipse.NegativeValue[2] = 0; + ellipse.CustomLoss = 0; + + double distMeanModeSq = ((cc.Mode.X - ellipse.X) * (cc.Mode.X - ellipse.X)) + ((cc.Mode.Y - ellipse.Y) * (cc.Mode.Y - ellipse.Y)); + + for (int c = 0; c < 3; c++) + { + for (int sy = -rectBounds; sy < cc.Bounds.Height + rectBounds; sy++) + { + int y = sy + cc.Bounds.Y0(); + + if (y < 0 || y >= rect.Height) + { + continue; + } + + Span row = img.PlaneRow(rect, c, y); + Span bgrow = background.PlaneRow(rect, c, y); + + for (int sx = -rectBounds; sx < cc.Bounds.Width + rectBounds; sx++) + { + int x = sx + cc.Bounds.X0(); + + if (x < 0 || x >= rect.Width) + { + continue; + } + + double target = row[x]; + double dotDelta = DotGaussianModel(x - ellipse.X, y - ellipse.Y, ct, st, ellipse.SigmaX, ellipse.SigmaY, ellipse.Intensity[c]); + + if (dotDelta > target + zeroEpsilon) + { + ellipse.NegativePixels++; + ellipse.NegativeValue[c] += dotDelta - target; + } + + double bkg = OptimizeBackground ? ellipse.BackgroundColor[c] : bgrow[x]; + double pred = bkg + dotDelta; + double diff = target - pred; + double l2 = channelGains[c] * diff * diff; + double l1 = channelGains[c] * Math.Abs(diff); + + ellipse.L2Loss += l2; + ellipse.L1Loss += l1; + + double w = DotGaussianModel(x - cc.Mode.X, y - cc.Mode.Y, 1.0, 0.0, 1.0 + ellipse.SigmaX, 1.0 + ellipse.SigmaY, 1.0); + ellipse.CustomLoss += w * l2; + n++; + } + } + } + + ellipse.L2Loss /= n; + ellipse.CustomLoss /= n; + ellipse.CustomLoss += (20.0 * distMeanModeSq) + ellipse.NegativeValue[1]; + ellipse.L1Loss /= n; + + double ridgeTerm = (sigmaR * ellipse.SigmaX) + (sigmaR * ellipse.SigmaY); + + for (int c = 0; c < 3; c++) + { + ridgeTerm += intensityR * ellipse.Intensity[c] * ellipse.Intensity[c]; + } + + ellipse.RidgeLoss = ellipse.L2Loss + ridgeTerm; + } + + public static JxlGaussianEllipse? FitGaussian(JxlConnectedComponent cc, Rectangle rect, JxlImage3F img, JxlImage3F background) + { + JxlGaussianEllipse? ellipseOrNull = FitGaussianFast(cc, rect, img, background); + + if (ellipseOrNull is null) + { + return null; + } + + JxlGaussianEllipse ellipse = ellipseOrNull.Value; + + if (ellipse.SigmaX < ellipse.SigmaY) + { + RuntimeUtility.Swap(ref ellipse.SigmaX, ref ellipse.SigmaY); + ellipse.Angle += MathF.PI / 2.0; + } + + ellipse.Angle -= Math.PI * Math.Floor(ellipse.Angle / Math.PI); + + if (Math.Abs(ellipse.Angle - Math.PI) < 1e-6 || Math.Abs(ellipse.Angle) < 1e-6) + { + ellipse.Angle = 0.0; + } + + if (!(ellipse.Angle >= 0 && ellipse.Angle <= Math.PI && ellipse.SigmaX >= ellipse.SigmaY)) + { + return null; + } + + return ellipse; + } + + public static bool ExtractComponent(Rectangle rect, JxlImageF img, List pixels, Point seed, double threshold) + { + Span neighbors = + [ + new(1, -1), + new(1, 0), + new(1, 1), + new(0, -1), + new(0, 1), + new(-1, -1), + new(-1, 1), + new(1, 0) + ]; + + List q = [seed]; + + while (q.Count > 0) + { + Point current = q[^1]; + q.RemoveAt(q.Count - 1); + + pixels.Add(current); + + if (pixels.Count > MaxCCSize) + { + return false; + } + + foreach (Point delta in neighbors) + { + Point child = new(current.X + delta.X, current.Y + delta.Y); + + if (child.X >= 0 && child.X < rect.Width && + child.Y >= 0 && child.Y < rect.Height) + { + ref float value = ref img.GetRow(rect, child.Y)[child.X]; + + if (value > threshold) + { + value = 0.0f; + q.Add(child); + } + } + } + } + + return true; + } + + public static JxlImageF? ComputeEnergyImage(Configuration configuration, JxlImage3F original, ref JxlImage3F smooth) + { + using JxlImage3F forig = new(configuration, original.XSize, original.YSize); + smooth = new(configuration, original.XSize, original.YSize); + + Rectangle rect = original.GetRectangle(); + + JxlWeightsSeparable5 weights1 = WeightsSeparable5Gaussian0_65(); + JxlWeightsSeparable5 weights3 = WeightsSeparable5Gaussian3(); + + for (int c = 0; c < 3; c++) + { + if (!Separable5(configuration, original.Plane(c), rect, weights3, forig.Plane(c))) + { + return null; + } + + if (!Separable5(configuration, forig.Plane(c), rect, weights3, smooth.Plane(c))) + { + return null; + } + + if (!Separable5(configuration, original.Plane(c), rect, weights1, forig.Plane(c))) + { + return null; + } + } + + return Ssd(configuration, forig, smooth); + } + + public static List FindCC(Configuration configuration, JxlImageF energy, Rectangle rect, double tLow, double tHigh, int maxWindow, double minScore) + { + const int extraRect = 4; + + using JxlImageF img = new(configuration, energy.XSize, energy.YSize); + + if (!JxlImageOperations.CopyImage(energy, img)) + { + throw new InvalidOperationException("Couldn't copy image"); + } + + List ans = []; + + for (int y = 0; y < rect.Height; y++) + { + Span row = img.GetRow(rect, y); + + for (int x = 0; x < rect.Width; x++) + { + if (row[x] > tHigh) + { + List pixels = []; + row[x] = 0.0f; + + Point seed = new(x, y); + bool success = ExtractComponent(rect, img, pixels, seed, tLow); + + if (!success) + { + continue; + } + + Rectangle bounds = BoundingRectangle(CollectionsMarshal.AsSpan(pixels)); + + if (bounds.Width < maxWindow && bounds.Height < maxWindow) + { + JxlConnectedComponent cc = new(bounds, pixels); + + cc.ComputeStats(energy, rect, extraRect); + + if (cc.Score < minScore) + { + continue; + } + + ans.Add(cc); + } + } + } + } + + return ans; + } + + public static List DetectGaussianEllipses( + Configuration configuration, + JxlImage3F opsin, + Rectangle rect, + JxlGaussianDetectParameters parameters, + JxlEllipseQuantParameters qparameters) + { + List dots = []; + + JxlImage3F smooth = new(configuration, opsin.XSize, opsin.YSize); + JxlImageF energy = ComputeEnergyImage(configuration, opsin, ref smooth) ?? throw new InvalidOperationException("Failed to compute energy image"); + List components = FindCC(configuration, energy, rect, parameters.TLow, parameters.THigh, parameters.MaxWindowSize, parameters.MinScore); + int numCC = Math.Min(parameters.MaxCC, (components.Count * parameters.PercCC) / 100); + + if (components.Count > numCC) + { + components.Sort((a, b) => b.Score.CompareTo(a.Score)); + components.RemoveRange(numCC, components.Count - numCC); + } + + foreach (JxlConnectedComponent cc in components) + { + JxlGaussianEllipse ellipse = FitGaussian(cc, rect, opsin, smooth) ?? throw new InvalidOperationException("Failed to fit Gaussian"); + + if (ellipse.X < 0.0 || Math.Ceiling(ellipse.X) >= rect.Width || + ellipse.Y < 0.0 || Math.Ceiling(ellipse.Y) >= rect.Height) + { + continue; + } + + if (ellipse.NegativePixels > parameters.MaxNegativePixels) + { + continue; + } + + double intensity = (0.21 * ellipse.Intensity[0]) + (0.72 * ellipse.Intensity[1]) + (0.07 * ellipse.Intensity[2]); + double intensitySq = intensity * intensity; + + double sqDistMeanMode = ((ellipse.X - cc.Mode.X) * (ellipse.X - cc.Mode.X)) + + ((ellipse.Y - cc.Mode.Y) * (ellipse.Y - cc.Mode.Y)); + + if (ellipse.L2Loss < parameters.MaxL2Loss && + ellipse.CustomLoss < parameters.MaxCustomLoss && + intensitySq > (parameters.MinIntensity * parameters.MinIntensity) && + sqDistMeanMode < parameters.MaxDistMeanMode * parameters.MaxDistMeanMode) + { + int x0 = cc.Bounds.X0(); + int y0 = cc.Bounds.Y0(); + + dots.Add(new()); + dots[^1].Second.Add(new Point(x0, y0)); + + JxlQuantizedPatch patch = dots[^1].First; + patch.Width = cc.Bounds.Width; + patch.Height = cc.Bounds.Height; + + for (int y = 0; y < patch.ysize; y++) + { + for (int x = 0; x < patch.xsize; x++) + { + for (int c = 0; c < 3; c++) + { + patch.fpixels[c][(y * patch.xsize) + x] = + opsin.PlaneRow(rect, c, y0 + y)[x0 + x] - + smooth.PlaneRow(rect, c, y0 + y)[x0 + x]; + } + } + } + } + } + + return dots; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlDotDictionary.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlDotDictionary.cs new file mode 100644 index 0000000000..e4c249b324 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlDotDictionary.cs @@ -0,0 +1,104 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.DotDetection; + +internal static class JxlDotDictionary +{ + /// + /// Quantization level for the position. + /// + private const int EllipsePosQ = 2; + + /// + /// Minimum sigma value. + /// + private const double EllipseMinSigma = 0.1; + + /// + /// Maximum sigma value. + /// + private const double EllipseMaxSigma = 3.1; + + /// + /// Number of quantization levels for sigma. + /// + private const int EllipseSigmaQ = 16; + + /// + /// Quantization level for the angle. + /// + private const int EllipseAngleQ = 8; + + public static List FindDotDictionary( + Configuration configuration, + JxlCompressParameters cparams, + JxlImage3F opsin, + Rectangle rect, + JxlColorCorrelation colorCorrelation) + { + if (JxlOverrideHelpers.ApplyOverride( + cparams.Dots, + cparams.ButteraugliDistance >= MinButteraugliForDots)) + { + JxlGaussianDetectParameters ellipseParams = default; + ellipseParams.THigh = 0.04; + ellipseParams.TLow = 0.02; + ellipseParams.MaxWindowSize = 5; + ellipseParams.MaxL2Loss = 0.005; + ellipseParams.MaxCustomLoss = 300; + ellipseParams.MinIntensity = 0.12; + ellipseParams.MaxDistMeanMode = 1.0; + ellipseParams.MaxNegativePixels = 0; + ellipseParams.MinScore = 12; + ellipseParams.MaxCC = 100; + ellipseParams.PercCC = 100; + + // Read-only + InlineArray3 ellipseMinIntensity = default; + ellipseMinIntensity[0] = -0.05; + ellipseMinIntensity[1] = 0; + ellipseMinIntensity[2] = 0.5; + + // Read-only + InlineArray3 ellipseMaxIntensity = default; + ellipseMaxIntensity[0] = 0.05; + ellipseMaxIntensity[1] = 1.0; + ellipseMaxIntensity[2] = 0.4; + + // Read-only + InlineArray3 ellipseIntensityQ = default; + ellipseIntensityQ[0] = 10; + ellipseIntensityQ[1] = 36; + ellipseIntensityQ[2] = 10; + + JxlEllipseQuantParameters qParams = new( + rect.Width, + rect.Height, + EllipsePosQ, + EllipseMinSigma, + EllipseMaxSigma, + EllipseSigmaQ, + EllipseAngleQ, + ellipseMinIntensity, + ellipseMaxIntensity, + ellipseIntensityQ, + EllipsePosQ <= 5, + colorCorrelation.YToXRatio(0), + colorCorrelation.YToBRatio(0)); + + return JxlDotDetectionUtils.DetectGaussianEllipses( + configuration, + opsin, + rect, + ellipseParams, + qparameters); + } + + return []; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlEllipseQuantParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlEllipseQuantParameters.cs new file mode 100644 index 0000000000..b337871dd9 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlEllipseQuantParameters.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.DotDetection; + +internal struct JxlEllipseQuantParameters +{ + public int YSize; + public int XSize; + public int QPosition; + public double MinSigma; + public double MaxSigma; + public int QSigma; + public int QAngle; + public InlineArray3 MinIntensity; + public InlineArray3 MaxIntensity; + public InlineArray3 QIntensity; + public bool SubtractQuantized; + public float YToX; + public float YToB; + + public JxlEllipseQuantParameters(int ySize, int xSize, int qPosition, double minSigma, double maxSigma, int qSigma, int qAngle, InlineArray3 minIntensity, InlineArray3 maxIntensity, InlineArray3 qIntensity, bool subtractQuantized, float yToX, float yToB) + { + this.YSize = ySize; + this.XSize = xSize; + this.QPosition = qPosition; + this.MinSigma = minSigma; + this.MaxSigma = maxSigma; + this.QSigma = qSigma; + this.QAngle = qAngle; + this.MinIntensity = minIntensity; + this.MaxIntensity = maxIntensity; + this.QIntensity = qIntensity; + this.SubtractQuantized = subtractQuantized; + this.YToX = yToX; + this.YToB = yToB; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlGaussianDetectParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlGaussianDetectParameters.cs new file mode 100644 index 0000000000..2a75b6d12c --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlGaussianDetectParameters.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.DotDetection; + +internal struct JxlGaussianDetectParameters() +{ + /// + /// At least one pixel must have a larger energy than THigh. + /// + public double THigh; + + /// + /// All pixels must have a larger energy than TLow. + /// + public double TLow; + + /// + /// Discard dots larger than this containing window. + /// + public int MaxWindowSize; + public double MaxL2Loss; + public double MaxCustomLoss; + + /// + /// If the intensity is too low, discard it. + /// + public double MinIntensity; + + /// + /// The mean and the mode must be close. + /// + public double MaxDistMeanMode; + + /// + /// Maximum number of negative pixels. + /// + public int MaxNegativePixels; + public int MinScore; + + /// + /// Maximum number of connected components to keep. + /// + public int MaxCC = 50; + + /// + /// Percentage in [0,100] of connected components to keep + /// + public int PercCC = 15; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlGaussianEllipse.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlGaussianEllipse.cs new file mode 100644 index 0000000000..5d3c95ad42 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlGaussianEllipse.cs @@ -0,0 +1,75 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.DotDetection; + +internal struct JxlGaussianEllipse +{ + // Make all of these fields, for consistency with + // the InlineArrays. InlineArrays can't be updated + // when they're a property. + + /// + /// Position in X + /// + public double X; + + /// + /// Position in Y + /// + public double Y; + + /// + /// Scale in X + /// + public double SigmaX; + + /// + /// Scale in Y + /// + public double SigmaY; + + /// + /// Ellipse rotation in radians + /// + public double Angle; + + /// + /// Intensity in each channel + /// + public InlineArray3 Intensity; + + /// + /// Error after the Gaussian was fit + /// + public double L2Loss; + + public double L1Loss; + + /// + /// The L2Loss + regularization term + /// + public double RidgeLoss; + + /// + /// Experimental custom loss + /// + public double CustomLoss; + + /// + /// Best background color + /// + public InlineArray3 BackgroundColor; + + /// + /// Number of negative pixels when subtracting dot + /// + public int NegativePixels; + + /// + /// Debt due to channel truncation + /// + public InlineArray3 NegativeValue; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs index 261f7bc668..80e4715097 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs @@ -561,16 +561,17 @@ public static uint MaxValue(Span data) /// Vector to duplicate. /// New vector that is duplicated across the width. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector LoadDuplicate128(Vector128 vec) + public static unsafe Vector LoadDuplicate128(Vector128 vec) where T : unmanaged { Span value = stackalloc T[Vector.Count]; - for (int i = 0; i < Vector.Count; i += 4) + + for (int i = 0; i < Vector.Count; i += sizeof(T)) { vec.CopyTo(value[i..]); } - return new(value); + return new Vector(value); } /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlMatrix3x3F.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlMatrix3x3F.cs index 2d66df2729..6832ede90c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlMatrix3x3F.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlMatrix3x3F.cs @@ -68,6 +68,26 @@ internal JxlMatrix3x3F(float[][] array) this.e22 = array[2][2]; } + /// + /// Gets a reference to the matrix element at the specified coordinates. + /// + /// The zero-based column index. + /// The zero-based row index. + /// A reference to the matrix element. + /// + /// Thrown when or is outside the range 0 through 2. + /// + public ref float this[int x, int y] + { + get + { + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)x, 3u, nameof(x)); + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)y, 3u, nameof(y)); + + return ref this.AsSpan()[(y * 3) + x]; + } + } + /// /// Wraps all these values into a Span. /// @@ -152,4 +172,15 @@ public static bool Invert(ref JxlMatrix3x3F matrix) return true; } + + /// + /// Gets a reference to the matrix element at the specified coordinates without performing bounds checks. + /// + /// The zero-based column index. + /// The zero-based row index. + /// A reference to the matrix element. + /// + /// The caller must ensure that and are both in the range 0 through 2. + /// + public ref float DangerousGetReferenceTo(int x, int y) => ref this.AsSpan()[(y * 3) + x]; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/UpsamplingStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/UpsamplingStage.cs new file mode 100644 index 0000000000..fc605a660a --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/UpsamplingStage.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +internal class UpsamplingStage : RenderPipelineStageBase +{ + private const int ChunkSize = 1024; + + private readonly float[] kernel = new float[64 * 25]; // 1600 floats, 6400 bytes (6.25 KB) + private readonly int c; + private readonly InlineArray3>> temp = default; + + private void PreprocessRowImpl(Buffer2D> inputRows, int x0, int length, int threadId) + { + Span colMin = MemoryMarshal.Cast(this.temp[0][threadId].Span); + Span colMax = MemoryMarshal.Cast(this.temp[1][threadId].Span); + + InlineArray5> rows = default; + rows[0] = + } +} diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/CoefficientOrderTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/CoefficientOrderTests.cs new file mode 100644 index 0000000000..3e2923f40f --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/CoefficientOrderTests.cs @@ -0,0 +1,67 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Processing; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; + +/// +/// Tests the ordering of coefficients against random +/// permutations and bit-stream representations. +/// +public class CoefficientOrderTests +{ + /// + /// Number of swaps. See + /// and . + /// + private const int Swaps = 32; + + /// + /// Identifies the kind of permutation for testing. + /// + private enum Permutation : byte + { + /// + /// Each permutation is incremental and starts at 0. + /// See . + /// + Identity, + + /// + /// Incremental permutation starting at 0, just like in + /// , but additionally, random values + /// are swapped times, producing a slightly + /// more randomized result. + /// + FewSwaps, + + /// + /// Incremental permutation starting at 0, just like in + /// , but additionally, random ranges + /// of offsets are moved backwards by one times. + /// + FewSlides, + + /// + /// Everything is random. + /// + Random + } + + private static void RoundtripPermutation(Span permutation, Span output, int len, out int size) + { + using MemoryStream ms = new(); + JxlBitWriter writer = new(ms); + + Assert.True(JxlCoefficientOrderEncoder.EncodePermutation(permutation, 0, len, writer)); + writer.ZeroPadToByte(); + + ms.Position = 0; + JxlBitReader reader = new(ms); + + //TODO + } +} diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/OpsinImageTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/OpsinImageTests.cs new file mode 100644 index 0000000000..b67f6f4c22 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/OpsinImageTests.cs @@ -0,0 +1,153 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Cms; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; + +public class OpsinImageTests +{ + /// + /// Converts the linear sRGB color space to JPEG XL Opsin XYB. + /// + /// Input Red channel + /// Input Green channel + /// Input Blue channel + /// Output X channel + /// Output Y channel + /// Output B channel + private static void LinearSRgbToOpsin(float rgbR, float rgbG, float rgbB, out float xybX, out float xybY, out float xybB) + { + using JxlImage3F linear = new(TestEnvironment.Configuration, 1, 1); + linear.PlaneRow(0, 0)[0] = rgbR; + linear.PlaneRow(1, 0)[0] = rgbG; + linear.PlaneRow(2, 0)[0] = rgbB; + Assert.True(JxlXybEncoder.ToXyb( + TestEnvironment.Configuration, + JxlColorEncoding.LinearSRgb, + JxlShared.DefaultIntensityTarget, + null, + new JxlImage3F(), + JxlCmsInterface.CreateDefaultCms(), + null)); + xybX = linear.PlaneRow(0, 0)[0]; + xybY = linear.PlaneRow(1, 0)[0]; + xybB = linear.PlaneRow(2, 0)[0]; + } + + /// + /// Converts JPEG XL Opsin XYB color space to linear sRGB. (Opposite + /// of ). + /// + /// Input X channel + /// Input Y channel + /// Input B channel + /// Output Red channel + /// Output Green channel + /// Output Blue channel + private static void OpsinToLinearSRgb(float xybX, float xybY, float xybB, out float rgbR, out float rgbG, out float rgbB) + { + using JxlImage3F opsin = new(TestEnvironment.Configuration, 1, 1); + opsin.PlaneRow(0, 0)[0] = xybX; + opsin.PlaneRow(1, 0)[0] = xybY; + opsin.PlaneRow(2, 0)[0] = xybB; + + using JxlImage3F linear = new(TestEnvironment.Configuration, 1, 1); + + JxlOpsinParameters opsinParameters = new(); + opsinParameters.Initialize(intensityTarget: 255.0f); + + Assert.True(JxlXybDecoder.OpsinToLinear( + opsin, + opsin.GetRectangle(), + linear, + opsinParameters)); + + rgbR = linear.PlaneRow(0, 0)[0]; + rgbG = linear.PlaneRow(1, 0)[0]; + rgbB = linear.PlaneRow(2, 0)[0]; + } + + private static void OpsinRoundtripTestRgb(float r, float g, float b) + { + // There will be a tiny floating-point gap. This is normal. + const float tolerance = 1e-3f; + + LinearSRgbToOpsin(r, g, b, out float xybX, out float xybY, out float xybB); + OpsinToLinearSRgb(xybX, xybY, xybB, out float r2, out float g2, out float b2); + + Assert.Equal(r, r2, tolerance); + Assert.Equal(g, g2, tolerance); + Assert.Equal(b, b2, tolerance); + } + + [Fact] + public void VerifyOpsinAbsorbanceInverseMatrix() + { + const float tolerance = 1e-6f; + + JxlMatrix3x3F matrix = JxlOpsinConstants.GetOpsinAbsorbanceInverseMatrix(); + + Assert.True(JxlMatrix3x3F.Invert(ref matrix)); + + for (int j = 0; j < 3; j++) + { + for (int i = 0; i < 3; i++) + { + Assert.Equal( + matrix.DangerousGetReferenceTo(j, i), + JxlOpsinConstants.OpsinAbsorbanceMatrix[j][i], + tolerance); + } + } + } + + [Fact] + public void OpsinRoundtrip() + { + OpsinRoundtripTestRgb(0, 0, 0); + OpsinRoundtripTestRgb(1.0f / 255, 1.0f / 255, 1.0f / 255); + OpsinRoundtripTestRgb(128.0f / 255, 128.0f / 255, 128.0f / 255); + OpsinRoundtripTestRgb(1, 1, 1); + + OpsinRoundtripTestRgb(0, 0, 1.0f / 255); + OpsinRoundtripTestRgb(0, 0, 128.0f / 255); + OpsinRoundtripTestRgb(0, 0, 1); + + OpsinRoundtripTestRgb(0, 1.0f / 255, 0); + OpsinRoundtripTestRgb(0, 128.0f / 255, 0); + OpsinRoundtripTestRgb(0, 1, 0); + + OpsinRoundtripTestRgb(1.0f / 255, 0, 0); + OpsinRoundtripTestRgb(128.0f / 255, 0, 0); + OpsinRoundtripTestRgb(1, 0, 0); + } + + [Fact] + public void VerifyZero() + { + LinearSRgbToOpsin(0, 0, 0, out float x, out float y, out float b); + Assert.Equal(0, x, 1e-9f); + Assert.Equal(0, y, 1e-7f); + Assert.Equal(0, b, 1e-7f); + } + + /// + /// Tests that grayscale colors have a fixed Y to B ratio and x == 0. + /// + [Fact] + public void VerifyGray() + { + for (int i = 1; i < 255; i++) + { + LinearSRgbToOpsin(i / 255.0f, i / 255.0f, i / 255.0f, out float x, out float y, out float b); + Assert.Equal(0, x, 1e-6f); + Assert.Equal(JxlOpsinConstants.YToBRatio, b / y, 3e-5f); + } + } +} From 08989882113763c5c3b11370fe6e5f72391dea17 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 23 Sep 2026 06:50:04 +0400 Subject: [PATCH 198/328] Add context map encoder --- .../Encoder/JxlContextMapEncoder.cs | 225 ++++++++++++++++++ .../Jxl/Processing/JxlBlockContextMap.cs | 2 +- 2 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlContextMapEncoder.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlContextMapEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlContextMapEncoder.cs new file mode 100644 index 0000000000..b60a90ab95 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlContextMapEncoder.cs @@ -0,0 +1,225 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; + +internal static class JxlContextMapEncoder +{ + public const int ClustersLimit = 128; + + private static int IndexOf(List v, byte value) + { + int i = 0; + for (; i < v.Count; ++i) + { + if (v[i] == value) + { + return i; + } + } + + return i; + } + + private static void MoveToFront(List v, int index) + { + byte value = v[index]; + + for (int i = index; i != 0; --i) + { + v[i] = v[i - 1]; + } + + v[0] = value; + } + + private static List MoveToFrontTransform(IReadOnlyList v) + { + if (v.Count == 0) + { + return []; + } + + byte maxValue = 0; + for (int i = 0; i < v.Count; ++i) + { + if (v[i] > maxValue) + { + maxValue = v[i]; + } + } + + List mtf = new(maxValue + 1); + for (int i = 0; i <= maxValue; ++i) + { + mtf.Add((byte)i); + } + + List result = new(v.Count); + for (int i = 0; i < v.Count; ++i) + { + int index = IndexOf(mtf, v[i]); + + if (index >= mtf.Count) + { + throw new InvalidOperationException("Move To Front index out of range"); + } + + result.Add((byte)index); + MoveToFront(mtf, index); + } + + return result; + } + + private static bool EncodeContextMap(IReadOnlyList contextMap, int numHistograms, JxlBitWriter writer) + { + if (numHistograms == 1) + { + // Simple code. + writer.Write(1, 1); + + // 0 bits per entry. + writer.Write(2, 0); + return true; + } + + List transformedSymbols = MoveToFrontTransform(contextMap); + + List> jxlTokens = + [ + new List(contextMap.Count) + ]; + + List> mtfJxlTokens = + [ + new List(transformedSymbols.Count) + ]; + + for (int i = 0; i < contextMap.Count; ++i) + { + jxlTokens[0].Add(new JxlToken(0, contextMap[i])); + } + + for (int i = 0; i < transformedSymbols.Count; ++i) + { + mtfJxlTokens[0].Add(new JxlToken(0, transformedSymbols[i])); + } + + JxlHistogramParameters parameters = new() + { + UIntMethod = JxlHybridUIntMethod.ContextMap + }; + + int ansCost; + int mtfCost; + { + JxlEntropyEncodingData codes = new(); + ansCost = BuildAndEncodeHistograms(parameters, 1, jxlTokens, codes, null, null); + } + + { + JxlEntropyEncodingData codes = new(); + mtfCost = BuildAndEncodeHistograms(parameters, 1, mtfJxlTokens, codes, null, null); + } + + bool useMtf = mtfCost < ansCost; + + jxlTokens[0].Clear(); + + for (int i = 0; i < transformedSymbols.Count; ++i) + { + jxlTokens[0].Add(new JxlToken(0, useMtf ? transformedSymbols[i] : contextMap[i])); + } + + int entryBits = JxlMath.CeilLog2Nonzero(numHistograms); + int simpleCost = entryBits * contextMap.Count; + + if (entryBits < 4 && simpleCost < ansCost && simpleCost < mtfCost) + { + return writer.WithMaxBits((ulong)(3 + (entryBits * contextMap.Count)), () => + { + writer.Write(1, 1); + writer.Write(2, entryBits); + + for (int i = 0; i < contextMap.Count; ++i) + { + writer.Write(entryBits, contextMap[i]); + } + + return true; + }); + } + + return writer.WithMaxBits(2 + (jxlTokens[0].Count * 24), () => + { + writer.Write(1, 0); + writer.Write(1, useMtf ? 1 : 0); + + JxlEntropyEncodingData codes = new(); + BuildAndEncodeHistograms(parameters, 1, jxlTokens, codes, writer); + + WriteJxlTokens(jxlTokens[0], codes, 0, writer); + return true; + }); + } + + public static bool EncodeBlockContextMap(JxlBlockContextMap blockCtxMap, JxlBitWriter writer) + { + List[] dct = blockCtxMap.DcThresholds; + List qf = blockCtxMap.QfThresholds; + byte[] ctxMap = blockCtxMap.ContextMap; + + int maxBits = ((dct[0].Count + dct[1].Count + dct[2].Count + qf.Count) * 34) + + 1 + + 4 + + 4 + + (ctxMap.Length * 10) + + 1024; + + return writer.WithMaxBits(maxBits, () => + { + if (dct[0].Count == 0 && + dct[1].Count == 0 && + dct[2].Count == 0 && + qf.Count == 0 && + ctxMap.Length == 21 && + ctxMap.AsSpan().SequenceEqual(JxlBlockContextMap.DefaultContextMap)) + { + writer.Write(1, 1); // all_default = 1 + return true; + } + + writer.Write(1, 0); // all_default = 0 + + for (int j = 0; j < 3; ++j) + { + writer.Write(4, dct[j].Count); + + foreach (int value in dct[j]) + { + if (!JxlU32Coder.Write(DcThresholdDist, JxlPackSigned.PackUnsigned(value), writer)) + { + throw new InvalidOperationException("Failed to encode DC threshold"); + } + } + } + + writer.Write(4, qf.Count); + + foreach (uint value in qf) + { + if (!JxlU32Coder.Write(QfThresholdDist, value - 1, writer)) + { + throw new InvalidOperationException("Failed to encode QF threshold"); + } + } + + return EncodeContextMap(ctxMap, blockCtxMap.ContextCount, writer); + }); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs index b47a949871..71b68c9063 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs @@ -15,7 +15,7 @@ public JxlBlockContextMap() this.DcContextCount = 1; } - private static ReadOnlySpan DefaultContextMap => + public static ReadOnlySpan DefaultContextMap => [ 0, 1, 2, 2, 3, 3, 4, 5, 6, 6, 6, 6, 6, 7, 8, 9, 9, 10, 11, 12, 13, 14, 14, 14, 14, 14, From 19a459a4ba8f2937a17af3ebece328d013b8c263 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:26:32 +0400 Subject: [PATCH 199/328] Add cluster encoder, complete heuristics FindBestBlockEntropyModel, add tone mapping stage, add entropy encoder, add performance optimizations --- src/ImageSharp/Common/Helpers/SimdUtils.cs | 8 +- .../Cms/ToneMapping/JxlHlgOotfToneMapper.cs | 19 +- .../Decoder/JxlExternalImageDecoder.cs | 28 + .../Processing/Encoder/Ans/JxlHistogram.cs | 19 +- .../DotDetection/JxlConnectedComponent.cs | 73 +-- .../DotDetection/JxlDotDetectionUtils.cs | 14 +- .../Jxl/Processing/Encoder/JxlCluster.cs | 508 ++++++++++++++++++ .../Encoder/JxlContextMapEncoder.cs | 42 +- .../Jxl/Processing/Encoder/JxlEntropyCoder.cs | 296 ++++++++++ .../Jxl/Processing/Encoder/JxlHeuristics.cs | 80 ++- .../Jxl/Processing/JxlBlockContextMap.cs | 6 +- .../Formats/Jxl/Processing/JxlSimdUtils.cs | 34 +- .../RenderPipeline/ToneMappingStage.cs | 139 +++++ .../RenderPipeline/WriteToOutputStage.cs | 6 +- 14 files changed, 1162 insertions(+), 110 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlExternalImageDecoder.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlCluster.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlEntropyCoder.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/ToneMappingStage.cs diff --git a/src/ImageSharp/Common/Helpers/SimdUtils.cs b/src/ImageSharp/Common/Helpers/SimdUtils.cs index c346e9e6bc..9eddf3c3a0 100644 --- a/src/ImageSharp/Common/Helpers/SimdUtils.cs +++ b/src/ImageSharp/Common/Helpers/SimdUtils.cs @@ -175,16 +175,14 @@ internal static Vector Pow2(Vector vector) internal static Vector GatherBytes(Vector indices, Vector table) { - Span indexValues = stackalloc byte[Vector.Count]; - Span tableValues = stackalloc int[Vector.Count]; - Span resultValues = stackalloc int[Vector.Count]; + Span tableValues = stackalloc int[Vector.Count]; + Span resultValues = stackalloc int[Vector.Count]; - indices.CopyTo(indexValues); table.CopyTo(tableValues); for (int i = 0; i < resultValues.Length; i++) { - byte index = indexValues[i]; + byte index = indices[i]; resultValues[i] = index < tableValues.Length ? tableValues[index] : 0; } diff --git a/src/ImageSharp/Formats/Jxl/Cms/ToneMapping/JxlHlgOotfToneMapper.cs b/src/ImageSharp/Formats/Jxl/Cms/ToneMapping/JxlHlgOotfToneMapper.cs index a62e639823..1d43008cfb 100644 --- a/src/ImageSharp/Formats/Jxl/Cms/ToneMapping/JxlHlgOotfToneMapper.cs +++ b/src/ImageSharp/Formats/Jxl/Cms/ToneMapping/JxlHlgOotfToneMapper.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Processing; namespace SixLabors.ImageSharp.Formats.Jxl.Cms.ToneMapping; @@ -10,16 +11,12 @@ internal readonly struct JxlHlgOotfToneMapper { private readonly float exponent; private readonly bool applyOotf; - private readonly float redY; - private readonly float greenY; - private readonly float blueY; + private readonly Vector3 luminances; public JxlHlgOotfToneMapper(float gamma, Vector3 luminances) { this.exponent = gamma - 1; - this.redY = luminances.X; - this.greenY = luminances.Y; - this.blueY = luminances.Z; + this.luminances = luminances; this.applyOotf = this.exponent is < -0.01f or > 0.01f; } @@ -40,19 +37,17 @@ public JxlHlgOotfToneMapper(float sourceLuminance, float targetLuminance, Vector gamma: (1 / 1.2f) * MathF.Pow(1.111f, -MathF.Log2(displayLuminance / 1000.0f)), luminances: primariesLuminances); - public readonly void Apply(Span rgb) + public readonly void Apply(Vector3 rgb) { if (!this.applyOotf) { return; } - float luminance = (this.redY * rgb[0]) + (this.greenY * rgb[1]) + (this.blueY * rgb[2]); + float luminance = (this.luminances.X * rgb[0]) + (this.luminances.Y * rgb[1]) + (this.luminances.Z * rgb[2]); float ratio = MathF.Min(MathF.Pow(luminance, this.exponent), 1e9f); - rgb[0] *= ratio; - rgb[1] *= ratio; - rgb[2] *= ratio; + rgb *= ratio; } public readonly void Apply(ref Vector red, ref Vector green, ref Vector blue) @@ -62,7 +57,7 @@ public readonly void Apply(ref Vector red, ref Vector green, ref V return; } - Vector luminance = (this.redY * red) + (this.greenY * green) + (this.blueY * blue); + Vector luminance = (this.luminances.X * red) + (this.luminances.Y * green) + (this.luminances.Z * blue); Vector ratio = Vector.Min(JxlSimdUtils.FastPowf(luminance, Vector.Create(this.exponent)), Vector.Create(1e9f)); red *= ratio; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlExternalImageDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlExternalImageDecoder.cs new file mode 100644 index 0000000000..eb62495b44 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlExternalImageDecoder.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +/// +/// Converts channels to different pixel formats and undoes +/// orientation. +/// +internal static class JxlExternalImageDecoder +{ + public static void FloatToU32(Span input, Span output, int num, float mul, int bitsPerSample) + { + Vector one = Vector.One; + Vector scale = Vector.Create(mul); + + for (int x = 0; x < num; x += Vector.Count) + { + Vector v = Vector.Create(input[x..]); + v = Vector.ClampNative(v, Vector.Zero, one); + + Vector i = Vector.ConvertToInt32Native(v * scale); + i.As().CopyTo(output[x..]); + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHistogram.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHistogram.cs index 52637c9898..333d8f119e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHistogram.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHistogram.cs @@ -8,6 +8,8 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; /// internal sealed class JxlHistogram(int length) { + public const int Rounding = 8; + public List Counts { get; set; } = new(length); public int TotalCount { get; set; } @@ -21,9 +23,24 @@ public void Clear() { this.Counts.Clear(); this.TotalCount = 0; - this.Entropy = 0f; } + /// + /// Calculates & returns Shannon entropy for this histogram. + /// https://en.wikipedia.org/wiki/Entropy_(information_theory) + /// + /// + /// The shannon entropy. The larger the entropy, the more difficult is the + /// data to compress further. + /// + public float ComputeShannonEntropy() + { + JxlCluster.HistogramEntropy(this); + return this.Entropy; + } + + public void Condition() => JxlCluster.HistogramCondition(this); + /// /// Adds a new symbol. /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlConnectedComponent.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlConnectedComponent.cs index 78fd91806a..df1cf129e8 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlConnectedComponent.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlConnectedComponent.cs @@ -8,42 +8,30 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.DotDetection; internal sealed class JxlConnectedComponent { - private Rectangle bounds; + private Stats stats; private readonly List points = []; - private float maxEnergy; - private float meanEnergy; - private float varEnergy; - private float meanBg; - private float varBg; - private Point mode; public JxlConnectedComponent(Rectangle bounds, List points) { - this.bounds = bounds; + this.stats.bounds = bounds; this.points = points; } - public Point Mode => this.mode; + public Point Mode => this.stats.mode; - public Rectangle Bounds => this.bounds; + public Rectangle Bounds => this.stats.bounds; public float Score { get; private set; } public void ComputeStats(JxlImageF energy, in Rectangle rect, int extra) { - this.maxEnergy = 0.0F; - this.meanEnergy = 0.0F; - this.varEnergy = 0.0F; - this.meanBg = 0.0F; - this.varBg = 0.0F; + this.stats = default; int nIn = 0; int nOut = 0; - this.mode.X = 0; - this.mode.Y = 0; - for (int sy = -extra; sy < (this.bounds.Height + extra); sy++) + for (int sy = -extra; sy < (this.stats.bounds.Height + extra); sy++) { - int y = sy + this.bounds.Y0(); + int y = sy + this.stats.bounds.Y0(); if (y < 0 || (uint)y >= rect.Height) { @@ -52,42 +40,57 @@ public void ComputeStats(JxlImageF energy, in Rectangle rect, int extra) Span erow = energy.GetRow(rect, y); - for (int sx = -extra; sx < (this.bounds.Width + extra); sx++) + for (int sx = -extra; sx < (this.stats.bounds.Width + extra); sx++) { - int x = sx + this.bounds.X0(); + int x = sx + this.stats.bounds.X0(); if (x < 0 || (uint)x >= rect.Width) { continue; } - if (erow[x] > this.maxEnergy) + if (erow[x] > this.stats.maxEnergy) { - this.maxEnergy = erow[x]; - this.mode.X = x; - this.mode.Y = y; + this.stats.maxEnergy = erow[x]; + this.stats.mode.X = x; + this.stats.mode.Y = y; } - if (JxlDotDetectionUtils.PointInRect(this.bounds, new Point(x, y))) + if (JxlDotDetectionUtils.PointInRect(this.stats.bounds, new Point(x, y))) { - this.meanEnergy += erow[x]; - this.varEnergy += erow[x] * erow[x]; + this.stats.meanEnergy += erow[x]; + this.stats.varEnergy += erow[x] * erow[x]; nIn++; } else { - this.meanBg += erow[x]; - this.varBg += erow[x] * erow[x]; + this.stats.meanBg += erow[x]; + this.stats.varBg += erow[x] * erow[x]; nOut++; } } } - this.meanEnergy /= nIn; - this.meanBg /= nOut; - this.varEnergy = (this.varEnergy / nIn) - (this.meanEnergy * this.meanEnergy); + this.stats.meanEnergy /= nIn; + this.stats.meanBg /= nOut; + this.stats.varEnergy = (this.stats.varEnergy / nIn) - (this.stats.meanEnergy * this.stats.meanEnergy); - this.varBg = (this.varBg / nOut) - (this.meanBg * this.meanBg); - this.Score = (this.meanEnergy - this.meanBg) / MathF.Sqrt(this.varBg); + this.stats.varBg = (this.stats.varBg / nOut) - (this.stats.meanBg * this.stats.meanBg); + this.Score = (this.stats.meanEnergy - this.stats.meanBg) / MathF.Sqrt(this.stats.varBg); + } + + private struct Stats + { +#pragma warning disable IDE1006 // Naming Styles +#pragma warning disable SA1307 // Accessible fields should begin with upper-case letter + public Rectangle bounds; + public float maxEnergy; + public float meanEnergy; + public float varEnergy; + public float meanBg; + public float varBg; + public Point mode; +#pragma warning restore IDE1006 // Naming Styles +#pragma warning restore SA1307 // Accessible fields should begin with upper-case letter } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlDotDetectionUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlDotDetectionUtils.cs index bbccda5211..fd548411a4 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlDotDetectionUtils.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlDotDetectionUtils.cs @@ -4,7 +4,6 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using System.Text.RegularExpressions; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; @@ -316,7 +315,7 @@ public static Rectangle BoundingRectangle(Span pixels) for (int y = yc - rectBounds; y <= yc + rectBounds; y++) { - if (y < 0 || y >= rect.Height) + if ((uint)y >= rect.Height) { continue; } @@ -326,7 +325,7 @@ public static Rectangle BoundingRectangle(Span pixels) for (int x = xc - rectBounds; x <= xc + rectBounds; x++) { - if (x < 0 || x >= rect.Width) + if ((uint)x >= rect.Width) { continue; } @@ -380,19 +379,24 @@ public static void ComputeDotLossless(ref JxlGaussianEllipse ellipse, JxlConnect { int y = sy + cc.Bounds.Y0(); - if (y < 0 || y >= rect.Height) + if ((uint)y >= rect.Height) { continue; } Span row = img.PlaneRow(rect, c, y); + + // The assignment is necessary, if the OptimizeBackground + // constant gets changed in the future. +#pragma warning disable IDE0059 // Unnecessary assignment of a value Span bgrow = background.PlaneRow(rect, c, y); +#pragma warning restore IDE0059 // Unnecessary assignment of a value for (int sx = -rectBounds; sx < cc.Bounds.Width + rectBounds; sx++) { int x = sx + cc.Bounds.X0(); - if (x < 0 || x >= rect.Width) + if ((uint)x >= rect.Width) { continue; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlCluster.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlCluster.cs new file mode 100644 index 0000000000..3fc4a41a7e --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlCluster.cs @@ -0,0 +1,508 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; + +/// +/// Cluster encoder +/// +internal static class JxlCluster +{ + public static Vector Entropy( + Vector count, + Vector inverseTotal, + Vector total) + => Vector.ConditionalSelect( + Vector.Equals(count, total), + Vector.Zero, + Vector.Zero - Vector.Multiply(count, Vector.Log2(inverseTotal * count))); + + public static void HistogramCondition(JxlHistogram histogram) + { + Vector total = Vector.Zero; + int nzPos = -Vector.Count; + + Span countsSpan = CollectionsMarshal.AsSpan(histogram.Counts); + ref int countsRef = ref MemoryMarshal.GetReference(countsSpan); + + for (int i = 0; i < countsSpan.Length; i += Vector.Count) + { + Vector counts = Vector.LoadUnsafe(ref Unsafe.Add(ref countsRef, i)); + bool isNonZero = !Vector.AllWhereAllBitsSet(Vector.Equals(counts, Vector.Zero)); + + total += counts; + + // Set the position of the non-zero coefficient + // if the value is non-zero + if (isNonZero) + { + nzPos = i; + } + } + + histogram.Counts.Resize(nzPos + Vector.Count); + histogram.TotalCount = Vector.Sum(total); + } + + public static void HistogramEntropy(JxlHistogram a) + { + a.Entropy = 0.0f; + + if (a.TotalCount == 0) + { + return; + } + + Vector inverseTotal = Vector.Create(1.0f / a.TotalCount); + Vector entropyLanes = Vector.Zero; + Vector total = Vector.Create((float)a.TotalCount); + + Span countsSpan = CollectionsMarshal.AsSpan(a.Counts); + ref int countsRef = ref MemoryMarshal.GetReference(countsSpan); + + for (int i = 0; i < countsSpan.Length; i += Vector.Count) + { + Vector counts = Vector.LoadUnsafe(ref Unsafe.Add(ref countsRef, i)); + entropyLanes += Entropy( + Vector.ConvertToSingle(counts), + inverseTotal, + total); + } + + a.Entropy += Vector.Sum(entropyLanes); + } + + public static float HistogramDistance(JxlHistogram a, JxlHistogram b) + { + if (a.TotalCount == 0 || b.TotalCount == 0) + { + return 0.0f; + } + + Vector inverseTotal = Vector.Create(1.0f / (a.TotalCount + b.TotalCount)); + Vector distanceLanes = Vector.Zero; + Vector total = Vector.Create((float)(a.TotalCount + b.TotalCount)); + + Span aCountsSpan = CollectionsMarshal.AsSpan(a.Counts); + ref int aCountsRef = ref MemoryMarshal.GetReference(aCountsSpan); + + Span bCountsSpan = CollectionsMarshal.AsSpan(b.Counts); + ref int bCountsRef = ref MemoryMarshal.GetReference(bCountsSpan); + + for (int i = 0; i < Math.Max(aCountsSpan.Length, bCountsSpan.Length); i += Vector.Count) + { + Vector aCounts = aCountsSpan.Length > i + ? Vector.LoadUnsafe(ref Unsafe.Add(ref aCountsRef, i)) + : Vector.Zero; + + Vector bCounts = bCountsSpan.Length > i + ? Vector.LoadUnsafe(ref Unsafe.Add(ref bCountsRef, i)) + : Vector.Zero; + + Vector counts = Vector.ConvertToSingle(aCounts + bCounts); + distanceLanes += Entropy(counts, inverseTotal, total); + } + + float totalDistance = Vector.Sum(distanceLanes); + return totalDistance - a.Entropy - b.Entropy; + } + + /// + /// Computes difference of the actual histogram from the histogram + /// we would use for encoding, based on the Kullback-Leibler + /// divergence. + /// https://en.wikipedia.org/wiki/Kullback%E2%80%93Leibler_divergence + /// + /// The actual histogram. + /// The histogram we use for encoding. + /// Divergence of the actual and encoding histogram. + public static float HistogramKLDivergence(JxlHistogram actual, JxlHistogram coding) + { + if (actual.TotalCount == 0) + { + return 0; + } + + if (coding.TotalCount == 0) + { + return float.PositiveInfinity; + } + + Vector codingInv = Vector.Create(1.0F / coding.TotalCount); + Vector costLanes = Vector.Zero; + + ReadOnlySpan actualCounts = CollectionsMarshal.AsSpan(actual.Counts); + ReadOnlySpan codingCounts = CollectionsMarshal.AsSpan(coding.Counts); + + for (int i = 0; i < actualCounts.Length; i += Vector.Count) + { + Vector counts = Vector.Create(actualCounts.Slice(i, Vector.Count)); + + Vector codingCountsVector = codingCounts.Length > i + ? Vector.Create(codingCounts.Slice(i, Vector.Count)) + : Vector.Zero; + + Vector codingProbs = Vector.ConvertToSingle(codingCountsVector) * codingInv; + + Vector negCodingCost = Vector.ConditionalSelect( + Vector.Equals(counts, Vector.Zero), + Vector.ConditionalSelect( + Vector.Equals(codingCountsVector, Vector.Zero), + Vector.Create(float.NegativeInfinity), + Vector.Log2(codingProbs)), + Vector.Zero); + + costLanes -= Vector.ConvertToSingle(counts) * negCodingCost; + } + + float totalCost = Vector.Sum(costLanes); + return totalCost - actual.Entropy; + } + + public static bool FastClusterHistograms( + IReadOnlyList input, + int maxHistograms, + List output, + List histogramSymbols) + { + int previousHistograms = output.Count; + _ = output.EnsureCapacity(maxHistograms); + const float minDistanceForDistinct = 48.0F; + + histogramSymbols.Clear(); + + for (int i = 0; i < input.Count; i++) + { + histogramSymbols.Add((uint)maxHistograms); + } + + float[] distances = ArrayPool.Shared.Rent(input.Count); + Array.Fill(distances, float.MaxValue); + + int largestIndex = 0; + + for (int i = 0; i < input.Count; i++) + { + if (input[i].TotalCount == 0) + { + histogramSymbols[i] = 0; + distances[i] = 0.0F; + continue; + } + + HistogramEntropy(input[i]); + + if (input[i].TotalCount > input[largestIndex].TotalCount) + { + largestIndex = i; + } + } + + if (previousHistograms > 0) + { + for (int j = 0; j < previousHistograms; j++) + { + HistogramEntropy(output[j]); + } + + for (int i = 0; i < input.Count; i++) + { + if (distances[i] == 0.0F) + { + continue; + } + + for (int j = 0; j < previousHistograms; j++) + { + distances[i] = MathF.Min( + HistogramKLDivergence(input[i], output[j]), + distances[i]); + } + } + + int maxDistanceIndex = 0; + + for (int i = 1; i < distances.Length; i++) + { + if (distances[i] > distances[maxDistanceIndex]) + { + maxDistanceIndex = i; + } + } + + if (distances[maxDistanceIndex] > 0.0F) + { + largestIndex = maxDistanceIndex; + } + } + + while (output.Count < maxHistograms) + { + histogramSymbols[largestIndex] = (uint)output.Count; + + output.Add(input[largestIndex]); + + distances[largestIndex] = 0.0F; + + largestIndex = 0; + + for (int i = 0; i < input.Count; i++) + { + if (distances[i] == 0.0F) + { + continue; + } + + distances[i] = MathF.Min( + HistogramDistance(input[i], output[^1]), + distances[i]); + + if (distances[i] > distances[largestIndex]) + { + largestIndex = i; + } + } + + if (distances[largestIndex] < minDistanceForDistinct) + { + break; + } + } + + for (int i = 0; i < input.Count; i++) + { + if (histogramSymbols[i] != (uint)maxHistograms) + { + continue; + } + + int best = 0; + float bestDistance = float.MaxValue; + + for (int j = 0; j < output.Count; j++) + { + float distance = + j < previousHistograms + ? HistogramKLDivergence(input[i], output[j]) + : HistogramDistance(input[i], output[j]); + + if (distance < bestDistance) + { + best = j; + bestDistance = distance; + } + } + + if (bestDistance >= float.MaxValue) + { + // distance must not overflow + ArrayPool.Shared.Return(distances); + return false; + } + + if (best >= previousHistograms) + { + output[best].AddHistogram(input[i]); + HistogramEntropy(output[best]); + } + + histogramSymbols[i] = (uint)best; + } + + ArrayPool.Shared.Return(distances); + return true; + } + + public static void HistogramReindex(List output, int previousHistograms, List symbols) + { + List temp = [.. output]; + Dictionary newIndex = []; + + for (int i = 0; i < previousHistograms; i++) + { + newIndex[(uint)i] = i; + } + + int nextIndex = previousHistograms; + + foreach (uint symbol in symbols) + { + if (!newIndex.ContainsKey(symbol)) + { + newIndex[symbol] = nextIndex; + output[nextIndex] = temp[(int)symbol]; + nextIndex++; + } + } + + if (output.Count > nextIndex) + { + output.RemoveRange(nextIndex, output.Count - nextIndex); + } + + for (int i = 0; i < symbols.Count; i++) + { + symbols[i] = (uint)newIndex[symbols[i]]; + } + } + + public static bool ClusterHistograms( + JxlHistogramParameters parameters, + IReadOnlyList input, + uint maxHistograms, + List output, + List histogramSymbols) + { + int previousHistograms = output.Count; + + maxHistograms = Math.Min(maxHistograms, parameters.MaxHistograms); + maxHistograms = Math.Min(maxHistograms, (uint)input.Count); + + if (parameters.Clustering == JxlClusteringType.Fastest) + { + maxHistograms = Math.Min(maxHistograms, 4); + } + + if (!FastClusterHistograms(input, (int)(previousHistograms + maxHistograms), output, histogramSymbols)) + { + return false; + } + + if (previousHistograms == 0 && parameters.Clustering == HistogramParams.ClusteringType.Best) + { + for (int i = 0; i < output.Count; i++) + { + output[i].Entropy = output[i].AnsPopulationCost(); + } + + uint nextVersion = 2; + + uint[] version = ArrayPool.Shared.Rent(output.Count); + Array.Fill(version, 1u); + + uint[] renumbering = ArrayPool.Shared.Rent(output.Count); + for (uint i = 0; i < renumbering.Length; i++) + { + renumbering[i] = i; + } + + PriorityQueue pairsToMerge = new(); + + for (uint i = 0; i < output.Count; i++) + { + for (uint j = i + 1; j < output.Count; j++) + { + JxlHistogram histogram = new(16); + histogram.AddHistogram(output[(int)i]); + histogram.AddHistogram(output[(int)j]); + + float cost = histogram.AnsEntropyCost() - output[(int)i].Entropy - output[(int)j].Entropy; + + if (cost >= 0) + { + continue; + } + + uint versionValue = Math.Max(version[i], version[j]); + HistogramPair pair = new(cost, i, j, versionValue); + pairsToMerge.Enqueue(pair, (cost, i, j, versionValue)); + } + } + + while (pairsToMerge.TryDequeue(out HistogramPair pair, out _)) + { + uint first = pair.First; + uint second = pair.Second; + uint versionValue = pair.Version; + + if (versionValue != Math.Max(version[first], version[second]) || version[first] == 0 || version[second] == 0) + { + continue; + } + + output[(int)first].AddHistogram(output[(int)second]); + output[(int)first].Entropy = output[(int)first].AnsPopulationCost(); + + for (int i = 0; i < renumbering.Length; i++) + { + if (renumbering[i] == second) + { + renumbering[i] = first; + } + } + + version[second] = 0; + version[first] = nextVersion++; + + for (uint j = 0; j < output.Count; j++) + { + if (j == first || version[j] == 0) + { + continue; + } + + JxlHistogram histogram = new(16); + histogram.AddHistogram(output[(int)first]); + histogram.AddHistogram(output[(int)j]); + + float mergeCost = histogram.AnsEntropyCost() - output[(int)first].Entropy - output[(int)j].Entropy; + + if (mergeCost >= 0) + { + continue; + } + + uint firstIndex = Math.Min(first, j); + uint secondIndex = Math.Max(first, j); + uint mergeVersion = Math.Max(version[first], version[j]); + + HistogramPair newPair = new(mergeCost, firstIndex, secondIndex, mergeVersion); + pairsToMerge.Enqueue(newPair, (mergeCost, firstIndex, secondIndex, mergeVersion)); + } + } + + uint[] reverseRenumbering = ArrayPool.Shared.Rent(output.Count); + Array.Fill(reverseRenumbering, uint.MaxValue); + + int numberAlive = 0; + + for (int i = 0; i < output.Count; i++) + { + if (version[i] == 0) + { + continue; + } + + output[numberAlive] = output[i]; + reverseRenumbering[i] = (uint)numberAlive; + numberAlive++; + } + + if (output.Count > numberAlive) + { + output.RemoveRange(numberAlive, output.Count - numberAlive); + } + + for (int i = 0; i < histogramSymbols.Count; i++) + { + uint item = histogramSymbols[i]; + histogramSymbols[i] = reverseRenumbering[renumbering[item]]; + } + + ArrayPool.Shared.Return(renumbering); + ArrayPool.Shared.Return(version); + ArrayPool.Shared.Return(reverseRenumbering); + } + + HistogramReindex(output, previousHistograms, histogramSymbols); + return true; + } + + private readonly record struct HistogramPair(float Cost, uint First, uint Second, uint Version); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlContextMapEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlContextMapEncoder.cs index b60a90ab95..2d5304d8b0 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlContextMapEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlContextMapEncoder.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; @@ -13,39 +14,34 @@ internal static class JxlContextMapEncoder private static int IndexOf(List v, byte value) { - int i = 0; - for (; i < v.Count; ++i) - { - if (v[i] == value) - { - return i; - } - } + ReadOnlySpan span = CollectionsMarshal.AsSpan(v); + int idx = span.IndexOf(value); - return i; + return idx >= 0 ? idx : span.Length; } private static void MoveToFront(List v, int index) { - byte value = v[index]; + Span spanv = CollectionsMarshal.AsSpan(v); + byte value = spanv[index]; for (int i = index; i != 0; --i) { - v[i] = v[i - 1]; + spanv[i] = spanv[i - 1]; } - v[0] = value; + spanv[0] = value; } - private static List MoveToFrontTransform(IReadOnlyList v) + private static List MoveToFrontTransform(Span v) { - if (v.Count == 0) + if (v.Length == 0) { return []; } byte maxValue = 0; - for (int i = 0; i < v.Count; ++i) + for (int i = 0; i < v.Length; ++i) { if (v[i] > maxValue) { @@ -59,8 +55,8 @@ private static List MoveToFrontTransform(IReadOnlyList v) mtf.Add((byte)i); } - List result = new(v.Count); - for (int i = 0; i < v.Count; ++i) + List result = new(v.Length); + for (int i = 0; i < v.Length; ++i) { int index = IndexOf(mtf, v[i]); @@ -76,7 +72,7 @@ private static List MoveToFrontTransform(IReadOnlyList v) return result; } - private static bool EncodeContextMap(IReadOnlyList contextMap, int numHistograms, JxlBitWriter writer) + private static bool EncodeContextMap(byte[] contextMap, int numHistograms, JxlBitWriter writer) { if (numHistograms == 1) { @@ -92,7 +88,7 @@ private static bool EncodeContextMap(IReadOnlyList contextMap, int numHist List> jxlTokens = [ - new List(contextMap.Count) + new List(contextMap.Length) ]; List> mtfJxlTokens = @@ -100,7 +96,7 @@ private static bool EncodeContextMap(IReadOnlyList contextMap, int numHist new List(transformedSymbols.Count) ]; - for (int i = 0; i < contextMap.Count; ++i) + for (int i = 0; i < contextMap.Length; ++i) { jxlTokens[0].Add(new JxlToken(0, contextMap[i])); } @@ -137,16 +133,16 @@ private static bool EncodeContextMap(IReadOnlyList contextMap, int numHist } int entryBits = JxlMath.CeilLog2Nonzero(numHistograms); - int simpleCost = entryBits * contextMap.Count; + int simpleCost = entryBits * contextMap.Length; if (entryBits < 4 && simpleCost < ansCost && simpleCost < mtfCost) { - return writer.WithMaxBits((ulong)(3 + (entryBits * contextMap.Count)), () => + return writer.WithMaxBits((ulong)(3 + (entryBits * contextMap.Length)), () => { writer.Write(1, 1); writer.Write(2, entryBits); - for (int i = 0; i < contextMap.Count; ++i) + for (int i = 0; i < contextMap.Length; ++i) { writer.Write(entryBits, contextMap[i]); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlEntropyCoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlEntropyCoder.cs new file mode 100644 index 0000000000..242d2288fd --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlEntropyCoder.cs @@ -0,0 +1,296 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; + +internal static class JxlEntropyCoder +{ + private static int NumNonZero8x8ExceptDC(ReadOnlySpan block, Span nzerosPos) + { + Vector zero = Vector.Zero; + Vector negSumZero = zero; + + Span dcMaskLanes = stackalloc int[Vector.Count]; + dcMaskLanes[0] = -1; + + for (int x = 0; x < 8; x += Vector.Count) + { + Vector dcMask = Vector.Create(dcMaskLanes.Slice(x, Vector.Count)); + Vector coef = (~dcMask) & Vector.Create(block.Slice(x, Vector.Count)); + + negSumZero += Vector.ConditionalSelect( + Vector.Equals(coef, zero), + Vector.One, + zero); + } + + for (int y = 1; y < 8; y++) + { + ReadOnlySpan row = block.Slice(y * 8, 8); + + for (int x = 0; x < 8; x += Vector.Count) + { + Vector coef = Vector.Create(row.Slice(x, Vector.Count)); + + negSumZero += Vector.ConditionalSelect( + Vector.Equals(coef, zero), + Vector.One, + zero); + } + } + + int nzeros = 64 + Vector.Sum(negSumZero); + nzerosPos[0] = nzeros; + return nzeros; + } + + private static int NumNonZeroExceptLlf(int cx, int cy, JxlAcStrategy acs, int coveredBlocks, int log2CoveredBlocks, ReadOnlySpan block, int nzerosStride, Span nzerosPos) + { + Vector zero = Vector.Zero; + Vector negSumZero = zero; + + Span llfMaskLanes = stackalloc int[JxlAcStrategy.MaximumCoefficientBlocks * (1 + JxlFrameDimensions.BlockDimensions)]; + llfMaskLanes[..JxlAcStrategy.MaximumCoefficientBlocks].Fill(-1); + + ReadOnlySpan llfMaskPos = llfMaskLanes[(JxlAcStrategy.MaximumCoefficientBlocks - cx)..]; + + // Rows with LLF: mask out the LLF. + int rowWidth = cx * JxlFrameDimensions.BlockDimensions; + + for (int y = 0; y < cy; y++) + { + int rowOffset = y * rowWidth; + + for (int x = 0; x < rowWidth; x += Vector.Count) + { + Vector llfMask = Vector.Create(llfMaskPos.Slice(x, Vector.Count)); + Vector coef = (~llfMask) & Vector.Create(block.Slice(rowOffset + x, Vector.Count)); + + negSumZero += Vector.ConditionalSelect( + Vector.Equals(coef, zero), + Vector.One, + zero); + } + } + + // Remaining rows: no mask. + for (int y = cy; y < cy * JxlFrameDimensions.BlockDimensions; y++) + { + int rowOffset = y * rowWidth; + + for (int x = 0; x < rowWidth; x += Vector.Count) + { + Vector coef = Vector.Create(block.Slice(rowOffset + x, Vector.Count)); + + negSumZero += Vector.ConditionalSelect( + Vector.Equals(coef, zero), + Vector.One, + zero); + } + } + + // We want area sum_zero, add because negSumZero is already negated. + int nzeros = (cx * cy * JxlFrameDimensions.DctBlockSize) + Vector.Sum(negSumZero); + + int shiftedNzeros = (nzeros + coveredBlocks - 1) >> log2CoveredBlocks; + + int coveredBlocksY = acs.CoveredBlocksY; + int coveredBlocksX = acs.CoveredBlocksX; + + for (int y = 0; y < coveredBlocksY; y++) + { + int rowOffset = y * nzerosStride; + + for (int x = 0; x < coveredBlocksX; x++) + { + nzerosPos[rowOffset + x] = shiftedNzeros; + } + } + + return nzeros; + } + + public static bool TokenizeCoefficients( + ReadOnlySpan orders, + Rectangle rect, + ReadOnlyMemory[] acRows, + JxlAcStrategyImage acStrategy, + JxlYCbCrChromaSubsampling cs, + JxlImage3I tmpNumNzeroes, + List output, + JxlImageB qdc, + JxlImageI qf, + JxlBlockContextMap blockCtxMap) + { + int xsizeBlocks = rect.Width; + int ysizeBlocks = rect.Height; + + output.Clear(); + + output.EnsureCapacity(3 * xsizeBlocks * ysizeBlocks * DctBlockSize); + + Span offset = stackalloc int[3]; + int nzerosStride = tmpNumNzeroes.PixelsPerRow; + + InlineArray3 sby = default; + InlineArray3 sbx = default; + + for (int by = 0; by < ysizeBlocks; by++) + { + sby[0] = by >> cs.VShift(0); + sby[1] = by >> cs.VShift(1); + sby[2] = by >> cs.VShift(2); + + // Span> is not legal C#, so keep the three planes explicitly. + Span rowNzeros0 = tmpNumNzeroes.PlaneRow(0, sby[0]); + Span rowNzeros1 = tmpNumNzeroes.PlaneRow(1, sby[1]); + Span rowNzeros2 = tmpNumNzeroes.PlaneRow(2, sby[2]); + + ReadOnlySpan rowNzerosTop0 = sby[0] == 0 ? ReadOnlySpan.Empty : tmpNumNzeroes.ConstPlaneRow(0, sby[0] - 1); + ReadOnlySpan rowNzerosTop1 = sby[1] == 0 ? ReadOnlySpan.Empty : tmpNumNzeroes.ConstPlaneRow(1, sby[1] - 1); + ReadOnlySpan rowNzerosTop2 = sby[2] == 0 ? ReadOnlySpan.Empty : tmpNumNzeroes.ConstPlaneRow(2, sby[2] - 1); + + ReadOnlySpan rowQdc = qdc.GetRow(rect.Y0() + by).Slice(rect.X0()); + + ReadOnlySpan rowQf = qf.GetRow(rect, by); + JxlAcStrategyRow acsRow = acStrategy.GetRow(rect, by); + + for (int bx = 0; bx < xsizeBlocks; bx++) + { + JxlAcStrategy acs = acsRow[bx]; + + if (!acs.IsFirstBlock) + { + continue; + } + + sbx[0] = bx >> cs.HShift(0); + sbx[1] = bx >> cs.HShift(1); + sbx[2] = bx >> cs.HShift(2); + + int cx = acs.CoveredBlocksX; + int cy = acs.CoveredBlocksY; + + int coveredBlocks = cx * cy; + int log2CoveredBlocks = (int)JxlMath.Num0BitsBelowLS1Bit_Nonzero(coveredBlocks); + + int size = coveredBlocks * JxlFrameDimensions.BlockDimensions; + + // Swap cx/cy to canonical order. + JxlForwardCoefficientOrder.CoefficientLayout(ref cy, ref cx); + + // C++: for (int c : {1, 0, 2}) + for (int c = 0; c < 3; c++) + { + int channel = c switch + { + 0 => 1, + 1 => 0, + _ => 2 + }; + + if ((sbx[channel] << cs.HShift(channel)) != bx) + { + continue; + } + + if ((sby[channel] << cs.VShift(channel)) != by) + { + continue; + } + + ReadOnlySpan block = acRows[channel].Span[offset[channel]..]; + + Span rowNzeros = channel switch + { + 0 => rowNzeros0, + 1 => rowNzeros1, + _ => rowNzeros2 + }; + + int nzeros; + + if (coveredBlocks == 1) + { + nzeros = NumNonZero8x8ExceptDC(block, rowNzeros[sbx[channel]..]); + } + else + { + nzeros = NumNonZeroExceptLlf( + cx, + cy, + acs, + coveredBlocks, + log2CoveredBlocks, + block, + nzerosStride, + rowNzeros[sbx[channel]..]); + } + + int ord = StrategyOrder[acs.RawStrategy]; + int coeffOrderOffset = CoeffOrderOffset(ord, channel); + ReadOnlySpan order = orders[coeffOrderOffset..]; + + ReadOnlySpan rowNzerosTop = channel switch + { + 0 => rowNzerosTop0, + 1 => rowNzerosTop1, + _ => rowNzerosTop2 + }; + + int predictedNzeros = PredictFromTopAndLeft( + rowNzerosTop, + rowNzeros, + sbx[channel], + 32); + + int blockCtx = blockCtxMap.Context( + rowQdc[bx], + (uint)rowQf[sbx[channel]], + ord, + channel); + + int nzeroCtx = blockCtxMap.NonZeroContext(predictedNzeros, blockCtx); + + output.Add(new JxlToken((JxlMaTreeContext)nzeroCtx, (uint)nzeros)); + + int histoOffset = blockCtxMap.ZeroDensityContextOffset(blockCtx); + int prev = nzeros > size / 16 ? 0 : 1; + + for (int k = coveredBlocks; k < size && nzeros != 0; k++) + { + int coeff = block[order[k]]; + + int ctx = + histoOffset + ZeroDensityContext( + nzeros, + k, + coveredBlocks, + log2CoveredBlocks, + prev); + + uint uCoeff = JxlPackSigned.PackUnsigned(coeff); + + output.Add(new JxlToken((JxlMaTreeContext)ctx, uCoeff)); + + prev = coeff != 0 ? 1 : 0; + nzeros -= prev; + } + + offset[channel] += size; + } + } + } + + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlHeuristics.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlHeuristics.cs index 846f791f69..bb32be6531 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlHeuristics.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlHeuristics.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Buffers; +using System.Numerics.Tensors; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; @@ -79,25 +80,21 @@ public static void FindBestBlockEntropyModel(JxlCompressParameters cparameters, } } - int[]? pooledCounts = null; - int[]? pooledRemap = null; - int[]? pooledClusters = null; + byte[]? pooledRemap = null; + byte[]? pooledClusters = null; int countsLength = JxlForwardCoefficientOrder.OrderCount * (qft.Count + 1); - Span counts = - countsLength <= 128 - ? stackalloc int[128].Slice(0, countsLength) - : pooledCounts = ArrayPool.Shared.Rent(countsLength); + int[] counts = ArrayPool.Shared.Rent(countsLength); - Span remap = + Span remap = countsLength <= 128 - ? stackalloc int[128].Slice(0, countsLength) - : pooledRemap = ArrayPool.Shared.Rent(countsLength); + ? stackalloc byte[128].Slice(0, countsLength) + : pooledRemap = ArrayPool.Shared.Rent(countsLength); - Span clusters = + Span clusters = countsLength <= 128 - ? stackalloc int[128].Slice(0, countsLength) - : pooledClusters = ArrayPool.Shared.Rent(countsLength); + ? stackalloc byte[128].Slice(0, countsLength) + : pooledClusters = ArrayPool.Shared.Rent(countsLength); int qftPos = 0; @@ -114,14 +111,65 @@ public static void FindBestBlockEntropyModel(JxlCompressParameters cparameters, } } - JxlSimdUtils.Iota(remap, 0); + JxlSimdUtils.Iota(remap, (byte)0); remap.CopyTo(clusters); int numClusters = Math.Clamp(total / sizeForContextModel / 2, 2, 9); int numClustersChroma = Math.Clamp(total / sizeForContextModel / 3, 1, 5); - // TODO: method incomplete - // do not forget to ArrayPool.Shared.Return pooledCounts, pooledRemap, pooledClusters if needed + while (clusters.Length > numClusters) + { + clusters.Sort((a, b) => counts[b].CompareTo(counts[a])); + counts[clusters[^2]] += counts[clusters[^1]]; + counts[^1] = 0; + remap[^1] = clusters[^2]; + clusters = clusters[..^1]; + } + + for (int i = 0; i < remap.Length; i++) + { + while (remap[remap[i]] != remap[i]) + { + remap[i] = remap[remap[i]]; + } + } + + Span remapRemap = stackalloc byte[remap.Length]; + remapRemap.Fill((byte)remap.Length); + + int num = 0; + for (int i = 0; i < remap.Length; i++) + { + if (remapRemap[remap[i]] == remap.Length) + { + remapRemap[remap[i]] = (byte)(num++); + } + + remap[i] = remapRemap[remap[i]]; + } + + // Write the block context map. + blockCtxMap.ContextMap = remap.ToArray(); + Array.Resize(ref blockCtxMap.ContextMap, remap.Length * 3); + + // For chroma, only use up to numClustersChroma separate block contexts + // (those for the biggest clusters) + for (int i = remap.Length; i < remap.Length * 3; i++) + { + blockCtxMap.ContextMap[i] = (byte)(num + Math.Clamp(remap[i % remap.Length], 0, numClustersChroma - 1)); + } + + blockCtxMap.ContextCount = TensorPrimitives.Max((ReadOnlySpan)blockCtxMap.ContextMap.AsSpan()) + 1; + + if (pooledRemap is not null) + { + ArrayPool.Shared.Return(pooledRemap); + } + + if (pooledClusters is not null) + { + ArrayPool.Shared.Return(pooledClusters); + } } private sealed class OccCounters : IDisposable diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs index 71b68c9063..613206a76d 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs @@ -26,7 +26,11 @@ public JxlBlockContextMap() public List QfThresholds { get; set; } = []; - public byte[] ContextMap { get; set; } = new byte[DefaultContextMap.Length]; + /// + /// We may need to get a ref to this field, so make this + /// public. + /// + public byte[] ContextMap = new byte[DefaultContextMap.Length]; public int ContextCount { get; set; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs index 80e4715097..1015e2b720 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs @@ -554,24 +554,40 @@ public static uint MaxValue(Span data) /// vector sizes. For example, some ARM CPUs support 2048-bit /// vectors through Vector<T>. In that specific case, this /// method can be used for future-proofing. - /// - /// Note that this method, albeit future-proof, may be considered - /// slow for smaller vector sizes (think CPUs with 256bit vectors). /// /// Vector to duplicate. /// New vector that is duplicated across the width. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe Vector LoadDuplicate128(Vector128 vec) + public static Vector LoadDuplicate128(Vector128 vec) where T : unmanaged { - Span value = stackalloc T[Vector.Count]; - - for (int i = 0; i < Vector.Count; i += sizeof(T)) + if (Vector.Count == Vector64.Count) + { + throw new NotSupportedException(); + } + else if (Vector.Count == Vector512.Count) + { + return Vector512.Create(vec).AsVector(); + } + else if (Vector.Count == Vector256.Count) + { + return Vector256.Create(vec).AsVector(); + } + else if (Vector.Count == Vector128.Count) { - vec.CopyTo(value[i..]); + return vec.AsVector(); } + else + { + Span value = stackalloc T[Vector.Count]; - return new Vector(value); + for (int i = 0; i <= Vector.Count - Vector128.Count; i += Vector128.Count) + { + vec.CopyTo(value[i..]); + } + + return new Vector(value); + } } /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/ToneMappingStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/ToneMappingStage.cs new file mode 100644 index 0000000000..cd12640c73 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/ToneMappingStage.cs @@ -0,0 +1,139 @@ +// 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.Cms; +using SixLabors.ImageSharp.Formats.Jxl.Cms.ToneMapping; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +internal class ToneMappingStage : RenderPipelineStageBase +{ + private readonly JxlRec2408ToneMapper? toneMapper; + private readonly JxlHlgOotfToneMapper? hlgOotf; + private readonly JxlOutputEncodingInfo outputEncodingInfo; + private readonly float toIntensityTarget = 1.0f; + private readonly float fromDesiredIntensityTarget = 1.0f; + + public ToneMappingStage(Configuration configuration, JxlOutputEncodingInfo outputEncodingInfo) + : base(configuration) + { + this.Settings = default; + this.outputEncodingInfo = outputEncodingInfo; + + if (outputEncodingInfo.DesiredIntensityTarget == outputEncodingInfo.OriginalIntensityTarget) + { + // No tone mapping requested + return; + } + + JxlCustomTransferFunction originalTf = outputEncodingInfo.OriginalColorEncoding!.TransferFunction; + JxlCustomTransferFunction destinationTf = outputEncodingInfo.ColorEncoding!.TransferFunction; + + if (originalTf.IsPq && outputEncodingInfo.DesiredIntensityTarget < outputEncodingInfo.OriginalIntensityTarget) + { + this.toneMapper = new( + sourceRange: CreateRange(0.0f, outputEncodingInfo.OriginalIntensityTarget), + targetRange: CreateRange(0.0f, outputEncodingInfo.DesiredIntensityTarget), + outputEncodingInfo.Luminances); + } + else if (originalTf.IsHlg && !destinationTf.IsHlg) + { + this.hlgOotf = new( + sourceLuminance: outputEncodingInfo.OriginalIntensityTarget, + targetLuminance: outputEncodingInfo.DesiredIntensityTarget, + outputEncodingInfo.Luminances); + } + + if (destinationTf.IsPq && (this.toneMapper.HasValue || this.hlgOotf.HasValue)) + { + this.toIntensityTarget = 10000f / outputEncodingInfo.OriginalIntensityTarget; + this.fromDesiredIntensityTarget = outputEncodingInfo.DesiredIntensityTarget / 10000f; + } + } + + public override string Name => "ToneMapping"; + + /// + /// Gets a value indicating whether there's a Rec2408 or HLG OOTF tone mapper. + /// + public bool IsNeeded => this.toneMapper.HasValue || this.hlgOotf.HasValue; + + public override void ProcessRow(Buffer2D> inputRows, Buffer2D> outputRows, int xExtraLeft, int xExtraRight, int width, int xPos, int yPos) + { + if (!this.IsNeeded) + { + // Tone mapping is unnecessary, so skip this stage. + return; + } + + Span row0 = this.GetInputRow(inputRows, 0, 0); + Span row1 = this.GetInputRow(inputRows, 1, 0); + Span row2 = this.GetInputRow(inputRows, 2, 0); + + ref float row0Ref = ref MemoryMarshal.GetReference(row0); + ref float row1Ref = ref MemoryMarshal.GetReference(row1); + ref float row2Ref = ref MemoryMarshal.GetReference(row2); + + Vector vToIntensityTarget = Vector.Create(this.toIntensityTarget); + Vector vFromDesiredIntensityTarget = Vector.Create(this.fromDesiredIntensityTarget); + + for (int x = 0; x < width; x += Vector.Count) + { + ref float currRow0 = ref Unsafe.Add(ref row0Ref, x); + ref float currRow1 = ref Unsafe.Add(ref row1Ref, x); + ref float currRow2 = ref Unsafe.Add(ref row2Ref, x); + + Vector r = Vector.LoadUnsafe(ref currRow0) * vToIntensityTarget; + Vector g = Vector.LoadUnsafe(ref currRow1) * vToIntensityTarget; + Vector b = Vector.LoadUnsafe(ref currRow2) * vToIntensityTarget; + + if (this.toneMapper.HasValue) + { + this.toneMapper.Value.ToneMap(ref r, ref g, ref b); + } + else + { + // We can safely assume that an HLG OOTF tone mapper + // is present, because we already checked for both to + // exist above (through this.IsNeeded). + this.hlgOotf!.Value.Apply(ref r, ref g, ref b); + } + + if (this.toneMapper.HasValue || this.hlgOotf!.Value.WarrantsGamutMapping) + { + JxlToneMappingUtils.GamutMap(ref r, ref g, ref b, this.outputEncodingInfo.Luminances); + } + + r *= vFromDesiredIntensityTarget; + g *= vFromDesiredIntensityTarget; + b *= vFromDesiredIntensityTarget; + + r.StoreUnsafe(ref currRow0); + g.StoreUnsafe(ref currRow1); + b.StoreUnsafe(ref currRow2); + } + } + + public override RenderPipelineChannelMode GetChannelMode(int channel) => + channel < 3 ? RenderPipelineChannelMode.InPlace : RenderPipelineChannelMode.Ignored; + + /// + /// Simple utility method that constructs a 2-element inline array + /// for use in the constructor. + /// + /// First item. + /// Second item. + /// The inline array. + private static InlineArray2 CreateRange(float x, float y) + { + InlineArray2 range = default; + range[0] = x; + range[1] = y; + return range; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs index 476beace38..786dddfa6b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs @@ -488,7 +488,7 @@ private static void StoreFloatRow(IJxlImageOutput output, Span input0, Sp } else if (output.PixelFormat.Channels == 2) { - for (int i = 0; i < length; i += Vector.Count) + for (int i = 0; i <= length - Vector.Count; i += Vector.Count) { JxlSimdUtils.StoreInterleaved( Vector.Create(input0[i..]), @@ -498,7 +498,7 @@ private static void StoreFloatRow(IJxlImageOutput output, Span input0, Sp } else if (output.PixelFormat.Channels == 3) { - for (int i = 0; i < length; i += Vector.Count) + for (int i = 0; i <= length - Vector.Count; i += Vector.Count) { JxlSimdUtils.StoreInterleaved( Vector.Create(input0[i..]), @@ -509,7 +509,7 @@ private static void StoreFloatRow(IJxlImageOutput output, Span input0, Sp } else if (output.PixelFormat.Channels == 4) { - for (int i = 0; i < length; i += Vector.Count) + for (int i = 0; i <= length - Vector.Count; i += Vector.Count) { JxlSimdUtils.StoreInterleaved( Vector.Create(input0[i..]), From f35e91b12327fc0d9c3e333e240991e9a21adcbc Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 30 Sep 2026 06:23:12 +0400 Subject: [PATCH 200/328] Complete patch dictionary --- .../Jxl/Processing/Blending/JxlBlending.cs | 57 ++- .../Decoder/Patch/JxlPatchDictionary.cs | 398 ++++++++++++++++-- .../Decoder/Patch/JxlReferenceFrame.cs | 8 + .../Jxl/Processing/Image/JxlImageBundle.cs | 2 + 4 files changed, 398 insertions(+), 67 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlReferenceFrame.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlending.cs b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlending.cs index a595406f36..2da31b8e66 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlending.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlending.cs @@ -5,7 +5,6 @@ using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Patch; -using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Blending; @@ -44,9 +43,9 @@ public static bool NeedsBlending(JxlFrameHeader header) public static void PerformBlending( Configuration configuration, - Buffer2D bg, - Buffer2D fg, - Buffer2D output, + Span> bg, + Span> fg, + Span> output, int x0, int xsize, JxlPatchBlending colorBlending, @@ -69,7 +68,7 @@ public static void PerformBlending( Span row = tmp.GetRow(i3); for (int x = 0; x < xsize; x++) { - row[x] = bg[i3, x + x0] + fg[i3, x + x0]; + row[x] = bg[i3].Span[x + x0] + fg[i3].Span[x + x0]; } continue; @@ -80,10 +79,10 @@ public static void PerformBlending( int alpha = ecBlending[i].AlphaChannel; bool isPremultiplied = extraChannelInfo[alpha].AlphaAssociated; - Span bgSpan3 = bg.DangerousGetRowSpan(i3)[x0..]; - Span bgSpan3Alpha = bg.DangerousGetRowSpan(3 + alpha)[x0..]; - Span fgSpan3 = fg.DangerousGetRowSpan(i3)[x0..]; - Span fgSpan3Alpha = fg.DangerousGetRowSpan(3 + alpha)[x0..]; + Span bgSpan3 = bg[i3].Span[x0..]; + Span bgSpan3Alpha = bg[3 + alpha].Span[x0..]; + Span fgSpan3 = fg[i3].Span[x0..]; + Span fgSpan3Alpha = fg[3 + alpha].Span[x0..]; JxlAlphaHelper.PerformAlphaBlending( bgSpan3, @@ -103,10 +102,10 @@ public static void PerformBlending( int alpha = ecBlending[i].AlphaChannel; bool isPremultiplied = extraChannelInfo[alpha].AlphaAssociated; - Span bgSpan3 = bg.DangerousGetRowSpan(i3)[x0..]; - Span bgSpan3Alpha = bg.DangerousGetRowSpan(3 + alpha)[x0..]; - Span fgSpan3 = fg.DangerousGetRowSpan(i3)[x0..]; - Span fgSpan3Alpha = fg.DangerousGetRowSpan(3 + alpha)[x0..]; + Span bgSpan3 = bg[i3].Span[x0..]; + Span bgSpan3Alpha = bg[3 + alpha].Span[x0..]; + Span fgSpan3 = fg[i3].Span[x0..]; + Span fgSpan3Alpha = fg[3 + alpha].Span[x0..]; JxlAlphaHelper.PerformAlphaBlending( bgSpan3, @@ -125,9 +124,9 @@ public static void PerformBlending( { int alpha = ecBlending[i].AlphaChannel; - Span bgSpan3 = bg.DangerousGetRowSpan(i3)[x0..]; - Span bgSpan3Alpha = bg.DangerousGetRowSpan(3 + alpha)[x0..]; - Span fgSpan3 = fg.DangerousGetRowSpan(i3)[x0..]; + Span bgSpan3 = bg[i3].Span[x0..]; + Span bgSpan3Alpha = bg[3 + alpha].Span[x0..]; + Span fgSpan3 = fg[i3].Span[x0..]; JxlAlphaHelper.PerformAlphaWeightedAdd( bgSpan3, @@ -144,9 +143,9 @@ public static void PerformBlending( { int alpha = ecBlending[i].AlphaChannel; - Span bgSpan3 = bg.DangerousGetRowSpan(i3)[x0..]; - Span bgSpan3Alpha = bg.DangerousGetRowSpan(3 + alpha)[x0..]; - Span fgSpan3 = fg.DangerousGetRowSpan(i3)[x0..]; + Span bgSpan3 = bg[i3].Span[x0..]; + Span bgSpan3Alpha = bg[3 + alpha].Span[x0..]; + Span fgSpan3 = fg[i3].Span[x0..]; JxlAlphaHelper.PerformAlphaWeightedAdd( fgSpan3, @@ -161,8 +160,8 @@ public static void PerformBlending( case JxlPatchBlendMode.Multiply: { - Span bgSpan3 = bg.DangerousGetRowSpan(i3)[x0..]; - Span fgSpan3 = fg.DangerousGetRowSpan(i3)[x0..]; + Span bgSpan3 = bg[i3].Span[x0..]; + Span fgSpan3 = fg[i3].Span[x0..]; JxlAlphaHelper.PerformMultiplyBlending( bgSpan3, @@ -177,7 +176,7 @@ public static void PerformBlending( case JxlPatchBlendMode.Replace: if (xsize > 0) { - Span fgSpan3 = fg.DangerousGetRowSpan(i3)[x0..]; + Span fgSpan3 = fg[i3].Span[x0..]; fgSpan3.Slice(0, xsize).CopyTo(tmp.GetRow(i3)); } @@ -186,7 +185,7 @@ public static void PerformBlending( case JxlPatchBlendMode.None: if (xsize > 0) { - Span bgSpan3 = bg.DangerousGetRowSpan(i3)[x0..]; + Span bgSpan3 = bg[i3].Span[x0..]; bgSpan3.Slice(0, xsize).CopyTo(tmp.GetRow(i3)); } @@ -196,13 +195,13 @@ public static void PerformBlending( int colorBlendingAlpha = colorBlending.AlphaChannel; - void Add() + void Add(Span> bg, Span> fg) { for (int p = 0; p < 3; p++) { Span output = tmp.GetRow(p); - Span bgSpan = bg.DangerousGetRowSpan(p); - Span fgSpan = fg.DangerousGetRowSpan(p); + Span bgSpan = bg[p].Span; + Span fgSpan = fg[p].Span; for (int x = 0; x < xsize; x++) { @@ -264,7 +263,7 @@ void Copy(Span src) { case JxlPatchBlendMode.Add: { - Add(); + Add(bg, fg); break; } @@ -276,7 +275,7 @@ void Copy(Span src) } else { - Add(); + Add(bg, fg); } break; @@ -290,7 +289,7 @@ void Copy(Span src) } else { - Add(); + Add(bg, fg); } break; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchDictionary.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchDictionary.cs index 096c0d3de2..30b259ff97 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchDictionary.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchDictionary.cs @@ -1,49 +1,55 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Blending; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Patch; internal sealed class JxlPatchDictionary { - private struct PatchTreeNode - { - public long LeftChild; - public long RightChild; - public int YCenter; - public int Start; - public int Count; - } + private const byte NumPatchBlendModes = (byte)JxlPatchBlendMode.AlphaWeightedAddBelow + 1; - private struct SortedPatch - { - public int First; - public int Second; - } - - private readonly JxlReferenceFrame[] referenceFrames = new JxlReferenceFrame[4]; + private JxlReferenceFrame[] referenceFrames = new JxlReferenceFrame[4]; private readonly List positions = []; private readonly List referencePositions = []; private readonly List blendings = []; private int blendingsStride; private readonly List patchTree = []; - private readonly List numPatches = []; + private List numPatches = []; private readonly List sortedPatchesY0 = []; private readonly List sortedPatchesY1 = []; public bool HasAny => this.positions.Count > 0; + public void SetShared(JxlReferenceFrame[] referenceFrames) => this.referenceFrames = referenceFrames; + public void Clear() { this.positions.Clear(); - ComputePatchTree(); + this.ComputePatchTree(); + } + + public int GetReferences() + { + int result = 0; + + foreach (JxlPatchReferencePosition refPos in this.referencePositions) + { + result |= 1 << refPos.Ref; + } + + return result; } public void Decode( - JxlMemoryManager memoryManager, + Configuration configuration, JxlBitReader br, ulong xsize, ulong ysize, @@ -57,7 +63,7 @@ public void Decode( JxlAnsCode code = new(); var status = DecodeHistograms( - memoryManager, + configuration, br, PatchDictionaryContexts, code, @@ -65,10 +71,10 @@ public void Decode( JxlAnsSymbolReader decoder = JxlAnsSymbolReader.Create(code, br); - ulong ReadNum(int context) - => decoder.ReadHybridUint(context, br, contextMap); + ulong ReadNum(JxlPatchContext context) + => decoder.ReadHybridUnsignedInteger(context, br, contextMap); - ulong numRefPatch = ReadNum(kNumRefPatchContext); + ulong numRefPatch = ReadNum(JxlPatchContext.NumRefPatch); ulong numPixels = xsize * ysize; ulong maxRefPatches = 1024 + (numPixels / 4); @@ -87,7 +93,7 @@ ulong ReadNum(int context) { JxlPatchReferencePosition refPos = new() { - Ref = ReadNum(kReferenceFrameContext) + Ref = (int)ReadNum(JxlPatchContext.ReferenceFrame) }; if (refPos.Ref >= kMaxNumReferenceFrames || this.referenceFrames[(int)refPos.Ref].Frame.XSize == 0) @@ -95,17 +101,17 @@ ulong ReadNum(int context) throw new InvalidOperationException("Invalid reference frame ID"); } - if (!this.referenceFrames[refPos.Ref].IsInXYB) + if (!this.referenceFrames[refPos.Ref].ImageBundleIsInXyb) { throw new InvalidOperationException("Patches cannot use frames saved post color transforms"); } JxlImageBundle ib = this.referenceFrames[refPos.Ref].Frame; - refPos.X0 = ReadNum(kPatchReferencePositionContext); - refPos.Y0 = ReadNum(kPatchReferencePositionContext); - refPos.XSize = ReadNum(kPatchSizeContext) + 1; - refPos.YSize = ReadNum(kPatchSizeContext) + 1; + refPos.X0 = (int)ReadNum(JxlPatchContext.PatchReferencePosition); + refPos.Y0 = (int)ReadNum(JxlPatchContext.PatchReferencePosition); + refPos.XSize = (int)ReadNum(JxlPatchContext.PatchSize) + 1; + refPos.YSize = (int)ReadNum(JxlPatchContext.PatchSize) + 1; if (refPos.X0 + refPos.XSize > ib.XSize) { @@ -117,7 +123,7 @@ ulong ReadNum(int context) throw new InvalidOperationException("Invalid position specified in reference frame"); } - ulong idCount = ReadNum(kPatchCountContext); + ulong idCount = ReadNum(JxlPatchContext.PatchCount); if (idCount > maxPatches) { @@ -158,12 +164,12 @@ ulong ReadNum(int context) if (i == 0) { - pos.X = ReadNum(kPatchPositionContext); - pos.Y = ReadNum(kPatchPositionContext); + pos.X = (int)ReadNum(JxlPatchContext.PatchPosition); + pos.Y = (int)ReadNum(JxlPatchContext.PatchPosition); } else { - long deltaX = JxlPackSigned.UnpackSigned(ReadNum(kPatchOffsetContext)); + long deltaX = JxlPackSigned.UnpackSigned((uint)ReadNum(JxlPatchContext.PatchOffset)); if (deltaX < 0 && (int)(-deltaX) > this.positions[^1].X) { @@ -172,7 +178,7 @@ ulong ReadNum(int context) pos.X = (int)(this.positions[^1].X + deltaX); - long deltaY = JxlPackSigned.UnpackSigned(ReadNum(kPatchOffsetContext)); + long deltaY = JxlPackSigned.UnpackSigned((uint)ReadNum(JxlPatchContext.PatchOffset)); if (deltaY < 0 && (int)(-deltaY) > this.positions[^1].Y) { @@ -194,9 +200,9 @@ ulong ReadNum(int context) for (int j = 0; j < this.blendingsStride; j++) { - uint blendMode = (uint)ReadNum(kPatchBlendModeContext); + uint blendMode = (uint)ReadNum(JxlPatchContext.PatchBlendMode); - if (blendMode >= kNumPatchBlendModes) + if ((int)blendMode >= (int)JxlPatchContext.PatchBlendMode) { throw new InvalidOperationException($"Invalid patch blend mode: {blendMode}"); } @@ -218,7 +224,7 @@ ulong ReadNum(int context) if (UsesAlpha(info.Mode) && chooseAlpha) { - info.AlphaChannel = (uint)ReadNum(kPatchAlphaChannelContext); + info.AlphaChannel = (int)ReadNum(JxlPatchContext.PatchAlphaChannel); if (info.AlphaChannel >= (int)numExtraChannels) { @@ -232,7 +238,7 @@ ulong ReadNum(int context) if (UsesClamp(info.Mode)) { - info.Clamp = ReadNum(kPatchClampContext) != 0; + info.Clamp = ReadNum(JxlPatchContext.PatchClamp) != 0; } else { @@ -245,7 +251,7 @@ ulong ReadNum(int context) this.positions.Add(pos); } - this.positions.Add(refPos); + this.referencePositions.Add(refPos); } this.positions.TrimExcess(); @@ -257,4 +263,320 @@ ulong ReadNum(int context) this.ComputePatchTree(); } + + private static bool UsesAlpha(JxlPatchBlendMode mode) => + mode is JxlPatchBlendMode.BlendAbove or + JxlPatchBlendMode.BlendBelow or + JxlPatchBlendMode.AlphaWeightedAddAbove or + JxlPatchBlendMode.AlphaWeightedAddBelow; + + private static bool UsesClamp(JxlPatchBlendMode mode) => + UsesAlpha(mode) || mode == JxlPatchBlendMode.Multiply; + + public void ComputePatchTree() + { + this.patchTree.Clear(); + this.numPatches.Clear(); + this.sortedPatchesY0.Clear(); + this.sortedPatchesY1.Clear(); + + if (this.positions.Count == 0) + { + return; + } + + PatchInterval[] intervals = ArrayPool.Shared.Rent(this.positions.Count); + + for (int i = 0; i < this.positions.Count; i++) + { + JxlPatchPosition pos = this.positions[i]; + intervals[i].Index = i; + intervals[i].Y0 = pos.Y; + intervals[i].Y1 = pos.Y + this.referencePositions[pos.ReferencePositionIndex].YSize; + } + + void SortByY0(int start, int end) => intervals.AsSpan()[start..end].Sort(static (i0, i1) => i0.Y0.CompareTo(i1.Y0)); + + void SortByY1(int start, int end) => intervals.AsSpan()[start..end].Sort(static (i0, i1) => i0.Y1.CompareTo(i1.Y1)); + + SortByY1(0, intervals.Length); + + // We have cleared the numPatches list at the start of this + // method. We have to assign a new one. + this.numPatches = new List(intervals[^1].Y1); + + // Finding frequency of each patch within any + // interval + foreach (PatchInterval interval in intervals) + { + for (int y = interval.Y0; y < interval.Y1; y++) + { + this.numPatches[y]++; + } + } + + PatchTreeNode root = default; + root.Start = 0; + root.Count = intervals.Length; + this.patchTree.Add(root); + + int next = 0; + while (next < this.patchTree.Count) + { + PatchTreeNode node = this.patchTree[next]; + int start = node.Start; + int end = node.Start + node.Count; + + SortByY0(start, end); + int middleIndex = start + (node.Count / 2); + node.YCenter = intervals[middleIndex].Y0; + + // Divide the intervals in [start, end) into three groups: + // - those completely to the right of YCenter: [rightStart, end) + // - those overlapping YCenter: [leftEnd, rightStart) + // - those completely to the left of YCenter: [start, leftEnd) + int rightStart = middleIndex; + while (rightStart < end && intervals[rightStart].Y0 == node.YCenter) + { + rightStart++; + } + + SortByY1(start, rightStart); + int leftEnd = rightStart; + + while (leftEnd > start && intervals[leftEnd - 1].Y1 > node.YCenter) + { + leftEnd--; + } + + node.Count = rightStart - leftEnd; + node.Start = this.sortedPatchesY0.Count; + + for (int i = rightStart - 1; i >= leftEnd; i--) + { + this.sortedPatchesY1.Add(new SortedPatch(intervals[i].Y1, intervals[i].Index)); + } + + SortByY0(leftEnd, rightStart); + + for (int i = leftEnd; i < rightStart; i++) + { + this.sortedPatchesY0.Add(new SortedPatch(intervals[i].Y0, intervals[i].Index)); + } + + node.LeftChild = node.RightChild = -1; + + if (leftEnd > start) + { + PatchTreeNode left = default; + left.Start = start; + left.Count = leftEnd - left.Start; + + ref PatchTreeNode nextNode = ref CollectionsMarshal.AsSpan(this.patchTree)[next]; + nextNode.LeftChild = this.patchTree.Count; + + this.patchTree.Add(left); + } + + if (rightStart < end) + { + PatchTreeNode right = default; + right.Start = rightStart; + right.Count = end - right.Start; + + ref PatchTreeNode nextNode = ref CollectionsMarshal.AsSpan(this.patchTree)[next]; + nextNode.RightChild = this.patchTree.Count; + + this.patchTree.Add(right); + } + + next++; + } + + ArrayPool.Shared.Return(intervals); + } + + public IMemoryOwner GetPatchesForRow(Configuration configuration, int y) + { + IMemoryOwner? result = null; + + Span sortedPatchesY0 = CollectionsMarshal.AsSpan(this.sortedPatchesY0); + Span sortedPatchesY1 = CollectionsMarshal.AsSpan(this.sortedPatchesY1); + + if (y < this.numPatches.Count && this.numPatches[y] > 0) + { + result = configuration.MemoryAllocator.Allocate(this.numPatches[y]); + Span spanResult = result.GetSpan(); + + // Instead of making result a List, we simply allocate + // a fixed-length memory, get a Span to it, and track the + // length separately. + int resultPointer = 0; + + for (int treeIdx = 0; treeIdx != -1;) + { + // Index of the tree must be within the bounds of the patch tree + if (treeIdx >= this.patchTree.Count) + { + throw new InvalidOperationException("Tree index out of bounds"); + } + + // Recomputing CollectionsMarshal.AsSpan is important as we'll + // be adding items to the backing list. Items should not be + // added or removed to the original list while the Span + // returned by CollectionsMarshal.AsSpan is in use. + Span patchTreeSpan = CollectionsMarshal.AsSpan(this.patchTree); + + ref PatchTreeNode node = ref patchTreeSpan[treeIdx]; + + if (y <= node.YCenter) + { + for (int i = 0; i < node.Count; ++i) + { + ref SortedPatch p = ref sortedPatchesY0[node.Start + i]; + + if (y < p.First) + { + break; + } + + spanResult[resultPointer++] = p.Second; + } + + treeIdx = y < node.YCenter ? (int)node.LeftChild : -1; + } + else + { + for (int i = 0; i < node.Count; ++i) + { + ref SortedPatch p = ref sortedPatchesY1[node.Start + i]; + + if (y >= p.First) + { + break; + } + + spanResult[resultPointer++] = p.Second; + } + + treeIdx = (int)node.RightChild; + } + } + + // Once we collect patches, we must make sure + // that the resulting patches are sorted. + // + // Span.Sort() in theory should be faster + // and more lightweight than an array's sort. + spanResult.Sort(); + } + + return result + ?? throw new InvalidOperationException("Y offset out of bounds"); + } + + public bool AddOneRow( + Configuration configuration, + Span> inOut, + int y, + int x0, + int width, + List extraChannelInfos) + { + int numEc = extraChannelInfos.Count; // number of Extra Channels (ECs) + + if (numEc + 1 > this.blendingsStride) + { + // out of bounds extra channel + return false; + } + + using IMemoryOwner patches = this.GetPatchesForRow(configuration, y); + Span patchesSpan = patches.GetSpan(); + + Span> foreground = new Memory[3 + numEc]; + + Span patchPositions = CollectionsMarshal.AsSpan(this.positions); + Span patchRefPositions = CollectionsMarshal.AsSpan(this.referencePositions); + + for (int i = 0; i < patchesSpan.Length; i++) + { + int posIdx = patchesSpan[i]; + int blendingIdx = posIdx * this.blendingsStride; + + ref JxlPatchPosition pos = ref patchPositions[posIdx]; + ref JxlPatchReferencePosition refPos = ref patchRefPositions[pos.ReferencePositionIndex]; + + int by = pos.Y; + int bx = pos.X; + int patchWidth = refPos.XSize; + + if (y < by || y >= by + refPos.YSize) + { + return false; + } + + int iy = y - by; + int @ref = refPos.Ref; + + if (bx >= x0 + width || bx + patchWidth < x0) + { + continue; + } + + int patchX0 = Math.Max(bx, x0); + int patchX1 = Math.Min(bx + patchWidth, x0 + width); + + for (int c = 0; c < 3; c++) + { + foreground[c] = this.referenceFrames[@ref].Frame.Color!.PlaneRowMemory(c, refPos.Y0 + iy)[(refPos.X0 + x0 - bx)..]; + } + + for (int j = 0; j < 3; j++) + { + foreground[3 + j] = this.referenceFrames[@ref].Frame.GetExtraChannel(j).GetRowMemory(refPos.Y0 + iy)[(refPos.X0 + x0 - bx)..]; + } + + JxlBlending.PerformBlending( + configuration, + inOut, + foreground, + inOut, + patchX0 - x0, + patchX1 - patchX0, + this.blendings[blendingIdx], + CollectionsMarshal.AsSpan(this.blendings)[(blendingIdx + 1)..], + extraChannelInfos); + } + + return true; + } + + private struct PatchTreeNode + { + public long LeftChild; + public long RightChild; + public int YCenter; + public int Start; + public int Count; + } + + private struct SortedPatch + { + public int First; + public int Second; + + public SortedPatch(int first, int second) + { + this.First = first; + this.Second = second; + } + } + + private struct PatchInterval + { + public int Index; + public int Y0; + public int Y1; + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlReferenceFrame.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlReferenceFrame.cs new file mode 100644 index 0000000000..049783385b --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlReferenceFrame.cs @@ -0,0 +1,8 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Patch; + +internal record JxlReferenceFrame(JxlImageBundle Frame, bool ImageBundleIsInXyb = false); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs index aa6d8c6a5d..487bce74cb 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs @@ -318,6 +318,8 @@ public bool TryGetColor(out JxlImage3F? color) /// The extra channel to be added to the image bundle. public void AddExtraChannel(JxlImageF channel) => this.extraChannels.Add(channel); + public JxlImageF GetExtraChannel(int i) => this.extraChannels[i]; + /// /// Sets the extra channels. /// From 17845fca3e26d0632439a92a9cde34c2d74790fd Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sun, 11 Oct 2026 05:43:33 +0400 Subject: [PATCH 201/328] Implement multiple codec components Expanded AC strategy encoder, added ANS encoder prototype with StoreVarLenUIntX (X=8,16), added prototype of CfL encoder, added LZ77 hash chain, added modular SIMD, patch colorspace info, frame encoder basic functions, expanded heuristics --- src/ImageSharp/Common/Helpers/SimdUtils.cs | 5 +- .../Processing/Decoder/Ans/JxlAnsReader.cs | 4 +- .../Decoder/FrameDecoder/JxlFrameDecoder.cs | 98 +++ .../Decoder/JxlExternalImageDecoder.cs | 2 +- .../AcStrategy/JxlAcStrategyConfiguration.cs | 44 ++ .../AcStrategy/JxlAcStrategyEncoder.cs | 610 ++++++++++++++++++ .../Processing/Encoder/Ans/JxlAnsEncoder.cs | 65 ++ .../Processing/Encoder/Ans/JxlHistogram.cs | 8 +- .../Encoder/ChromaFromLuma/JxlCflFunction.cs | 101 +++ .../JxlChromaFromLumaEncoder.cs | 27 + .../Encoder/JxlAcStrategyEncoder.cs | 256 -------- .../Encoder/JxlAdaptiveQuantization.cs | 1 + .../Jxl/Processing/Encoder/JxlCluster.cs | 8 +- .../Encoder/JxlConvolveSeparable5.cs | 28 + .../Jxl/Processing/Encoder/JxlFrameEncoder.cs | 145 +++++ .../Jxl/Processing/Encoder/JxlHeuristics.cs | 198 +++++- .../Encoder/JxlQuantizerWeightsEncoder.cs | 1 + .../Jxl/Processing/Encoder/Lz77/JxlLz77.cs | 135 +++- .../Encoder/Lz77/JxlLz77HashChain.cs | 323 ++++++++++ .../Encoder/Lz77/JxlLz77MatchFoundCallback.cs | 6 + .../Encoder/Modular/JxlModularFrameEncoder.cs | 38 ++ .../Encoder/Modular/JxlModularSimd.cs | 310 +++++++++ .../PatchDictionary/JxlPatchColorspaceInfo.cs | 60 ++ .../Formats/Jxl/Processing/JxlSimdUtils.cs | 6 +- 24 files changed, 2208 insertions(+), 271 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyConfiguration.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyEncoder.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlAnsEncoder.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlCflFunction.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlChromaFromLumaEncoder.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAcStrategyEncoder.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlConvolveSeparable5.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFrameEncoder.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77HashChain.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77MatchFoundCallback.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Modular/JxlModularFrameEncoder.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Modular/JxlModularSimd.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/PatchDictionary/JxlPatchColorspaceInfo.cs diff --git a/src/ImageSharp/Common/Helpers/SimdUtils.cs b/src/ImageSharp/Common/Helpers/SimdUtils.cs index 9eddf3c3a0..a786deb160 100644 --- a/src/ImageSharp/Common/Helpers/SimdUtils.cs +++ b/src/ImageSharp/Common/Helpers/SimdUtils.cs @@ -175,8 +175,9 @@ internal static Vector Pow2(Vector vector) internal static Vector GatherBytes(Vector indices, Vector table) { - Span tableValues = stackalloc int[Vector.Count]; - Span resultValues = stackalloc int[Vector.Count]; + Span values = stackalloc int[Vector.Count * 2]; + Span tableValues = values[..Vector.Count]; + Span resultValues = values[Vector.Count..]; table.CopyTo(tableValues); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsReader.cs index 683ee27835..d1e5d0ba5c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsReader.cs @@ -11,7 +11,7 @@ internal static class JxlAnsReader { private const int WindowSize = 1 << 20; - private const int NumSpecialDistances = 120; + public const int NumSpecialDistances = 120; // Prefer jagged arrays over multidimensional arrays // for performance. Collection expressions help represent @@ -56,7 +56,7 @@ internal static class JxlAnsReader ]; [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int SpecialDistance(int index, int multiplier) + public static int SpecialDistance(int index, int multiplier) { Span indexDistance = SpecialDistances[index]; int dist = indexDistance[0] + (multiplier * indexDistance[1]); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/FrameDecoder/JxlFrameDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/FrameDecoder/JxlFrameDecoder.cs index 64f77f5b2e..69cea35289 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/FrameDecoder/JxlFrameDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/FrameDecoder/JxlFrameDecoder.cs @@ -41,6 +41,104 @@ internal sealed class JxlFrameDecoder /// private bool ContainsDcGroupToDecode => this.decodedDcGroups.Any(x => x == 0); + public int References + { + get + { + if (!this.isFinalized) + { + return 0; + } + + if (!this.HasEverything) + { + return 0; + } + + int result = 0; + + if (this.frameHeader.FrameType is JxlFrameType.RegularFrame or JxlFrameType.SkipProgressive) + { + bool cropped = this.frameHeader.CustomSizeOrOrigin; + if (cropped || this.frameHeader.BlendingInfo!.BlendMode != JxlBlendMode.Replace) + { + result |= 1 << (int)this.frameHeader.BlendingInfo!.Source; + } + + List extra = this.frameHeader.ExtraChannelBlendingInfo; + foreach (JxlBlendingInfo extraChannelBlendingInfo in extra) + { + if (cropped || extraChannelBlendingInfo.BlendMode != JxlBlendMode.Replace) + { + result |= 1 << (int)extraChannelBlendingInfo.Source; + } + } + } + + // Patches + if ((this.frameHeader.Flags & (ulong)JxlFrameHeaderFlags.Patches) != 0) + { + result |= this.decoderState.Shared.ImageFeatures.PatchDictionary.GetReferences(); + } + + // DC Level + if ((this.frameHeader.Flags & (ulong)JxlFrameHeaderFlags.Dc) != 0) + { + // Reads from the next DC level + int dcLevel = (int)this.frameHeader.DcLevel + 1; + + // Bits 16, 32, 64, 128 for DC level + result |= 16 << (dcLevel - 1); + } + + return result; + } + } + + public bool HasEverything + { + get + { + if (!this.decodedDcGlobal) + { + return false; + } + + if (!this.decodedAcGlobal) + { + return false; + } + + if (this.HasDcGroupToDecode) + { + return false; + } + + if (this.decodedPassesPerAcGroup.Any(nbp => nbp < this.frameHeader.Passes.NumPasses)) + { + return false; + } + + return true; + } + } + + public static int SavedAs(JxlFrameHeader header) + { + if (header.FrameType == JxlFrameType.DcFrame) + { + // Bits 16, 32, 64, 128 for DC level + return 16 << ((int)header.DcLevel - 1); + } + else if (header.CanBeReferenced) + { + // Bits 1, 2, 4 and 8 for the references + return 1 << (int)header.SaveAsReference; + } + + return 0; + } + private static int GetStride(int width, JxlPixelFormat format) { if (!JxlMath.SafeMultiply(BytesPerChannel(format.DataType), format.Channels, out int xStride)) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlExternalImageDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlExternalImageDecoder.cs index eb62495b44..efaf292a38 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlExternalImageDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlExternalImageDecoder.cs @@ -16,7 +16,7 @@ public static void FloatToU32(Span input, Span output, int num, flo Vector one = Vector.One; Vector scale = Vector.Create(mul); - for (int x = 0; x < num; x += Vector.Count) + for (int x = 0; x <= num - Vector.Count; x += Vector.Count) { Vector v = Vector.Create(input[x..]); v = Vector.ClampNative(v, Vector.Zero, one); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyConfiguration.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyConfiguration.cs new file mode 100644 index 0000000000..241d4060ad --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyConfiguration.cs @@ -0,0 +1,44 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AcStrategy; + +internal sealed class JxlAcStrategyConfiguration(JxlDequantMatrices dequant) +{ + [SuppressMessage( + "StyleCop.CSharp.MaintainabilityRules", + "SA1401:Fields should be private", + Justification = "when an InlineArray is a property its values cannot be mutated")] + public InlineArray3> SourceRows; + + public JxlDequantMatrices Dequant { get; set; } = dequant; + + public Memory QuantFieldRow { get; set; } + + public int QuantFieldStride { get; set; } + + public Memory MaskingFieldRow { get; set; } + + public int MaskingFieldStride { get; set; } + + public Memory Masking1x1FieldRow { get; set; } + + public int Masking1x1FieldStride { get; set; } + + public int Mask1x1Width { get; set; } + + public int SourceStride { get; set; } + + public float InfoLossMultiplier { get; set; } + + public float CostDelta { get; set; } + + public float ZerosMultiplier { get; set; } + + public ref float Pixel(int c, int x, int y) + => ref this.SourceRows[c].Span[(y * this.SourceStride) + x]; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyEncoder.cs new file mode 100644 index 0000000000..f987d70786 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyEncoder.cs @@ -0,0 +1,610 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AcStrategy; + +internal static class JxlAcStrategyEncoder +{ + private static readonly byte[][] TypeColors = + [ + [0xFF, 0xFF, 0x00], // DCT8 | yellow + [0xFF, 0x80, 0x80], // HORNUSS | vivid tangerine + [0xFF, 0x80, 0x80], // DCT2x2 | vivid tangerine + [0xFF, 0x80, 0x80], // DCT4x4 | vivid tangerine + [0x80, 0xFF, 0x00], // DCT16x16 | chartreuse + [0x00, 0xC0, 0x00], // DCT32x32 | waystone green + [0xC0, 0xFF, 0x00], // DCT16x8 | lime + [0xC0, 0xFF, 0x00], // DCT8x16 | lime + [0x00, 0xFF, 0x00], // DCT32x8 | green + [0x00, 0xFF, 0x00], // DCT8x32 | green + [0x00, 0xFF, 0x00], // DCT32x16 | green + [0x00, 0xFF, 0x00], // DCT16x32 | green + [0xFF, 0x80, 0x00], // DCT4x8 | orange juice + [0xFF, 0x80, 0x00], // DCT8x4 | orange juice + [0xFF, 0xFF, 0x80], // AFV0 | butter + [0xFF, 0xFF, 0x80], // AFV1 | butter + [0xFF, 0xFF, 0x80], // AFV2 | butter + [0xFF, 0xFF, 0x80], // AFV3 | butter + [0x00, 0xC0, 0xFF], // DCT64x64 | capri + [0x00, 0xFF, 0xFF], // DCT64x32 | aqua + [0x00, 0xFF, 0xFF], // DCT32x64 | aqua + [0x00, 0x40, 0xFF], // DCT128x128 | rare blue + [0x00, 0x80, 0xFF], // DCT128x64 | magic ink + [0x00, 0x80, 0xFF], // DCT64x128 | magic ink + [0x00, 0x00, 0xC0], // DCT256x256 | keese blue + [0x00, 0x00, 0xFF], // DCT256x128 | blue + [0x00, 0x00, 0xFF], // DCT128x256 | blue + [0x00, 0x00, 0x00] // invalid | black + ]; + +#pragma warning disable // Indentation warnings only + + private static readonly byte[][] Mask = + [ + [ + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + ], // DCT8 + [ + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 1, 0, 0, 1, 0, 0, // + 0, 0, 1, 0, 0, 1, 0, 0, // + 0, 0, 1, 1, 1, 1, 0, 0, // + 0, 0, 1, 0, 0, 1, 0, 0, // + 0, 0, 1, 0, 0, 1, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + ], // HORNUSS + [ + 1, 1, 1, 1, 1, 1, 1, 1, // + 1, 0, 1, 0, 1, 0, 1, 0, // + 1, 1, 1, 1, 1, 1, 1, 1, // + 1, 0, 1, 0, 1, 0, 1, 0, // + 1, 1, 1, 1, 1, 1, 1, 1, // + 1, 0, 1, 0, 1, 0, 1, 0, // + 1, 1, 1, 1, 1, 1, 1, 1, // + 1, 0, 1, 0, 1, 0, 1, 0, // + ], // 2x2 + [ + 0, 0, 0, 0, 1, 0, 0, 0, // + 0, 0, 0, 0, 1, 0, 0, 0, // + 0, 0, 0, 0, 1, 0, 0, 0, // + 0, 0, 0, 0, 1, 0, 0, 0, // + 1, 1, 1, 1, 1, 1, 1, 1, // + 0, 0, 0, 0, 1, 0, 0, 0, // + 0, 0, 0, 0, 1, 0, 0, 0, // + 0, 0, 0, 0, 1, 0, 0, 0, // + ], // 4x4 + [ ], // DCT16x16 (unused) + [ ], // DCT32x32 (unused) + [ ], // DCT16x8 (unused) + [ ], // DCT8x16 (unused) + [ ], // DCT32x8 (unused) + [ ], // DCT8x32 (unused) + [ ], // DCT32x16 (unused) + [ ], // DCT16x32 (unused) + [ + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 1, 1, 1, 1, 1, 1, 1, 1, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + ], // DCT4x8 + [ + 0, 0, 0, 0, 1, 0, 0, 0, // + 0, 0, 0, 0, 1, 0, 0, 0, // + 0, 0, 0, 0, 1, 0, 0, 0, // + 0, 0, 0, 0, 1, 0, 0, 0, // + 0, 0, 0, 0, 1, 0, 0, 0, // + 0, 0, 0, 0, 1, 0, 0, 0, // + 0, 0, 0, 0, 1, 0, 0, 0, // + 0, 0, 0, 0, 1, 0, 0, 0, // + ], // DCT8x4 + [ + 1, 1, 1, 1, 1, 0, 0, 0, // + 1, 1, 1, 1, 0, 0, 0, 0, // + 1, 1, 1, 0, 0, 0, 0, 0, // + 1, 1, 0, 0, 0, 0, 0, 0, // + 1, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + ], // AFV0 + [ + 0, 0, 0, 0, 1, 1, 1, 1, // + 0, 0, 0, 0, 0, 1, 1, 1, // + 0, 0, 0, 0, 0, 0, 1, 1, // + 0, 0, 0, 0, 0, 0, 0, 1, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + ], // AFV1 + [ + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 1, 0, 0, 0, 0, 0, 0, 0, // + 1, 1, 0, 0, 0, 0, 0, 0, // + 1, 1, 1, 0, 0, 0, 0, 0, // + 1, 1, 1, 1, 0, 0, 0, 0, // + ], // AFV2 + [ + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 1, // + 0, 0, 0, 0, 0, 0, 1, 1, // + 0, 0, 0, 0, 0, 1, 1, 1, // + ], // AFV3 + [ ] // invalid + ]; + +#pragma warning restore + + private static readonly TransformAttempt8x8[] Transforms8x8 = + [ + new(JxlAcStrategyType.DCT, 9, 0.8), + new(JxlAcStrategyType.DCT4X4, 5, 1.08), + new(JxlAcStrategyType.DCT2X2, 5, 0.95), + new(JxlAcStrategyType.DCT4X8, 4, 0.85931637428340035), + new(JxlAcStrategyType.DCT8X4, 4, 0.85931637428340035), + new(JxlAcStrategyType.IDENTITY, 5, 1.0427542510634957), + new(JxlAcStrategyType.AFV0, 4, 0.81779489591359944), + new(JxlAcStrategyType.AFV1, 4, 0.81779489591359944), + new(JxlAcStrategyType.AFV2, 4, 0.81779489591359944), + new(JxlAcStrategyType.AFV3, 4, 0.81779489591359944), + ]; + + public static ReadOnlySpan TypeColor(int rawStrategy) + { + DebugGuard.IsTrue(JxlAcStrategy.IsRawStrategyValid(rawStrategy), $"Raw strategy must be a valid strategy: {rawStrategy}"); + rawStrategy = Math.Clamp(rawStrategy, 0, JxlAcStrategy.NumberOfValidStrategies); + return TypeColors[rawStrategy]; + } + + public static ReadOnlySpan TypeMask(int rawStrategy) + { + DebugGuard.IsTrue(JxlAcStrategy.IsRawStrategyValid(rawStrategy), $"Raw strategy must be a valid strategy: {rawStrategy}"); + rawStrategy = Math.Clamp(rawStrategy, 0, JxlAcStrategy.NumberOfValidStrategies); + return Mask[rawStrategy]; + } + + public static bool MultiBlockTransformCrossesHorizontalBoundary(JxlAcStrategyImage acStrategyImage, int startX, int y, int endX) + { + if (startX >= acStrategyImage.XSize || y >= acStrategyImage.YSize) + { + return false; + } + + if (y % 8 == 0) + { + // Nothing crosses 64x64 boundaries, and the memory on the other side + // of the 64x64 block may still uninitialized. + return false; + } + + endX = Math.Min(endX, acStrategyImage.XSize); + + // The first multiblock might be before the start_x, let's adjust it + // to point to the first IsFirstBlock() == true block we find by backward + // tracing. + JxlAcStrategyRow row = acStrategyImage.GetRow(y); + int startXLimit = startX & ~7; + + while (startX != startXLimit && !row[startX].IsFirstBlock) + { + startX--; + } + + for (int x = startX; x < endX;) + { + if (row[x].IsFirstBlock) + { + x += row[x].CoveredBlocksX; + } + else + { + return true; + } + } + + return false; + } + + public static bool MultiBlockTransformCrossesVerticalBoundary(JxlAcStrategyImage acStrategyImage, int x, int startY, int endY) + { + if (x >= acStrategyImage.XSize || startY >= acStrategyImage.YSize) + { + return false; + } + + if (x % 8 == 0) + { + // Nothing crosses 64x64 boundaries, and the memory on the other side + // of the 64x64 block may still uninitialized. + return false; + } + + endY = Math.Min(endY, acStrategyImage.YSize); + + // The first multiblock might be before the start_y, let's adjust it + // to point to the first IsFirstBlock() == true block we find by backward + // tracing. + int startYLimit = startY & ~7; + while (startY != startYLimit && !acStrategyImage.GetRow(startY)[x].IsFirstBlock) + { + startY--; + } + + for (int y = startY; y < endY;) + { + JxlAcStrategyRow row = acStrategyImage.GetRow(y); + + if (row[x].IsFirstBlock) + { + y += row[x].CoveredBlocksY; + } + else + { + return true; + } + } + + return false; + } + + public static bool EstimateEntropy( + JxlAcStrategy acs, + float entropyMultiplier, + int x, + int y, + JxlAcStrategyConfiguration config, + Span cmapFactors, + Span block, + Span fullScratchSpace, + Span quantized, + out float entropy) + { + entropy = 0; + Span memory = fullScratchSpace; + Span scratchSpace = fullScratchSpace[JxlAcStrategy.MaximumCoefficientArea..]; + int size = (1 << acs.Log2CoveredBlocks) * JxlFrameDimensions.DctBlockSize; + + for (int c = 0; c < 3; c++) + { + Span blockC = block[(size * c)..]; + JxlTransformsEncoder.TransformFromPixels( + acs.Strategy, + config.PixelSpan(c, x, y), + config.SourceStride, + blockC, + scratchSpace); + } + + int numBlocks = acs.CoveredBlocksX * acs.CoveredBlocksY; + float quantNorm16 = 0; + + if (numBlocks == 1) + { + // There's only one 8x8. We don't need aggregation of values. + quantNorm16 = config.Quant(x / 8, y / 8); + } + else if (numBlocks == 2) + { + if (acs.CoveredBlocksY == 2) + { + quantNorm16 = Math.Max(config.Quant(x / 8, y / 8), config.Quant(x / 8, (y / 8) + 1)); + } + else + { + quantNorm16 = Math.Max(config.Quant(x / 8, y / 8), config.Quant((x / 8) + 1, y / 8)); + } + } + else + { + // Load quantizer field value, calculate empirical heuristic on masking field + // for weighting the information loss. Information loss manifests + // itself as ringing, and masking could hide it. + for (int iy = 0; iy < acs.CoveredBlocksY; iy++) + { + int yPos = (y / 8) + iy; + + for (int ix = 0; ix < acs.CoveredBlocksX; ix++) + { + float qval = config.Quant((x / 8) + ix, yPos); + qval *= qval; + qval *= qval; + qval *= qval; + quantNorm16 += qval * qval; + } + } + + quantNorm16 /= numBlocks; + quantNorm16 = MathF.Pow(quantNorm16, 1.0f / 16.0f); + } + + // Computation of entropy. + // We have to process 8 coeffs at once. Not sure if we can use + // Vector here... + Vector256 quant = Vector256.Create(quantNorm16); + Vector256 loss = Vector256.Zero; + for (int c = 0; c < 3; c++) + { + Span inverseMatrix = config.Dequant.GetInverseMatrix(acs.Strategy, c); + Span matrix = config.Dequant.GetMatrix(acs.Strategy, c); + Vector256 cmapFactor = Vector256.Create(cmapFactors[c]); + + Vector256 entropyV = Vector256.Zero; + Vector256 numZerosV = Vector256.Zero; + + for (int i = 0; i < (numBlocks * JxlFrameDimensions.DctBlockSize) - Vector.Count; i += Vector.Count) + { + Vector256 input = Vector256.Create(block[((c * size) + i)..]); + Vector256 inY = Vector256.Create(block[(size + i)..]) * cmapFactor; + Vector256 im = Vector256.Create(inverseMatrix[i..]); + Vector256 value = (input - inY) * (im * quant); + Vector256 rvalue = Vector256.Round(value); + Vector256 diff = value - rvalue; + Vector256 m = Vector256.Create(matrix[i..]); + + (m * diff).CopyTo(memory[i..]); + + Vector256 q = Vector256.Abs(rvalue); + Vector256 qIsZero = Vector256.Equals(q, Vector256.Zero); + + entropyV = Vector256.Sqrt(q) + entropyV; + numZerosV += Vector256.ConditionalSelect(qIsZero, Vector256.Zero, Vector256.One); + } + + { + Span maskULut = [12.0f, 0.0f, 4.0f]; + Vector256 maskUOffset = Vector256.Create(maskULut[c]); + Vector256 lossC = Vector256.Zero; + + JxlTransformsDecoder.TransformToPixels( + acs.Strategy, + memory, + block, + acs.CoveredBlocksX * 8, + scratchSpace); + + for (int iy = 0; iy < acs.CoveredBlocksY; iy++) + { + for (int ix = 0; ix < acs.CoveredBlocksX; ix++) + { + for (int dy = 0; dy < JxlFrameDimensions.BlockDimensions; ++dy) + { + // This loop here is expected to be vector size-aligned. + // No need for <= N - VectorSize. + for (int dx = 0; dx < JxlFrameDimensions.BlockDimensions; dx += Vector256.Count) + { + Vector256 input = Vector256.Create(block[ + ((((iy * JxlFrameDimensions.BlockDimensions) + dy) * (acs.CoveredBlocksX * JxlFrameDimensions.BlockDimensions)) + (ix * JxlFrameDimensions.BlockDimensions) + dx)..]); + + if (x + (ix * 8) + dx + Vector256.Count <= config.Mask1x1Width) + { + Vector256 masku = + Vector256.LoadUnsafe(config.MaskingReference1x1( + x + (ix * 8) + dx, + y + (iy * 8) + dy)) * maskUOffset; + input = masku * input; + input *= input; + input *= input; + input *= input; + lossC += input; + } + } + } + } + } + + Span channelMultipliers = [ + MathF.Pow(8.2f, 8.0f), + MathF.Pow(1.0f, 8.0f), + MathF.Pow(1.03f, 8.0f) + ]; + + lossC *= Vector256.Create(channelMultipliers[c]); + loss += lossC; + } + + entropy += config.CostDelta * Vector256.Sum(entropyV); + int numNZeroes = (int)Vector256.Sum(numZerosV); + int nbits = JxlMath.CeilLog2Nonzero(numNZeroes + 1) + 1; + entropy += config.ZerosMultiplier * (JxlMath.CeilLog2Nonzero(nbits + 17) + nbits); + + if (c == 0 && numBlocks >= 2) + { + // It is X channel (red-green) and we often see ringing + // in the large blocks. Let's punish that more here. + float w = 1.0f + MathF.Max(3.0f, numBlocks / 8.0f); + entropy *= w; + loss *= Vector256.Create(w); + } + } + + float lossScalar = MathF.Pow(Vector256.Sum(loss) / (numBlocks * JxlFrameDimensions.BlockDimensions), 1.0f / 8.0f) * (numBlocks * JxlFrameDimensions.DctBlockSize) / quantNorm16; + + entropy *= entropyMultiplier; + entropy += config.InfoLossMultiplier * lossScalar; + + return true; + } + + public static JxlAcStrategyType AcsSquare(int blocks) + { + if (blocks == 2) + { + return JxlAcStrategyType.DCT16X16; + } + else if (blocks == 4) + { + return JxlAcStrategyType.DCT32X32; + } + else + { + return JxlAcStrategyType.DCT64X64; + } + } + + public static JxlAcStrategyType AcsVerticalSplit(int blocks) + { + if (blocks == 2) + { + return JxlAcStrategyType.DCT16X8; + } + else if (blocks == 4) + { + return JxlAcStrategyType.DCT32X16; + } + else + { + return JxlAcStrategyType.DCT64X32; + } + } + + public static JxlAcStrategyType AcsHorizontalSplit(int blocks) + { + if (blocks == 2) + { + return JxlAcStrategyType.DCT8X16; + } + else if (blocks == 4) + { + return JxlAcStrategyType.DCT16X32; + } + else + { + return JxlAcStrategyType.DCT32X64; + } + } + + public static void SetEntropyForTransform(int cx, int cy, JxlAcStrategyType acsRaw, float entropy, Span entropyEstimate) + { + JxlAcStrategy acs = new(acsRaw); + + for (int dy = 0; dy < acs.CoveredBlocksY; dy++) + { + for (int dx = 0; dx < acs.CoveredBlocksX; dx++) + { + entropyEstimate[((cy + dy) * 8) + cx + dx] = 0.0f; + } + } + + entropyEstimate[(cy * 8) + cx] = entropy; + } + + public static void FindBest8x8Transform( + int x, + int y, + int encodingSpeedTier, + float butteraugliTarget, + JxlAcStrategyConfiguration config, + Span contextMapFactors, + Span block, + Span scratchSpace, + Span quantized, + out float entropyOut, + out JxlAcStrategyType bestTx) + { + double best = 1e30; + bestTx = Transforms8x8[0].Type; + + foreach (TransformAttempt8x8 tx in Transforms8x8) + { + if (tx.EncodingSpeedTierMaxLimit < encodingSpeedTier) + { + // This transform is too slow. User settings + // indicate higher speeds. + continue; + } + + JxlAcStrategy acs = new(tx.Type); + float entropyMultiplier = (float)(tx.EntropyMultiplier / Transforms8x8[0].EntropyMultiplier); + + if (tx.Type is JxlAcStrategyType.DCT2X2 or JxlAcStrategyType.IDENTITY && butteraugliTarget < 5.0) + { + const float favor2X2AtHighQuality = 0.4f; + float weight = MathF.Pow((5.0f - butteraugliTarget) / 5.0f, 2.0f); + entropyMultiplier -= favor2X2AtHighQuality * weight; + } + + if ((tx.Type != JxlAcStrategyType.DCT && tx.Type != JxlAcStrategyType.DCT2X2 && tx.Type != JxlAcStrategyType.IDENTITY) && butteraugliTarget > 4.0) + { + const float kAvoidEntropyOfTransforms = 0.5f; + float mul = 1.0f; + + if (butteraugliTarget < 12.0) + { + mul *= (float)((12.0f - 4.0) / (butteraugliTarget - 4.0f)); + } + + entropyMultiplier += kAvoidEntropyOfTransforms * mul; + } + + if (!EstimateEntropy( + acs, + entropyMultiplier, + x, + y, + config, + contextMapFactors, + block, + scratchSpace, + quantized, + out float entropy)) + { + throw new InvalidOperationException("Failed to estimate entropy"); + } + + if (entropy < best) + { + bestTx = tx.Type; + best = entropy; + } + } + + entropyOut = (float)best; + } + + /// + /// For , represents a small + /// model which the encoder uses to try all available 8x8 + /// transforms and see which one encodes best. + /// + /// + /// The transform type for this model. + /// + /// + /// If the encoding speed tier is higher (indicating + /// higher speeds but worse compression) than this + /// value, the attempt shall be ignored as it'll be + /// too slow. + /// + /// + /// Multiplication for the entropy. + /// + private readonly record struct TransformAttempt8x8( + JxlAcStrategyType Type, + int EncodingSpeedTierMaxLimit, + double EntropyMultiplier); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlAnsEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlAnsEncoder.cs new file mode 100644 index 0000000000..0a50606de3 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlAnsEncoder.cs @@ -0,0 +1,65 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; + +internal static class JxlAnsEncoder +{ + public static void StoreVarLenUInt8(int n, JxlBitWriter writer) + { + DebugGuard.MustBeLessThanOrEqualTo(n, 255, nameof(n)); + + // vl_uint8(n) { + // /* n limited to 0..255 */ + // is_nonzero_flag == B(1) + // if (is_nonzero_flag == 0) { + // /* Value is zero. */ + // return 0; + // } else { + // vl_suffix_length = B(3) + // vl_suffix = B(vl_suffix_length) + // return vl_suffix_length + // } + // } + if (n == 0) + { + writer.Write(1, 0); + } + else + { + writer.Write(1, 1); + int nbits = JxlMath.FloorLog2Nonzero(n); + writer.Write(3, nbits); + writer.Write(nbits, n - (1 << nbits)); + } + } + + public static void StoreVarLenUInt16(int n, JxlBitWriter writer) + { + DebugGuard.MustBeLessThanOrEqualTo(n, 65535, nameof(n)); + + // vl_uint8(n) { + // /* n limited to 0..65535 */ + // is_nonzero_flag == B(1) + // if (is_nonzero_flag == 0) { + // /* Value is zero. */ + // return 0; + // } else { + // vl_suffix_length = B(4) + // vl_suffix = B(vl_suffix_length) + // return vl_suffix_length + // } + // } + if (n == 0) + { + writer.Write(1, 0); + } + else + { + writer.Write(1, 1); + int nbits = JxlMath.FloorLog2Nonzero(n); + writer.Write(4, nbits); + writer.Write(nbits, n - (1 << nbits)); + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHistogram.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHistogram.cs index 333d8f119e..b4a7b8f6a9 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHistogram.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHistogram.cs @@ -6,11 +6,15 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; /// /// ANS histogram. /// -internal sealed class JxlHistogram(int length) +internal sealed class JxlHistogram { public const int Rounding = 8; - public List Counts { get; set; } = new(length); + public JxlHistogram(int length) => this.Counts = new(length); + + public JxlHistogram() => this.Counts = []; + + public List Counts { get; set; } public int TotalCount { get; set; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlCflFunction.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlCflFunction.cs new file mode 100644 index 0000000000..15d0f97ce6 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlCflFunction.cs @@ -0,0 +1,101 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.ChromaFromLuma; + +internal readonly ref struct JxlCflFunction +{ + private const float Coefficient = 1.0f / 3f; + private const float Threshold = 100f; + private const float InverseColorFactor = 1.0f / JxlChromaFromLuma.DefaultColorFactor; + + private readonly Span valuesM; + private readonly Span valuesS; + private readonly int num; + private readonly float @base; + private readonly float distanceMultiplier; + + public JxlCflFunction( + Span valuesM, + Span valuesS, + int num, + float @base, + float distanceMultiplier) + { + this.valuesM = valuesM; + this.valuesS = valuesS; + this.num = num; + this.@base = @base; + this.distanceMultiplier = distanceMultiplier; + } + + // Returns f'(x), where f is 1/3 * sum ((|color residual| + 1)^2-1) + + // distance_mul * x^2 * num. + public readonly float Compute(float x, float eps, ref float fpeps, ref float fmeps) + { + float firstDerivative = 2 * this.distanceMultiplier * this.num * x; + float firstDerivativePeps = 2 * this.distanceMultiplier * this.num * (x + eps); + float firstDerivativeMeps = 2 * this.distanceMultiplier * this.num * (x - eps); + + Vector inverseColorFactor = Vector.Create(InverseColorFactor); + Vector thres = Vector.Create(Threshold); + Vector coeffx2 = Vector.Create(Coefficient * 2.0f); + Vector one = Vector.One; + Vector zero = Vector.Zero; + Vector baseV = Vector.Create(this.@base); + Vector xV = Vector.Create(x); + Vector xpeV = Vector.Create(x + eps); + Vector xmeV = Vector.Create(x - eps); + Vector fdV = Vector.Zero; + Vector fdpeV = Vector.Zero; + Vector fdmeV = Vector.Zero; + + for (int i = 0; i <= this.num - Vector.Count; i += Vector.Count) + { + // color residual = ax + b + Vector a = inverseColorFactor * Vector.Create(this.valuesM[i..]); + Vector b = (baseV * Vector.Create(this.valuesM[i..])) - Vector.Create(this.valuesS[i..]); + + Vector v = Vector.FusedMultiplyAdd(a, xV, b); + Vector vpe = Vector.FusedMultiplyAdd(a, xpeV, b); + Vector vme = Vector.FusedMultiplyAdd(a, xmeV, b); + + // Abs of three above vectors + Vector av = Vector.Abs(v); + Vector avpe = Vector.Abs(vpe); + Vector avme = Vector.Abs(vme); + + Vector acoeffx2 = coeffx2 * a; + Vector d = acoeffx2 * (av + one); + Vector dpe = acoeffx2 * (avpe + one); + Vector dme = acoeffx2 * (avme + one); + + // if (V < 0) { D = -D } else { D } + d = Vector.ConditionalSelect( + Vector.LessThan(v, zero), + zero - d, + d); + dpe = Vector.ConditionalSelect( + Vector.LessThan(vpe, zero), + zero - dpe, + dpe); + dme = Vector.ConditionalSelect( + Vector.LessThan(vme, zero), + zero - dme, + dme); + + Vector above = Vector.GreaterThanOrEqual(av, thres); + + // F += above ? 0 : D + fdV += Vector.ConditionalSelect(above, zero, d); + fdpeV += Vector.ConditionalSelect(above, zero, dpe); + fdmeV += Vector.ConditionalSelect(above, zero, dme); + } + + fpeps = firstDerivativePeps + Vector.Sum(fdpeV); + fmeps = firstDerivativeMeps + Vector.Sum(fdmeV); + return firstDerivative + Vector.Sum(fdV); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlChromaFromLumaEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlChromaFromLumaEncoder.cs new file mode 100644 index 0000000000..bd61563e05 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlChromaFromLumaEncoder.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.ChromaFromLuma; + +internal static class JxlChromaFromLumaEncoder +{ + public static JxlImageF InitializeDcStorage(Configuration configuration, int numBlocks) + { + JxlImageF dcValues = new(configuration, JxlMath.RoundUpTo(numBlocks, Vector.Count), 4); + + for (int y = 0; y < 4; y++) + { + Span row = dcValues.GetRow(y); + + for (int x = dcValues.XSize - Vector.Count; x < dcValues.XSize; x++) + { + row[x] = 0; + } + } + + return dcValues; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAcStrategyEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAcStrategyEncoder.cs deleted file mode 100644 index 0cdce9b6a8..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAcStrategyEncoder.cs +++ /dev/null @@ -1,256 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; - -internal static class JxlAcStrategyEncoder -{ - private static readonly byte[][] TypeColors = - [ - [0xFF, 0xFF, 0x00], // DCT8 | yellow - [0xFF, 0x80, 0x80], // HORNUSS | vivid tangerine - [0xFF, 0x80, 0x80], // DCT2x2 | vivid tangerine - [0xFF, 0x80, 0x80], // DCT4x4 | vivid tangerine - [0x80, 0xFF, 0x00], // DCT16x16 | chartreuse - [0x00, 0xC0, 0x00], // DCT32x32 | waystone green - [0xC0, 0xFF, 0x00], // DCT16x8 | lime - [0xC0, 0xFF, 0x00], // DCT8x16 | lime - [0x00, 0xFF, 0x00], // DCT32x8 | green - [0x00, 0xFF, 0x00], // DCT8x32 | green - [0x00, 0xFF, 0x00], // DCT32x16 | green - [0x00, 0xFF, 0x00], // DCT16x32 | green - [0xFF, 0x80, 0x00], // DCT4x8 | orange juice - [0xFF, 0x80, 0x00], // DCT8x4 | orange juice - [0xFF, 0xFF, 0x80], // AFV0 | butter - [0xFF, 0xFF, 0x80], // AFV1 | butter - [0xFF, 0xFF, 0x80], // AFV2 | butter - [0xFF, 0xFF, 0x80], // AFV3 | butter - [0x00, 0xC0, 0xFF], // DCT64x64 | capri - [0x00, 0xFF, 0xFF], // DCT64x32 | aqua - [0x00, 0xFF, 0xFF], // DCT32x64 | aqua - [0x00, 0x40, 0xFF], // DCT128x128 | rare blue - [0x00, 0x80, 0xFF], // DCT128x64 | magic ink - [0x00, 0x80, 0xFF], // DCT64x128 | magic ink - [0x00, 0x00, 0xC0], // DCT256x256 | keese blue - [0x00, 0x00, 0xFF], // DCT256x128 | blue - [0x00, 0x00, 0xFF], // DCT128x256 | blue - [0x00, 0x00, 0x00] // invalid | black - ]; - -#pragma warning disable // Indentation warnings only - - private static readonly byte[][] Mask = - [ - [ - 0, 0, 0, 0, 0, 0, 0, 0, // - 0, 0, 0, 0, 0, 0, 0, 0, // - 0, 0, 0, 0, 0, 0, 0, 0, // - 0, 0, 0, 0, 0, 0, 0, 0, // - 0, 0, 0, 0, 0, 0, 0, 0, // - 0, 0, 0, 0, 0, 0, 0, 0, // - 0, 0, 0, 0, 0, 0, 0, 0, // - 0, 0, 0, 0, 0, 0, 0, 0, // - ], // DCT8 - [ - 0, 0, 0, 0, 0, 0, 0, 0, // - 0, 0, 0, 0, 0, 0, 0, 0, // - 0, 0, 1, 0, 0, 1, 0, 0, // - 0, 0, 1, 0, 0, 1, 0, 0, // - 0, 0, 1, 1, 1, 1, 0, 0, // - 0, 0, 1, 0, 0, 1, 0, 0, // - 0, 0, 1, 0, 0, 1, 0, 0, // - 0, 0, 0, 0, 0, 0, 0, 0, // - ], // HORNUSS - [ - 1, 1, 1, 1, 1, 1, 1, 1, // - 1, 0, 1, 0, 1, 0, 1, 0, // - 1, 1, 1, 1, 1, 1, 1, 1, // - 1, 0, 1, 0, 1, 0, 1, 0, // - 1, 1, 1, 1, 1, 1, 1, 1, // - 1, 0, 1, 0, 1, 0, 1, 0, // - 1, 1, 1, 1, 1, 1, 1, 1, // - 1, 0, 1, 0, 1, 0, 1, 0, // - ], // 2x2 - [ - 0, 0, 0, 0, 1, 0, 0, 0, // - 0, 0, 0, 0, 1, 0, 0, 0, // - 0, 0, 0, 0, 1, 0, 0, 0, // - 0, 0, 0, 0, 1, 0, 0, 0, // - 1, 1, 1, 1, 1, 1, 1, 1, // - 0, 0, 0, 0, 1, 0, 0, 0, // - 0, 0, 0, 0, 1, 0, 0, 0, // - 0, 0, 0, 0, 1, 0, 0, 0, // - ], // 4x4 - [ ], // DCT16x16 (unused) - [ ], // DCT32x32 (unused) - [ ], // DCT16x8 (unused) - [ ], // DCT8x16 (unused) - [ ], // DCT32x8 (unused) - [ ], // DCT8x32 (unused) - [ ], // DCT32x16 (unused) - [ ], // DCT16x32 (unused) - [ - 0, 0, 0, 0, 0, 0, 0, 0, // - 0, 0, 0, 0, 0, 0, 0, 0, // - 0, 0, 0, 0, 0, 0, 0, 0, // - 0, 0, 0, 0, 0, 0, 0, 0, // - 1, 1, 1, 1, 1, 1, 1, 1, // - 0, 0, 0, 0, 0, 0, 0, 0, // - 0, 0, 0, 0, 0, 0, 0, 0, // - 0, 0, 0, 0, 0, 0, 0, 0, // - ], // DCT4x8 - [ - 0, 0, 0, 0, 1, 0, 0, 0, // - 0, 0, 0, 0, 1, 0, 0, 0, // - 0, 0, 0, 0, 1, 0, 0, 0, // - 0, 0, 0, 0, 1, 0, 0, 0, // - 0, 0, 0, 0, 1, 0, 0, 0, // - 0, 0, 0, 0, 1, 0, 0, 0, // - 0, 0, 0, 0, 1, 0, 0, 0, // - 0, 0, 0, 0, 1, 0, 0, 0, // - ], // DCT8x4 - [ - 1, 1, 1, 1, 1, 0, 0, 0, // - 1, 1, 1, 1, 0, 0, 0, 0, // - 1, 1, 1, 0, 0, 0, 0, 0, // - 1, 1, 0, 0, 0, 0, 0, 0, // - 1, 0, 0, 0, 0, 0, 0, 0, // - 0, 0, 0, 0, 0, 0, 0, 0, // - 0, 0, 0, 0, 0, 0, 0, 0, // - 0, 0, 0, 0, 0, 0, 0, 0, // - ], // AFV0 - [ - 0, 0, 0, 0, 1, 1, 1, 1, // - 0, 0, 0, 0, 0, 1, 1, 1, // - 0, 0, 0, 0, 0, 0, 1, 1, // - 0, 0, 0, 0, 0, 0, 0, 1, // - 0, 0, 0, 0, 0, 0, 0, 0, // - 0, 0, 0, 0, 0, 0, 0, 0, // - 0, 0, 0, 0, 0, 0, 0, 0, // - 0, 0, 0, 0, 0, 0, 0, 0, // - ], // AFV1 - [ - 0, 0, 0, 0, 0, 0, 0, 0, // - 0, 0, 0, 0, 0, 0, 0, 0, // - 0, 0, 0, 0, 0, 0, 0, 0, // - 0, 0, 0, 0, 0, 0, 0, 0, // - 1, 0, 0, 0, 0, 0, 0, 0, // - 1, 1, 0, 0, 0, 0, 0, 0, // - 1, 1, 1, 0, 0, 0, 0, 0, // - 1, 1, 1, 1, 0, 0, 0, 0, // - ], // AFV2 - [ - 0, 0, 0, 0, 0, 0, 0, 0, // - 0, 0, 0, 0, 0, 0, 0, 0, // - 0, 0, 0, 0, 0, 0, 0, 0, // - 0, 0, 0, 0, 0, 0, 0, 0, // - 0, 0, 0, 0, 0, 0, 0, 0, // - 0, 0, 0, 0, 0, 0, 0, 1, // - 0, 0, 0, 0, 0, 0, 1, 1, // - 0, 0, 0, 0, 0, 1, 1, 1, // - ], // AFV3 - [ ] // invalid - ]; - -#pragma warning restore - - public static ReadOnlySpan TypeColor(int rawStrategy) - { - DebugGuard.IsTrue(JxlAcStrategy.IsRawStrategyValid(rawStrategy), $"Raw strategy must be a valid strategy: {rawStrategy}"); - rawStrategy = Math.Clamp(rawStrategy, 0, JxlAcStrategy.NumberOfValidStrategies); - return TypeColors[rawStrategy]; - } - - public static ReadOnlySpan TypeMask(int rawStrategy) - { - DebugGuard.IsTrue(JxlAcStrategy.IsRawStrategyValid(rawStrategy), $"Raw strategy must be a valid strategy: {rawStrategy}"); - rawStrategy = Math.Clamp(rawStrategy, 0, JxlAcStrategy.NumberOfValidStrategies); - return Mask[rawStrategy]; - } - - public static bool MultiBlockTransformCrossesHorizontalBoundary(JxlAcStrategyImage acStrategyImage, int startX, int y, int endX) - { - if (startX >= acStrategyImage.XSize || y >= acStrategyImage.YSize) - { - return false; - } - - if (y % 8 == 0) - { - // Nothing crosses 64x64 boundaries, and the memory on the other side - // of the 64x64 block may still uninitialized. - return false; - } - - endX = Math.Min(endX, acStrategyImage.XSize); - - // The first multiblock might be before the start_x, let's adjust it - // to point to the first IsFirstBlock() == true block we find by backward - // tracing. - JxlAcStrategyRow row = acStrategyImage.GetRow(y); - int startXLimit = startX & ~7; - - while (startX != startXLimit && !row[startX].IsFirstBlock) - { - startX--; - } - - for (int x = startX; x < endX;) - { - if (row[x].IsFirstBlock) - { - x += row[x].CoveredBlocksX; - } - else - { - return true; - } - } - - return false; - } - - public static bool MultiBlockTransformCrossesVerticalBoundary(JxlAcStrategyImage acStrategyImage, int x, int startY, int endY) - { - if (x >= acStrategyImage.XSize || startY >= acStrategyImage.YSize) - { - return false; - } - - if (x % 8 == 0) - { - // Nothing crosses 64x64 boundaries, and the memory on the other side - // of the 64x64 block may still uninitialized. - return false; - } - - endY = Math.Min(endY, acStrategyImage.YSize); - - // The first multiblock might be before the start_y, let's adjust it - // to point to the first IsFirstBlock() == true block we find by backward - // tracing. - int startYLimit = startY & ~7; - while (startY != startYLimit && !acStrategyImage.GetRow(startY)[x].IsFirstBlock) - { - startY--; - } - - for (int y = startY; y < endY;) - { - JxlAcStrategyRow row = acStrategyImage.GetRow(y); - - if (row[x].IsFirstBlock) - { - y += row[x].CoveredBlocksY; - } - else - { - return true; - } - } - - return false; - } -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs index 8af29b2665..8302b826da 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs @@ -15,6 +15,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Group; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Comparator; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Modular; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlCluster.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlCluster.cs index 3fc4a41a7e..52145e4ba0 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlCluster.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlCluster.cs @@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; /// -/// Cluster encoder +/// Histogram clustering & utilities. /// internal static class JxlCluster { @@ -31,7 +31,7 @@ public static void HistogramCondition(JxlHistogram histogram) Span countsSpan = CollectionsMarshal.AsSpan(histogram.Counts); ref int countsRef = ref MemoryMarshal.GetReference(countsSpan); - for (int i = 0; i < countsSpan.Length; i += Vector.Count) + for (int i = 0; i <= countsSpan.Length - Vector.Count; i += Vector.Count) { Vector counts = Vector.LoadUnsafe(ref Unsafe.Add(ref countsRef, i)); bool isNonZero = !Vector.AllWhereAllBitsSet(Vector.Equals(counts, Vector.Zero)); @@ -66,7 +66,7 @@ public static void HistogramEntropy(JxlHistogram a) Span countsSpan = CollectionsMarshal.AsSpan(a.Counts); ref int countsRef = ref MemoryMarshal.GetReference(countsSpan); - for (int i = 0; i < countsSpan.Length; i += Vector.Count) + for (int i = 0; i <= countsSpan.Length - Vector.Count; i += Vector.Count) { Vector counts = Vector.LoadUnsafe(ref Unsafe.Add(ref countsRef, i)); entropyLanes += Entropy( @@ -95,7 +95,7 @@ public static float HistogramDistance(JxlHistogram a, JxlHistogram b) Span bCountsSpan = CollectionsMarshal.AsSpan(b.Counts); ref int bCountsRef = ref MemoryMarshal.GetReference(bCountsSpan); - for (int i = 0; i < Math.Max(aCountsSpan.Length, bCountsSpan.Length); i += Vector.Count) + for (int i = 0; i <= Math.Max(aCountsSpan.Length, bCountsSpan.Length) - Vector.Count; i += Vector.Count) { Vector aCounts = aCountsSpan.Length > i ? Vector.LoadUnsafe(ref Unsafe.Add(ref aCountsRef, i)) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlConvolveSeparable5.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlConvolveSeparable5.cs new file mode 100644 index 0000000000..82e2912932 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlConvolveSeparable5.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; + +internal static class JxlConvolveSeparable5 +{ + public static Vector HorizontalConvolve(ref float pos, Vector wh0, Vector wh1, Vector wh2) + { + Vector c = Vector.LoadUnsafe(ref pos); + Vector mul0 = c * wh0; + + Vector l1 = Vector.LoadUnsafe(ref Unsafe.Subtract(ref pos, 1)); + Vector r1 = Vector.LoadUnsafe(ref Unsafe.Add(ref pos, 1)); + Vector l2 = Vector.LoadUnsafe(ref Unsafe.Subtract(ref pos, 2)); + Vector r2 = Vector.LoadUnsafe(ref Unsafe.Add(ref pos, 2)); + + Vector sum1 = l1 + r1; + Vector mul1 = Vector.FusedMultiplyAdd(sum1, wh1, mul0); + Vector sum2 = l2 + r2; + Vector mul2 = Vector.FusedMultiplyAdd(sum2, wh2, mul1); + + return mul2; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFrameEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFrameEncoder.cs new file mode 100644 index 0000000000..3a9979e684 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFrameEncoder.cs @@ -0,0 +1,145 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; + +internal class JxlFrameEncoder +{ + public static uint GetGroupSizeShift(int width, int height, JxlCompressParameters cparams) + { + if (!cparams.ModularMode) + { + return 1u; + } + + if (cparams.ModularGroupSizeShift >= 0) + { + return (uint)cparams.ModularGroupSizeShift; + } + + if (cparams.DecodingSpeedTier >= 2 || + (cparams.DecodingSpeedTier >= 1 && cparams.Responsive == 1 && cparams.IsLossless)) + { + return 0; + } + + // No point using groups when only one group is full and the others are + // less than half full: multithreading will not really help much, while + // compression does suffer; but no reason to have group larger than image. + if (width <= 128 && height <= 128) + { + return 0; + } + + if (width <= 256 && height <= 256) + { + return 1; + } + + if (width <= 400 && height <= 400) + { + return 2; + } + + return 1; + } + + public static void SimplifyInvisible(JxlImage3F image, JxlImageF alpha, bool lossless) + { + // Invisible (alpha = 0) pixels tend to be a mess in optimized PNGs. + // Since they have no visual impact, we can replace them with + // something that compresses better and reduces artifacts near the edges. This + // does some kind of smoothing that seems to work. + // Replace invisible pixels with a weighted average of the pixel to the left, + // the pixel to the topright, and non-invisible neighbours. + // Produces downward-blurry smears, with in the upwards direction only a 1px + // edge duplication but not more. It would probably be better to smear in all + // directions. That requires an alpha-weighed convolution with a large enough + // kernel though, which might be too much. + for (int c = 0; c < 3; c++) + { + for (int y = 0; y < image.YSize; y++) + { + Span row = image.PlaneRow(c, y); + Span prow = y > 0 ? image.PlaneRow(c, y - 1) : []; + Span nrow = y + 1 < image.YSize ? image.PlaneRow(c, y + 1) : []; + Span a = alpha.GetRow(y); + Span pa = y > 0 ? alpha.GetRow(y - 1) : []; + Span na = y + 1 < image.YSize ? alpha.GetRow(y + 1) : []; + + for (int x = 0; x < image.XSize; ++x) + { + if (a[x] == 0) + { + if (lossless) + { + row[x] = 0; + continue; + } + + float d = 0.0f; + row[x] = 0; + + if (x > 0) + { + row[x] += row[x - 1]; + d++; + + if (a[x - 1] > 0.0f) + { + row[x] += row[x - 1]; + d++; + } + } + + if (x + 1 < image.XSize) + { + if (y > 0) + { + row[x] += prow[x + 1]; + d++; + } + + if (a[x + 1] > 0.0f) + { + row[x] += 2.0f * row[x + 1]; + d += 2.0f; + } + + if (y > 0 && pa[x + 1] > 0.0f) + { + row[x] += 2.0f * prow[x + 1]; + d += 2.0f; + } + + if (y + 1 < image.YSize && na[x + 1] > 0.0f) + { + row[x] += 2.0f * nrow[x + 1]; + d += 2.0f; + } + } + + if (y > 0 && pa[x] > 0.0f) + { + row[x] += 2.0f * prow[x]; + d += 2.0f; + } + + if (y + 1 < image.YSize && na[x] > 0.0f) + { + row[x] += 2.0f * nrow[x]; + d += 2.0f; + } + + if (d > 1.0f) + { + row[x] /= d; + } + } + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlHeuristics.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlHeuristics.cs index bb32be6531..46ec51d4e0 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlHeuristics.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlHeuristics.cs @@ -2,8 +2,9 @@ // Licensed under the Six Labors Split License. using System.Buffers; +using System.Numerics; using System.Numerics.Tensors; -using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; @@ -12,6 +13,11 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; internal static class JxlHeuristics { + /// + /// Default upsampling kernels used by the decoder upsampler. + /// + private const int Size = 5; + private static ReadOnlySpan SimpleContextMap => [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, @@ -172,6 +178,196 @@ public static void FindBestBlockEntropyModel(JxlCompressParameters cparameters, } } + public static void StoreMin2(float v, ref float min1, ref float min2) + { + if (v < min2) + { + if (v < min1) + { + min2 = min1; + min1 = v; + } + else + { + min2 = v; + } + } + } + + public static void CreateMask(JxlImageF image, JxlImageF mask) + { + for (int y = 0; y < image.YSize; y++) + { + Span rowN = y > 0 ? image.GetRow(y - 1) : image.GetRow(y); + Span rowIn = image.GetRow(y); + Span rowS = y + 1 < image.YSize ? image.GetRow(y + 1) : image.GetRow(y); + Span rowOut = mask.GetRow(y); + + for (int x = 0; x < image.XSize; x++) + { + // Center, west, east, north, south values and their absolute difference + float c = rowIn[x]; + float w = x > 0 ? rowIn[x - 1] : rowIn[x]; + float e = x + 1 < image.XSize ? rowIn[x + 1] : rowIn[x]; + float n = rowN[x]; + float s = rowS[x]; + + // absolute + float dw = MathF.Abs(c - w); + float de = MathF.Abs(c - e); + float dn = MathF.Abs(c - n); + float ds = MathF.Abs(c - s); + + float min = float.MaxValue; + float min2 = float.MaxValue; + + StoreMin2(dw, ref min, ref min2); + StoreMin2(de, ref min, ref min2); + StoreMin2(dn, ref min, ref min2); + StoreMin2(ds, ref min, ref min2); + + rowOut[x] = min2; + } + } + } + + public static void ReduceRinging(JxlImageF initial, JxlImageF mask, JxlImageF down) + { + int xsize2 = down.XSize; + int ysize2 = down.YSize; + + for (int y = 0; y < down.YSize; y++) + { + Span rowDown = down.GetRow(y); + Span rowInitial = initial.GetRow(y); + + Span rowMask = mask.GetRow(y); + Span rowOut = down.GetRow(y); + + for (int x = 0; x < down.XSize; x++) + { + float v = rowDown[x]; + float min = rowInitial[x]; + float max = rowInitial[x]; + + for (int yi = -1; yi < 2; yi++) + { + for (int xi = -1; xi < 2; xi++) + { + int x2 = x + xi; + int y2 = y + yi; + + if (x2 < 0 || y2 < 0 || x2 >= xsize2 || y2 >= ysize2) + { + continue; + } + + min = MathF.Min(min, initial.GetRow(y2)[x2]); + max = MathF.Max(max, initial.GetRow(y2)[x2]); + } + } + + rowOut[x] = v; + + const float maskMultiplier = 2; + float a = rowMask[x] * maskMultiplier; + float clipMin = min - a; + float clipMax = max + a; + + if (rowOut[x] < clipMin) + { + rowOut[x] = clipMin; + } + + if (rowOut[x] > clipMax) + { + rowOut[x] = clipMax; + } + } + } + } + + public static float UpsamplerDerivative(int x2, int y2, int x, int y) + { + ReadOnlySpan kernel = Kernel00; + + if ((x & 1) != 0 && (y & 1) != 0) + { + kernel = Kernel11; + } + else if ((x & 1) != 0) + { + kernel = Kernel10; + } + else if ((y & 1) != 0) + { + kernel = Kernel01; + } + + int ix = x / 2; + int iy = y / 2; + int kx = x2 - ix + (Size / 2); + int ky = y2 - iy + (Size / 2); + + if (kx < 0 || kx >= Size || ky < 0 || ky >= Size) + { + // Shouldn't happen. + return 0; + } + + return kernel[(ky * Size) + kx]; + } + + private static void ElementwiseMultiply(JxlPlane image1, JxlPlane image2, JxlPlane output) + where T : unmanaged, + IMultiplyOperators, + IMultiplicativeIdentity + { + int width = image1.XSize; + int height = image1.YSize; + + if (width != image2.XSize || + width != output.XSize || + height != image2.YSize || + height != output.YSize) + { + throw new InvalidOperationException("Each plane must have equal sizes"); + } + + for (int y = 0; y < height; y++) + { + ReadOnlySpan row1 = image1.GetRow(y); + ReadOnlySpan row2 = image2.GetRow(y); + Span rowOut = output.GetRow(y); + + TensorPrimitives.Multiply(row1, row2, rowOut); + } + } + + private static void ElementwiseDivide(JxlPlane image1, JxlPlane image2, JxlPlane output) + where T : unmanaged, IDivisionOperators + { + int width = image1.XSize; + int height = image1.YSize; + + if (width != image2.XSize || + width != output.XSize || + height != image2.YSize || + height != output.YSize) + { + throw new InvalidOperationException("Each plane must have equal sizes"); + } + + for (int y = 0; y < height; y++) + { + ReadOnlySpan row1 = image1.GetRow(y); + ReadOnlySpan row2 = image2.GetRow(y); + Span rowOut = output.GetRow(y); + + TensorPrimitives.Divide(row1, row2, rowOut); + } + } + private sealed class OccCounters : IDisposable { private readonly int[] qfCounts; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlQuantizerWeightsEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlQuantizerWeightsEncoder.cs index 1c63ff26e1..af84af99fb 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlQuantizerWeightsEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlQuantizerWeightsEncoder.cs @@ -5,6 +5,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Modular; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77.cs index 0feb95b989..1a4c3354fe 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77.cs @@ -2,8 +2,10 @@ // Licensed under the Six Labors Split License. using System.Buffers; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using SixLabors.ImageSharp.Memory; @@ -14,7 +16,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Lz77; /// internal static class JxlLz77 { - public static List> ApplyRunLengthEncoding( + public static List[] ApplyRunLengthEncoding( Configuration configuration, JxlHistogramParameters parameters, int numContexts, @@ -44,8 +46,137 @@ public static List> ApplyRunLengthEncoding( for (int i = 0; i < input.Count; i++) { uintConfig.Encode(input[i].Value, out uint token, out uint numBits, out _); - symbolCost + symbolCostSpan[i + 1] = sce.GetBits((int)input[i].Context, (int)token) + numBits + symbolCostSpan[i]; + } + + for (int i = 0; i < input.Count; i++) + { + int numToCopy = 0; + int distanceSymbol = 0; // 1 for RLE. + + if (distanceMultiplier != 0) + { + distanceSymbol = 1; // Special distance 1 if enabled. + } + + if (i > 0) + { + for (; i + numToCopy < input.Count; numToCopy++) + { + if (input[i + numToCopy].Value != input[i - 1].Value) + { + break; + } + } + } + + if (numToCopy == 0) + { + output.Add(input[i]); + continue; + } + + float cost = symbolCostSpan[i + numToCopy] - symbolCostSpan[i]; + + // This subtraction might overflow, but that's OK. + int lz77Length = numToCopy - (int)lz77.MinimumLength; + float lz77Cost = numToCopy >= lz77.MinimumLength + ? JxlMath.CeilLog2Nonzero(lz77Length + 1) + 1 + : 0; + + if (numToCopy < lz77.MinimumLength || cost <= lz77Cost) + { + for (int j = 0; j < numToCopy; j++) + { + output.Add(input[i + j]); + } + + i += numToCopy - 1; + continue; + } + + output.Add(new JxlToken(input[i].Context, (uint)lz77Length)); + output[^1] = output[^1] with + { + IsLz77Length = true + }; + + i += numToCopy - 1; + bitDecrease += cost - lz77Cost; + + // Output the LZ77 copy distance. + output.Add( + new JxlToken( + (JxlMaTreeContext)lz77.NonserializedDistanceContext, + (uint)distanceSymbol)); } } + + if (bitDecrease > (totalSymbols * 0.2f) + 16) + { + return tokensLz77; + } + + return []; } + + /// + /// Computes a Murmur-style mix hash. + /// + /// + /// Number of items to compute the hash for. + /// + /// + /// The offset of the first item. + /// + /// + /// Actual values to find the hash. + /// + /// + /// The 32-bit hash + /// + public static uint GetHash(int hashSize, int pos, ReadOnlySpan data) + { + if (pos + hashSize <= data.Length) + { + // do the hash at offset 'pos', 'hashSize' times + uint h = 0; + + for (int i = 0; i < hashSize; i++) + { + h ^= data[pos + i] + 0x9e3779b9 + (h << 6) + (h >> 2); + } + + h ^= h >> 16; + h *= 0x85ebca6bu; + h ^= h >> 13; + h *= 0xc2b2ae35u; + h ^= h >> 16; + + return h; + } + else + { + // data is out of bounds + return 0; + } + } + + /// + /// Computes a Murmur-style mix hash. + /// + /// + /// Number of items to compute the hash for. + /// + /// + /// The offset of the first item. + /// + /// + /// Actual values to find the hash. + /// + /// + /// The 32-bit hash + /// + public static uint GetHash(int hashSize, int pos, List data) + => GetHash(hashSize, pos, CollectionsMarshal.AsSpan(data)); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77HashChain.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77HashChain.cs new file mode 100644 index 0000000000..20787d8882 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77HashChain.cs @@ -0,0 +1,323 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Lz77; + +internal sealed class JxlLz77HashChain +{ + private readonly int maxChainLength; + private readonly int size; + private readonly IMemoryOwner data; + + private uint hashNumValues = 32768; + private uint hashMask = 32767; + private uint hashShift = 5; + + private readonly IMemoryOwner head; + private readonly IMemoryOwner chain; + private readonly IMemoryOwner value; + + private readonly IMemoryOwner headZ; + private readonly IMemoryOwner chainZ; + private readonly IMemoryOwner zeros; + private uint numZeros; + + private readonly int windowSize; + private readonly int windowMask; + private readonly int minLength; + private readonly int maxLength; + + private readonly Dictionary specialDistTable = []; + private readonly int numSpecialDistances; + + private readonly Configuration configuration; + + public JxlLz77HashChain( + Configuration configuration, + int maxChainLength, + Span data, + int size, + int windowSize, + int minLength, + int maxLength, + int distanceMultiplier) + { + this.maxChainLength = maxChainLength; + this.size = size; + this.windowSize = windowSize; + this.windowMask = windowSize - 1; + this.minLength = minLength; + this.maxLength = maxLength; + this.configuration = configuration; + + this.data = configuration.MemoryAllocator.Allocate(size); + Span dataSpan = this.data.GetSpan(); + + for (int i = 0; i < size; i++) + { + dataSpan[i] = data[i].Value; + } + + this.head = configuration.MemoryAllocator.Allocate((int)this.hashNumValues); + this.value = configuration.MemoryAllocator.Allocate(this.windowSize); + this.chain = configuration.MemoryAllocator.Allocate(this.windowSize); + + this.head.GetSpan().Fill(-1); + this.value.GetSpan().Fill(-1); + + Span chainSpan = this.chain.GetSpan(); + + for (uint i = 0; i < this.windowSize; ++i) + { + // Same value as index indicates uninitialized + chainSpan[unchecked((int)i)] = i; + } + + this.zeros = configuration.MemoryAllocator.Allocate(this.windowSize); + this.headZ = configuration.MemoryAllocator.Allocate(this.windowSize + 1); + this.chainZ = configuration.MemoryAllocator.Allocate(this.windowSize); + + this.headZ.GetSpan().Fill(-1); + Span chainZSpan = this.chainZ.GetSpan(); + + for (uint i = 0; i < this.windowSize; ++i) + { + chainZSpan[unchecked((int)i)] = i; + } + + // Translation of distance to special distance code + if (distanceMultiplier > 0) + { + // Count down, so if due to small distance multiplier multiple distances + // map to the same code, the smallest code will be used in the end. + for (int i = JxlAnsReader.NumSpecialDistances - 1; i >= 0; --i) + { + this.specialDistTable[JxlAnsReader.SpecialDistance(i, distanceMultiplier)] = i; + } + + this.numSpecialDistances = JxlAnsReader.NumSpecialDistances; + } + } + + public uint CountZeros(int pos, uint previousPos) + { + int end = pos + this.windowSize; + + if (end > this.size) + { + end = this.size; + } + + Span dataSpan = this.data.GetSpan(); + + if (previousPos > 0) + { + if (previousPos >= this.windowMask + && dataSpan[end - 1] == 0 && + end == pos + this.windowSize) + { + return previousPos; + } + else + { + return previousPos - 1; + } + } + + uint num = 0; + + while (pos + num < end && + dataSpan[unchecked((int)(pos + num))] == 0) + { + num++; + } + + return num; + } + + public void Update(int pos) + { + uint hashValue = JxlLz77.GetHash(pos, this.data.GetSpan()) & this.hashMask; + int wpos = pos & this.windowMask; + + Span valueSpan = this.value.GetSpan(); + Span headSpan = this.head.GetSpan(); + Span chainSpan = this.chain.GetSpan(); + Span dataSpan = this.data.GetSpan(); + Span zerosSpan = this.zeros.GetSpan(); + Span headZSpan = this.headZ.GetSpan(); + Span chainZSpan = this.chainZ.GetSpan(); + + valueSpan[wpos] = (int)hashValue; + if (headSpan[(int)hashValue] != -1) + { + chainSpan[wpos] = (uint)headSpan[(int)hashValue]; + } + + headSpan[(int)hashValue] = wpos; + if (pos > 0 && dataSpan[pos] != dataSpan[pos - 1]) + { + this.numZeros = 0; + } + + this.numZeros = this.CountZeros(pos, this.numZeros); + + zerosSpan[wpos] = this.numZeros; + if (headZSpan[(int)this.numZeros] != -1) + { + chainZSpan[wpos] = (uint)headZSpan[(int)this.numZeros]; + } + + headZSpan[(int)this.numZeros] = wpos; + } + + public void Update(int pos, int len) + { + for (int i = 0; i < len; i++) + { + this.Update(pos + i); + } + } + + public void FindMatches(int pos, int maxDist, JxlLz77MatchFoundCallback foundMatch) + { + Span data = this.data.GetSpan(); + Span chain = this.chain.GetSpan(); + Span zeros = this.zeros.GetSpan(); + Span chainz = this.chainZ.GetSpan(); + Span headz = this.headZ.GetSpan(); + Span value = this.value.GetSpan(); + + int wpos = pos & this.windowMask; + uint hashValue = JxlLz77.GetHash(pos, data) & this.hashMask; + uint hashPos = chain[wpos]; + + int previousDistance = 0; + int end = Math.Min(pos + this.maxLength, this.size); + + int chainLength = 0; + int bestLength = 0; + + while (true) + { + int dist = hashPos <= wpos + ? (wpos - (int)hashPos) + : (wpos - (int)hashPos + this.windowMask + 1); + + if (dist < previousDistance) + { + break; + } + + previousDistance = dist; + uint len = 0; + + if (dist > 0) + { + int i = pos; + int j = pos - dist; + + if (this.numZeros > 3) + { + int r = (int)Math.Min(this.numZeros - 1, zeros[(int)hashPos]); + + if (i + r >= end) + { + r = end - i - 1; + } + + i += r; + j += r; + } + + while (i < end && data[i] == data[j]) + { + i++; + j++; + } + + len = (uint)(i - pos); + + // This can trigger even if the new length is slightly smaller than the + // best length, since it's possible for a slightly cheaper distance + // symbol to occur. + if (len >= this.minLength && len + 2 >= bestLength) + { + int distSymbol = this.specialDistTable.TryGetValue(dist, out int symbol) + ? symbol + : this.numSpecialDistances + dist - 1; + + foundMatch(len, distSymbol); + + if (len > bestLength) + { + bestLength = (int)len; + } + } + } + + chainLength++; + if (chainLength >= this.maxChainLength) + { + break; + } + + if (this.numZeros >= 3 && len > this.numZeros) + { + if (hashPos == chainz[(int)hashPos]) + { + break; + } + + hashPos = chainz[(int)hashPos]; + if (zeros[(int)hashPos] != this.numZeros) + { + break; + } + } + else + { + if (hashPos == chain[(int)hashPos]) + { + break; + } + + hashPos = chain[(int)hashPos]; + if (value[(int)hashPos] != (int)hashValue) + { + // outdated hash value + break; + } + } + } + } + + public unsafe void FindMatch(int pos, int maxDist, int* resultDistSymbol, int* resultLength) + { + *resultDistSymbol = 0; + *resultLength = 1; + + this.FindMatches(pos, maxDist, (len, distSymbol) => + { + if (len > *resultLength || + (len == *resultLength && *resultDistSymbol > distSymbol)) + { + *resultLength = (int)len; + *resultDistSymbol = distSymbol; + } + }); + } + + public unsafe void FindMatch(int pos, int maxDist, ref int resultDistSymbol, ref int resultLength) + { + int* pDistSymbol = (int*)Unsafe.AsPointer(ref resultDistSymbol); + int* pLength = (int*)Unsafe.AsPointer(ref resultLength); + this.FindMatch(pos, maxDist, pDistSymbol, pLength); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77MatchFoundCallback.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77MatchFoundCallback.cs new file mode 100644 index 0000000000..40d09ab376 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77MatchFoundCallback.cs @@ -0,0 +1,6 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Lz77; + +internal delegate void JxlLz77MatchFoundCallback(uint length, int distanceSymbol); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Modular/JxlModularFrameEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Modular/JxlModularFrameEncoder.cs new file mode 100644 index 0000000000..a5c42b8342 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Modular/JxlModularFrameEncoder.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Modular; + +internal class JxlModularFrameEncoder +{ + public static void QuantizeChannel(JxlModularChannel ch, int q) + { + if (q == 1) + { + // No quantization - q=1 keeps values as-is. + return; + } + + JxlImageI plane = ch.Plane; + + for (int y = 0; y < plane.YSize; y++) + { + Span row = plane.GetRow(y); + + for (int x = 0; x < plane.XSize; x++) + { + if (row[x] < 0) + { + row[x] = -((-row[x] + (q / 2)) / q) * q; + } + else + { + row[x] = ((row[x] + (q / 2)) / q) * q; + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Modular/JxlModularSimd.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Modular/JxlModularSimd.cs new file mode 100644 index 0000000000..0adcaceabb --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Modular/JxlModularSimd.cs @@ -0,0 +1,310 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Modular; + +internal static class JxlModularSimd +{ + private const int LargeThreshold = 501; + private const int LastContext = 16; + + public static ReadOnlySpan ContextMap => + [ + 0, 1, 1, 2, 2, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, + 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10, 10, 10, + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, + 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, + 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, + 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 13, 13, 13, 13, 13, 13, 13, + 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, + 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, + 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, + 13, 13, 13, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, + 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, + 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, + 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, + 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, + 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, + 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 16 + ]; + + public static ReadOnlySpan ScalarCutoffs => + [ + 0, 1, 3, 5, 7, 11, 15, 23, 31, + 47, 63, 95, 127, 191, 255, 392, 500 + ]; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Max3(int x, int y, int z) => Math.Max(x, Math.Max(y, z)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Min3(int x, int y, int z) => Math.Min(x, Math.Min(y, z)); + + public static float EstimateCost(Configuration configuration, JxlModularImage image) + { + int histogramCost = 0; + float histogramCostFraction = 0f; + int extraBits = 0; + + if (Vector.Count == 1 || + Vector.Count == 1 || + Vector.Count == 1) + { + // Scalar + JxlAnsHybridUIntConfiguration config = new(); + int numCutoffs = ScalarCutoffs.Length + 1; + + // stackalloc doesn't work on managed types + JxlHistogram[] histograms = ArrayHelpers.CreateAndInitialize(numCutoffs, () => new JxlHistogram()); + + foreach (JxlModularChannel ch in image.Channels) + { + int oneRow = ch.Plane.PixelsPerRow; + + for (int y = 0; y < ch.Height; y++) + { + Span r = ch.GetRow(y); + ref int referenceToR = ref MemoryMarshal.GetReference(r); + + for (int x = 0; x < ch.Width; x++) + { + int left = x > 0 + ? r[x - 1] + : y > 0 + ? Unsafe.Add(ref referenceToR, x - oneRow) + : 0; + + int top = y > 0 + ? Unsafe.Add(ref referenceToR, x - oneRow) + : left; + + int topleft = x > 0 && y > 0 + ? Unsafe.Add(ref referenceToR, x - 1 - oneRow) + : left; + + int max_diff = Max3(left, top, topleft) - Min3(left, top, topleft); + int ctx = 0; + + foreach (uint c in ScalarCutoffs) + { + ctx += (max_diff < c) ? 1 : 0; + } + + int res = r[x] - JxlContextPrediction.ClampedGradient(top, left, topleft); + config.Encode(JxlPackSigned.PackUnsigned(res), out uint token, out uint nbits, out uint bits); + histograms[ctx].Add((int)token); + extraBits += (int)nbits; + } + } + + foreach (JxlHistogram h in histograms) + { + float floatingCost = h.ComputeShannonEntropy(); + int integerCost = (int)floatingCost; + histogramCost += integerCost; + histogramCostFraction += floatingCost - integerCost; + h.Clear(); + } + } + } + else + { + // Vectorized + Vector one = Vector.One; + Vector split = Vector.Create(16u); + Vector expOffset2 = Vector.Create(129u); // 127 + 2 + Vector tokenBias = Vector.Create(8u); + Vector tokenMul = Vector.Create(4u); + Vector msbMask = Vector.Create(3u); + Vector maxDiffCap = Vector.Create((uint)(LargeThreshold - 1)); + Vector lanes = Vector.Create((uint)Vector.Count); + Vector iota = JxlSimdUtils.Iota(0u); + Vector largeThreshold = Vector.Create((uint)((1 << 2) - 1)); + const int largeShiftValue = 10; + Vector largeShift = Vector.Create((uint)largeShiftValue); + + int maxWidth = 0; + + foreach (JxlModularChannel channel in image.Channels) + { + if (channel.Height == 0) + { + // No pixels. So skip it. + continue; + } + + maxWidth = Math.Max(maxWidth, channel.Width); + } + + maxWidth = JxlMath.RoundUpTo(maxWidth, Vector.Count); + maxWidth = Math.Max(maxWidth, 2 * Vector.Count); + + using IMemoryOwner buffer = configuration.MemoryAllocator.Allocate(maxWidth * 2); + + Span maxDiffRow = buffer.GetSpan(); + Span tokenRow = maxDiffRow[maxWidth..]; + Span primer = MemoryMarshal.Cast(maxDiffRow); + Span topPrimer = primer[maxWidth..]; + + JxlAnsHybridUIntConfiguration config = new(); + JxlHistogram[] histograms = ArrayHelpers.CreateAndInitialize(LastContext + 1, () => new JxlHistogram(32 * 4)); + Vector extraBitsLanes = Vector.Zero; + + foreach (JxlModularChannel ch in image.Channels) + { + if (ch.Width == 0 || ch.Height == 0) + { + continue; + } + + Span r = ch.GetRow(0); + Span last = primer; + + primer[0] = 0; + + Vector.Create(r).CopyTo(primer[1..]); + Vector pos = iota; + Vector lastPos = Vector.Create((uint)ch.Width); + + for (int x = 0; x < ch.Width - Vector.Count; x += Vector.Count) + { + Vector left = Vector.Create(last); + Vector central = Vector.Create(r[x..]); + + Vector ures = (central - left).As(); + Vector packed = (ures << 1) ^ ((~ures >> 31) - one); + + Vector isLarge = Vector.GreaterThan(packed, largeThreshold); + Vector packedShifted = packed >> largeShiftValue; + Vector notLiteral = Vector.GreaterThanOrEqual(packed, split); + Vector packedFixed = Vector.ConditionalSelect(isLarge, packedShifted, packed); + + Vector v = Vector.ConvertToSingle(packedFixed).As(); + Vector ebRaw = (v >>> 23) - expOffset2; + Vector eb = Vector.ConditionalSelect(isLarge, ebRaw + largeShift, ebRaw); + + Vector token = (tokenBias + (eb * tokenMul)) + ((v >>> 21) & msbMask); + Vector tailMask = Vector.LessThan(pos, lastPos); + Vector ebFixed = Vector.ConditionalSelect(notLiteral, eb, Vector.Zero); + Vector tokenFixed = Vector.ConditionalSelect(notLiteral, token, packed); + extraBitsLanes += Vector.ConditionalSelect(tailMask, ebFixed, Vector.Zero); + + tokenFixed.CopyTo(tokenRow[x..]); + pos += lanes; + last = r[(x + Vector.Count - 1)..]; + } + + for (int x = 0; x < ch.Width; x++) + { + histograms[0].FastAdd((int)tokenRow[x]); + } + + for (int y = 1; y < ch.Height; y++) + { + r = ch.GetRow(y); + Span t = ch.GetRow(y - 1); + primer[0] = t[0]; + + Vector.Create(r).CopyTo(primer[1..]); + + topPrimer[0] = t[0]; + Vector.Create(t).CopyTo(topPrimer[1..]); + + Span topLast = topPrimer; + pos = iota; + + for (int x = 0; x < ch.Width; x += Vector.Count) + { + // Loading neighbors + Vector left = Vector.Create(last); + Vector central = Vector.Create(r[x..]); + Vector topleft = Vector.Create(topLast); + Vector top = Vector.Create(t[x..]); + + Vector lGeT = Vector.GreaterThanOrEqual(left, top); + Vector m = Vector.ConditionalSelect(lGeT, top, left); + Vector M = Vector.ConditionalSelect(lGeT, left, top); + Vector maxx = Vector.Max(topleft, M); + Vector minn = Vector.Min(topleft, m); + Vector maxDiff = (maxx - minn).As(); + + Vector.Min(maxDiff, maxDiffCap).CopyTo(maxDiffRow[x..]); + + Vector overshoot = Vector.LessThan(topleft, m); + Vector undershoot = Vector.GreaterThan(topleft, M); + Vector grad = ((top.As() + left.As()) - topleft.As()).As(); + + Vector prediction = Vector.ConditionalSelect(undershoot, m, Vector.ConditionalSelect(overshoot, M, grad)); + Vector ures = (central - prediction).As(); + Vector packed = (ures << 1) ^ ((~ures >> 31) - one); + Vector isLarge = Vector.GreaterThan(packed, largeThreshold); + Vector packedShifted = packed >> largeShiftValue; + Vector notLiteral = Vector.GreaterThanOrEqual(packed, split); + Vector packedFixed = Vector.ConditionalSelect(isLarge, packedShifted, packed); + Vector v = Vector.ConvertToSingle(packedFixed).As(); + Vector ebRaw = (v >> 23) - expOffset2; + Vector eb = Vector.ConditionalSelect(isLarge, ebRaw + largeShift, ebRaw); + Vector token = (tokenBias + (eb * tokenMul)) + ((v >> 21) & msbMask); + Vector tailMask = Vector.LessThan(pos, lastPos); + Vector ebFixed = Vector.ConditionalSelect(notLiteral, eb, Vector.Zero); + Vector tokenFixed = Vector.ConditionalSelect(notLiteral, token, packed); + + extraBitsLanes += Vector.ConditionalSelect(tailMask, ebFixed, Vector.Zero); + tokenFixed.CopyTo(tokenRow[x..]); + + pos += lanes; + last = r[(x + Vector.Count - 1)..]; + topLast = t[(x + Vector.Count - 1)..]; + } + + for (int x = 0; x < ch.Width; x++) + { + int ctx = ContextMap[(int)maxDiffRow[x]]; + histograms[ctx].FastAdd((int)tokenRow[x]); + } + } + + foreach (JxlHistogram h in histograms) + { + h.Condition(); + + float floatingCost = h.ComputeShannonEntropy(); + int integerCost = (int)floatingCost; + + histogramCost += integerCost; + histogramCostFraction += floatingCost - integerCost; + + h.Clear(); + } + } + + extraBits = (int)Vector.Sum(extraBitsLanes); + } + + int totalCost = extraBits + histogramCost + (int)histogramCostFraction; + return totalCost; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/PatchDictionary/JxlPatchColorspaceInfo.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/PatchDictionary/JxlPatchColorspaceInfo.cs new file mode 100644 index 0000000000..6538478cd6 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/PatchDictionary/JxlPatchColorspaceInfo.cs @@ -0,0 +1,60 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.PatchDictionary; + +internal struct JxlPatchColorspaceInfo +{ + public InlineArray3 ChannelDequant; + public InlineArray3 ChannelWeights; + + public JxlPatchColorspaceInfo(bool isXyb) + { + if (isXyb) + { + this.ChannelDequant[0] = 0.01615f; + this.ChannelDequant[1] = 0.08875f; + this.ChannelDequant[2] = 0.1922f; + this.ChannelWeights[0] = 30.0f; + this.ChannelWeights[1] = 3.0f; + this.ChannelWeights[2] = 1.0f; + } + else + { + this.ChannelDequant[0] = 20.0f / 255; + this.ChannelDequant[1] = 22.0f / 255; + this.ChannelDequant[2] = 20.0f / 255; + this.ChannelWeights[0] = 0.017f * 255; + this.ChannelWeights[1] = 0.02f * 255; + this.ChannelWeights[2] = 0.017f * 255; + } + } + + public static explicit operator JxlPatchColorspaceInfo(bool isXyb) => new(isXyb); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly float ScaleForQuantization(float value, int c) => value / this.ChannelDequant[c]; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly int Quantize(float value, int c) + { + float scaled = this.ScaleForQuantization(value, c); + float clamped = Math.Clamp(scaled, -32768.0f, 32767.0f); + return (int)MathF.Truncate(clamped); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly bool IsSimilar(Span v1, Span v2, float threshold) + { + float distance = 0; + + for (int c = 0; c < 3; c++) + { + distance += MathF.Abs(v1[c] - v2[c]) * this.ChannelWeights[c]; + } + + return distance <= threshold; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs index 1015e2b720..fddb21dc09 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs @@ -358,7 +358,7 @@ public static void Iota(Span span, T start) public static Vector Iota(T start) where T : unmanaged, INumber - => IotaMask.IncrementMask + Vector.Create(start); + => IotaMask.BaseMask + Vector.Create(start); /// /// Vectorized floating-point error function (precise approximate). @@ -605,17 +605,21 @@ private static class IotaMask where T : unmanaged, INumber { public static readonly Vector IncrementMask; + public static readonly Vector BaseMask; static IotaMask() { Span values = stackalloc T[Vector.Count]; + Span baseValues = stackalloc T[Vector.Count]; for (int i = 0; i < Vector.Count; i++) { values[i] = T.CreateSaturating(i + 1); + baseValues[i] = T.CreateSaturating(i); } IncrementMask = Vector.Create(values); + BaseMask = Vector.Create(baseValues); } } } From e9fe89048dc7f16df86751cba70232c051608b10 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sun, 11 Oct 2026 05:51:24 +0400 Subject: [PATCH 202/328] Create group encoder --- .../Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs new file mode 100644 index 0000000000..0b2e6549ed --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs @@ -0,0 +1,8 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; + +internal static class JxlGroupEncoder +{ +} From fde4550f276f9e582a00119ceccfb9dbd50d0274 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sun, 11 Oct 2026 05:54:15 +0400 Subject: [PATCH 203/328] Add constants and lookups in group encoder --- .../Jxl/Processing/Encoder/JxlGroupEncoder.cs | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs index 0b2e6549ed..3f34d0881f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs @@ -5,4 +5,61 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; internal static class JxlGroupEncoder { + private const double QuantNormalizer = 2.2942708343284721; + + private static ReadOnlySpan Limit => [0.46, 0.46, 0.46, 0.46]; + + private static ReadOnlySpan Multipliers => [0.9999, 0.9999, 0.9999, 0.9999]; + + private static readonly double[,] Mul1Data = + { + { + 0.22080615753848404, + 0.45797479824262011, + 0.29859235095977965, + }, + { + 0.70109486510286834, + 0.16185281305512639, + 0.14387691730035473, + }, + { + 0.114985964456218638, + 0.44656840441027695, + 0.10587658215149048, + }, + { + 0.46849665264409396, + 0.41239077937781954, + 0.088667407767185444, + }, + }; + + private static readonly double[,] Mul2Data = + { + { + 0.27450281941822197, + 1.1255766549984996, + 0.98950459134128388, + }, + { + 0.4652168675598285, + 0.40945807983455818, + 0.36581899811751367, + }, + { + 0.28034972424715715, + 0.9182653201929738, + 1.5581531543057416, + }, + { + 0.26873118114033728, + 0.68863712390392484, + 1.2082185408666786, + }, + }; + + private static readonly DenseMatrix Mul1 = new(Mul1Data); + private static readonly DenseMatrix Mul2 = new(Mul2Data); + } From 0cebb94eb187c81569baaa1223fbb30fe32e9354 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 12 Oct 2026 23:44:48 +0400 Subject: [PATCH 204/328] Finish ANS decoder, implement upsampling stage constructor, make CustomTransformData inline array fields public, remove unnecessary usings from noise decoder, document JxlAnsHelper --- src/ImageSharp/Formats/Jxl/Cms/JxlCmsImpl.cs | 41 ++ .../Formats/Jxl/IO/Entropy/JxlAnsHelper.cs | 20 +- .../Formats/Jxl/IO/Jpeg/JpegDataEncoder.cs | 53 +++ .../Jxl/IO/Jpeg/JpegEncodeScanStage.cs | 14 + .../Formats/Jxl/IO/Jpeg/JpegWriter.cs | 2 +- .../Jxl/IO/Metadata/JxlCustomTransformData.cs | 30 +- .../Jxl/Processing/Decoder/Ans/JxlAnsCode.cs | 4 +- .../Processing/Decoder/Ans/JxlAnsReader.cs | 193 +++++++- .../Decoder/Ans/JxlAnsSymbolReader.cs | 412 +++++++++++++++++- .../Processing/Decoder/JxlHuffmanDecoder.cs | 6 +- .../Jxl/Processing/Decoder/JxlNoiseDecoder.cs | 3 - .../RenderPipeline/LowMemoryRenderPipeline.cs | 9 + .../RenderPipeline/UpsamplingStage.cs | 52 ++- 13 files changed, 779 insertions(+), 60 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlCmsImpl.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegDataEncoder.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegEncodeScanStage.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/LowMemoryRenderPipeline.cs diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlCmsImpl.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlCmsImpl.cs new file mode 100644 index 0000000000..87986aa742 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlCmsImpl.cs @@ -0,0 +1,41 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms; + +/// +/// Implementation of the Color Management System. +/// +// TODO +internal class JxlCmsImpl +{ + public Memory IccSource { get; set; } + + public Memory IccDestination { get; set; } + + public bool ApplyHlgOotf { get; set; } + + public int HlgOotfNumChannels { get; set; } + + public float[] HlgOotfLuminances { get; set; } = new float[3]; + + public int ChannelsSource { get; set; } + + public int ChannelsDestination { get; set; } + + public List SourceStorage { get; set; } = []; + + public List> BufferSource { get; set; } = []; + + public List DestinationStorage { get; set; } = []; + + public List> BufferDestination { get; set; } = []; + + public float IntensityTarget { get; set; } + + public bool SkipLCms { get; set; } + + public JxlExtraTransferFunction PreProcess { get; set; } = JxlExtraTransferFunction.None; + + public JxlExtraTransferFunction PostProcess { get; set; } = JxlExtraTransferFunction.None; +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHelper.cs b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHelper.cs index 7feda2848d..b4315ebb12 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHelper.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHelper.cs @@ -6,6 +6,10 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; +/// +/// Utilities for Asymmetric Numeral Systems, a kind of high-performance +/// entropy coding used by the JPEG XL format. +/// internal static class JxlAnsHelper { [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -63,7 +67,7 @@ public static JxlAnsSymbol Lookup(ReadOnlySpan table, int value, in return symbol; } - public static bool InitAliasTable(Span preDistribution, uint logRange, int logAlphaSize, Span entries) + public static bool InitAliasTable(Span preDistribution, uint logRange, int logAlphaSize, Span entries) { DebugGuard.MustBeLessThan(logAlphaSize, (int)logRange, nameof(logAlphaSize)); @@ -79,11 +83,11 @@ public static bool InitAliasTable(Span preDistribution, uint logRange, int if (distributionPointer < 0) { - preDistribution[0] = range; + preDistribution[0] = (uint)range; distributionPointer = 0; } - Span distribution = preDistribution[..(distributionPointer + 1)]; + Span distribution = preDistribution[..(distributionPointer + 1)]; if (distribution.Length > tableSize) { @@ -92,11 +96,11 @@ public static bool InitAliasTable(Span preDistribution, uint logRange, int int entrySize = range >> logAlphaSize; int singleSymbol = -1; - int sum = 0; + uint sum = 0; for (int sym = 0; sym < distribution.Length; sym++) { - int value = distribution[sym]; + uint value = distribution[sym]; sum += value; if (value == JxlAnsConstants.AnsTableSize) @@ -146,7 +150,7 @@ public static bool InitAliasTable(Span preDistribution, uint logRange, int for (int i = 0; i < distribution.Length; i++) { - uint currentCutoff = (uint)distribution[i]; + uint currentCutoff = distribution[i]; cutoffs[i] = currentCutoff; @@ -226,9 +230,9 @@ public static bool InitAliasTable(Span preDistribution, uint logRange, int entry.Cutoff = (byte)currentCutoff; } - int freq0 = i < distribution.Length ? distribution[(int)i] : 0; + uint freq0 = i < distribution.Length ? distribution[(int)i] : 0; int i1 = entry.RightValue; - int freq1 = i1 < distribution.Length ? distribution[i1] : 0; + uint freq1 = i1 < distribution.Length ? distribution[i1] : 0; entry.Frequency0 = (ushort)freq0; entry.Frequency1XorFrequency0 = (ushort)(freq1 ^ freq0); diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegDataEncoder.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegDataEncoder.cs new file mode 100644 index 0000000000..086ea5970d --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegDataEncoder.cs @@ -0,0 +1,53 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; +using SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg; + +internal static class JpegDataEncoder +{ + public static void SetChromaSubsamplingFromJpegData(JpegData jpg, JxlYCbCrChromaSubsampling cs) + { + int numberOfComponents = jpg.Components.Count; + + if (numberOfComponents is not 1 and not 3) + { + throw new InvalidOperationException("JPEGs only support 1 or 3 channels"); + } + + if (numberOfComponents == 3) + { + Span hsample = [0, 0, 0]; + Span vsample = [0, 0, 0]; + + for (int i = 0; i < 3; i++) + { + hsample[i] = (byte)jpg.Components[i].HorizontalSampleFactor; + vsample[i] = (byte)jpg.Components[i].VerticalSampleFactor; + } + + if (!cs.Set(hsample, vsample)) + { + throw new InvalidOperationException("Could not set horizontal / vertical sample"); + } + } + else if (numberOfComponents == 1) + { + Span hsample = [0, 0, 0]; + Span vsample = [0, 0, 0]; + + for (int i = 0; i < 3; i++) + { + hsample[i] = (byte)jpg.Components[0].HorizontalSampleFactor; + vsample[i] = (byte)jpg.Components[0].VerticalSampleFactor; + } + + if (!cs.Set(hsample, vsample)) + { + throw new InvalidOperationException("Could not set horizontal / vertical sample"); + } + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegEncodeScanStage.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegEncodeScanStage.cs new file mode 100644 index 0000000000..eeff3687fc --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegEncodeScanStage.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg; + +/// +/// Scan encoding stage +/// +internal enum JpegEncodeScanStage : byte +{ + Head, + + Body +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegWriter.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegWriter.cs index 0df15a4bea..48d366f88b 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegWriter.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegWriter.cs @@ -967,7 +967,7 @@ int GetNextResetPoint() { codingState.Flush(bw); - if (!bw.JumpToByteBoundary(ref state.PadBits)) + if (!bw.JumpToByteBoundary(ref state.PadBits.Span)) { return JpegSerializationStatus.Error; } diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCustomTransformData.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCustomTransformData.cs index 882615d73d..4d897e43c1 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCustomTransformData.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCustomTransformData.cs @@ -8,9 +8,9 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; internal sealed class JxlCustomTransformData : IJxlFields { - private InlineArray15 upsampling2Weights; - private InlineArray55 upsampling4Weights; - private InlineArray210 upsampling8Weights; + public InlineArray15 Upsampling2Weights; + public InlineArray55 Upsampling4Weights; + public InlineArray210 Upsampling8Weights; private uint customWeightsMask; private bool allDefault; @@ -100,24 +100,6 @@ public uint CustomWeightsMask set => this.customWeightsMask = value; } - public InlineArray15 Upsampling2Weights - { - get => this.upsampling2Weights; - set => this.upsampling2Weights = value; - } - - public InlineArray55 Upsampling4Weights - { - get => this.upsampling4Weights; - set => this.upsampling4Weights = value; - } - - public InlineArray210 Upsampling8Weights - { - get => this.upsampling8Weights; - set => this.upsampling8Weights = value; - } - public bool Visit(JxlVisitor visitor) { if (visitor.AllDefault(this, ref this.allDefault)) @@ -143,7 +125,7 @@ public bool Visit(JxlVisitor visitor) { for (int i = 0; i < 15; i++) { - if (!visitor.F16(Weights2[i], ref this.upsampling2Weights[i])) + if (!visitor.F16(Weights2[i], ref this.Upsampling2Weights[i])) { return false; } @@ -154,7 +136,7 @@ public bool Visit(JxlVisitor visitor) { for (int i = 0; i < 55; i++) { - if (!visitor.F16(Weights4[i], ref this.upsampling4Weights[i])) + if (!visitor.F16(Weights4[i], ref this.Upsampling4Weights[i])) { return false; } @@ -165,7 +147,7 @@ public bool Visit(JxlVisitor visitor) { for (int i = 0; i < 210; i++) { - if (!visitor.F16(Weights8[i], ref this.upsampling8Weights[i])) + if (!visitor.F16(Weights8[i], ref this.Upsampling8Weights[i])) { return false; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsCode.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsCode.cs index e5cd1f783b..d0674bf409 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsCode.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsCode.cs @@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; internal sealed class JxlAnsCode { - public List HuffmanData { get; set; } = []; + public List HuffmanData { get; set; } = []; public List UIntConfig { get; set; } = []; @@ -22,6 +22,8 @@ internal sealed class JxlAnsCode public int MaxNumBits { get; set; } + public JxlAnsEntry[]? AliasTables { get; set; } + public void UpdateMaxNumBits(int ctx, int symbol) { Span configs = CollectionsMarshal.AsSpan(this.UIntConfig); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsReader.cs index d1e5d0ba5c..418d710eac 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsReader.cs @@ -3,13 +3,17 @@ using System.Buffers; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.IO; using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; internal static class JxlAnsReader { - private const int WindowSize = 1 << 20; + public const int WindowSize = 1 << 20; public const int NumSpecialDistances = 120; @@ -278,4 +282,191 @@ public static uint DecodeVariableLengthUint16(JxlBitReader reader) return counts; } + + public static void DecodeAnsCodes(Configuration configuration, int numHistograms, int maxAlphabetSize, JxlBitReader input, JxlAnsCode result) + { + result.DegenerateSymbols = new(numHistograms); + CollectionsMarshal.AsSpan(result.DegenerateSymbols).Fill(-1); + + if (result.UsePrefixCode) + { + result.HuffmanData.Resize(numHistograms); + Span alphabetSizes = stackalloc ushort[numHistograms]; + + for (int c = 0; c < numHistograms; c++) + { + alphabetSizes[c] = (ushort)(DecodeVariableLengthUint16(input) + 1); + if (alphabetSizes[c] > maxAlphabetSize) + { + throw new InvalidOperationException($"Alphabet size is too long ({alphabetSizes[c]})"); + } + } + + for (int c = 0; c < numHistograms; c++) + { + if (alphabetSizes[c] > 1) + { + if (!result.HuffmanData[c].ReadFromBitStream(alphabetSizes[c], input)) + { + throw new InvalidOperationException($"Invalid huffman tree number: {c}, alphabet size = {alphabetSizes[c]}"); + } + } + else + { + // 0-bit codes don't require extension tables + result.HuffmanData[c].Table = new JxlHuffmanCode[1u << JxlHuffmanDecoder.HuffmanTableBits]; + } + + foreach (JxlHuffmanCode h in result.HuffmanData[c].Table) + { + if (h.Bits <= JxlHuffmanDecoder.HuffmanTableBits) + { + result.UpdateMaxNumBits(c, h.Value); + } + } + } + } + else + { + if (maxAlphabetSize > JxlAnsConstants.AnsMaxAlphabetSize) + { + throw new InvalidOperationException("Alphabet size overflows"); + } + + int size = numHistograms * (1 << result.LogAlphaSize); + result.AliasTables = new JxlAnsEntry[size]; + + Span aliasTables = result.AliasTables.AsSpan(); + + for (int c = 0; c < numHistograms; c++) + { + using IMemoryOwner? countsMemory = ReadHistogram(configuration, JxlAnsConstants.AnsLogTableSize, input); + if (countsMemory is null) + { + continue; + } + + Span counts = countsMemory.GetSpan(); + if (counts.Length > maxAlphabetSize) + { + throw new InvalidOperationException("Alphabet size is too long"); + } + + while (counts.Length > 0 && counts[^1] == 0) + { + counts = counts[..^1]; + } + + for (int s = 0; s < counts.Length; s++) + { + if (counts[s] != 0) + { + result.UpdateMaxNumBits(c, s); + } + } + + int degenerateSymbol = counts.Length == 0 ? 0 : counts.Length - 1; + for (int s = 0; s < degenerateSymbol; ++s) + { + if (counts[s] != 0) + { + degenerateSymbol = -1; + break; + } + } + + result.DegenerateSymbols[c] = degenerateSymbol; + + if (!JxlAnsHelper.InitAliasTable( + counts, + JxlAnsConstants.AnsLogTableSize, + result.LogAlphaSize, + aliasTables[(c * (1 << result.LogAlphaSize))..])) + { + throw new InvalidOperationException("Could not initialize alias table"); + } + } + } + } + + public static void DecodeUIntConfiguration(int logAlphaSize, out JxlAnsHybridUIntConfiguration uintConfig, JxlBitReader br) + { + br.Refill(); + + uint splitExponent = br.ReadBits32((uint)JxlMath.CeilLog2Nonzero(logAlphaSize + 1)); + uint msbInToken = 0; + uint lsbInToken = 0; + + if (splitExponent != logAlphaSize) + { + uint numBits = JxlMath.CeilLog2Nonzero(splitExponent + 1); + msbInToken = br.ReadBits32(numBits); + + if (msbInToken > splitExponent) + { + throw new InvalidOperationException("Invalid hybrid uint configuration"); + } + + numBits = JxlMath.CeilLog2Nonzero(splitExponent - msbInToken + 1); + lsbInToken = br.ReadBits32(numBits); + } + + if (lsbInToken + msbInToken > splitExponent) + { + throw new InvalidOperationException("Invalid hybrid uint configuration"); + } + + uintConfig = new(splitExponent, msbInToken, lsbInToken); + } + + public static void DecodeUIntConfigurations(int logAlphaSize, Span uintConfig, JxlBitReader br) + { + for (int i = 0; i < uintConfig.Length; i++) + { + DecodeUIntConfiguration(logAlphaSize, out JxlAnsHybridUIntConfiguration currentCfg, br); + uintConfig[i] = currentCfg; + } + } + + public static void DecodeHistograms(Configuration configuration, JxlBitReader br, int numContexts, JxlAnsCode code, ref List contextMap, bool disallowLz77) + { + if (!JxlBundle.Read(br, code.Lz77)) + { + throw new InvalidOperationException("Could not decode lz77 parameters"); + } + + if (code.Lz77.Enabled) + { + numContexts++; + DecodeUIntConfiguration(8, out JxlAnsHybridUIntConfiguration cfg, br); + code.Lz77.LengthUintConfig = cfg; + } + + if (code.Lz77.Enabled && disallowLz77) + { + throw new InvalidOperationException("Using LZ77 even though it is explicitly disallowed"); + } + + int numHistograms = 1; + contextMap.Resize(numContexts); + + if (numContexts > 1) + { + JxlContextMapDecoder.DecodeContextMap(configuration, contextMap, ref numHistograms, br); + } + + code.Lz77.NonserializedDistanceContext = contextMap[^1]; + code.UsePrefixCode = br.ReadBoolean(); + + code.LogAlphaSize = code.UsePrefixCode + ? (byte)JxlAnsConstants.PrefixMaxBits + : (byte)(br.ReadBits32(2) + 5); + + code.UIntConfig.Resize(numHistograms); + + DecodeUIntConfigurations(code.LogAlphaSize, CollectionsMarshal.AsSpan(code.UIntConfig), br); + + int maxAlphabetSize = 1 << code.LogAlphaSize; + DecodeAnsCodes(configuration, numHistograms, maxAlphabetSize, br, code); + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsSymbolReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsSymbolReader.cs index 2de97c74f7..9ae7cb4d0b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsSymbolReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsSymbolReader.cs @@ -1,35 +1,415 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; +using SixLabors.ImageSharp.Formats.Jxl.IO; using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; -internal sealed class JxlAnsSymbolReader +internal sealed class JxlAnsSymbolReader : IDisposable { private const int MaxCheckpointInterval = 512; - // Use class because the Lz77Window property uses 2KB memory - private sealed class Checkpoint + private const int WindowMask = JxlAnsReader.WindowSize - 1; + + private readonly JxlAnsEntry[] aliasTables = []; + private readonly List huffmanDecoders = []; + private readonly bool usePrefixCode; + private uint state = JxlAnsConstants.AnsTableSize << 16; + private readonly List configs = []; + private readonly int logAlphaSize; + private readonly int logEntrySize; + private readonly int entrySizeMinus1; + private int numDecoded; + private int numToCopy; + private int copyPos; + private readonly int lz77Context; + private readonly int lz77MinLength; + private readonly int lz77Threshold = 1 << 20; + private readonly int numSpecialDistances; + private readonly JxlAnsHybridUIntConfiguration lz77LengthUInt = new(); + private readonly int[]? lz77Window; + private readonly int[] specialDistances = new int[JxlAnsReader.NumSpecialDistances]; + + private JxlAnsSymbolReader(JxlAnsCode code, JxlBitReader br, int distanceMultiplier, int[]? lz77WindowStorage) { - public uint State { get; set; } + this.aliasTables = code.AliasTables!; + this.huffmanDecoders = code.HuffmanData; + this.usePrefixCode = code.UsePrefixCode; + this.configs = code.UIntConfig; - public uint NumToCopy { get; set; } + if (!this.usePrefixCode) + { + this.state = br.ReadBits32(32u); + this.logAlphaSize = code.LogAlphaSize; + this.logEntrySize = JxlAnsConstants.AnsLogTableSize - code.LogAlphaSize; + this.entrySizeMinus1 = (1 << this.logEntrySize) - 1; + } + else + { + this.state = JxlAnsConstants.AnsSignature << 16; + } - public uint CopyPos { get; set; } + if (!code.Lz77.Enabled) + { + return; + } - public uint NumDecoded { get; set; } + this.lz77Window = lz77WindowStorage; + this.lz77Context = code.Lz77.NonserializedDistanceContext; + this.lz77LengthUInt = code.Lz77.LengthUintConfig; + this.lz77Threshold = (int)code.Lz77.MinimumSymbol; + this.lz77MinLength = (int)code.Lz77.MinimumLength; + this.numSpecialDistances = distanceMultiplier == 0 ? 0 : JxlAnsReader.NumSpecialDistances; - public uint[] Lz77Window { get; set; } = new uint[MaxCheckpointInterval]; + for (int i = 0; i < this.numSpecialDistances; i++) + { + this.specialDistances[i] = JxlAnsReader.SpecialDistance(i, distanceMultiplier); + } } - private readonly JxlAnsEntry[] aliasTables = []; - private JxlHuffmanDecodingData huffmanData; - private bool usePrefixCode; - private uint state = AnsSignature << 16u; - private List configs = []; - private int logAlphaSize; - private int logEntrySize; - private int entrySizeMinus1; + public bool IsHuffRleOnly + { + get + { + if (this.lz77Window is null || !this.usePrefixCode) + { + return false; + } + + JxlHuffmanDecoder huffDecoder = this.huffmanDecoders[this.lz77Context]; + + for (int i = 0; i < JxlHuffmanDecoder.HuffmanTableBits; i++) + { + ref JxlHuffmanCode code = ref huffDecoder.Table[i]; + + if (code.Bits > 0 || code.Value != 1) + { + return false; + } + } + + if (this.configs[this.lz77Context].SplitToken > 1) + { + return false; + } + + return true; + } + } + + public bool UsesLz77 => this.lz77Window is not null; + + public static JxlAnsSymbolReader Create(JxlAnsCode code, JxlBitReader br, int distanceMultiplier) + { + int[]? lz77 = code.Lz77.Enabled + ? ArrayPool.Shared.Rent(JxlAnsReader.WindowSize) + : null; + + return new JxlAnsSymbolReader(code, br, distanceMultiplier, lz77); + } + + public bool CheckAnsFinalState() => this.state == JxlAnsConstants.AnsTableSize << 16; + + public int ReadSymbolAnsWithoutRefill(int histogramIndex, JxlBitReader br) + { + uint res = this.state & (JxlAnsConstants.AnsTableSize - 1); + + Span table = this.aliasTables.AsSpan(0, histogramIndex << this.logAlphaSize); + JxlAnsSymbol symbol = JxlAnsHelper.Lookup(table, (int)res, this.logEntrySize, this.entrySizeMinus1); + + this.state = (uint)((symbol.Frequency * (this.state >> JxlAnsConstants.AnsLogTableSize)) + symbol.Offset); + + bool shouldNormalize = this.state < (1 << 16); + if (shouldNormalize) + { + // New state to normalize is 16 bits + uint newState = (this.state << 16) | br.ReadBits32(16u); + this.state = newState; + } + + uint nextRes = this.state & (JxlAnsConstants.AnsTableSize - 1); + JxlAnsHelper.Prefetch(table, nextRes, this.logEntrySize); + + return symbol.Value; + } + + public int ReadSymbolHuffmanWithoutRefill(int histogramIndex, JxlBitReader br) + => this.huffmanDecoders[histogramIndex].ReadSymbol(br); + + public int ReadSymbolWithoutRefill(int histogramIndex, JxlBitReader br) + { + if (this.usePrefixCode) + { + return this.ReadSymbolHuffmanWithoutRefill(histogramIndex, br); + } + else + { + return this.ReadSymbolAnsWithoutRefill(histogramIndex, br); + } + } + + public int ReadSymbol(int histogramIndex, JxlBitReader br) + { + br.Refill(); + return this.ReadSymbolWithoutRefill(histogramIndex, br); + } + + public static int ReadHybridUIntConfiguration(JxlAnsHybridUIntConfiguration config, int token, JxlBitReader br) + { + uint splitToken = config.SplitToken; + uint msbInToken = config.MsbInToken; + uint lsbInToken = config.LsbInToken; + uint splitExponent = config.SplitToken; + + if (token < splitToken) + { + return token; + } + + uint numBits = splitExponent - (msbInToken + lsbInToken) + + (uint)((token - splitToken) >> (int)(msbInToken + lsbInToken)); + numBits &= 31u; + + int low = token & ((1 << (int)lsbInToken) - 1); + token >>= (int)lsbInToken; + + uint bits = br.ReadBits32(numBits); + + int msbInToken1Lsh = 1 << (int)msbInToken; + uint result = (uint)(((((msbInToken1Lsh | (token & (msbInToken1Lsh - 1))) + << (int)numBits) | (int)bits) + << (int)lsbInToken) | low); + + return (int)result; + } + + public void ReadHybridUIntClusteredHuffRleOnly(int context, JxlBitReader br, ref int value, ref int run) + { + DebugGuard.IsTrue(this.IsHuffRleOnly, "This method is meant for huffman RLE only"); + br.Refill(); + + int token = this.ReadSymbolHuffmanWithoutRefill(context, br); + if (token >= this.lz77Threshold) + { + run = + ReadHybridUIntConfiguration(this.lz77LengthUInt, token - this.lz77Threshold, br) + + this.lz77MinLength - 1; + } + + value = ReadHybridUIntConfiguration(this.configs[context], token, br); + } + + public int ReadHybridUIntClustered(bool usesLz77, int context, JxlBitReader br) + { + int ret; + + if (usesLz77) + { + if (this.numToCopy > 0) + { + ret = this.lz77Window![(this.copyPos++) & WindowMask]; + this.numToCopy--; + this.lz77Window[(this.numDecoded++) & WindowMask] = ret; + return ret; + } + } + + br.Refill(); + int token = this.ReadSymbolWithoutRefill(context, br); + if (usesLz77) + { + if (token >= this.lz77Threshold) + { + this.numToCopy = ReadHybridUIntConfiguration(this.lz77LengthUInt, token - this.lz77Threshold, br) + this.lz77MinLength; + br.Refill(); + + int d_token = this.ReadSymbolWithoutRefill(this.lz77Context, br); + int distance = ReadHybridUIntConfiguration(this.configs[this.lz77Context], d_token, br); + + if (distance < this.numSpecialDistances) + { + distance = this.specialDistances[distance]; + } + else + { + distance = distance + 1 - this.numSpecialDistances; + } + + if (distance > this.numDecoded) + { + distance = this.numDecoded; + } + + if (distance > JxlAnsReader.WindowSize) + { + distance = JxlAnsReader.WindowSize; + } + + this.copyPos = this.numDecoded - distance; + + if (distance == 0) + { + if (this.lz77Window is null) + { + throw new InvalidOperationException("Missing LZ77 window"); + } + + int toFill = Math.Min(this.numToCopy, JxlAnsReader.WindowSize); + this.lz77Window.AsSpan(0, toFill).Clear(); + } + + if (this.numToCopy < this.lz77MinLength) + { + this.numToCopy = 0; + throw new InvalidOperationException("ANS decoder error"); + } + + ret = this.lz77Window![(this.copyPos++) & WindowMask]; + this.numToCopy--; + this.lz77Window[(this.numDecoded++) & WindowMask] = ret; + return ret; + } + } + + ret = ReadHybridUIntConfiguration(this.configs[context], token, br); + + if (usesLz77 && this.lz77Window is not null) + { + this.lz77Window[(this.numDecoded++) & WindowMask] = ret; + } + + return ret; + } + + public int ReadHybridUIntInlined(bool usesLz77, int context, JxlBitReader br, Span contextMap) + => this.ReadHybridUIntClustered(usesLz77, contextMap[context], br); + + public int ReadHybridUIntInlined(bool usesLz77, int context, JxlBitReader br, List contextMap) + => this.ReadHybridUIntClustered(usesLz77, contextMap[context], br); + + public int ReadHybridUInt(int context, JxlBitReader br, Span contextMap) + => this.ReadHybridUIntClustered(true, contextMap[context], br); + + public int ReadHybridUInt(int context, JxlBitReader br, List contextMap) + => this.ReadHybridUIntClustered(true, contextMap[context], br); + + public bool IsSingleValueAndAdvance(int context, ref int value, int count) + { + if (this.usePrefixCode || this.numToCopy != 0) + { + return false; + } + + uint res = this.state & (JxlAnsConstants.AnsTableSize - 1); + Span table = this.aliasTables.AsSpan()[(context << this.logAlphaSize)..]; + JxlAnsSymbol symbol = JxlAnsHelper.Lookup(table, (int)res, this.logEntrySize, this.entrySizeMinus1); + + if (symbol.Frequency != JxlAnsConstants.AnsTableSize || + this.configs[context].SplitToken <= symbol.Value || + symbol.Value >= this.lz77Threshold) + { + return false; + } + + value = symbol.Value; + + if (this.lz77Window is not null) + { + for (int i = 0; i < count; i++) + { + this.lz77Window[(this.numDecoded++) & WindowMask] = symbol.Value; + } + } + + return true; + } + + public void Save(Checkpoint checkpoint) + { + checkpoint.State = this.state; + checkpoint.NumDecoded = this.numDecoded; + checkpoint.NumToCopy = this.numToCopy; + checkpoint.CopyPos = this.copyPos; + + if (this.lz77Window is not null) + { + int windowStart = this.numDecoded & WindowMask; + int windowEnd = (this.numDecoded + MaxCheckpointInterval) & WindowMask; + + if (windowEnd > windowStart) + { + this.lz77Window.AsSpan(windowStart, windowEnd - windowStart) + .CopyTo(this.lz77Window); + } + else + { + this.lz77Window.AsSpan(windowStart, JxlAnsReader.WindowSize - windowStart) + .CopyTo(this.lz77Window); + + this.lz77Window.AsSpan(0, windowEnd) + .CopyTo(this.lz77Window.AsSpan(JxlAnsReader.WindowSize - windowStart, windowEnd)); + } + } + } + + private void Restore(Checkpoint checkpoint) + { + this.state = checkpoint.State; + + if (this.numDecoded > checkpoint.NumDecoded + MaxCheckpointInterval) + { + throw new InvalidOperationException("numDecoded is too large"); + } + + this.numDecoded = checkpoint.NumDecoded; + this.numToCopy = checkpoint.NumToCopy; + this.copyPos = checkpoint.CopyPos; + + if (this.lz77Window is not null) + { + int windowStart = this.numDecoded & WindowMask; + int windowEnd = (this.numDecoded + MaxCheckpointInterval) & WindowMask; + + if (windowEnd > windowStart) + { + checkpoint.Lz77Window.AsSpan(0, windowEnd - windowStart) + .CopyTo(this.lz77Window.AsSpan(windowStart)); + } + else + { + int firstLength = JxlAnsReader.WindowSize - windowStart; + + checkpoint.Lz77Window.AsSpan(0, firstLength) + .CopyTo(this.lz77Window.AsSpan(windowStart)); + + checkpoint.Lz77Window.AsSpan(firstLength, windowEnd) + .CopyTo(this.lz77Window); + } + } + } + + public void Dispose() + { + if (this.lz77Window is not null) + { + ArrayPool.Shared.Return(this.lz77Window); + } + } + + // Use class because the Lz77Window property uses 2KB memory + public sealed class Checkpoint + { + public uint State { get; set; } + + public int NumToCopy { get; set; } + + public int CopyPos { get; set; } + + public int NumDecoded { get; set; } + + public int[] Lz77Window { get; set; } = new int[MaxCheckpointInterval]; + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlHuffmanDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlHuffmanDecoder.cs index 7201a511aa..b42bd79282 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlHuffmanDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlHuffmanDecoder.cs @@ -15,7 +15,7 @@ internal sealed class JxlHuffmanDecoder /// /// Number of bits that a huffman table uses. /// - private const int HuffmanTableBits = 8; + public const int HuffmanTableBits = 8; private const int GoalSize = 1 << HuffmanTableBits; @@ -34,6 +34,10 @@ internal sealed class JxlHuffmanDecoder new(2, 0), new(2, 4), new(2, 3), new(3, 2), new(2, 0), new(2, 4), new(2, 3), new(4, 5), ]; + public JxlHuffmanDecoder() + { + } + private static ReadOnlySpan CodeLengthCodeOrder => [ 1, 2, 3, 4, 0, 5, 17, 6, 16, 7, 8, 9, 10, 11, 12, 13, 14, 15, diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs index 47e685bdfc..9621d6d608 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Net.NetworkInformation; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -9,8 +8,6 @@ using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; using SixLabors.ImageSharp.Formats.Jxl.Processing.Noise; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; -using SixLabors.ImageSharp.Formats.Tiff.Utils; -using static System.Net.Mime.MediaTypeNames; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/LowMemoryRenderPipeline.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/LowMemoryRenderPipeline.cs new file mode 100644 index 0000000000..17704e6ccc --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/LowMemoryRenderPipeline.cs @@ -0,0 +1,9 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +// TODO: render_pipeline/low_memory_render_pipeline.cc +internal class LowMemoryRenderPipeline +{ +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/UpsamplingStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/UpsamplingStage.cs index fc605a660a..d4b28a1226 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/UpsamplingStage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/UpsamplingStage.cs @@ -3,6 +3,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; @@ -15,12 +16,53 @@ internal class UpsamplingStage : RenderPipelineStageBase private readonly int c; private readonly InlineArray3>> temp = default; - private void PreprocessRowImpl(Buffer2D> inputRows, int x0, int length, int threadId) + public UpsamplingStage(JxlCustomTransformData upsFactors, int c, int shift) { - Span colMin = MemoryMarshal.Cast(this.temp[0][threadId].Span); - Span colMax = MemoryMarshal.Cast(this.temp[1][threadId].Span); + this.c = c; + this.Settings = RenderPipelineStageConfiguration.CreateSymmetric(shift, 2); - InlineArray5> rows = default; - rows[0] = + Span kernel = this.kernel; + + Span weights = shift == 1 ? upsFactors.Upsampling2Weights + : shift == 2 ? upsFactors.Upsampling4Weights + : upsFactors.Upsampling8Weights; + + int n = 1 << shift; + int h = n / 2; + + for (int ky = 0; ky < h; ++ky) + { + for (int kx = 0; kx < h; ++kx) + { + int offset0 = ((ky * n) + kx) * 25; + int offset1 = ((ky * n) + (n - 1 - kx)) * 25; + int offset2 = (((n - 1 - ky) * n) + kx) * 25; + int offset3 = (((n - 1 - ky) * n) + (n - 1 - kx)) * 25; + + for (int py = 0; py < 5; ++py) + { + for (int px = 0; px < 5; ++px) + { + int j = (5 * ky) + py; + int i = (5 * kx) + px; + int my = Math.Min(i, j); + int mx = Math.Max(i, j); + float w = weights[(5 * h * my) - (my * (my - 1) / 2) + mx - my]; + + kernel[offset0 + (py * 5) + px] = w; + kernel[offset1 + (py * 5) + (4 - px)] = w; + kernel[offset2 + ((4 - py) * 5) + px] = w; + kernel[offset3 + ((4 - py) * 5) + (4 - px)] = w; + } + } + } + } } + + public override string Name => "Upsample"; + + public override RenderPipelineChannelMode GetChannelMode(int channel) => + channel == this.c + ? RenderPipelineChannelMode.InOut + : RenderPipelineChannelMode.Ignored; } From b0f8ab88ce2c0448cfb7eef791db2fffaf4488e6 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 15 Oct 2026 07:17:01 +0400 Subject: [PATCH 205/328] Add AdjustQuantBlockAC to group encoder --- .../Jxl/Processing/Encoder/JxlGroupEncoder.cs | 314 +++++++++++++++++- .../Jxl/Processing/Encoder/JxlHeuristics.cs | 52 +++ .../Encoder/Modular/JxlModularFrameEncoder.cs | 38 --- .../Processing/Quantization/JxlQuantizer.cs | 2 +- 4 files changed, 365 insertions(+), 41 deletions(-) delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Modular/JxlModularFrameEncoder.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs index 3f34d0881f..be399afb3e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs @@ -1,15 +1,20 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; + namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; internal static class JxlGroupEncoder { - private const double QuantNormalizer = 2.2942708343284721; + private const float QuantNormalizer = 2.2942708343284721f; private static ReadOnlySpan Limit => [0.46, 0.46, 0.46, 0.46]; - private static ReadOnlySpan Multipliers => [0.9999, 0.9999, 0.9999, 0.9999]; + private static ReadOnlySpan Multipliers => [0.9999f, 0.9999f, 0.9999f, 0.9999f]; private static readonly double[,] Mul1Data = { @@ -62,4 +67,309 @@ internal static class JxlGroupEncoder private static readonly DenseMatrix Mul1 = new(Mul1Data); private static readonly DenseMatrix Mul2 = new(Mul2Data); + public static void QuantizeBlockAc( + JxlQuantizer quantizer, + int c, + float qmMultiplier, + JxlAcStrategyType quantKind, + int width, + int height, + Span thresholds, + Span blockIn, + int quant, + Span blockOut) + { + ReadOnlySpan qm = quantizer.InverseDequantMatrix(quantKind, c); + float quantAc = quantizer.Scale * quant; + + if (c != 1 && width * height >= 4) + { + float sub = 0.00744f * width * width; + + for (int i = 0; i < 4; i++) + { + thresholds[i] -= sub; + + if (thresholds[i] < 0.5f) + { + thresholds[i] = 0.5f; + } + } + } + + // We need 8 floats at once; reference software + // uses a fixed-size of 8 items in the vector (HWY_CAPPED(float, 8)). + // TODO: can we update this to use Vector? + Vector256 quantV = Vector256.Create(quantAc * qmMultiplier); + + int yGeTarget = height * 8 / 2; + int xGeTarget = width * 8 / 2; + + for (int y = 0; y < height * 8; y++) + { + int yFix = (y >= yGeTarget ? 1 : 0) * 2; + int off = (y * 8) + width; + + for (int x = 0; x < width * 8; x += 8) + { + Vector256 threshold = Vector256.Zero; + + if (width == 1) + { + // 🐛: in reference software, it seems like + // this: + // Load(kMask + x) + // has a potential for a program crash. + // https://github.com/libjxl/libjxl/blob/ef67fde2ec16d52e644c5a0969230b9a85e3eb31/lib/jxl/enc_group.cc#L85 + // x can be > 0 + // and there's 8 items to load, with kMask itself having 8 items + // So if x is 1 or higher and it tries loading kMask[1..], then there's only + // 7 items remaining - kaboom 💥 + // So we're not implementing this. Seems like a bug. + throw new InvalidOperationException("width=1 not supported"); + } + else + { + threshold = Vector256.Create(thresholds[yFix + (x >= xGeTarget ? 1 : 0)]); + } + + Vector256 q = Vector256.Create(qm[(off + x)..]) * quantV; + Vector256 input = Vector256.Create(blockIn[(off + x)..]); + Vector256 value = q * input; + Vector256 nonZeroMask = Vector256.GreaterThanOrEqual(Vector256.Abs(value), threshold); + Vector256 v = Vector256.ConvertToInt32Native(Vector256.ConditionalSelect(nonZeroMask, Vector256.Round(value), Vector256.Zero)); + v.CopyTo(blockOut[(off + x)..]); + } + } + } + + public static void AdjustQuantBlockAC( + JxlQuantizer quantizer, + int c, + float qmMultiplier, + JxlAcStrategyType quantKind, + int width, + int height, + Span thresholds, + Span blockIn, + ref int quant) + { + const int partialBlockKinds = + (1 << (int)JxlAcStrategyType.IDENTITY) | + (1 << (int)JxlAcStrategyType.DCT2X2) | + (1 << (int)JxlAcStrategyType.DCT4X4) | + (1 << (int)JxlAcStrategyType.DCT4X8) | + (1 << (int)JxlAcStrategyType.DCT8X4) | + (1 << (int)JxlAcStrategyType.AFV0) | + (1 << (int)JxlAcStrategyType.AFV1) | + (1 << (int)JxlAcStrategyType.AFV2) | + (1 << (int)JxlAcStrategyType.AFV3); + + if (((1 << (int)quantKind) & partialBlockKinds) != 0) + { + return; + } + + ReadOnlySpan qm = quantizer.InverseDequantMatrix(quantKind, c); + float qac = quantizer.Scale * quant; + + if (width > 1 || height > 1) + { + for (int i = 0; i < 4; ++i) + { + thresholds[i] -= Math.Clamp(0.003f * width * height, 0f, 0.08f); + + if (thresholds[i] < 0.54) + { + thresholds[i] = 0.54f; + } + } + } + + float sumOfHighestFreqRowAndColumn = 0; + float sumOfError = 0; + float sumOfVals = 0; + + InlineArray4 hfNonZeros = default; + InlineArray4 hfMaxError = default; + + for (int y = 0; y < height * JxlFrameDimensions.BlockDimensions; y++) + { + for (int x = 0; x < width * JxlFrameDimensions.BlockDimensions; x++) + { + int pos = (y * JxlFrameDimensions.BlockDimensions * width) + x; + if (x < width && y < height) + { + continue; + } + + int hfix = ((y >= height * JxlFrameDimensions.BlockDimensions / 2 ? 1 : 0) * 2) + (x >= width * JxlFrameDimensions.BlockDimensions / 2 ? 1 : 0); + + float val = blockIn[pos] * (qm[pos] * qac * qmMultiplier); + float v = (MathF.Abs(val) < thresholds[hfix]) ? 0 : MathF.Round(val, MidpointRounding.ToEven); + float error = MathF.Abs(val - v); + sumOfError += error; + + sumOfVals += MathF.Abs(v); + + if (c == 1 && v == 0) + { + if (hfMaxError[hfix] < error) + { + hfMaxError[hfix] = error; + } + } + + if (v != 0.0f) + { + hfNonZeros[hfix] += MathF.Abs(v); + + bool inCorner = y >= 7 * height && x >= 7 * width; + bool onBorder = y == (height * JxlFrameDimensions.BlockDimensions) - 1 || x == (width * JxlFrameDimensions.BlockDimensions) - 1; + bool inLargerCorner = x >= 4 * width && y >= 4 * height; + + if (inCorner || (onBorder && inLargerCorner)) + { + sumOfHighestFreqRowAndColumn += MathF.Abs(val); + } + } + } + } + + if (c == 1 && sumOfVals * 8 < width * height) + { + int originalQuant = quant; + int newQuant = quant; + + for (int i = 1; i < 4; ++i) + { + if (hfNonZeros[i] == 0.0 && hfMaxError[i] > Limit[i]) + { + newQuant = originalQuant + 1; + break; + } + } + + quant = newQuant; + + if (hfNonZeros[3] == 0.0 && hfMaxError[3] > Limit[3]) + { + thresholds[3] = Multipliers[3] * hfMaxError[3] * newQuant / originalQuant; + } + else if ((hfNonZeros[1] == 0.0 && hfMaxError[1] > Limit[1]) || (hfNonZeros[2] == 0.0 && hfMaxError[2] > Limit[2])) + { + thresholds[1] = Multipliers[1] * Math.Max(hfMaxError[1], hfMaxError[2]) * newQuant / originalQuant; + thresholds[2] = thresholds[1]; + } + else if (hfNonZeros[0] == 0.0 && hfMaxError[0] > Limit[0]) + { + thresholds[0] = Multipliers[0] * hfMaxError[0] * newQuant / originalQuant; + } + } + + { + float all = hfNonZeros[0] + hfNonZeros[1] + hfNonZeros[2] + hfNonZeros[3] + 1; + ReadOnlySpan multipliers = [70f, 30f, 60f]; + + if (multipliers[c] * sumOfHighestFreqRowAndColumn >= all) + { + quant += (int)(multipliers[c] * sumOfHighestFreqRowAndColumn / all); + + if (quant >= JxlQuantizer.MaxQuant) + { + quant = JxlQuantizer.MaxQuant - 1; + } + } + } + + if (quantKind == JxlAcStrategyType.DCT) + { + // If this 8x8 block is too flat, increase the adaptive quantization level + // a bit to reduce visible block boundaries and requantize the block. + if (hfNonZeros[0] + hfNonZeros[1] + hfNonZeros[2] + hfNonZeros[3] < 11) + { + quant++; + + if (quant >= JxlQuantizer.MaxQuant) + { + quant = JxlQuantizer.MaxQuant - 1; + } + } + } + + { + sumOfError *= QuantNormalizer; + sumOfVals *= QuantNormalizer; + + if (quantKind >= JxlAcStrategyType.DCT16X16) + { + int ix = 3; + if (quantKind is JxlAcStrategyType.DCT32X16 or JxlAcStrategyType.DCT16X32) + { + ix = 1; + } + else if (quantKind == JxlAcStrategyType.DCT16X16) + { + ix = 0; + } + else if (quantKind == JxlAcStrategyType.DCT32X32) + { + ix = 2; + } + + int step = (int)(sumOfError / ((Mul1[ix, c] * width * height * JxlFrameDimensions.BlockDimensions * JxlFrameDimensions.BlockDimensions) + (Mul2[ix, c] * sumOfVals))); + + if (step >= 2) + { + step = 2; + } + + if (step < 0) + { + step = 0; + } + + if (sumOfError > (Mul1[ix, c] * width * height * JxlFrameDimensions.BlockDimensions * JxlFrameDimensions.BlockDimensions) + (Mul2[ix, c] * sumOfVals)) + { + quant += step; + + if (quant >= JxlQuantizer.MaxQuant) + { + quant = JxlQuantizer.MaxQuant - 1; + } + } + } + } + + { + // Reduce quant in highly active areas + int div = width * height; + float minHfNonZeroes = MathF.Min(hfNonZeros[0], MathF.Min(hfNonZeros[1], MathF.Min(hfNonZeros[2], hfNonZeros[3]))); + int activity = 15; + + if (minHfNonZeroes < 15.0f * div) + { + activity = ((int)minHfNonZeroes + (div / 2)) / div; + } + + int qp = quant - activity; + + if (c == 1) + { + for (int i = 1; i < 4; ++i) + { + thresholds[i] += 0.01f * activity; + } + } + + int originalQpLimit = Math.Max(4, quant / 2); + + if (qp < originalQpLimit) + { + qp = originalQpLimit; + } + + quant = qp; + } + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlHeuristics.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlHeuristics.cs index 46ec51d4e0..621a397060 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlHeuristics.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlHeuristics.cs @@ -25,6 +25,58 @@ internal static class JxlHeuristics 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, ]; + private static ReadOnlySpan Downsample2SharperKernel => + [ + -0.000314256996835f, -0.000314256996835f, -0.000897597057705f, + -0.000562751488849f, -0.000176807273646f, 0.001864627368902f, + 0.001864627368902f, -0.000176807273646f, -0.000562751488849f, + -0.000897597057705f, -0.000314256996835f, -0.000314256996835f, + -0.000314256996835f, -0.001527942804748f, -0.000121760530512f, + 0.000191123989093f, 0.010193185932466f, 0.058637519197110f, + 0.058637519197110f, 0.010193185932466f, 0.000191123989093f, + -0.000121760530512f, -0.001527942804748f, -0.000314256996835f, + -0.000897597057705f, -0.000121760530512f, 0.000946363683751f, + 0.007113577630288f, 0.000437956841058f, -0.000372823835211f, + -0.000372823835211f, 0.000437956841058f, 0.007113577630288f, + 0.000946363683751f, -0.000121760530512f, -0.000897597057705f, + -0.000562751488849f, 0.000191123989093f, 0.007113577630288f, + 0.044592622228814f, 0.000222278879007f, -0.162864473015945f, + -0.162864473015945f, 0.000222278879007f, 0.044592622228814f, + 0.007113577630288f, 0.000191123989093f, -0.000562751488849f, + -0.000176807273646f, 0.010193185932466f, 0.000437956841058f, + 0.000222278879007f, -0.000913092543974f, -0.017071696107902f, + -0.017071696107902f, -0.000913092543974f, 0.000222278879007f, + 0.000437956841058f, 0.010193185932466f, -0.000176807273646f, + 0.001864627368902f, 0.058637519197110f, -0.000372823835211f, + -0.162864473015945f, -0.017071696107902f, 0.414660099370354f, + 0.414660099370354f, -0.017071696107902f, -0.162864473015945f, + -0.000372823835211f, 0.058637519197110f, 0.001864627368902f, + 0.001864627368902f, 0.058637519197110f, -0.000372823835211f, + -0.162864473015945f, -0.017071696107902f, 0.414660099370354f, + 0.414660099370354f, -0.017071696107902f, -0.162864473015945f, + -0.000372823835211f, 0.058637519197110f, 0.001864627368902f, + -0.000176807273646f, 0.010193185932466f, 0.000437956841058f, + 0.000222278879007f, -0.000913092543974f, -0.017071696107902f, + -0.017071696107902f, -0.000913092543974f, 0.000222278879007f, + 0.000437956841058f, 0.010193185932466f, -0.000176807273646f, + -0.000562751488849f, 0.000191123989093f, 0.007113577630288f, + 0.044592622228814f, 0.000222278879007f, -0.162864473015945f, + -0.162864473015945f, 0.000222278879007f, 0.044592622228814f, + 0.007113577630288f, 0.000191123989093f, -0.000562751488849f, + -0.000897597057705f, -0.000121760530512f, 0.000946363683751f, + 0.007113577630288f, 0.000437956841058f, -0.000372823835211f, + -0.000372823835211f, 0.000437956841058f, 0.007113577630288f, + 0.000946363683751f, -0.000121760530512f, -0.000897597057705f, + -0.000314256996835f, -0.001527942804748f, -0.000121760530512f, + 0.000191123989093f, 0.010193185932466f, 0.058637519197110f, + 0.058637519197110f, 0.010193185932466f, 0.000191123989093f, + -0.000121760530512f, -0.001527942804748f, -0.000314256996835f, + -0.000314256996835f, -0.000314256996835f, -0.000897597057705f, + -0.000562751488849f, -0.000176807273646f, 0.001864627368902f, + 0.001864627368902f, -0.000176807273646f, -0.000562751488849f, + -0.000897597057705f, -0.000314256996835f, -0.000314256996835f + ]; + public static void FindBestBlockEntropyModel(JxlCompressParameters cparameters, JxlImageI rqf, JxlAcStrategyImage acStrategy, JxlBlockContextMap blockCtxMap) { if (cparameters.DecodingSpeedTier >= 1) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Modular/JxlModularFrameEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Modular/JxlModularFrameEncoder.cs deleted file mode 100644 index a5c42b8342..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Modular/JxlModularFrameEncoder.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; -using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Modular; - -internal class JxlModularFrameEncoder -{ - public static void QuantizeChannel(JxlModularChannel ch, int q) - { - if (q == 1) - { - // No quantization - q=1 keeps values as-is. - return; - } - - JxlImageI plane = ch.Plane; - - for (int y = 0; y < plane.YSize; y++) - { - Span row = plane.GetRow(y); - - for (int x = 0; x < plane.XSize; x++) - { - if (row[x] < 0) - { - row[x] = -((-row[x] + (q / 2)) / q) * q; - } - else - { - row[x] = ((row[x] + (q / 2)) / q) * q; - } - } - } - } -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs index 5788160141..68ff16318f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs @@ -49,7 +49,7 @@ internal sealed class JxlQuantizer /// The maximum value for a quant. Quant cannot be greater than this - /// if attempted to, it will be limited to this value. /// - private const int MaxQuant = 256; + public const int MaxQuant = 256; /// /// Represents the multipliers for the DC coefficients. From 3d9978dbcaa2315f3fd776c1138b3d2ea8572031 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 15 Oct 2026 08:22:51 +0400 Subject: [PATCH 206/328] Complete chroma from luma encoder --- .../JxlChromaFromLumaEncoder.cs | 295 ++++++++++++++++++ .../Encoder/Heuristics/JxlCflHeuristics.cs | 92 ++++++ .../Encoder/{ => Heuristics}/JxlHeuristics.cs | 58 +++- .../Jxl/Processing/Encoder/JxlBitWriter.cs | 5 + 4 files changed, 449 insertions(+), 1 deletion(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlCflHeuristics.cs rename src/ImageSharp/Formats/Jxl/Processing/Encoder/{ => Heuristics}/JxlHeuristics.cs (91%) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlChromaFromLumaEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlChromaFromLumaEncoder.cs index bd61563e05..e1b98b45c9 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlChromaFromLumaEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlChromaFromLumaEncoder.cs @@ -2,7 +2,13 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using SixLabors.ImageSharp.Formats.Jxl.Cms; +using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.ChromaFromLuma; @@ -24,4 +30,293 @@ public static JxlImageF InitializeDcStorage(Configuration configuration, int num return dcValues; } + + public static int FindBestMultiplier(Span valuesM, Span valuesS, int num, float baseValue, float distanceMul, bool fast) + { + if (num == 0) + { + return 0; + } + + float x; + + if (fast) + { + const float inverseColorFactor = 1.0f / JxlChromaFromLuma.DefaultColorFactor; + + Vector ca = Vector.Zero; + Vector cb = Vector.Zero; + Vector invColorFactor = new(inverseColorFactor); + Vector baseV = new(baseValue); + + int i = 0; + int lanes = Vector.Count; + + for (; i + lanes <= num; i += lanes) + { + Vector m = new(valuesM[i..]); + Vector s = new(valuesS[i..]); + + Vector a = invColorFactor * m; + Vector b = (baseV * m) - s; + + ca += a * a; + cb += a * b; + } + + float sumCa = Vector.Sum(ca); + float sumCb = Vector.Sum(cb); + + for (; i < num; i++) + { + float m = valuesM[i]; + float s = valuesS[i]; + float a = inverseColorFactor * m; + float b = (baseValue * m) - s; + + sumCa += a * a; + sumCb += a * b; + } + + x = -sumCb / (sumCa + (num * distanceMul * 0.5f)); + } + else + { + const float eps = 100.0f; + const float clamp = 20.0f; + + JxlCflFunction fn = new(valuesM, valuesS, num, baseValue, distanceMul); + x = 0; + + for (int i = 0; i < 20; i++) + { + float dFPeps = 0; + float dFMeps = 0; + float dF = fn.Compute(x, eps, ref dFPeps, ref dFMeps); + + float ddF = (dFPeps - dFMeps) / (2.0f * eps); + const float experimentalInsignificantStabilizer = 0.85f; + + float step = dF / (ddF + experimentalInsignificantStabilizer); + + x -= MathF.Min(clamp, MathF.Max(-clamp, step)); + + if (MathF.Abs(step) < 3e-3f) + { + break; + } + } + } + + const float towardsZero = 2.6f; + + if (x >= towardsZero) + { + x -= towardsZero; + } + else if (x <= -towardsZero) + { + x += towardsZero; + } + else + { + x = 0; + } + + return (int)Math.Clamp(MathF.Round(x), -128.0f, 127.0f); + } + + public static void ComputeTile(JxlImage3F opsin, Rectangle opsinRect, JxlDequantMatrices dequant, JxlAcStrategyImage? acStrategy, JxlImageI? rawQuantField, JxlQuantizer quantizer, Rectangle rect, bool fast, bool useDct8, JxlImageSB mapX, JxlImageSB mapB, JxlImageF dcValues, Span mem) + { + int widthBlocks = opsinRect.Width / JxlFrameDimensions.BlockDimensions; + const float distanceMultiplierAC = 1e-9f; + int dctScratchSize = 3 * (JxlSimdUtils.MaxVectorSize / sizeof(float)) * JxlAcStrategy.MaximumBlockDimension; + + int y0 = rect.Y0(); + int x0 = rect.X0(); + int x1 = rect.X0() + rect.Width; + int y1 = rect.Y0() + rect.Height; + + int ty = y0 / JxlChromaFromLuma.ColorTileDimensionInBlocks; + int tx = x0 / JxlChromaFromLuma.ColorTileDimensionInBlocks; + + Span rowOutX = mapX.GetRow(ty); + Span rowOutB = mapB.GetRow(ty); + + Span dcValuesYX = dcValues.GetRow(0); + Span dcValuesX = dcValues.GetRow(1); + Span dcValuesYB = dcValues.GetRow(2); + Span dcValuesB = dcValues.GetRow(3); + + Span blockY = mem; + Span blockX = blockY[JxlAcStrategy.MaximumCoefficientArea..]; + Span blockB = blockX[JxlAcStrategy.MaximumCoefficientArea..]; + + mem = mem[(3 * JxlAcStrategy.MaximumCoefficientArea)..]; + + Span coeffsYX = mem; + Span coeffsX = coeffsYX[(JxlChromaFromLuma.ColorTileDimension * JxlChromaFromLuma.ColorTileDimension)..]; + Span coeffsYB = coeffsX[(JxlChromaFromLuma.ColorTileDimension * JxlChromaFromLuma.ColorTileDimension)..]; + Span coeffsB = coeffsYB[(JxlChromaFromLuma.ColorTileDimension * JxlChromaFromLuma.ColorTileDimension)..]; + + mem = mem[(4 * JxlAcStrategy.MaximumCoefficientArea)..]; + + const int dcSize = JxlAcStrategy.MaximumCoefficientBlocks * JxlAcStrategy.MaximumCoefficientBlocks; + + Span dcY = mem; + Span dcX = dcY[dcSize..]; + Span dcB = dcX[dcSize..]; + + mem = mem[(3 * dcSize)..]; + + Span scratchSpace = mem; + + if (mem.Length != (2 * JxlAcStrategy.MaximumCoefficientArea) + dctScratchSize) + { + throw new InvalidOperationException("Invalid memory length"); + } + + int numAC = 0; + + for (int y = y0; y < y1; y++) + { + Span rowY = opsin.PlaneRow(opsinRect, 1, y * JxlFrameDimensions.BlockDimensions); + Span rowX = opsin.PlaneRow(opsinRect, 0, y * JxlFrameDimensions.BlockDimensions); + Span rowB = opsin.PlaneRow(opsinRect, 2, y * JxlFrameDimensions.BlockDimensions); + int stride = opsin.PixelsPerRow; + + for (int x = x0; x < x1; x++) + { + JxlAcStrategy acs = useDct8 + ? new(JxlAcStrategyType.DCT) + : acStrategy!.GetRow(y)[x]; // useDct8 = acStrategy == null, so acStrategy isn't null here + + if (!acs.IsFirstBlock) + { + continue; + } + + int xs = acs.CoveredBlocksX; + + // Y + JxlTransformsEncoder.TransformFromPixels(acs.Strategy, rowY[(x * JxlFrameDimensions.BlockDimensions)..], stride, blockY, scratchSpace); + JxlTransformsEncoder.DcFromLowestFrequencies(acs.Strategy, blockY, dcY, xs, scratchSpace); + + // X + JxlTransformsEncoder.TransformFromPixels(acs.Strategy, rowX[(x * JxlFrameDimensions.BlockDimensions)..], stride, blockX, scratchSpace); + JxlTransformsEncoder.DcFromLowestFrequencies(acs.Strategy, blockX, dcX, xs, scratchSpace); + + // B + JxlTransformsEncoder.TransformFromPixels(acs.Strategy, rowB[(x * JxlFrameDimensions.BlockDimensions)..], stride, blockB, scratchSpace); + JxlTransformsEncoder.DcFromLowestFrequencies(acs.Strategy, blockB, dcB, xs, scratchSpace); + + Span qmX = dequant.GetInverseMatrix(acs.Strategy, 0); + Span qmB = dequant.GetInverseMatrix(acs.Strategy, 2); + float qDcX = useDct8 ? 1 : 1.0f / quantizer.GetInverseDcStep(0); + float qDcB = useDct8 ? 1 : 1.0f / quantizer.GetInverseDcStep(2); + + for (int iy = 0; iy < acs.CoveredBlocksY; iy++) + { + for (int ix = 0; ix < xs; ix++) + { + dcValuesYX[((iy + y) * widthBlocks) + ix + x] = dcY[(iy * xs) + ix] * qDcX; + dcValuesX[((iy + y) * widthBlocks) + ix + x] = dcX[(iy * xs) + ix] * qDcX; + dcValuesYB[((iy + y) * widthBlocks) + ix + x] = dcY[(iy * xs) + ix] * qDcB; + dcValuesB[((iy + y) * widthBlocks) + ix + x] = dcB[(iy * xs) + ix] * qDcB; + } + } + + if (acs.CoveredBlocksX + x0 > x1 || acs.CoveredBlocksY + y0 > y1) + { + continue; + } + + int cx = acs.CoveredBlocksX; + int cy = acs.CoveredBlocksY; + + JxlForwardCoefficientOrder.CoefficientLayout(ref cy, ref cx); + + for (int iy = 0; iy < cy; iy++) + { + for (int ix = 0; ix < cx; ix++) + { + blockY[(cx * JxlFrameDimensions.BlockDimensions * iy) + ix] = 0; + blockX[(cx * JxlFrameDimensions.BlockDimensions * iy) + ix] = 0; + blockB[(cx * JxlFrameDimensions.BlockDimensions * iy) + ix] = 0; + } + } + + float qq = rawQuantField is null ? 1.0f : rawQuantField.GetRow(y)[x]; + float strangeMultiplier = 128f; + + float q = useDct8 ? 1 : quantizer.Scale * strangeMultiplier * qq; + + Vector qv = Vector.Create(q); + for (int i = 0; i < cx * cy * 64; i += Vector.Count) + { + Vector bY = Vector.Create(blockY[i..]); + Vector bX = Vector.Create(blockX[i..]); + Vector bB = Vector.Create(blockB[i..]); + Vector qqmX = qv * Vector.Create(qmX[i..]); + Vector qqmB = qv * Vector.Create(qmB[i..]); + + (bY * qqmX).CopyTo(coeffsYX[numAC..]); + (bX * qqmX).CopyTo(coeffsX[numAC..]); + (bY * qqmB).CopyTo(coeffsYB[numAC..]); + (bB * qqmB).CopyTo(coeffsB[numAC..]); + + numAC += Vector.Count; + } + } + } + + if (numAC % Vector.Count != 0) + { + throw new InvalidOperationException("Number of AC wasn't aligned"); + } + + rowOutX[tx] = (sbyte)FindBestMultiplier(coeffsYX, coeffsX, numAC, 0f, distanceMultiplierAC, fast); + rowOutB[tx] = (sbyte)FindBestMultiplier(coeffsYB, coeffsB, numAC, JxlOpsinConstants.YToBRatio, distanceMultiplierAC, fast); + } + + public static bool ColorCorrelationEncodeDc(JxlColorCorrelation colorCorrelation, JxlBitWriter writer, JxlLayerType layer, JxlAuxiliaryOutput auxOut) + { + float colorFactor = colorCorrelation.ColorFactor; + float baseCorrelationX = colorCorrelation.BaseCorrelationX; + float baseCorrelationB = colorCorrelation.BaseCorrelationB; + int yToXDc = colorCorrelation.YToXDc; + int yToBDc = colorCorrelation.YToBDc; + + return writer.WithMaxBits(1 + (2 * JxlMath.BitsPerByte) + 12 + 32, layer, auxOut, () => + { + if (yToXDc == 0 && yToBDc == 0 && + colorFactor == JxlChromaFromLuma.DefaultColorFactor && baseCorrelationX == 0.0f && + baseCorrelationB == JxlOpsinConstants.YToBRatio) + { + writer.Write(1, 1); + return true; + } + + writer.Write(1, 0); + if (!JxlU32Coder.Write(JxlChromaFromLuma.ColorFactorDistribution, (uint)colorFactor, writer)) + { + return false; + } + + if (!JxlF16Coder.Write(baseCorrelationX, writer)) + { + return false; + } + + if (!JxlF16Coder.Write(baseCorrelationB, writer)) + { + return false; + } + + writer.Write(JxlMath.BitsPerByte, yToXDc - sbyte.MinValue); + writer.Write(JxlMath.BitsPerByte, yToBDc - sbyte.MinValue); + + return true; + }); + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlCflHeuristics.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlCflHeuristics.cs new file mode 100644 index 0000000000..759418dcd2 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlCflHeuristics.cs @@ -0,0 +1,92 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.ChromaFromLuma; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Heuristics; + +/// +/// Chroma from luma heuristics +/// +internal sealed class JxlCflHeuristics(Configuration configuration) : IDisposable +{ + // TODO + private Memory scratchSpace; + private JxlImageF? dcValues; + private IMemoryOwner? scratchSpaceMemoryOwner; + + private static int ItemsPerThread + { + get + { + int dctScratchSize = 3 * (JxlSimdUtils.MaxVectorSize / sizeof(float)) * JxlAcStrategy.MaximumBlockDimension; + int dcScratchSize = 3 * JxlAcStrategy.MaximumCoefficientBlocks * JxlAcStrategy.MaximumCoefficientBlocks; + + return (JxlAcStrategy.MaximumCoefficientArea * 3) + + (JxlChromaFromLuma.ColorTileDimension * JxlChromaFromLuma.ColorTileDimension * 4) + + (JxlAcStrategy.MaximumCoefficientArea * 2) + + dctScratchSize + dcScratchSize; + } + } + + public void PrepareForThreads(int numThreads) + { + int memBytes = numThreads * ItemsPerThread * sizeof(float); + + this.scratchSpaceMemoryOwner = configuration.MemoryAllocator.Allocate(memBytes); + this.scratchSpace = this.scratchSpaceMemoryOwner.Memory; + } + + public void Initialize(Rectangle rect) + { + int xSizeBlocks = rect.Width / JxlFrameDimensions.BlockDimensions; + int ySizeBlocks = rect.Height / JxlFrameDimensions.BlockDimensions; + + this.dcValues = JxlChromaFromLumaEncoder.InitializeDcStorage(configuration, xSizeBlocks * ySizeBlocks); + } + + public void ComputeTile( + Rectangle rectangle, + JxlImage3F opsin, + Rectangle opsinRect, + JxlDequantMatrices dequant, + JxlAcStrategyImage? acStrategy, + JxlImageI rawQuantField, + JxlQuantizer quantizer, + bool fast, + int thread, + JxlColorCorrelationMap cmap) + { + if (this.dcValues is null) + { + throw new InvalidOperationException("CfL heuristics not initialized; call .Initialize(Rectangle) first"); + } + + bool useDct8 = acStrategy is null; + int itemsPerThread = ItemsPerThread; + + Span scratch = MemoryMarshal.Cast(this.scratchSpace.Span).Slice(thread * itemsPerThread, itemsPerThread); + JxlChromaFromLumaEncoder.ComputeTile( + opsin, + opsinRect, + dequant, + acStrategy, + rawQuantField, + quantizer, + rectangle, + fast, + useDct8, + cmap.YToXMap!, + cmap.YToBMap!, + this.dcValues, + scratch); + } + + public void Dispose() => this.scratchSpaceMemoryOwner?.Dispose(); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlHeuristics.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs similarity index 91% rename from src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlHeuristics.cs rename to src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs index 621a397060..88b761d5e5 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlHeuristics.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs @@ -9,7 +9,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Heuristics; internal static class JxlHeuristics { @@ -420,6 +420,62 @@ private static void ElementwiseDivide(JxlPlane image1, JxlPlane image2, } } + public static void AntiUpsample(JxlImageF input, JxlImageF d) + { + int xsize = input.XSize; + int ysize = input.YSize; + int xsize2 = d.XSize; + int ysize2 = d.YSize; + int k0 = Size - 1; + int k1 = Size; + + for (int y2 = 0; y2 < ysize2; ++y2) + { + Span row = d.GetRow(y2); + + for (int x2 = 0; x2 < xsize2; ++x2) + { + int x0 = (x2 * 2) - k0; + if (x0 < 0) + { + x0 = 0; + } + + int x1 = (x2 * 2) + k1 + 1; + if (x1 > xsize) + { + x1 = xsize; + } + + int y0 = (y2 * 2) - k0; + if (y0 < 0) + { + y0 = 0; + } + + int y1 = (y2 * 2) + k1 + 1; + if (y1 > ysize) + { + y1 = ysize; + } + + float sum = 0; + for (int y = y0; y < y1; ++y) + { + Span rowIn = input.GetRow(y); + + for (int x = x0; x < x1; ++x) + { + float deriv = UpsamplerDerivative(x2, y2, x, y); + sum += deriv * rowIn[x]; + } + } + + row[x2] = sum; + } + } + } + private sealed class OccCounters : IDisposable { private readonly int[] qfCounts; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlBitWriter.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlBitWriter.cs index 924dac7c92..1096cf11b1 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlBitWriter.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlBitWriter.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; + namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; internal sealed class JxlBitWriter(Stream stream) @@ -69,6 +71,9 @@ public void ZeroPadToByte() this.Write((int)remainderBits, 0); } + public bool WithMaxBits(ulong maxBits, JxlLayerType layer, JxlAuxiliaryOutput, Func func) + => this.WithMaxBits(maxBits, func); + public bool WithMaxBits(ulong maxBits, Func func) { bool previousIsLimited = this.isLimited; From f8942699d1222efdb703fc4a2fea349316eff9df Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 15 Oct 2026 08:23:34 +0400 Subject: [PATCH 207/328] Formatting --- .../Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs index 88b761d5e5..4a865ab21d 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs @@ -402,10 +402,8 @@ private static void ElementwiseDivide(JxlPlane image1, JxlPlane image2, int width = image1.XSize; int height = image1.YSize; - if (width != image2.XSize || - width != output.XSize || - height != image2.YSize || - height != output.YSize) + if (width != image2.XSize || width != output.XSize || + height != image2.YSize || height != output.YSize) { throw new InvalidOperationException("Each plane must have equal sizes"); } From 1c4ec00c1432ca6a41e8a4b47528f29ccc67de15 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 15 Oct 2026 13:51:18 +0400 Subject: [PATCH 208/328] Add group decoder cache --- .../Decoder/Group/JxlGroupDecoder.cs | 11 +-- .../Decoder/Group/JxlGroupDecoderCache.cs | 94 ++++++++++++++++++- 2 files changed, 97 insertions(+), 8 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs index fcc2cbcf16..2c96040bd5 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs @@ -636,7 +636,7 @@ public static bool DecodeGroup( { groupDecCache.InitializeDcBufferOnce(); - JxlYCbCrChromaSubsampling cs = frameHeader.ChromaSubsampling; + JxlYCbCrChromaSubsampling cs = frameHeader.ChromaSubsampling!; for (int c = 0; c < 3; c++) { @@ -679,7 +679,7 @@ public static bool DecodeGroup( (dcPlane.XSize >> hs) - srcRect.X0(); - Span row = groupDecCache.DcBuffer.Row(y); + Span row = groupDecCache.DcBuffer.GetRow(y); for (int ix = 0; ix < 2; ix++) { @@ -826,12 +826,9 @@ public static bool DecodeGroupForRoundtrip( groupIndex, frameHeader.Passes.Shift); - if (!groupDecCache.InitializeOnce( + groupDecCache.InitializeOnce( numPasses: 0, - usedAcs: (1 << JxlAcStrategy.NumberOfValidStrategies) - 1)) - { - return false; - } + usedACStrategy: (1 << JxlAcStrategy.NumberOfValidStrategies) - 1); return DecodeGroupCore( frameHeader, diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoderCache.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoderCache.cs index 306256d7a2..1019219635 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoderCache.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoderCache.cs @@ -1,12 +1,104 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Group; /// /// Cache for the JPEG XL Group Decoder. /// -internal sealed class JxlGroupDecoderCache +internal sealed class JxlGroupDecoderCache(Configuration configuration) { + private IMemoryOwner? floatMemory; + private IMemoryOwner? int32Memory; + private IMemoryOwner? int16Memory; + private int maxBlockArea; + + public Memory DecodeGroupBlockScratchSpace { get; set; } + + public Memory DecodeGroupQBlockScratchSpace { get; set; } + + public Memory DecodeGroupQBlock16ScratchSpace { get; set; } + + /// + /// Gets or sets the scratch-space for . + /// + public Memory ScratchSpaceMemory { get; set; } + + /// + /// Gets or sets the number of non-zero coefficients. + /// + public JxlImage3I[] NumberOfNonZeroes { get; set; } = ArrayHelpers.CreateAndInitialize(JxlShared.MaximumNumberOfPasses, () => new JxlImage3I()); + + public JxlImageF DcBuffer { get; set; } = new(); + + public void InitializeDcBufferOnce() + { + if (this.DcBuffer.XSize == 0) + { + // When we do new() on a JxlImage of any type, + // its width/height is 0. So that way we know + // it's not initted. + this.DcBuffer = new( + configuration, + JxlFrameDimensions.GroupDimensionsInBlocks + (RenderPipelineStageBase.RenderPipelineXOffset * 2), + JxlFrameDimensions.GroupDimensionsInBlocks + 4); + } + } + + public void InitializeOnce(int numPasses, int usedACStrategy) + { + for (int i = 0; i < numPasses; i++) + { + if (this.NumberOfNonZeroes[i].XSize == 0) + { + // Uninitialized. + this.NumberOfNonZeroes[i] = new( + configuration, + JxlFrameDimensions.GroupDimensionsInBlocks, + JxlFrameDimensions.GroupDimensionsInBlocks); + } + } + + int maxBlockArea = 0; + + for (int o = 0; o < JxlAcStrategy.NumberOfValidStrategies; ++o) + { + JxlAcStrategy acs = new(o); + if ((usedACStrategy & (1 << o)) == 0) + { + continue; + } + + int area = acs.CoveredBlocksX * acs.CoveredBlocksY * JxlFrameDimensions.DctBlockSize; + maxBlockArea = Math.Max(area, maxBlockArea); + } + + if (maxBlockArea > this.maxBlockArea) + { + this.maxBlockArea = maxBlockArea; + + this.floatMemory = configuration.MemoryAllocator.Allocate(this.maxBlockArea * 7); + this.int32Memory = configuration.MemoryAllocator.Allocate(this.maxBlockArea * 3); + this.int16Memory = configuration.MemoryAllocator.Allocate(this.maxBlockArea * 3); + } + + this.DecodeGroupBlockScratchSpace = this.floatMemory?.Memory ?? Memory.Empty; + this.DecodeGroupQBlockScratchSpace = this.int32Memory?.Memory ?? Memory.Empty; + this.DecodeGroupQBlock16ScratchSpace = this.int16Memory?.Memory ?? Memory.Empty; + if (this.DecodeGroupBlockScratchSpace.Length > 0) + { + this.ScratchSpaceMemory = this.DecodeGroupBlockScratchSpace[(this.maxBlockArea * 3)..]; + } + else + { + this.ScratchSpaceMemory = Memory.Empty; + } + } } From 135c271a4ee4b5c59999c5e2c9283fc2b20de36f Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Fri, 16 Oct 2026 06:49:54 +0400 Subject: [PATCH 209/328] Implement additional AC strategy encoder functions --- .../Decoder/JxlPassesDecoderState.cs | 31 +- .../AcStrategy/JxlAcStrategyConfiguration.cs | 6 + .../AcStrategy/JxlAcStrategyEncoder.cs | 285 +++++++++++++++++- 3 files changed, 317 insertions(+), 5 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs index 388ee1497f..53dc70d924 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs @@ -2,10 +2,13 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; +using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; @@ -20,7 +23,7 @@ public JxlPassesDecoderState(JxlFrameHeader header, Configuration configuration) this.MainOutput.Callback = PixelCallback; this.MainOutput.Buffer = null; - this.UndoOrientation = JxlOrientation.Identity; + this.UndoOrientation = JxlExifOrientation.Identity; this.Upsampler8x = GetUpsamplingImage(configuration, this.Shared.CodecMetadata.CustomTransformData, 0, 3); if (header.LoopFilter?.EpfIterations > 0) @@ -63,7 +66,7 @@ public JxlPassesDecoderState(JxlFrameHeader header, Configuration configuration) public bool UnpremultiplyAlpha { get; set; } - public JxlOrientation UndoOrientation { get; set; } + public JxlExifOrientation UndoOrientation { get; set; } public int VisibleFrameIndex { get; set; } @@ -78,4 +81,28 @@ public JxlPassesDecoderState(JxlFrameHeader header, Configuration configuration) public JxlImageBundle FrameStorageForReferencing { get; set; } public JxlOutputEncodingInfo OutputEncodingInfo { get; set; } = new(); + + public void InitializeForAc(int numPasses) + { + this.SharedStorage.CoeffOrderSize = 0; + + for (int o = 0; o < JxlAcStrategy.NumberOfValidStrategies; o++) + { + if (((1 << o) & this.UsedAcs) == 0) + { + continue; + } + + int order = JxlCoefficientOrder.StrategyOrder[o]; + this.SharedStorage.CoeffOrderSize = Math.Max( + JxlCoefficientOrder.CoefficientOrderOffsets[3 * (order + 1)] * JxlFrameDimensions.DctBlockSize, + this.SharedStorage.CoeffOrderSize); + } + + int sz = numPasses * this.SharedStorage.CoeffOrderSize; + if (sz > this.SharedStorage.CoeffOrders.Count) + { + this.SharedStorage.CoeffOrders.Resize(sz); + } + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyConfiguration.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyConfiguration.cs index 241d4060ad..44dd37336a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyConfiguration.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyConfiguration.cs @@ -41,4 +41,10 @@ internal sealed class JxlAcStrategyConfiguration(JxlDequantMatrices dequant) public ref float Pixel(int c, int x, int y) => ref this.SourceRows[c].Span[(y * this.SourceStride) + x]; + + public Span PixelSpan(int c, int x, int y) + => this.SourceRows[c].Span[((y * this.SourceStride) + x)..]; + + public ref float Quant(int bx, int by) + => ref this.QuantFieldRow.Span[(by * this.QuantFieldStride) + bx]; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyEncoder.cs index f987d70786..0d7cbd36cb 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyEncoder.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using System.Runtime.CompilerServices; using System.Runtime.Intrinsics; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; @@ -281,7 +282,6 @@ public static bool EstimateEntropy( Span cmapFactors, Span block, Span fullScratchSpace, - Span quantized, out float entropy) { entropy = 0; @@ -522,7 +522,6 @@ public static void FindBest8x8Transform( Span contextMapFactors, Span block, Span scratchSpace, - Span quantized, out float entropyOut, out JxlAcStrategyType bestTx) { @@ -570,7 +569,6 @@ public static void FindBest8x8Transform( contextMapFactors, block, scratchSpace, - quantized, out float entropy)) { throw new InvalidOperationException("Failed to estimate entropy"); @@ -586,6 +584,287 @@ public static void FindBest8x8Transform( entropyOut = (float)best; } + public static bool TryMergeAcStrategy( + JxlAcStrategyType acsRaw, + int bx, + int by, + int cx, + int cy, + JxlAcStrategyConfiguration config, + Span cmapFactors, + JxlAcStrategyImage acStrategy, + float entropyMul, + byte candidatePriority, + Span priority, + Span entropyEstimate, + Span block, + Span scratchSpace, + Span quantized) + { + JxlAcStrategy acs = new(acsRaw); + float currentEntropy = 0; + + for (int iy = 0; iy < acs.CoveredBlocksY; ++iy) + { + for (int ix = 0; ix < acs.CoveredBlocksX; ++ix) + { + if (priority[((cy + iy) * 8) + (cx + ix)] >= candidatePriority) + { + // Transform would reuse already allocated blocks and + // lead to invalid overlaps, for example DCT64X32 vs. + // DCT32X64. + return true; + } + + currentEntropy += entropyEstimate[((cy + iy) * 8) + (cx + ix)]; + } + } + + if (!EstimateEntropy( + acs, + entropyMul, + (bx + cx) * 8, + (by + cy) * 8, + config, + cmapFactors, + block, + scratchSpace, + out float entropyCandidate)) + { + return false; + } + + if (entropyCandidate >= currentEntropy) + { + return true; + } + + for (int iy = 0; iy < acs.CoveredBlocksY; iy++) + { + for (int ix = 0; ix < acs.CoveredBlocksX; ix++) + { + entropyEstimate[((cy + iy) * 8) + cx + ix] = 0; + priority[((cy + iy) * 8) + cx + ix] = candidatePriority; + } + } + + if (!acStrategy.Set(bx + cx, by + cy, acsRaw)) + { + return false; + } + + entropyEstimate[(cy * 8) + cx] = entropyCandidate; + return true; + } + + public static bool FindBestFirstLevelDivisionForSquare( + int blocks, + bool allowSquareTransform, + int bx, + int by, + int cx, + int cy, + JxlAcStrategyConfiguration config, + Span cmapFactors, + JxlAcStrategyImage acStrategy, + float entropyMulJXK, + float entropyMulJXJ, + Span entropyEstimate, + Span block, + Span scratchSpace, + ref int quantized) + { + int blocksHalf = blocks / 2; + JxlAcStrategyType acsRawJXK = AcsVerticalSplit(blocks); + JxlAcStrategyType acsRawKXJ = AcsHorizontalSplit(blocks); + JxlAcStrategyType acsRawJXJ = AcsSquare(blocks); + JxlAcStrategy acsJXK = new(acsRawJXK); + JxlAcStrategy acsKXJ = new(acsRawKXJ); + JxlAcStrategy acsJXJ = new(acsRawJXJ); + JxlAcStrategyRow row0 = acStrategy.GetRow(by + cy + 0); + JxlAcStrategyRow row1 = acStrategy.GetRow(by + cy + blocksHalf); + + if (MultiBlockTransformCrossesHorizontalBoundary(acStrategy, bx + cx, by + cy, bx + cx + blocks) || + MultiBlockTransformCrossesHorizontalBoundary(acStrategy, bx + cx, by + cy + blocks, bx + cx + blocks) || + MultiBlockTransformCrossesVerticalBoundary(acStrategy, bx + cx, by + cy, by + cy + blocks) || + MultiBlockTransformCrossesVerticalBoundary(acStrategy, bx + cx + blocks, by + cy, by + cy + blocks)) + { + return true; + } + + bool allowJXK = !MultiBlockTransformCrossesVerticalBoundary(acStrategy, bx + cx + blocksHalf, by + cy, by + cy + blocks); + bool allowKXJ = !MultiBlockTransformCrossesHorizontalBoundary(acStrategy, bx + cx, by + cy + blocksHalf, bx + cx + blocks); + + InlineArray2> entropy = default; + + for (int dy = 0; dy < blocks; ++dy) + { + for (int dx = 0; dx < blocks; ++dx) + { + entropy[dy / blocksHalf][dx / blocksHalf] += entropyEstimate[((cy + dy) * 8) + (cx + dx)]; + } + } + + float entropyJXKLeft = float.MaxValue; + float entropyJXKRight = float.MaxValue; + float entropyKXJTop = float.MaxValue; + float entropyKXJBottom = float.MaxValue; + float entropyJXJ = float.MaxValue; + + if (allowJXK) + { + if (row0[bx + cx + 0].Strategy != acsRawJXK) + { + if (!EstimateEntropy( + acsJXK, + entropyMulJXK, + (bx + cx + 0) * 8, + (by + cy + 0) * 8, + config, + cmapFactors, + block, + scratchSpace, + out entropyJXKLeft)) + { + return false; + } + } + + if (row0[bx + cx + blocksHalf].Strategy != acsRawJXK) + { + if (!EstimateEntropy( + acsJXK, + entropyMulJXK, + (bx + cx + blocksHalf) * 8, + (by + cy + 0) * 8, + config, + cmapFactors, + block, + scratchSpace, + out entropyJXKRight)) + { + return false; + } + } + } + + if (allowKXJ) + { + if (row0[bx + cx].Strategy != acsRawKXJ) + { + if (!EstimateEntropy( + acsKXJ, + entropyMulJXK, + (bx + cx + 0) * 8, + (by + cy + 0) * 8, + config, + cmapFactors, + block, + scratchSpace, + out entropyKXJTop)) + { + return false; + } + } + + if (row1[bx + cx].Strategy != acsRawKXJ) + { + if (!EstimateEntropy( + acsKXJ, + entropyMulJXK, + (bx + cx + 0) * 8, + (by + cy + blocksHalf) * 8, + config, + cmapFactors, + block, + scratchSpace, + out entropyKXJBottom)) + { + return false; + } + } + } + + if (allowSquareTransform) + { + if (!EstimateEntropy( + acsJXJ, + entropyMulJXJ, + (bx + cx + 0) * 8, + (by + cy + 0) * 8, + config, + cmapFactors, + block, + scratchSpace, + out entropyJXJ)) + { + return false; + } + } + + float costJxN = MathF.Min(entropyJXKLeft, entropy[0][0] + entropy[1][0]) + + MathF.Min(entropyJXKRight, entropy[0][1] + entropy[1][1]); + + float costNxJ = MathF.Min(entropyKXJTop, entropy[0][0] + entropy[0][1]) + + MathF.Min(entropyKXJBottom, entropy[1][0] + entropy[1][1]); + + if (entropyJXJ < costJxN && entropyJXJ < costNxJ) + { + if (!acStrategy.Set(bx + cx, by + cy, acsRawJXJ)) + { + return false; + } + + SetEntropyForTransform(cx, cy, acsRawJXJ, entropyJXJ, entropyEstimate); + } + else if (costJxN < costNxJ) + { + if (entropyJXKLeft < entropy[0][0] + entropy[1][0]) + { + if (!acStrategy.Set(bx + cx, by + cy, acsRawJXK)) + { + return false; + } + + SetEntropyForTransform(cx, cy, acsRawJXK, entropyJXKLeft, entropyEstimate); + } + + if (entropyJXKRight < entropy[0][1] + entropy[1][1]) + { + if (!acStrategy.Set(bx + cx + blocksHalf, by + cy, acsRawJXK)) + { + return false; + } + + SetEntropyForTransform(cx + blocksHalf, cy, acsRawJXK, entropyJXKRight, entropyEstimate); + } + } + else + { + if (entropyKXJTop < entropy[0][0] + entropy[0][1]) + { + if (!acStrategy.Set(bx + cx, by + cy, acsRawKXJ)) + { + return false; + } + + SetEntropyForTransform(cx, cy, acsRawKXJ, entropyKXJTop, entropyEstimate); + } + + if (entropyKXJBottom < entropy[1][0] + entropy[1][1]) + { + if (!acStrategy.Set(bx + cx, by + cy + blocksHalf, acsRawKXJ)) + { + return false; + } + + SetEntropyForTransform(cx, cy + blocksHalf, acsRawKXJ, entropyKXJBottom, entropyEstimate); + } + } + + return true; + } + /// /// For , represents a small /// model which the encoder uses to try all available 8x8 From 842276eea4803ba38268d0137f5cf3f12b01667c Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Fri, 16 Oct 2026 07:37:46 +0400 Subject: [PATCH 210/328] Finish AC strategy encoder --- .../Formats/Jxl/Memory/JxlPlane{T}.cs | 13 + .../AcStrategy/JxlAcStrategyEncoder.cs | 328 +++++++++++++++++- .../Heuristics/JxlAcStrategyHeuristics.cs | 188 ++++++++++ 3 files changed, 527 insertions(+), 2 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlAcStrategyHeuristics.cs diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs index d420a16f95..956c043eff 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs @@ -100,6 +100,19 @@ public Span GetRow(Rectangle rectangle, int y) return this.GetRow(y + rectangle.Top)[rectangle.Left..]; } + /// + /// Returns a span for the specified row within the specified rectangle bounds. + /// + /// The bounds. + /// The row index. + /// A span which covers memory for the specified row with the rectangle offsets. + public Memory GetRowMemory(Rectangle rectangle, int y) + { + DebugGuard.MustBeGreaterThanOrEqualTo(y + rectangle.Top, 0, nameof(y)); + + return this.GetRowMemory(y + rectangle.Top)[rectangle.Left..]; + } + /// /// Checks if the specified rectangle is within the bounds image. /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyEncoder.cs index 0d7cbd36cb..fce68912fb 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyEncoder.cs @@ -598,8 +598,7 @@ public static bool TryMergeAcStrategy( Span priority, Span entropyEstimate, Span block, - Span scratchSpace, - Span quantized) + Span scratchSpace) { JxlAcStrategy acs = new(acsRaw); float currentEntropy = 0; @@ -865,6 +864,324 @@ public static bool FindBestFirstLevelDivisionForSquare( return true; } + public static bool ProcessAcStrategyRectangle( + JxlCompressionParameters cparams, + JxlAcStrategyConfiguration config, + Rectangle rect, + JxlColorCorrelationMap cmap, + Span block, + ref int quantized, + JxlAcStrategyImage acStrategy) + { + float butteraugliTarget = cparams.ButteraugliDistance; + Span scratchSpace = block[(3 * JxlAcStrategy.MaximumCoefficientArea)..]; + int bx = rect.X0(); + int by = rect.Y0(); + + if (rect.Width > 8 || rect.Height > 8) + { + throw new InvalidOperationException("Rectangle is too large"); + } + + int tx = bx / JxlChromaFromLuma.ColorTileDimensionInBlocks; + int ty = by / JxlChromaFromLuma.ColorTileDimensionInBlocks; + + InlineArray3 cmapFactors = default; + cmapFactors[0] = cmap.Base.YToXRatio(cmap.YToXMap!.GetRow(ty)[tx]); + cmapFactors[1] = 0f; + cmapFactors[2] = cmap.Base.YToBRatio(cmap.YToBMap!.GetRow(ty)[tx]); + + if (cparams.SpeedTier > JxlSpeedTier.Hare) + { + return true; + } + + Span entropyEstimate = stackalloc float[64]; + entropyEstimate.Clear(); + + const float k8x8mul1 = -0.4f; + const float k8x8mul2 = 1.0f; + const float k8x8base = 1.4f; + float mul8x8 = k8x8mul2 + (k8x8mul1 / (butteraugliTarget + k8x8base)); + + for (int iy = 0; iy < rect.Height; iy++) + { + for (int ix = 0; ix < rect.Width; ix++) + { + FindBest8x8Transform( + 8 * (bx + ix), + 8 * (by + iy), + (int)cparams.SpeedTier, + butteraugliTarget, + config, + cmapFactors, + acStrategy, + block, + scratchSpace, + quantized, + out float entropy, + out JxlAcStrategyType bestOf8x8s); + + if (!acStrategy.Set(bx + ix, by + iy, bestOf8x8s)) + { + return false; + } + + entropyEstimate[(iy * 8) + ix] = entropy * mul8x8; + } + } + + const float entropy_mul16X8 = 1.21f; + const float entropy_mul16X16 = 1.34f; + const float entropy_mul16X32 = 1.49f; + const float entropy_mul32X32 = 1.48f; + const float entropy_mul64X32 = 2.25f; + const float entropy_mul64X64 = 2.25f; + + Span transformsForMerge = + [ + new(JxlAcStrategyType.DCT16X8, 2, 4, 5, entropy_mul16X8), + new(JxlAcStrategyType.DCT8X16, 2, 4, 5, entropy_mul16X8), + new(JxlAcStrategyType.DCT16X32, 4, 4, 4, entropy_mul16X32), + new(JxlAcStrategyType.DCT32X16, 4, 4, 4, entropy_mul16X32), + new(JxlAcStrategyType.DCT64X32, 6, 1, 3, entropy_mul64X32), + new(JxlAcStrategyType.DCT32X64, 6, 1, 3, entropy_mul64X32), + default, + default, + default + ]; + + Span priority = stackalloc byte[64]; + priority.Clear(); + + bool enable32x32 = cparams.DecodingSpeedTier < 4; + foreach (MergeAttempt mt in transformsForMerge) + { + if (mt.DecodingSpeedTierMaxLimit < cparams.DecodingSpeedTier) + { + continue; + } + + JxlAcStrategy acs = new(mt.Type); + + for (int cy = 0; cy + acs.CoveredBlocksY - 1 < rect.Height; cy += acs.CoveredBlocksY) + { + for (int cx = 0; cx + acs.CoveredBlocksX - 1 < rect.Width; cx += acs.CoveredBlocksX) + { + if (cy + 7 < rect.Height && cx + 7 < rect.Width) + { + if (cparams.DecodingSpeedTier < 4 && mt.Type == JxlAcStrategyType.DCT32X64) + { + if ((cy | cx) % 8 == 0) + { + if (!FindBestFirstLevelDivisionForSquare( + 8, + true, + bx, + by, + cx, + cy, + config, + cmapFactors, + acStrategy, + mt.EntropyMultiplier, + entropy_mul64X64, + entropyEstimate, + block, + scratchSpace, + ref quantized)) + { + return false; + } + } + + continue; + } + else if (mt.Type == JxlAcStrategyType.DCT32X16) + { + continue; + } + } + + if ((mt.Type == JxlAcStrategyType.DCT16X32 && cy % 4 != 0) || (mt.Type == JxlAcStrategyType.DCT32X16 && cx % 4 != 0)) + { + continue; + } + + if (cy + 3 < rect.Height && cx + 3 < rect.Width) + { + if (mt.Type == JxlAcStrategyType.DCT16X32) + { + if ((cy | cx) % 4 == 0) + { + if (!FindBestFirstLevelDivisionForSquare( + 4, + enable32x32, + bx, + by, + cx, + cy, + config, + cmapFactors, + acStrategy, + mt.EntropyMultiplier, + entropy_mul32X32, + entropyEstimate, + block, + scratchSpace, + ref quantized)) + { + return false; + } + } + + continue; + } + else if (mt.Type == JxlAcStrategyType.DCT32X16) + { + continue; + } + } + + if ((mt.Type == JxlAcStrategyType.DCT16X32 && cy % 4 != 0) || + (mt.Type == JxlAcStrategyType.DCT32X16 && cx % 4 != 0)) + { + continue; + } + + if (cy + 1 < rect.Height && cx + 1 < rect.Width) + { + if (mt.Type == JxlAcStrategyType.DCT8X16) + { + if ((cy | cx) % 2 == 0) + { + if (!FindBestFirstLevelDivisionForSquare( + 2, + true, + bx, + by, + cx, + cy, + config, + cmapFactors, + acStrategy, + mt.EntropyMultiplier, + entropy_mul16X16, + entropyEstimate, + block, + scratchSpace, + ref quantized)) + { + return false; + } + } + + continue; + } + else if (mt.Type == JxlAcStrategyType.DCT16X8) + { + continue; + } + } + + if ((mt.Type == JxlAcStrategyType.DCT8X16 && cy % 2 == 1) || + (mt.Type == JxlAcStrategyType.DCT16X8 && cx % 2 == 1)) + { + continue; + } + + if (!TryMergeAcStrategy( + mt.Type, + bx, + by, + cx, + cy, + config, + cmapFactors, + acStrategy, + mt.EntropyMultiplier, + mt.Priority, + priority, + entropyEstimate, + block, + scratchSpace)) + { + return false; + } + } + } + } + + if (cparams.SpeedTier >= JxlSpeedTier.Hare) + { + return true; + } + + for (int cy = 0; cy + 1 < rect.Height; ++cy) + { + for (int cx = 0; cx + 1 < rect.Width; ++cx) + { + if ((cy | cx) % 2 != 0) + { + if (!FindBestFirstLevelDivisionForSquare( + 2, + true, + bx, + by, + cx, + cy, + config, + cmapFactors, + acStrategy, + entropy_mul16X8, + entropy_mul16X16, + entropyEstimate, + block, + scratchSpace, + ref quantized)) + { + return false; + } + } + } + } + + int step = cparams.SpeedTier >= JxlSpeedTier.Tortoise ? 2 : 1; + + for (int cy = 0; cy + 3 < rect.Height; cy += step) + { + for (int cx = 0; cx + 3 < rect.Width; cx += step) + { + if ((cy | cx) % 4 == 0) + { + continue; + } + + if (!FindBestFirstLevelDivisionForSquare( + 4, + enable32x32, + bx, + by, + cx, + cy, + config, + cmapFactors, + acStrategy, + entropy_mul16X32, + entropy_mul32X32, + entropyEstimate, + block, + scratchSpace, + ref quantized)) + { + return false; + } + } + } + + return true; + } + /// /// For , represents a small /// model which the encoder uses to try all available 8x8 @@ -886,4 +1203,11 @@ private readonly record struct TransformAttempt8x8( JxlAcStrategyType Type, int EncodingSpeedTierMaxLimit, double EntropyMultiplier); + + private readonly record struct MergeAttempt( + JxlAcStrategyType Type, + byte Priority, + byte DecodingSpeedTierMaxLimit, + byte EncodingSpeedTierMaxLimit, + float EntropyMultiplier); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlAcStrategyHeuristics.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlAcStrategyHeuristics.cs new file mode 100644 index 0000000000..fb6dfffbfe --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlAcStrategyHeuristics.cs @@ -0,0 +1,188 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AcStrategy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Heuristics; + +internal class JxlAcStrategyHeuristics(Configuration configuration, JxlCompressionParameters cparams) : IDisposable +{ + private IMemoryOwner? memoryOwner; + private IMemoryOwner? qMemoryOwner; + + public Configuration Configuration { get; set; } = configuration; + + public JxlCompressionParameters CompressionParameters { get; set; } = cparams; + + public JxlAcStrategyConfiguration Config { get; set; } = new(new()); + + public int MemoryPerThread { get; set; } + + public Memory Memory { get; set; } + + public int QMemoryPerThread { get; set; } + + public Memory QMemory { get; set; } + + public void Dispose() + { + this.memoryOwner?.Dispose(); + this.qMemoryOwner?.Dispose(); + } + + public bool Initialize(JxlImage3F src, Rectangle rectIn, JxlImageF quantField, JxlImageF mask, JxlImageF mask1x1, JxlDequantMatrices matrices) + { + this.Config.Dequant = matrices; + + if (this.CompressionParameters.SpeedTier < JxlSpeedTier.Cheetah) + { + uint acsMask = 0; + + for (int i = 0; i < (int)JxlAcStrategyType.DCT128X128; i++) + { + acsMask |= (uint)(1 << i); + } + + if (!matrices.EnsureComputed(this.Configuration, (int)acsMask)) + { + return false; + } + } + else + { + if (!matrices.EnsureComputed(this.Configuration, 1)) + { + return false; + } + } + + this.Config.QuantFieldRow = quantField.GetRowMemory(0); + this.Config.QuantFieldStride = quantField.PixelsPerRow; + + if (mask.XSize > 0 && mask.YSize > 0) + { + this.Config.MaskingFieldRow = mask.GetRowMemory(0); + this.Config.MaskingFieldStride = mask.PixelsPerRow; + } + + this.Config.Mask1x1Width = mask1x1.XSize; + + if (mask1x1.XSize > 0 && mask1x1.YSize > 0) + { + this.Config.Masking1x1FieldRow = mask1x1.GetRowMemory(0); + this.Config.Masking1x1FieldStride = mask1x1.PixelsPerRow; + } + + this.Config.SourceRows[0] = src.Plane(0).GetRowMemory(rectIn, 0); + this.Config.SourceRows[1] = src.Plane(1).GetRowMemory(rectIn, 0); + this.Config.SourceRows[2] = src.Plane(2).GetRowMemory(rectIn, 0); + this.Config.SourceStride = src.PixelsPerRow; + + this.Config.InfoLossMultiplier = 1.2f; + this.Config.ZerosMultiplier = 9.3089059022677905f; + this.Config.CostDelta = 10.833273317067883f; + + const float bias = 0.13731742964354549f; + float ratio = (this.CompressionParameters.ButteraugliDistance + bias) / (1.0f + bias); + + const float pow1 = 0.33677806662454718f; + const float pow2 = 0.50990926717963703f; + const float pow3 = 0.36702940662370243f; + + this.Config.InfoLossMultiplier *= MathF.Pow(ratio, pow1); + this.Config.ZerosMultiplier *= MathF.Pow(ratio, pow2); + this.Config.CostDelta *= MathF.Pow(ratio, pow3); + + return true; + } + + public void PrepareForThreads(int numThreads) + { + int dctScratchSize = 3 * (JxlSimdUtils.MaxVectorSize / sizeof(float)) * JxlAcStrategy.MaximumBlockDimension; + + this.MemoryPerThread = (6 * JxlAcStrategy.MaximumCoefficientArea) + dctScratchSize; + int memBytes = numThreads * this.MemoryPerThread; + this.memoryOwner = this.Configuration.MemoryAllocator.Allocate(memBytes); + this.Memory = this.memoryOwner.Memory; + + this.QMemoryPerThread = JxlAcStrategy.MaximumCoefficientArea; + int qmemBytes = numThreads * this.QMemoryPerThread; + this.qMemoryOwner = this.Configuration.MemoryAllocator.Allocate(qmemBytes); + this.QMemory = this.qMemoryOwner.Memory; + } + + public bool ProcessRectangle(Rectangle rect, JxlColorCorrelationMap cmap, JxlAcStrategyImage acStrategy, int thread) + { + if (this.CompressionParameters.SpeedTier >= JxlSpeedTier.Cheetah) + { + acStrategy.FillDct8(in rect); + return true; + } + + ref int qmem = ref this.QMemory.Span[thread * this.QMemoryPerThread]; + + bool status = JxlAcStrategyEncoder.ProcessAcStrategyRectangle( + this.CompressionParameters, + this.Config, + rect, + cmap, + this.Memory[(thread * this.MemoryPerThread)..], + ref qmem, + acStrategy); + + return status; + } + + public void Finalize(JxlAcStrategyImage acStrategy, JxlAuxiliaryOutput? auxOut) + { + if (auxOut is not null) + { + auxOut.NumberOfSmallBlocks = + acStrategy.CountBlocks(JxlAcStrategyType.IDENTITY) + + acStrategy.CountBlocks(JxlAcStrategyType.DCT2X2) + + acStrategy.CountBlocks(JxlAcStrategyType.DCT4X4); + + auxOut.NumberOfDct4x8Blocks = + acStrategy.CountBlocks(JxlAcStrategyType.DCT4X8) + + acStrategy.CountBlocks(JxlAcStrategyType.DCT8X4); + + auxOut.NumberOfAfvBlocks = acStrategy.CountBlocks(JxlAcStrategyType.AFV0) + + acStrategy.CountBlocks(JxlAcStrategyType.AFV1) + + acStrategy.CountBlocks(JxlAcStrategyType.AFV2) + + acStrategy.CountBlocks(JxlAcStrategyType.AFV3); + + auxOut.NumberOfDct8Blocks = acStrategy.CountBlocks(JxlAcStrategyType.DCT); + + auxOut.NumberOfDct8x16Blocks = + acStrategy.CountBlocks(JxlAcStrategyType.DCT8X16) + + acStrategy.CountBlocks(JxlAcStrategyType.DCT16X8); + + auxOut.NumberOfDct8x32Blocks = + acStrategy.CountBlocks(JxlAcStrategyType.DCT8X32) + + acStrategy.CountBlocks(JxlAcStrategyType.DCT32X8); + + auxOut.NumberOfDct16Blocks = + acStrategy.CountBlocks(JxlAcStrategyType.DCT16X16); + + auxOut.NumberOfDct16x32Blocks = + acStrategy.CountBlocks(JxlAcStrategyType.DCT16X32) + + acStrategy.CountBlocks(JxlAcStrategyType.DCT32X16); + + auxOut.NumberOfDct32Blocks = + acStrategy.CountBlocks(JxlAcStrategyType.DCT32X32); + + auxOut.NumberOfDct32x64Blocks = + acStrategy.CountBlocks(JxlAcStrategyType.DCT32X64) + + acStrategy.CountBlocks(JxlAcStrategyType.DCT64X32); + + auxOut.NumberOfDct64Blocks = + acStrategy.CountBlocks(JxlAcStrategyType.DCT64X64); + } + } +} From 1554eefdddd01eb3ab683d0d036565c649a8ecec Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Fri, 16 Oct 2026 16:03:58 +0400 Subject: [PATCH 211/328] Add QuantizeRoundtripYBlockAc to group encoder --- .../Jxl/Processing/Encoder/JxlGroupEncoder.cs | 118 ++++++++++++++++++ .../Quantization/JxlQuantizerSimd.cs | 28 +++++ 2 files changed, 146 insertions(+) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs index be399afb3e..9fe91bdd59 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Drawing; +using System.Numerics.Tensors; using System.Runtime.Intrinsics; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; @@ -372,4 +374,120 @@ public static void AdjustQuantBlockAC( quant = qp; } } + + public static void QuantizeRoundtripYBlockAc( + JxlPassesEncoderState encoderState, + int size, + JxlQuantizer quantizer, + bool errorDiffusion, + JxlAcStrategyType quantKind, + int width, + int height, + ReadOnlySpan biases, + ref int quant, + Span inOut, + Span quantized) + { + Span thresholdY = [0.58f, 0.64f, 0.64f, 0.64f]; + + if (encoderState.CompressionParameters.SpeedTier <= JxlSpeedTier.Hare) + { + int maxQuant = 0; + int originalQuant = quant; + + Span val = + [ + encoderState.XQmMultiplier, + 1.0f, + encoderState.BQmMultiplier + ]; + + foreach (int channel in (ReadOnlySpan)[1, 0, 2]) + { + Span thresholds = [0.58f, 0.64f, 0.64f, 0.64f]; + quant = originalQuant; + + AdjustQuantBlockAC( + quantizer, + channel, + val[channel], + quantKind, + width, + height, + thresholds, + inOut[(channel * size)..], + ref quant); + + if (channel == 1) + { + thresholds.CopyTo(thresholdY); + } + + maxQuant = Math.Max(quant, maxQuant); + } + + quant = maxQuant; + } + else + { + thresholdY[0] = 0.56f; + thresholdY[1] = 0.62f; + thresholdY[2] = 0.62f; + thresholdY[3] = 0.62f; + } + + QuantizeBlockAc( + quantizer, + 1, + 1.0f, + quantKind, + width, + height, + thresholdY, + inOut[size..], + quant, + quantized[size..]); + + ReadOnlySpan dequantMatrix = quantizer.DequantMatrix(quantKind, 1); + + /* + Implementing this is slightly tricky as we have to + process at 64 lanes at a time. + + This is the reference code snippet from libjxl: + + HWY_CAPPED(float, kDCTBlockSize) df; + HWY_CAPPED(int32_t, kDCTBlockSize) di; + const auto inv_qac = Set(df, quantizer.inv_quant_ac(*quant)); + for (size_t k = 0; k < kDCTBlockSize * xsize * ysize; k += Lanes(df)) { + const auto oquant = Load(di, quantized + size + k); + const auto adj_quant = AdjustQuantBias(di, 1, oquant, biases); + const auto dequantm = Load(df, dequant_matrix + k); + Store(Mul(Mul(adj_quant, dequantm), inv_qac), df, inout + size + k); + } + */ + + // Don't call .Clear(). All items will be populated. + Span inverseQAc = stackalloc float[64]; + Span oquant = stackalloc int[64]; + Span adjQuant = stackalloc float[64]; + Span dequantm = stackalloc float[64]; + + Span mulAdjQuantDequantm = stackalloc float[64]; + Span mulAdjQuantDequantmInverseQAc = stackalloc float[64]; + + inverseQAc.Fill(quantizer.InverseQuantAc(quant)); + + for (int k = 0; k < JxlFrameDimensions.DctBlockSize * width * height; k += 64) + { + quantized.Slice(size + k, 64).CopyTo(oquant); + JxlQuantizerSimd.AdjustQuantBias64(1, oquant, biases, adjQuant); + dequantMatrix.Slice(k, 64).CopyTo(dequantm); + + TensorPrimitives.Multiply(adjQuant, dequantm, mulAdjQuantDequantm); + TensorPrimitives.Multiply(mulAdjQuantDequantm, inverseQAc, mulAdjQuantDequantmInverseQAc); + + inverseQAc.CopyTo(inOut.Slice(size + k, 64)); + } + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerSimd.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerSimd.cs index 09e182a1ab..6c80d29151 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerSimd.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerSimd.cs @@ -2,6 +2,8 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; @@ -25,4 +27,30 @@ public static Vector AdjustQuantBias(int c, Vector quantI, ReadOnlyS return Vector.ConditionalSelect(is01, oneBias, bias); } + + // a variant with 64 element spans + public static void AdjustQuantBias64(int c, Span quantI, ReadOnlySpan biases, Span output) + { + if (Vector.Count > 64 || Vector.Count > 64) + { + // TODO: in the future we may want to support > 64 items if users start encountering the problem + // Problem is, this function works on strictly 64 items, and if the Vector has > 64 items, + // then that's too much to process. + throw new NotImplementedException("This CPU has more than 64 lanes for float or int types"); + } + + ref int quantIRef = ref MemoryMarshal.GetReference(quantI); + ref float outputRef = ref MemoryMarshal.GetReference(output); + + // This loop should be SIMD-aligned + for (int i = 0; i < 64; i += Vector.Count) + { + Vector result = AdjustQuantBias( + c: c, + quantI: Vector.LoadUnsafe(ref Unsafe.Add(ref quantIRef, i)), + biases); + + result.StoreUnsafe(ref Unsafe.Add(ref outputRef, i)); + } + } } From 5a944e244088eb0440f2a57df0a217dc193a3504 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Fri, 16 Oct 2026 16:57:24 +0400 Subject: [PATCH 212/328] Complete group encoder --- .../{AcStrategy => Dct}/IJxlDctAcImage.cs | 18 +- .../Jxl/Processing/Dct/JxlDctAcImage{T}.cs | 15 +- .../Processing/Dct/JxlDctAcPointerMemory.cs | 21 ++ .../Jxl/Processing/Encoder/JxlGroupEncoder.cs | 235 ++++++++++++++++++ 4 files changed, 285 insertions(+), 4 deletions(-) rename src/ImageSharp/Formats/Jxl/Processing/{AcStrategy => Dct}/IJxlDctAcImage.cs (75%) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctAcPointerMemory.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/IJxlDctAcImage.cs b/src/ImageSharp/Formats/Jxl/Processing/Dct/IJxlDctAcImage.cs similarity index 75% rename from src/ImageSharp/Formats/Jxl/Processing/AcStrategy/IJxlDctAcImage.cs rename to src/ImageSharp/Formats/Jxl/Processing/Dct/IJxlDctAcImage.cs index 6d773d7cd3..9c06ab45c1 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/IJxlDctAcImage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Dct/IJxlDctAcImage.cs @@ -1,9 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; /// /// Base DCT AC coefficient image @@ -40,6 +38,20 @@ internal interface IJxlDctAcImage /// public JxlDctAcPointer GetPlaneRow(int channel, int y, int xBase = 0); + /// + /// Returns a reference to the coefficients at the specified row. + /// + /// The plane index (which channel). + /// The row index within plane specified by . + /// The X offset at the specified row. + /// + /// A reference to the coefficients inside the channel + /// specified by index , at index of + /// the row specified by , with X offset + /// specified by . + /// + public JxlDctAcPointerMemory GetPlaneRowMemory(int channel, int y, int xBase = 0); + /// /// Returns a reference to the coefficients at the specified row. /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctAcImage{T}.cs b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctAcImage{T}.cs index 002342ae57..dccc8c4d35 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctAcImage{T}.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctAcImage{T}.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Jxl.Memory; -using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; @@ -43,6 +42,20 @@ public unsafe JxlDctAcPointer GetPlaneRow(int channel, int y, int xBase = 0) } } + public unsafe JxlDctAcPointerMemory GetPlaneRowMemory(int channel, int y, int xBase = 0) + { + if (sizeof(T) == 4) + { + Memory span = (this.image as JxlImage3)!.PlaneRowMemory(channel, y)[xBase..]; + return new JxlDctAcPointerMemory() { Pointer32 = span }; + } + else + { + Memory span = (this.image as JxlImage3)!.PlaneRowMemory(channel, y)[xBase..]; + return new JxlDctAcPointerMemory() { Pointer16 = span }; + } + } + public unsafe JxlDctReadOnlyAcPointer GetReadOnlyPlaneRow(int channel, int y, int xBase = 0) { if (sizeof(T) == 4) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctAcPointerMemory.cs b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctAcPointerMemory.cs new file mode 100644 index 0000000000..fda82d916d --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctAcPointerMemory.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; + +/// +/// Pointer to DCT AC coefficients using a backing +/// pointer type. +/// +internal ref struct JxlDctAcPointerMemory() +{ + /// + /// 16-bit pointer (if any) + /// + public Memory Pointer16 = Memory.Empty; + + /// + /// 32-bit pointer (if any) + /// + public Memory Pointer32 = Memory.Empty; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs index 9fe91bdd59..af016a6e3b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs @@ -1,10 +1,15 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; using System.Drawing; using System.Numerics.Tensors; +using System.Runtime.CompilerServices; using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; @@ -490,4 +495,234 @@ process at 64 lanes at a time. inverseQAc.CopyTo(inOut.Slice(size + k, 64)); } } + + public static void ComputeCoefficients(Configuration configuration, int groupIndex, JxlPassesEncoderState encoderState, JxlImage3F opsin, Rectangle rectangle, JxlImage3F dc) + { + Rectangle blockGroupRect = encoderState.Shared.FrameDimensions.BlockGroupRect(groupIndex); + Rectangle cmapRect = new( + blockGroupRect.X0() / JxlChromaFromLuma.ColorTileDimensionInBlocks, + blockGroupRect.Y0() / JxlChromaFromLuma.ColorTileDimensionInBlocks, + JxlMath.DivCeil(blockGroupRect.Width, JxlChromaFromLuma.ColorTileDimensionInBlocks), + JxlMath.DivCeil(blockGroupRect.Height, JxlChromaFromLuma.ColorTileDimensionInBlocks)); + Rectangle groupRect = encoderState.Shared.FrameDimensions.GroupRect(groupIndex).Translate(rectangle.X0(), rectangle.Y0()); + + int widthBlocks = blockGroupRect.Width; + int heightBlocks = blockGroupRect.Height; + + int dcStride = dc.PixelsPerRow; + int opsinStride = opsin.PixelsPerRow; + + JxlImageI fullQuantField = encoderState.RawQuantField; + JxlCompressParameters cparams = encoderState.CompressParameters; + + int dctScratchSize = 3 * (JxlSimdUtils.MaxVectorSize / sizeof(float)) * JxlAcStrategy.MaximumBlockDimension; + + using IMemoryOwner mem = configuration.MemoryAllocator.Allocate(3 * JxlAcStrategy.MaximumCoefficientArea); + using IMemoryOwner fmem = configuration.MemoryAllocator.Allocate((5 * JxlAcStrategy.MaximumCoefficientArea) + dctScratchSize); + + Span scratchSpace = fmem.Memory.Span[(3 * JxlAcStrategy.MaximumCoefficientArea)..]; + + { + // Error diffusion is slow. Only allow it on squirrel 🐿️ speed tiers + // or below. + bool errorDiffusion = cparams.SpeedTier <= JxlSpeedTier.Squirrel; + + InlineArray3>> coeffs = default; + int numPasses = encoderState.ProgressiveSplitter.GetNumPasses(); + + if (numPasses <= 0) + { + throw new InvalidOperationException("Image must have at least one pass; found " + numPasses); + } + + for (int i = 0; i < numPasses; i++) + { + IJxlDctAcImage acimg = encoderState.Coefficients[i]; + + if (acimg.Type != JxlDctAcType.Ac32) + { + throw new InvalidOperationException("AC image must be 32-bit"); + } + + coeffs[0][i] = acimg.GetPlaneRowMemory(0, groupIndex, 0).Pointer32; + coeffs[1][i] = acimg.GetPlaneRowMemory(1, groupIndex, 0).Pointer32; + coeffs[2][i] = acimg.GetPlaneRowMemory(2, groupIndex, 0).Pointer32; + } + + Span coeffsIn = fmem.Memory.Span; + Span quantized = mem.Memory.Span; + + // SIMD on 64-bit lanes equivalent + // No need to call .Clear(). All values will zero-init. + Span xFactor = stackalloc float[64]; + Span bFactor = stackalloc float[64]; + Span inX = stackalloc float[64]; + Span inY = stackalloc float[64]; + Span inB = stackalloc float[64]; + Span outX = stackalloc float[64]; + Span outB = stackalloc float[64]; + + for (int by = 0; by < heightBlocks; by++) + { + Span rowQuantAc = fullQuantField.GetRow(blockGroupRect, by); + int ty = by / JxlChromaFromLuma.ColorTileDimensionInBlocks; + + InlineArray3> rowCMap = default; + rowCMap[0] = encoderState.Shared.ContextMap.YToXMap.GetRow(cmapRect, ty); + rowCMap[1] = default; + rowCMap[2] = encoderState.Shared.ContextMap.YToBMap.GetRow(cmapRect, ty); + + InlineArray3> opsinRows = default; + opsinRows[0] = opsin.Plane(0).GetRowMemory(groupRect, by * JxlFrameDimensions.BlockDimensions); + opsinRows[1] = opsin.Plane(1).GetRowMemory(groupRect, by * JxlFrameDimensions.BlockDimensions); + opsinRows[2] = opsin.Plane(2).GetRowMemory(groupRect, by * JxlFrameDimensions.BlockDimensions); + + InlineArray3> dcRows = default; + dcRows[0] = dc.Plane(0).GetRowMemory(blockGroupRect, by); + dcRows[1] = dc.Plane(1).GetRowMemory(blockGroupRect, by); + dcRows[2] = dc.Plane(2).GetRowMemory(blockGroupRect, by); + + JxlAcStrategyRow acStrategyRow = encoderState.Shared.AcStrategy.GetRow(blockGroupRect, by); + + for (int tx = 0; tx < JxlMath.DivCeil(widthBlocks, JxlChromaFromLuma.ColorTileDimensionInBlocks); tx++) + { + xFactor.Fill(encoderState.Shared.ContextMap.Base.YToXRatio(rowCMap[0].Span[tx])); + bFactor.Fill(encoderState.Shared.ContextMap.Base.YToBRatio(rowCMap[2].Span[tx])); + + for (int bx = tx * JxlChromaFromLuma.ColorTileDimensionInBlocks; bx < widthBlocks && bx < (tx + 1) * JxlChromaFromLuma.ColorTileDimensionInBlocks; bx++) + { + JxlAcStrategy acs = acStrategyRow[bx]; + + if (!acs.IsFirstBlock) + { + continue; + } + + int xBlocks = acs.CoveredBlocksX; + int yBlocks = acs.CoveredBlocksY; + + JxlForwardCoefficientOrder.CoefficientLayout(ref yBlocks, ref xBlocks); + + int size = JxlFrameDimensions.DctBlockSize * xBlocks * yBlocks; + int quantAc = rowQuantAc[bx]; + + for (int c = 0; c < 3; c++) + { + JxlTransformsEncoder.TransformFromPixels( + acs.Strategy, + opsinRows[c].Span[(bx * JxlFrameDimensions.BlockDimensions)..], + opsinStride, + coeffsIn[(c * size)..], + scratchSpace); + } + + JxlTransformsEncoder.DcFromLowestFrequencies( + acs.Strategy, + coeffsIn[size..], + dcRows[1].Span[bx..], + dcStride, + scratchSpace); + + QuantizeRoundtripYBlockAc( + encoderState, + size, + encoderState.Shared.Quantizer, + errorDiffusion, + acs.Strategy, + widthBlocks, + heightBlocks, + JxlQuantizer.DefaultQuantBias, + ref quantAc, + coeffsIn, + quantized); + + for (int k = 0; k <= size - 64; k += 64) + { + coeffsIn.Slice(k, 64).CopyTo(inX); + coeffsIn.Slice(size + k, 64).CopyTo(inY); + coeffsIn.Slice((2 * size) + k, 64).CopyTo(inB); + NegMulAdd(xFactor, inY, inX, outX); + NegMulAdd(bFactor, inY, inB, outB); + outX.CopyTo(coeffsIn.Slice(k, 64)); + outB.CopyTo(coeffsIn.Slice((2 * size) + k, 64)); + } + + foreach (int c in (ReadOnlySpan)[0, 2]) + { + Span thres = [0.58f, 0.62f, 0.62f, 0.62f]; + + QuantizeBlockAc( + encoderState.Shared.Quantizer, + c, + c == 0 ? encoderState.XQmMultiplier : encoderState.BQmMultiplier, + acs.Strategy, + xBlocks, + yBlocks, + thres, + coeffsIn[(c * size)..], + quantAc, + quantized[(c * size)..]); + + JxlTransformsEncoder.DcFromLowestFrequencies( + acs.Strategy, + coeffsIn[(c * size)..], + dcRows[c].Span[bx..], + dcStride, + scratchSpace); + } + + rowQuantAc[bx] = quantAc; + + for (int c = 0; c < 3; c++) + { + encoderState.ProgressiveSplitter.SplitAcCoefficients(quantized[(c * size)..], acs, bx, by, coeffs[c]); + + for (int p = 0; p < numPasses; p++) + { + coeffs[c][p] = coeffs[c][p][size..]; + } + } + } + } + } + } + } + + private static void NegMulAdd(Span a, Span b, Span c, Span d) + { + // d[n] = c[n] - (a[n] * b[n]) + TensorPrimitives.Multiply(a, b, d); + TensorPrimitives.Subtract(c, d, d); + } + + public static bool EncodeGroupTokenizedCoefficients(int groupIndex, int passIndex, int histogramIndex, JxlPassesEncoderState encState, JxlBitWriter writer, JxlAuxiliaryOutput auxOut) + { + int numHistograms = encState.Shared.HistogramCount; + + if (!(numHistograms == 0 || histogramIndex < numHistograms)) + { + throw new InvalidOperationException("Histogram index is out of bounds"); + } + + int histogramSelectorBits = JxlMath.CeilLog2Nonzero(numHistograms); + + if (histogramSelectorBits != 0) + { + return writer.WithMaxBits((ulong)histogramSelectorBits, JxlLayerType.Ac, auxOut, () => + { + writer.Write(histogramSelectorBits, histogramIndex); + return true; + }); + } + + int contextOffset = histogramIndex * encState.Shared.BlockContextMap.NumAcContexts; + + return WriteTokens( + encState.Passes[passIndex].AcTokens[groupIndex], + encState.Passes[passIndex].Codes, + contextOffset, + writer, + JxlLayerType.AcTokens, + auxOut); + } } From 58e3bb78986af21034ee8ccbf14e6383fa6af7eb Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Fri, 16 Oct 2026 16:57:39 +0400 Subject: [PATCH 213/328] Remove unused namespace --- src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs index af016a6e3b..a9dfe03665 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Buffers; -using System.Drawing; using System.Numerics.Tensors; using System.Runtime.CompilerServices; using System.Runtime.Intrinsics; From 6a2ea7f3d821e29650413680df664516704fb535 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sun, 18 Oct 2026 23:09:23 +0400 Subject: [PATCH 214/328] Partial ANS encoding histogram --- .../Encoder/Ans/JxlAnsEncodingHistogram.cs | 409 ++++++++++++++++++ 1 file changed, 409 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlAnsEncodingHistogram.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlAnsEncodingHistogram.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlAnsEncodingHistogram.cs new file mode 100644 index 0000000000..b99854344f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlAnsEncodingHistogram.cs @@ -0,0 +1,409 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; + +internal class JxlAnsEncodingHistogram +{ + private const int MaxNumSymbolsForSmallCode = 2; + private int method; + private int omitPos; + private int alphabetSize; + private int numSymbols; + private InlineArray2 symbols; + private static readonly uint[] lg2 = new uint[JxlAnsConstants.AnsTableSize + 1]; + + private static ReadOnlySpan BitWidthLengths => [5, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 6, 7, 7]; + + private static ReadOnlySpan BitWidthSymbols => [17, 11, 15, 3, 9, 7, 4, 2, 5, 6, 0, 33, 1, 65]; + + public List Counts { get; } = []; + + public float Cost { get; } + + private bool RebalanceHistogram(JxlHistogram histo) + { + int tableSize = JxlAnsConstants.AnsTableSize; + int shift = this.method - 1; + + Span balanceInc = stackalloc long[JxlAnsConstants.AnsLogTableSize - 1]; + Span balanceDec = stackalloc long[JxlAnsConstants.AnsLogTableSize - 1]; + + var ac = this.allowedCounts.Array[shift]; + var ai = this.allowedCounts.Index[shift]; + + long DeltaEntropyInc(EntropyDelta a) + { + var allowed = ac[a.CountIndex]; + return ((long)a.Frequency * allowed.delta_lg2) + - balanceInc[allowed.step_log]; + } + + long DeltaEntropyDec(EntropyDelta a) + { + var allowed = ac[a.CountIndex + 1]; + return ((long)a.Frequency * allowed.delta_lg2) + - balanceDec[allowed.step_log]; + } + + bool IncLess(EntropyDelta a, EntropyDelta b) => (DeltaEntropyInc(a) >> ac[a.CountIndex].step_log) + < (DeltaEntropyInc(b) >> ac[b.CountIndex].step_log); + + bool DecLess(EntropyDelta a, EntropyDelta b) => (DeltaEntropyDec(a) >> ac[a.CountIndex + 1].step_log) + < (DeltaEntropyDec(b) >> ac[b.CountIndex + 1].step_log); + + List bins = new(256); + double norm = (double)tableSize / histo.TotalCount; + + int remainderPos = 0; + long maxFreq = 0; + int rest = tableSize; + + for (int n = 0; n < this.alphabetSize; n++) + { + int freq = histo.Counts[n]; + + if (freq > maxFreq) + { + remainderPos = n; + maxFreq = freq; + } + + double target = freq * norm; + + int count = (int)Math.Max( + Math.Round(target, MidpointRounding.AwayFromZero), + freq > 0 ? 1 : 0); + + count = Math.Min(count, tableSize - 1); + + uint stepLog = SmallestIncrementLog(count, shift); + int inc = 1 << (int)stepLog; + count &= ~(inc - 1); + + this.Counts[n] = count; + rest -= count; + + if (target > 1.0) + { + bins.Add(new EntropyDelta(freq, ai[count], n)); + } + } + + // Exclude the balancing bin from the adjustable bins. + int balancingIndex = bins.FindIndex(a => a.BinIndex == remainderPos); + if (balancingIndex >= 0) + { + bins.RemoveAt(balancingIndex); + } + + // Rest is now the balancing bin's frequency. + rest += this.Counts[remainderPos]; + + if (bins.Count > 0) + { + uint maxLog = ac[1].step_log; + + while (true) + { + // Update penalties for the balancing bin. + for (int log = 0; log <= maxLog; log++) + { + int delta = 1 << log; + + if (rest >= tableSize) + { + // Permit increases; forbid decreases. + balanceInc[log] = 0; + balanceDec[log] = 0; + } + else if (rest > 1) + { + balanceInc[log] = rest > delta + ? maxFreq * (lg2[rest] - lg2[rest - delta]) + : long.MaxValue; + + balanceDec[log] = rest + delta < tableSize + ? maxFreq * (lg2[rest + delta] - lg2[rest]) + : 0; + } + else + { + // Forbid increases; permit decreases. + balanceInc[log] = long.MaxValue; + balanceDec[log] = long.MaxValue; + } + } + + // Find the bin with the greatest increase score. + int bestIncIndex = 0; + + for (int i = 1; i < bins.Count; i++) + { + if (IncLess(bins[bestIncIndex], bins[i])) + { + bestIncIndex = i; + } + } + + EntropyDelta bestBinInc = bins[bestIncIndex]; + + if (DeltaEntropyInc(bestBinInc) > 0) + { + // Grow the selected bin. + rest -= (int)(1 << (int)ac[bestBinInc.CountIndex].step_log); + + bestBinInc.CountIndex--; + } + else + { + // Find the bin with the smallest decrease score. + int bestDecIndex = 0; + + for (int i = 1; i < bins.Count; i++) + { + if (DecLess(bins[i], bins[bestDecIndex])) + { + bestDecIndex = i; + } + } + + EntropyDelta bestBinDec = bins[bestDecIndex]; + + // Stop when no reverse step improves entropy. + if (DeltaEntropyDec(bestBinDec) >= 0) + { + break; + } + + // Decrease the selected bin. + bestBinDec.CountIndex++; + + rest += (int)(1 << (int)ac[bestBinDec.CountIndex].step_log); + } + } + + // Write the adjusted frequencies. + foreach (EntropyDelta a in bins) + { + this.Counts[a.BinIndex] = ac[a.CountIndex].count; + } + + for (int n = 0; n < remainderPos; n++) + { + if (this.Counts[n] >= 2048) + { + this.Counts[remainderPos] = this.Counts[n]; + remainderPos = n; + break; + } + } + } + + // Set the balancing bin. + this.Counts[remainderPos] = rest; + this.omitPos = remainderPos; + + return this.Counts[remainderPos] > 0; + } + + private static float EstimateDataBitsFlat(JxlHistogram histo) + { + int length = histo.GetAlphabetSize(); + long flatBits = (long)lg2[length] * JxlAnsConstants.AnsLogTableSize; + return MathF.ScaleB(histo.TotalCount * flatBits, -31); + } + + private float EstimateDataBits(JxlHistogram histo) + { + long sum = 0; + + for (int i = 0; i < this.alphabetSize; i++) + { + sum += histo.Counts[i] * lg2[this.Counts[i]]; + } + + return (histo.TotalCount - MathF.ScaleB(sum, -31)) * JxlAnsConstants.AnsLogTableSize; + } + + public bool Encode(JxlBitWriter writer) + { + if (this.alphabetSize > JxlAnsConstants.AnsMaxAlphabetSize) + { + throw new InvalidOperationException("Alphabet is out of range"); + } + + // Flat histogram + if (this.method == 0) + { + // Mark non-small tree + writer.Write(1, 0); + + // Mark uniform histogram + writer.Write(1, 1); + + if (this.alphabetSize <= 0) + { + throw new InvalidOperationException("Alphabet size cannot be 0 or negative"); + } + + // Encode alphabet size + JxlAnsEncoder.StoreVarLenUInt8(this.alphabetSize - 1, writer); + return true; + } + + // Small tree + if (this.numSymbols <= MaxNumSymbolsForSmallCode) + { + // Small tree marker to encode 1-2 symbols. + writer.Write(1, 1); + + if (this.numSymbols == 0) + { + writer.Write(1, 0); + JxlAnsEncoder.StoreVarLenUInt8(0, writer); + } + else + { + writer.Write(1, this.numSymbols - 1); + + for (int i = 0; i < this.numSymbols; ++i) + { + JxlAnsEncoder.StoreVarLenUInt8(this.symbols[i], writer); + } + } + + if (this.numSymbols == 2) + { + writer.Write(JxlAnsConstants.AnsLogTableSize, this.Counts[this.symbols[0]]); + } + + return true; + } + + // General tree + // Mark non-small tree + writer.Write(1, 0); + + // Mark non-flat histogram + writer.Write(1, 0); + + int upperBoundLog = JxlMath.FloorLog2Nonzero(JxlAnsConstants.AnsLogTableSize + 1); + int log = JxlMath.FloorLog2Nonzero(this.method); + + writer.Write(log, (1 << log) - 1); + + if (log != upperBoundLog) + { + writer.Write(1, 0); + } + + writer.Write(log, ((1 << log) - 1) & this.method); + + JxlAnsEncoder.StoreVarLenUInt8(this.alphabetSize - 3, writer); + + Span same = stackalloc byte[JxlAnsConstants.AnsMaxAlphabetSize]; + same.Clear(); + int last = 0; + + for (int i = 1; i <= this.alphabetSize; i++) + { + if (i == this.alphabetSize || i == this.omitPos || i == this.omitPos + 1 || this.Counts[i] != this.Counts[last]) + { + same[last] = (byte)(i - last); + last = i; + } + } + + Span bitWidth = stackalloc byte[JxlAnsConstants.AnsMaxAlphabetSize]; + bitWidth.Clear(); + int omitWidth = 10; + + for (int i = 0; i < this.alphabetSize; ++i) + { + if (i != this.omitPos && this.Counts[i] > 0) + { + bitWidth[i] = (byte)(JxlMath.FloorLog2Nonzero(this.Counts[i]) + 1); + omitWidth = Math.Max(omitWidth, bitWidth[i] + (i < this.omitPos ? 1 : 0)); + } + } + + bitWidth[this.omitPos] = (byte)omitWidth; + + // Minimum repeats + const byte minReps = 5; + const int rep = JxlAnsConstants.AnsLogTableSize + 1; + + for (int i = 0; i < this.alphabetSize; ++i) + { + writer.Write(BitWidthLengths[bitWidth[i]], BitWidthSymbols[bitWidth[i]]); + + if (same[i] >= minReps) + { + writer.Write(BitWidthLengths[rep], BitWidthSymbols[rep]); + JxlAnsEncoder.StoreVarLenUInt8(same[i] - minReps, writer); + + i += same[i] - 1; + } + } + + int shift = this.method - 1; + + if (shift != 0) + { + for (int i = 0; i < this.alphabetSize; ++i) + { + if (bitWidth[i] > 1 && i != this.omitPos) + { + int bitcount = JxlAnsHelper.GetPopulationCountPrecision(bitWidth[i] - 1, shift); + int dropBits = bitWidth[i] - 1 - bitcount; + + if ((this.Counts[i] & ((1 << dropBits) - 1)) != 0) + { + throw new InvalidOperationException("Incorrect counts"); + } + + writer.Write(bitcount, (this.Counts[i] >> dropBits) - (1 << bitcount)); + } + + if (same[i] >= minReps) + { + // skip symbols encoded by RLE + i += same[i] - 1; + } + } + } + + return true; + } + + private static int SmallestIncrementLog(int count, int shift) + { + if (count == 0) + { + return 0; + } + + int bits = JxlMath.FloorLog2Nonzero(count); + int dropBits = bits - JxlAnsHelper.GetPopulationCountPrecision(bits, shift); + + return dropBits; + } + + private struct EntropyDelta + { + public int Frequency; + public int CountIndex; + public int BinIndex; + + public EntropyDelta(int freq, int countInd, int binInd) + { + this.Frequency = freq; + this.CountIndex = countInd; + this.BinIndex = binInd; + } + } +} From 513abd11c0c5d2faf8abef4baf34ed2844a170f0 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sun, 18 Oct 2026 23:13:10 +0400 Subject: [PATCH 215/328] Add kernels to heuristics --- .../Encoder/Heuristics/JxlHeuristics.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs index 4a865ab21d..d36fe734f7 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs @@ -77,6 +77,42 @@ internal static class JxlHeuristics -0.000897597057705f, -0.000314256996835f, -0.000314256996835f ]; + private static ReadOnlySpan Kernel00 => + [ + -0.01716200f, -0.03452303f, -0.04022174f, -0.02921014f, -0.00624645f, + -0.03452303f, 0.14111091f, 0.28896755f, 0.00278718f, -0.01610267f, + -0.04022174f, 0.28896755f, 0.56661550f, 0.03777607f, -0.01986694f, + -0.02921014f, 0.00278718f, 0.03777607f, -0.03144731f, -0.01185068f, + -0.00624645f, -0.01610267f, -0.01986694f, -0.01185068f, -0.00213539f, + ]; + + private static ReadOnlySpan Kernel01 => + [ + -0.00624645f, -0.01610267f, -0.01986694f, -0.01185068f, -0.00213539f, + -0.02921014f, 0.00278718f, 0.03777607f, -0.03144731f, -0.01185068f, + -0.04022174f, 0.28896755f, 0.56661550f, 0.03777607f, -0.01986694f, + -0.03452303f, 0.14111091f, 0.28896755f, 0.00278718f, -0.01610267f, + -0.01716200f, -0.03452303f, -0.04022174f, -0.02921014f, -0.00624645f, + ]; + + private static ReadOnlySpan Kernel10 => + [ + -0.00624645f, -0.02921014f, -0.04022174f, -0.03452303f, -0.01716200f, + -0.01610267f, 0.00278718f, 0.28896755f, 0.14111091f, -0.03452303f, + -0.01986694f, 0.03777607f, 0.56661550f, 0.28896755f, -0.04022174f, + -0.01185068f, -0.03144731f, 0.03777607f, 0.00278718f, -0.02921014f, + -0.00213539f, -0.01185068f, -0.01986694f, -0.01610267f, -0.00624645f, + ]; + + private static ReadOnlySpan Kernel11 => + [ + -0.00213539f, -0.01185068f, -0.01986694f, -0.01610267f, -0.00624645f, + -0.01185068f, -0.03144731f, 0.03777607f, 0.00278718f, -0.02921014f, + -0.01986694f, 0.03777607f, 0.56661550f, 0.28896755f, -0.04022174f, + -0.01610267f, 0.00278718f, 0.28896755f, 0.14111091f, -0.03452303f, + -0.00624645f, -0.02921014f, -0.04022174f, -0.03452303f, -0.01716200f, + ]; + public static void FindBestBlockEntropyModel(JxlCompressParameters cparameters, JxlImageI rqf, JxlAcStrategyImage acStrategy, JxlBlockContextMap blockCtxMap) { if (cparameters.DecodingSpeedTier >= 1) From 15c0b1d9d6380f4de1f35d141c885795024c3fe1 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sun, 18 Oct 2026 23:18:19 +0400 Subject: [PATCH 216/328] Add image upsampling --- .../Encoder/Heuristics/JxlHeuristics.cs | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs index d36fe734f7..25d2a6aeb8 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs @@ -510,6 +510,118 @@ public static void AntiUpsample(JxlImageF input, JxlImageF d) } } + public static void UpsampleImage(JxlImageF input, JxlImageF output) + { + int xsize = input.XSize; + int ysize = input.YSize; + int xsize2 = output.XSize; + int ysize2 = output.YSize; + + for (int y = 0; y < ysize2; y++) + { + for (int x = 0; x < xsize2; x++) + { + ReadOnlySpan kernel = Kernel00; + + if (((x & 1) & (y & 1)) != 0) + { + kernel = Kernel11; + } + else if ((x & 1) != 0) + { + kernel = Kernel10; + } + else if ((y & 1) != 0) + { + kernel = Kernel01; + } + + float sum = 0; + int x2 = x / 2; + int y2 = y / 2; + + // The min, max is initially opposite + float min = float.MaxValue; + float max = float.MinValue; + + for (int ky = 0; ky < Size; ky++) + { + for (int kx = 0; kx < Size; kx++) + { + int xi = x2 - (Size / 2) + kx; + int yi = y2 - (Size / 2) + ky; + + if (xi < 0) + { + xi = 0; + } + + if (xi >= xsize) + { + xi = input.XSize - 1; + } + + if (yi < 0) + { + yi = 0; + } + + if (yi >= ysize) + { + yi = input.YSize - 1; + } + + min = MathF.Min(min, input.GetRow(yi)[xi]); + max = MathF.Max(max, input.GetRow(yi)[xi]); + } + } + + for (int ky = 0; ky < Size; ky++) + { + for (int kx = 0; kx < Size; kx++) + { + int xi = x2 - (Size / 2) + kx; + int yi = y2 - (Size / 2) + ky; + + if (xi < 0) + { + xi = 0; + } + + if (xi >= xsize) + { + xi = input.XSize - 1; + } + + if (yi < 0) + { + yi = 0; + } + + if (yi >= ysize) + { + yi = input.YSize - 1; + } + + sum += input.GetRow(yi)[xi] * kernel[(ky * Size) + kx]; + } + } + + output.GetRow(y)[x] = sum; + + if (output.GetRow(y)[x] < min) + { + output.GetRow(y)[x] = min; + } + + if (output.GetRow(y)[x] > max) + { + output.GetRow(y)[x] = max; + } + } + } + } + private sealed class OccCounters : IDisposable { private readonly int[] qfCounts; From 367851e491df1474a9ca4ff50e0967f40e3a7917 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sun, 18 Oct 2026 23:19:14 +0400 Subject: [PATCH 217/328] Init modular encoder --- .../Processing/Encoder/Modular/JxlModularFrameEncoder.cs | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Modular/JxlModularFrameEncoder.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Modular/JxlModularFrameEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Modular/JxlModularFrameEncoder.cs new file mode 100644 index 0000000000..2e53d28cb6 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Modular/JxlModularFrameEncoder.cs @@ -0,0 +1,8 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Modular; + +internal static class JxlModularFrameEncoder +{ +} From 8166abcbfac3d05a3864baa681156302b4858295 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sun, 18 Oct 2026 23:23:35 +0400 Subject: [PATCH 218/328] Add lookups & constants used by modular frame encoder --- .../Encoder/Modular/JxlModularFrameEncoder.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Modular/JxlModularFrameEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Modular/JxlModularFrameEncoder.cs index 2e53d28cb6..bd03888586 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Modular/JxlModularFrameEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Modular/JxlModularFrameEncoder.cs @@ -5,4 +5,49 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Modular; internal static class JxlModularFrameEncoder { + public const float SqueezeQualityFactor = 0.35f; + + public const float SqueezeLumaFactor = 1.1f; + + public const float SqueezeQualityFactorXyb = 4.0f; + + public const float SqueezeQualityFactorY = 1.5f; + + /// + /// Quantization table for the XYB Squeeze transform. + /// + public static readonly float[][] SqueezeXybQTable = + [ + [ + 163.84f, 81.92f, 40.96f, 20.48f, 10.24f, 5.12f, 2.56f, 1.28f, 0.64f, 0.32f, 0.16f, + 0.08f, 0.04f, 0.02f, 0.01f, 0.005f + ], // Y + [ + 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1, 0.5f, 0.5f, 0.5f, 0.5f, + 0.5f + ], // X + [ + 2048, 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1, 0.5f, 0.5f, 0.5f, + 0.5f + ], // B-Y + ]; + + /// + /// Gets the quantization table for the Luma Squeeze transform. + /// + public static ReadOnlySpan SqueezeLumaQTable => + [ + 163.84f, 81.92f, 40.96f, 20.48f, 10.24f, 5.12f, + 2.56f, 1.28f, 0.64f, 0.32f, 0.16f, 0.08f, + 0.04f, 0.02f, 0.01f, 0.005f + ]; + + /// + /// Gets the quantization table for the Chroma Squeeze transform. + /// + public static ReadOnlySpan SqueezeChromaQTable => + [ + 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1, + 0.5f, 0.5f, 0.5f, 0.5f, 0.5f + ]; } From 54f7acd636a5c6a9eef0f28a7d39817d68e60318 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sun, 18 Oct 2026 23:30:18 +0400 Subject: [PATCH 219/328] Add a few modular frame encoder functions --- .../Encoder/Modular/JxlModularFrameEncoder.cs | 179 ++++++++++++++++++ 1 file changed, 179 insertions(+) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Modular/JxlModularFrameEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Modular/JxlModularFrameEncoder.cs index bd03888586..84db8ab481 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Modular/JxlModularFrameEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Modular/JxlModularFrameEncoder.cs @@ -1,6 +1,12 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; +using StaticPropertyRange = System.Runtime.CompilerServices.InlineArray2>; +using Tree = System.Collections.Generic.List; + namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Modular; internal static class JxlModularFrameEncoder @@ -50,4 +56,177 @@ internal static class JxlModularFrameEncoder 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f ]; + + private static bool MergeTrees(List trees, IReadOnlyList treeSplits, int begin, int end, Tree tree) + { + if (trees.Count + 1 != treeSplits.Count) + { + return false; + } + + if (end <= begin || end > trees.Count) + { + return false; + } + + if (end == begin + 1) + { + // Insert the tree, adding the appropriate offset to all child nodes. + // This will make the leaf IDs wrong, but subsequent roundtripping will fix them. + int sz = tree.Count; + tree.AddRange(trees[begin]); + + for (int i = sz; i < tree.Count; i++) + { + JxlPropertyDecisionNode decisionNode = tree[i]; + tree[i] = decisionNode with + { + LeftChild = decisionNode.LeftChild + sz, + RightChild = decisionNode.RightChild + sz + }; + } + + return true; + } + + int mid = (begin + end) / 2; + int splitval = treeSplits[mid] - 1; + int cur = tree.Count; + + tree.Add(new JxlPropertyDecisionNode(1, splitval, 0, 0, JxlPredictor.Zero, 0, 1)); + + tree[cur] = tree[cur] with + { + LeftChild = tree.Count + }; + + if (!MergeTrees(trees, treeSplits, mid, end, tree)) + { + return false; + } + + tree[cur] = tree[cur] with + { + RightChild = tree.Count + }; + + if (!MergeTrees(trees, treeSplits, begin, mid, tree)) + { + return false; + } + + return true; + } + + private static bool FloatToInt(ReadOnlySpan rowIn, Span rowOut, int xsize, uint bits, uint expBits, bool fp, double dfactor) + { + if (bits > sizeof(int) * 8) + { + return false; + } + + if (!fp) + { + if (bits > 22) + { + for (int x = 0; x < xsize; x++) + { + rowOut[x] = (int)((rowIn[x] * dfactor) + (rowIn[x] < 0 ? -0.5 : 0.5)); + } + } + else + { + float factor = (float)dfactor; + + for (int x = 0; x < xsize; x++) + { + rowOut[x] = (int)((rowIn[x] * factor) + (rowIn[x] < 0 ? -0.5f : 0.5f)); + } + } + + return true; + } + + if (bits == 32) + { + if (expBits != 8) + { + return false; + } + + MemoryMarshal.Cast(rowIn[..xsize]).CopyTo(MemoryMarshal.Cast(rowOut[..xsize])); + return true; + } + + if (bits == 0) + { + return false; + } + + int expBias = (1 << ((int)expBits - 1)) - 1; + int maxExp = (1 << (int)expBits) - 1; + uint sign = 1u << ((int)bits - 1); + int mantBits = (int)bits - (int)expBits - 1; + int mantShift = 23 - mantBits; + + for (int x = 0; x < xsize; x++) + { + uint f = BitConverter.SingleToUInt32Bits(rowIn[x]); + int signBit = (int)(f >> 31); + f &= 0x7FFFFFFF; + + if (f == 0) + { + rowOut[x] = signBit != 0 ? (int)sign : 0; + continue; + } + + int exp = (int)(f >> 23) - 127; + int mantissa = (int)(f & 0x007FFFFF); + + if (exp == 128) + { + f = signBit != 0 ? sign : 0; + f |= ((1u << (int)expBits) - 1) << mantBits; + f |= (uint)(mantissa >> mantShift); + rowOut[x] = (int)f; + continue; + } + + exp += expBias; + + if (exp <= 0) + { + mantissa |= 0x00800000; + + if (exp < -mantBits) + { + return false; + } + + mantissa >>= 1 - exp; + exp = 0; + } + + if (exp >= maxExp) + { + return false; + } + + if ((mantissa & ((1 << mantShift) - 1)) != 0) + { + return false; + } + + mantissa >>= mantShift; + + f = signBit != 0 ? sign : 0; + f |= (uint)(exp << mantBits); + f |= (uint)mantissa; + + rowOut[x] = (int)f; + } + + return true; + } } From 1f90cac230b6e42fb01d031bf0cb2f6f99f52cbc Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sun, 18 Oct 2026 23:36:21 +0400 Subject: [PATCH 220/328] A few more modular frame encoder functions --- .../Encoder/Modular/JxlModularFrameEncoder.cs | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Modular/JxlModularFrameEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Modular/JxlModularFrameEncoder.cs index 84db8ab481..6957420abe 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Modular/JxlModularFrameEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Modular/JxlModularFrameEncoder.cs @@ -2,8 +2,14 @@ // Licensed under the Six Labors Split License. using System.Runtime.InteropServices; +using System.Threading.Channels; +using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular; using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding; using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using StaticPropertyRange = System.Runtime.CompilerServices.InlineArray2>; using Tree = System.Collections.Generic.List; @@ -229,4 +235,107 @@ private static bool FloatToInt(ReadOnlySpan rowIn, Span rowOut, int return true; } + + private static float EstimateWeightedPredictionCost(JxlModularImage img, int i) + { + int extraBits = 0; + float histoCost = 0; + JxlAnsHybridUIntConfiguration config = new(); + + ReadOnlySpan cutoffs = + [ + -500, -392, -255, -191, -127, -95, -63, -47, -31, + -23, -15, -11, -7, -4, -3, -1, 0, 1, + 3, 5, 7, 11, 15, 23, 31, 47, 63, + 95, 127, 191, 255, 392, 500 + ]; + + int nc = cutoffs.Length + 1; + JxlHistogram[] histo = new JxlHistogram[nc]; + + for (int j = 0; j < nc; j++) + { + histo[j] = new JxlHistogram(); + } + + JxlModularHeader wpHeader = new(); + JxlContextPrediction.SetPredictorMode(i, wpHeader); + + foreach (JxlModularChannel ch in img.Channels) + { + int oneRow = ch.Plane.PixelsPerRow; + JxlModularState wpState = new(wpHeader, ch.Width); + Span properties = [1]; + bool unhealthy = false; + + for (int y = 0; y < ch.Height; y++) + { + ReadOnlySpan r = ch.GetRow(y); + + for (int x = 0; x < ch.Width; x++) + { + int offset = 0; + + int left = x != 0 ? r[x - 1] : y != 0 ? r[x - oneRow] : 0; + int top = y != 0 ? r[x - oneRow] : left; + int topleft = x != 0 && y != 0 ? r[x - 1 - oneRow] : left; + int topright = x + 1 < ch.Width && y != 0 ? r[x + 1 - oneRow] : top; + int toptop = y > 1 ? r[x - oneRow - oneRow] : top; + + long guess = wpState.Predict(true, x, y, ch.Width, top, left, topright, topleft, toptop, properties, offset); + + int ctx = 0; + + foreach (int c in cutoffs) + { + ctx += c >= properties[0] ? 1 : 0; + } + + unhealthy |= JxlMath.SubOverflow((uint)r[x], (uint)guess, out uint res); + + config.Encode(JxlPackSigned.PackUnsigned((int)res), out uint token, out uint nbits, out uint bits); + + histo[ctx].Add((int)token); + extraBits += (int)nbits; + + wpState.UpdatePredictionErrors(r[x], x, y, ch.Width); + } + } + + if (unhealthy) + { + return float.MaxValue; + } + + foreach (JxlHistogram h in histo) + { + histoCost += h.ComputeShannonEntropy(); + h.Clear(); + } + } + + return histoCost + extraBits; + } + + private static bool DoTransform(Configuration configuration, JxlModularImage image, JxlTransform tr, JxlGroupHeader wpHeader, bool forceJxlArt = false) + { + JxlTransform t = tr; + bool didIt = true; + + if (forceJxlArt) + { + t.MetaApply(configuration, image); + } + else + { + didIt = TransformForward(configuration, t, image, wpHeader); + } + + if (didIt) + { + image.Transforms.Add(t); + } + + return didIt; + } } From 056a6d5781229ff03457ad4d21b5ce78bb08c91e Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 19 Oct 2026 00:15:39 +0400 Subject: [PATCH 221/328] Complete LZ77 encoder --- .../Jxl/Processing/Encoder/Lz77/JxlLz77.cs | 352 +++++++++++++++++- .../Encoder/Lz77/JxlLz77HashChain.cs | 21 ++ .../Processing/Encoder/Lz77/JxlLz77HashMap.cs | 140 +++++++ .../Lz77/JxlLz77SymbolCostEstimator.cs | 2 +- 4 files changed, 511 insertions(+), 4 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77HashMap.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77.cs index 1a4c3354fe..05e5f0eefc 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77.cs @@ -16,21 +16,47 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Lz77; /// internal static class JxlLz77 { + public static List[] Apply( + Configuration configuration, + JxlHistogramParameters parameters, + int numContexts, + List[] tokens, + JxlAnsLz77Parameters lz77) + => parameters.Lz77Method switch + { + JxlLz77Method.Rle => ApplyRunLengthEncoding(configuration, parameters, numContexts, tokens, lz77), + JxlLz77Method.Lz77b1w3f => ApplyLz77Default(parameters, numContexts, tokens, lz77, 1, 3, false), + JxlLz77Method.Lz77b3w3f => ApplyLz77Default(parameters, numContexts, tokens, lz77, 3, 3, false), + JxlLz77Method.Lz77b7w3f => ApplyLz77Default(parameters, numContexts, tokens, lz77, 7, 3, false), + JxlLz77Method.Lz77b15w3f => ApplyLz77Default(parameters, numContexts, tokens, lz77, 15, 3, false), + JxlLz77Method.Lz77b31w3f => ApplyLz77Default(parameters, numContexts, tokens, lz77, 31, 3, false), + JxlLz77Method.Lz77b1w3t => ApplyLz77Default(parameters, numContexts, tokens, lz77, 1, 3, true), + JxlLz77Method.Lz77b3w3t => ApplyLz77Default(parameters, numContexts, tokens, lz77, 3, 3, true), + JxlLz77Method.Lz77b7w3t => ApplyLz77Default(parameters, numContexts, tokens, lz77, 7, 3, true), + JxlLz77Method.Lz77b15w3t => ApplyLz77Default(parameters, numContexts, tokens, lz77, 15, 3, true), + JxlLz77Method.Lz77b31w3t => ApplyLz77Default(parameters, numContexts, tokens, lz77, 31, 3, true), + JxlLz77Method.Optc1 => ApplyOptimal(configuration, 1, parameters, numContexts, tokens, lz77), + JxlLz77Method.Optc3 => ApplyOptimal(configuration, 3, parameters, numContexts, tokens, lz77), + JxlLz77Method.Optc8 => ApplyOptimal(configuration, 8, parameters, numContexts, tokens, lz77), + JxlLz77Method.Optc256 => ApplyOptimal(configuration, 256, parameters, numContexts, tokens, lz77), + _ => [], + }; + public static List[] ApplyRunLengthEncoding( Configuration configuration, JxlHistogramParameters parameters, int numContexts, - List> tokens, + List[] tokens, JxlAnsLz77Parameters lz77) { - List[] tokensLz77 = ArrayHelpers.CreateAndInitialize>(tokens.Count, () => []); + List[] tokensLz77 = ArrayHelpers.CreateAndInitialize>(tokens.Length, () => []); JxlLz77SymbolCostEstimator sce = new(numContexts, parameters.ForceHuffman, tokens, lz77); float bitDecrease = 0; int totalSymbols = 0; JxlAnsHybridUIntConfiguration uintConfig = new(); - for (int stream = 0; stream < tokens.Count; stream++) + for (int stream = 0; stream < tokens.Length; stream++) { float distanceMultiplier = parameters.ImageWidths.Count > stream ? parameters.ImageWidths[stream] @@ -120,6 +146,315 @@ public static List[] ApplyRunLengthEncoding( return []; } + private static List[] ApplyLz77Default( + JxlHistogramParameters parameters, + int numContexts, + List[] tokens, + JxlAnsLz77Parameters lz77, + int bucketSize = 7, + int hashSize = 3, + bool runtimeCostComparison = false) + { + List[] tokensLz77 = new List[tokens.Length]; + JxlLz77SymbolCostEstimator sce = new(numContexts, parameters.ForceHuffman, tokens, lz77); + float bitDecrease = 0; + int totalSymbols = 0; + JxlAnsHybridUIntConfiguration uintConfig = new(); + List symCost = []; + + for (int stream = 0; stream < tokens.Length; stream++) + { + int distanceMultiplier = parameters.ImageWidths.Count > stream ? parameters.ImageWidths[stream] : 0; + List input = tokens[stream]; + List output = tokensLz77[stream]; + + totalSymbols += input.Count; + List data = new(input.Count); + + symCost.Resize(input.Count + 1); + for (int pos = 0; pos < input.Count; pos++) + { + uintConfig.Encode(input[pos].Value, out uint tok, out uint nbits, out _); + symCost[pos + 1] = sce.GetBits((int)input[pos].Context, (int)tok) + nbits + symCost[pos]; + data[pos] = input[pos].Value; + } + + _ = output.EnsureCapacity(input.Count); + int mininumLength = (int)lz77.MinimumLength; + + if (mininumLength < 3) + { + throw new InvalidOperationException("Minimum length is below 3"); + } + + JxlLz77HashMap hashMap = new(data, distanceMultiplier, bucketSize, hashSize); + + for (int pos = 0; pos < input.Count; pos++) + { + output.Add(input[pos]); + + // Guard against reading past input bounds + if (pos + hashSize >= input.Count) + { + continue; + } + + hashMap.FindMatch(pos, out int len, out int dist_symbol, mininumLength); + + if (len < mininumLength) + { + continue; + } + + // Bit cost comparison. Raw tokens vs LZ77 pair + float cost = symCost[pos + len] - symCost[pos]; + int lz77Length = len - (int)lz77.MinimumLength; + float lz77Cost = JxlLz77Costs.GetLengthCost(lz77Length) + JxlLz77Costs.GetDistanceCost(dist_symbol); + + if (runtimeCostComparison && lz77Cost > cost) + { + for (int offset = 1; offset < len; offset++) + { + output.Add(input[pos + offset]); + hashMap.Update(pos + offset); + } + + pos += len - 1; + continue; + } + + bitDecrease += cost - lz77Cost - sce.GetAddSymbolCost((int)output[^1].Context); + + // Emit LZ77 length and distance tokens + output[^1] = output[^1] with + { + Value = (uint)(len - mininumLength), + IsLz77Length = true + }; + + output.Add( + new JxlToken( + (JxlMaTreeContext)lz77.NonserializedDistanceContext, + (uint)dist_symbol)); + + // Update hash map for skipped positions inside the match + for (int offset = 1; offset < len; offset++) + { + hashMap.Update(pos + offset); + } + + pos += len - 1; + } + } + + // Return LZ77 streams only if net bit savings justify overhead + if (bitDecrease > (totalSymbols * 0.2f) + 16) + { + return tokensLz77; + } + + return []; + } + + public static List[] ApplyOptimal( + Configuration configuration, + int maxChainLength, + JxlHistogramParameters parameters, + int numContexts, + List[] tokens, + JxlAnsLz77Parameters lz77) + { + List[] tokensForCostEstimate = ApplyLz77Default(parameters, numContexts, tokens, lz77); + + if (tokensForCostEstimate.Length == 0) + { + return []; + } + + JxlLz77SymbolCostEstimator sce = new(numContexts + 1, parameters.ForceHuffman, tokensForCostEstimate, lz77); + List[] tokensLz77 = new List[tokens.Length]; + + JxlAnsHybridUIntConfiguration uintConfig = new(); + List symbolCost = []; + List distanceSymbols = []; + + for (int stream = 0; stream < tokens.Length; stream++) + { + int distanceMultiplier = parameters.ImageWidths.Count > stream ? parameters.ImageWidths[stream] : 0; + List input = tokens[stream]; + List output = tokensLz77[stream]; + + symbolCost.Resize(input.Count + 1); + + for (int i = 0; i < input.Count; i++) + { + uintConfig.Encode(input[i].Value, out uint tok, out uint nbits, out uint unusedBits); + symbolCost[i + 1] = sce.GetBits((int)input[i].Context, (int)tok) + nbits + symbolCost[i]; + } + + _ = output.EnsureCapacity(output.Count + input.Count); + + int maxDistance = input.Count; + int minLength = (int)lz77.MinimumLength; + + if (minLength < 3) + { + throw new InvalidOperationException("Minimum length is below 3"); + } + + int maxLength = input.Count; + int windowSize = 1; + + while (windowSize < maxDistance && windowSize < WindowSize) + { + windowSize <<= 1; + } + + JxlLz77HashChain chain = new(configuration, maxChainLength, input, windowSize, minLength, maxLength, distanceMultiplier); + + static MatchInfo CreateMatchInfo() => new() + { + Length = 0, + DistanceSymbol = 0, + Context = 0, + TotalCost = float.MaxValue + }; + + MatchInfo[] prefixCosts = new MatchInfo[input.Count + 1]; + + for (int i = 0; i < prefixCosts.Length; i++) + { + prefixCosts[i] = CreateMatchInfo(); + } + + prefixCosts[0].TotalCost = 0; + + int rleLength = 0; + int skipLz77 = 0; + + for (int i = 0; i < input.Count; i++) + { + chain.Update(i); + + float litCost = prefixCosts[i].TotalCost + symbolCost[i + 1] - symCost[i]; + + if (prefixCosts[i + 1].TotalCost > litCost) + { + prefixCosts[i + 1].DistanceSymbol = 0; + prefixCosts[i + 1].Length = 1; + prefixCosts[i + 1].Context = (int)input[i].Context; + prefixCosts[i + 1].TotalCost = litCost; + } + + if (skipLz77 > 0) + { + skipLz77--; + continue; + } + + distanceSymbols.Clear(); + + chain.FindMatches(i, maxDistance, (uint ulen, int distSymbol) => + { + int len = (int)ulen; + if (distanceSymbols.Count <= len) + { + int oldCount = distanceSymbols.Count; + distanceSymbols.Resize(len + 1); + + for (int j = oldCount; j < distanceSymbols.Count; j++) + { + distanceSymbols[j] = distSymbol; + } + } + + if (distSymbol < distanceSymbols[len]) + { + distanceSymbols[len] = distSymbol; + } + }); + + if (distanceSymbols.Count <= minLength) + { + continue; + } + + { + int bestCost = distanceSymbols[^1]; + + for (int j = distanceSymbols.Count - 1; j >= minLength; j--) + { + if (distanceSymbols[j] < bestCost) + { + bestCost = distanceSymbols[j]; + } + + distanceSymbols[j] = bestCost; + } + } + + for (int j = minLength; j < distanceSymbols.Count; j++) + { + float lz77Cost = sce.GetLengthCost((int)input[i].Context, j - minLength, lz77) + sce.GetDistanceCost(distanceSymbols[j], lz77); + float cost = prefixCosts[i].TotalCost + lz77Cost; + + if (prefixCosts[i + j].TotalCost > cost) + { + prefixCosts[i + j].Length = j; + prefixCosts[i + j].DistanceSymbol = distanceSymbols[j] + 1; + prefixCosts[i + j].Context = (int)input[i].Context; + prefixCosts[i + j].TotalCost = cost; + } + } + + // Skip most symbols in long RLE sequences to avoid quadratic costs. + if ((distanceSymbols[^1] == 0 && distanceMultiplier == 0) || (distanceSymbols[^1] == 1 && distanceMultiplier != 0)) + { + rleLength++; + } + else + { + rleLength = 0; + } + + if (rleLength >= 8 && distanceSymbols.Count > 9) + { + skipLz77 = distanceSymbols.Count - 10; + rleLength = 0; + } + } + + int pos = input.Count; + + while (pos > 0) + { + bool isLz77Length = prefixCosts[pos].DistanceSymbol != 0; + + if (isLz77Length) + { + int distSymbol = prefixCosts[pos].DistanceSymbol - 1; + output.Add(new JxlToken((JxlMaTreeContext)lz77.NonserializedDistanceContext, (uint)distSymbol)); + } + + uint val = isLz77Length + ? (uint)(prefixCosts[pos].Length - minLength) + : input[pos - 1].Value; + + output.Add(new JxlToken((JxlMaTreeContext)prefixCosts[pos].Context, val)); + output[^1] = output[^1] with + { + IsLz77Length = isLz77Length + }; + + pos -= prefixCosts[pos].Length; + } + + output.Reverse(); + } + + return tokensLz77; + } + /// /// Computes a Murmur-style mix hash. /// @@ -179,4 +514,15 @@ public static uint GetHash(int hashSize, int pos, ReadOnlySpan data) /// public static uint GetHash(int hashSize, int pos, List data) => GetHash(hashSize, pos, CollectionsMarshal.AsSpan(data)); + + private struct MatchInfo() + { + public int Length { get; set; } + + public int DistanceSymbol { get; set; } + + public int Context { get; set; } + + public float TotalCost { get; set; } = float.MaxValue; + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77HashChain.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77HashChain.cs index 20787d8882..6f230b551e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77HashChain.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77HashChain.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; using SixLabors.ImageSharp.Memory; @@ -105,6 +106,26 @@ public JxlLz77HashChain( } } + public JxlLz77HashChain( + Configuration configuration, + int maxChainLength, + List data, + int windowSize, + int minLength, + int maxLength, + int distanceMultiplier) + : this( + configuration, + maxChainLength, + CollectionsMarshal.AsSpan(data), + data.Count, + windowSize, + minLength, + maxLength, + distanceMultiplier) + { + } + public uint CountZeros(int pos, uint previousPos) { int end = pos + this.windowSize; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77HashMap.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77HashMap.cs new file mode 100644 index 0000000000..2765750bf4 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77HashMap.cs @@ -0,0 +1,140 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Lz77; + +internal sealed class JxlLz77HashMap +{ + private readonly List data; + private readonly uint hashMask; + private readonly HashBucket[] hashTable; + private readonly List specialDistTable = []; + private readonly int bucketSize; + private readonly int hashSize; + private readonly int numSpecialDistances; + + public JxlLz77HashMap(List data, int distanceMultiplier, int bucketSize, int hashSize, int hashBits = 14) + { + this.data = data; + this.bucketSize = bucketSize; + this.hashSize = hashSize; + this.hashMask = (1u << hashBits) - 1; + this.hashTable = new HashBucket[(int)this.hashMask + 1]; + + for (int i = 0; i < this.hashTable.Length; i++) + { + this.hashTable[i] = new HashBucket(bucketSize); + } + + this.numSpecialDistances = 0; + + if (distanceMultiplier != 0) + { + this.numSpecialDistances = JxlAnsReader.NumSpecialDistances; + this.specialDistTable = new List((distanceMultiplier * 8) + 8); + + for (int i = 0; i < (distanceMultiplier * 8) + 8; i++) + { + this.specialDistTable.Add(-1); + } + + for (int i = 0; i < JxlAnsReader.NumSpecialDistances; i++) + { + this.specialDistTable[JxlAnsReader.SpecialDistance(i, distanceMultiplier)] = (sbyte)i; + } + } + } + + public void FindMatch(int pos, out int len, out int distSymbol, int minLen) + { + uint h = JxlLz77.GetHash(this.hashSize, pos, this.data) & this.hashMask; + HashBucket bucket = this.hashTable[(int)h]; + + len = 0; + distSymbol = 0; + + for (int i = 0; i < bucket.Size; i++) + { + uint candidate = bucket.Data[i]; + int dist = pos - (int)candidate; + int curLength = this.MatchLength((int)candidate, pos); + + if (curLength < len || curLength < minLen) + { + continue; + } + + int curDistSymbol = this.numSpecialDistances + dist - 1; + + if (dist < this.specialDistTable.Count) + { + int lookup = this.specialDistTable[dist]; + + if (lookup >= 0) + { + curDistSymbol = lookup; + } + } + + if (curLength == len && curDistSymbol >= distSymbol) + { + continue; + } + + len = curLength; + distSymbol = curDistSymbol; + } + + this.Insert(pos, (int)h); + } + + public void Update(int pos) + { + uint h = JxlLz77.GetHash(this.hashSize, pos, this.data) & this.hashMask; + this.Insert(pos, (int)h); + } + + private void Insert(int pos, int hash) + { + ref HashBucket bucket = ref this.hashTable[hash]; + + bucket.Size = Math.Min(bucket.Size + 1, this.bucketSize); + bucket.Data[bucket.Index] = (uint)pos; + bucket.Index = (bucket.Index + 1) % this.bucketSize; + } + + private int MatchLength(int a, int b) + { + if (a >= b) + { + throw new InvalidOperationException("a must be less than b"); + } + + int len = 0; + + while (b + len < this.data.Count && this.data[a + len] == this.data[b + len]) + { + len++; + } + + return len; + } + + private struct HashBucket + { + public HashBucket(int bucketSize) + { + this.Data = new uint[bucketSize]; + this.Index = 0; + this.Size = 0; + } + + public uint[] Data { get; set; } + + public int Index { get; set; } + + public int Size { get; set; } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77SymbolCostEstimator.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77SymbolCostEstimator.cs index 7a0d84ee74..06be893dc7 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77SymbolCostEstimator.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77SymbolCostEstimator.cs @@ -14,7 +14,7 @@ internal sealed class JxlLz77SymbolCostEstimator private readonly float[] bits; private readonly float[] addSymbolCost; - public JxlLz77SymbolCostEstimator(int numContexts, bool forceHuffman, List> tokens, JxlAnsLz77Parameters lz77) + public JxlLz77SymbolCostEstimator(int numContexts, bool forceHuffman, List[] tokens, JxlAnsLz77Parameters lz77) { JxlHistogram[] builder = ArrayPool.Shared.Rent(numContexts); From c19c7b95713e81fd0f44c22077d61f0c3d4b9650 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 19 Oct 2026 10:51:17 +0400 Subject: [PATCH 222/328] Complete Jpeg Huffman decoder --- .../Formats/Jxl/IO/Jpeg/JpegHuffmanDecoder.cs | 97 ++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegHuffmanDecoder.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegHuffmanDecoder.cs index d0e11df072..03556e71a1 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegHuffmanDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegHuffmanDecoder.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; + namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg; /// @@ -11,7 +13,9 @@ internal static class JpegHuffmanDecoder public const int RootTableBits = 8; public const int LookupSize = 1024; - private static int NextTableBitSize(Span count, int length) + private const int MaxBitLength = JpegDataConstants.JpegHuffmanMaxBitLength; + + public static int NextTableBitSize(Span count, int length) { int left = 1 << (length - RootTableBits); while (length < MaxBitLength) @@ -30,6 +34,97 @@ private static int NextTableBitSize(Span count, int length) return length - RootTableBits; } + public static void BuildJpegHuffmanTable(Span count, Span symbols, Span lut) + { + HuffmanTableEntry code = default; + + Span tempCount = stackalloc int[MaxBitLength + 1]; + tempCount.Clear(); + + int totalCount = 0; + + for (int len = 1; len <= MaxBitLength; ++len) + { + tempCount[len] = count[len]; + totalCount += tempCount[len]; + } + + Span table = lut; + int tableOffset = 0; + + int tableBits = RootTableBits; + int tableSize = 1 << tableBits; + + int key; + + // Special case code, with one value. + if (totalCount == 1) + { + code.Bits = 0; + code.Value = (ushort)symbols[0]; + + for (key = 0; key < tableSize; ++key) + { + table[key] = code; + } + + return; + } + + // Filling in the root table + key = 0; + int idx = 0; + for (int len = 1; len <= RootTableBits; ++len) + { + for (; tempCount[len] > 0; --tempCount[len]) + { + code.Bits = (byte)len; + code.Value = (ushort)symbols[idx++]; + int reps = 1 << (RootTableBits - len); + + while (reps-- != 0) + { + table[key++] = code; + } + } + } + + // Fill 2nd level tables; add pointers to root table + table = table[tableSize..]; + tableOffset += tableSize; + tableSize = 0; + int low = 0; + + for (int len = RootTableBits + 1; len <= MaxBitLength; ++len) + { + for (; tempCount[len] > 0; --tempCount[len]) + { + // Start a new sub-table if the previous one is full. + if (low >= tableSize) + { + table = table[tableSize..]; + tableOffset += tableSize; + + tableBits = NextTableBitSize(tempCount, len); + tableSize = 1 << tableBits; + low = 0; + lut[key].Bits = (byte)(tableBits + RootTableBits); + lut[key].Value = (ushort)(tableOffset - key); + ++key; + } + + code.Bits = (byte)(len - RootTableBits); + code.Value = (ushort)symbols[idx++]; + int reps = 1 << (tableBits - code.Bits); + + while (reps-- != 0) + { + table[low++] = code; + } + } + } + } + public struct HuffmanTableEntry() { public byte Bits = 0; From bdd4ac08ab4c7d9a468ad9a1288a29b32d85bff0 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 19 Oct 2026 10:57:11 +0400 Subject: [PATCH 223/328] Don't split JxlDecoderCore; rename it to JxlDecoderInternal --- .../Formats/Jxl/IO/JxlCodestreamProcessor.cs | 100 ------------ ...xlDecoderCore.cs => JxlDecoderInternal.cs} | 150 ++++++++++++++++-- .../Formats/Jxl/Processing/JxlEntropyCoder.cs | 2 +- .../Processing/Decoder/JxlDecoderCoreTests.cs | 6 +- 4 files changed, 139 insertions(+), 119 deletions(-) delete mode 100644 src/ImageSharp/Formats/Jxl/IO/JxlCodestreamProcessor.cs rename src/ImageSharp/Formats/Jxl/Processing/Decoder/{JxlDecoderCore.cs => JxlDecoderInternal.cs} (94%) diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlCodestreamProcessor.cs b/src/ImageSharp/Formats/Jxl/IO/JxlCodestreamProcessor.cs deleted file mode 100644 index e83d4a1481..0000000000 --- a/src/ImageSharp/Formats/Jxl/IO/JxlCodestreamProcessor.cs +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Formats.Jxl.Fields; -using SixLabors.ImageSharp.Formats.Jxl.Processing; -using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; - -namespace SixLabors.ImageSharp.Formats.Jxl.IO; - -internal sealed class JxlCodestreamProcessor -{ - private bool parsedCodestreamSignature; - private bool parsedBasicInfo; - private long codestreamSizeInBits; - private readonly Stream stream; - private readonly JxlBitReader bitReader; - - public JxlCodestreamProcessor(Stream stream) - { - this.stream = stream; - this.bitReader = new(stream); - } - - public void ReadBasicInfo() - { - this.MaybeParseCodestreamSignature(); - - long total = this.bitReader.TotalBitsConsumed; - - this.ReadBundle(this.metadata!.Size!); - this.ReadBundle(this.metadata!.ImageMetadata!); - - long delta = total - this.bitReader.TotalBitsConsumed; - this.UpdateCodestreamSize(delta); - this.parsedBasicInfo = true; - this.basicInfoSizeHint = 0; - this.imageMetadata = this.metadata.ImageMetadata; - - if (!CheckSizeLimit(this.metadata.Size!.XSize, this.metadata.Size.YSize)) - { - throw new InvalidOperationException("The image is too large"); - } - } - - /// - /// Reads a single bundle into . - /// - /// Type of the bundle to read. - /// The bundle to parse. - private void ReadBundle(T bundle) - where T : IJxlFields - { - if (!JxlBundle.Read(this.bitReader, bundle)) - { - JxlCodestreamProcessorThrowHelper.ThrowNotEnoughSourceDataForBundle(); - } - } - - /// - /// Checks if width * height can be represented safely as a - /// positive integer after rounding the width up to the next - /// multiple of 32. - /// - /// Input width. - /// Input height. - /// - /// Boolean indicating whether the padded image dimensions fit - /// within a signed 32-bit integer when calculating the total - /// pixel count. - /// - /// - /// Negative values aren't rejected, but will produce incorrect - /// results. This method is meant to be used with positive values only. - /// - private static bool CheckSizeLimit(int width, int height) - { - if (width == 0 || height == 0) - { - return true; - } - - int paddedWidth = JxlMath.DivCeil(width, 32) * 32; - - if (paddedWidth < width) - { - // Overflow - return false; - } - - int pixelCount = paddedWidth * height; - - if (pixelCount / paddedWidth != height) - { - // Overflow - return false; - } - - return true; - } -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderInternal.cs similarity index 94% rename from src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs rename to src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderInternal.cs index 4831b7f8a4..2c1797cf9f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderInternal.cs @@ -19,9 +19,12 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; /// -/// Internal decoder for JPEG XL. +/// An internal decoder for JPEG XL. This decoder (mostly) does the +/// internal heavy lifting: parsing the container/codestream, +/// feeding it to a frame decoder, doing lossless Jpeg-Jxl conversion, +/// etc. /// -internal sealed class JxlDecoderCore : ImageDecoderCore, IDisposable +internal sealed class JxlDecoderInternal : ImageDecoderCore, IDisposable { private const long NumBuffersLimit = 1 << 20; @@ -120,12 +123,12 @@ internal sealed class JxlDecoderCore : ImageDecoderCore, IDisposable /// /// Type of the box currently being decoded. /// - private int boxType; + private JxlBoxType boxType; /// /// Underlying type for brob boxes. /// - private int boxDecodedType; + private JxlBoxType boxDecodedType; private bool boxEvent; @@ -359,11 +362,6 @@ internal sealed class JxlDecoderCore : ImageDecoderCore, IDisposable private bool bufferingJxlpIsLast; - /// - /// The JPEG decoder allows lossless decoding from JPEG XL to JPEG. - /// - private readonly JxlToJpegDecoder jpegDecoder = new(); - /// /// Decompresses box contents. /// @@ -448,7 +446,7 @@ internal sealed class JxlDecoderCore : ImageDecoderCore, IDisposable /// private JxlBitDepthMetadata imageOutputBitDepth = new(); - public JxlDecoderCore(DecoderOptions options) + public JxlDecoderInternal(DecoderOptions options) : base(options) => this.Reset(); @@ -1194,6 +1192,48 @@ public bool TryRequestMoreInput() /// True if the decoder can use code stream input. Otherwise false. public bool CanUseMoreCodestreamInput() => this.decoderStage != JxlDecoderStage.CodeStreamFinished; + /// + /// Checks if width * height can be represented safely as a + /// positive integer after rounding the width up to the next + /// multiple of 32. + /// + /// Input width. + /// Input height. + /// + /// Boolean indicating whether the padded image dimensions fit + /// within a signed 32-bit integer when calculating the total + /// pixel count. + /// + /// + /// Negative values aren't rejected, but will produce incorrect + /// results. This method is meant to be used with positive values only. + /// + public static bool CheckSizeLimit(int width, int height) + { + if (width == 0 || height == 0) + { + return true; + } + + int paddedWidth = JxlMath.DivCeil(width, 32) * 32; + + if (paddedWidth < width) + { + // Overflow + return false; + } + + int pixelCount = paddedWidth * height; + + if (pixelCount / paddedWidth != height) + { + // Overflow + return false; + } + + return true; + } + /// /// Resets the decoder state to its default values, and, /// additionally, releases memory used by buffers and replaces @@ -1429,6 +1469,83 @@ private void BeforeUpdateState(string propertyName) } } + /// + /// Reads a single bundle into . + /// + /// Type of the bundle to read. + /// Stream to read data from. + /// Bit reader to continue from. + /// The bundle to parse. + /// Status of parsing the bundle. + private bool ReadBundle(Stream stream, JxlBitReader br, T bundle) + where T : IJxlFields + { + JxlBitReader reader = new(stream); + reader.SkipBits64((ulong)br.TotalBitsConsumed); + + bool canRead = JxlBundle.CanRead(reader, bundle); + + if (!canRead) + { + return this.TryRequestMoreInput(); + } + + if (!JxlBundle.Read(reader, bundle)) + { + return false; + } + + return true; + } + + /// + /// Reads all basic metadata and headers. + /// + /// Thrown if the data is incorrect. + /// Thrown if the data is malformed. + public void ReadBasicInfo(Stream stream) + { + if (!this.gotCodestreamSignature) + { + Span fileSignature = [0, 0]; + stream.ReadExactly(fileSignature); + + if (fileSignature[0] != 0xFF || fileSignature[1] != JxlShared.CodestreamMarker) + { + throw new InvalidOperationException("The file signature is invalid"); + } + + this.gotCodestreamSignature = true; + } + + JxlBitReader bitReader = new(stream); + + if (!this.ReadBundle(stream, bitReader, this.metadata!.Size!)) + { + throw new InvalidDataException("Could not parse the size header"); + } + + if (!this.ReadBundle(stream, bitReader, this.metadata!.ImageMetadata!)) + { + throw new InvalidDataException("Could not parse the image metadata"); + } + + long totalBits = bitReader.TotalBitsConsumed; + + (long div, long rem) = Math.DivRem(totalBits, JxlMath.BitsPerByte); + this.AdvanceCodeStream(div); + + this.codestreamBitsAhead = rem; + this.gotBasicInfo = true; + this.basicInfoSizeHint = 0; + this.imageMetadata = this.metadata.ImageMetadata; + + if (!CheckSizeLimit(this.metadata.Size!.XSize, this.metadata.Size.YSize)) + { + throw new InvalidOperationException("The image is too large"); + } + } + /// /// Parses all necessary headers. /// @@ -1625,7 +1742,10 @@ public int ProcessCodestream() this.imageBundle ??= new(this.imageMetadata!); - this.jpegDecoder.SetImageBundleJpegData(this.imageBundle!); + if (!this.jpegDecoder.SetImageBundleJpegData(this.imageBundle!)) + { + throw new InvalidOperationException("Cannot set JXL->JPEG decoder image bundle"); + } this.frameDecoder = new(this.passesState!, this.metadata!, useSlowRenderingPipeline: false); this.frameHeader = new() @@ -2125,12 +2245,12 @@ public int ProcessBoxes(Stream stream) if (this.reconstructionExifSize > 0) { - _ = JxlToJpegDecoder.TrySetExif(this.exifMetadata!.Memory.Span, jpegData); + JxlToJpegDecoder.TrySetExif(this.exifMetadata!.Memory.Span, jpegData); } if (this.reconstructionXmpSize > 0) { - _ = JxlToJpegDecoder.TrySetXmp(this.xmpMetadata!.Memory.Span, jpegData); + JxlToJpegDecoder.TrySetXmp(this.xmpMetadata!.Memory.Span, jpegData); } this.reconstructionOutputJpeg = JpegReconstructionStage.Output; @@ -2138,7 +2258,7 @@ public int ProcessBoxes(Stream stream) if (this.reconstructionOutputJpeg == JpegReconstructionStage.Output && !this.JbrdNeedsMoreBoxes()) { - this.jpegDecoder!.WriteOutput(this.imageBundle!.JpegData!); + this.jpegDecoder!.WriteOutput(this.imageBundle!.JpegData); this.reconstructionOutputJpeg = JpegReconstructionStage.None; this.imageBundle.Reset(); @@ -2599,7 +2719,7 @@ public void DecodeInput(Stream stream) } } - private static void ThrowNotEnoughData() => throw new EndOfStreamException("Not enough data"); + private static void ThrowNotEnoughData() => throw new InvalidOperationException("Not enough data"); protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) => throw new NotImplementedException(); diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlEntropyCoder.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlEntropyCoder.cs index b36517e30b..965bb6a65f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlEntropyCoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlEntropyCoder.cs @@ -106,7 +106,7 @@ public static bool DecodeBlockContextMap(Configuration configuration, JxlBitRead contextMap.ContextMap = ctxMap; - if (!JxlDecoderCore.DecodeContextMap(configuration, ref ctxMap, contextMap.ContextCount, reader)) + if (!JxlDecoderInternal.DecodeContextMap(configuration, ref ctxMap, contextMap.ContextCount, reader)) { return false; } diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/Decoder/JxlDecoderCoreTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Decoder/JxlDecoderCoreTests.cs index da39c56499..b603a42de0 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/Decoder/JxlDecoderCoreTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Decoder/JxlDecoderCoreTests.cs @@ -722,7 +722,7 @@ public void TestOutOfOrderJxlpDuplicateIndexTest() using MemoryStream ms = new(byteArrayData); ms.Position = 0; - using JxlDecoderCore decoderCore = new(DecoderOptions.Default); + using JxlDecoderInternal decoderCore = new(DecoderOptions.Default); decoderCore.SubscribeEvents(JxlDecoderStatus.BasicInfo); decoderCore.DecodeInput(ms); @@ -800,7 +800,7 @@ public void TestSpotColors() Align = 0 }; - using JxlDecoderCore decoder = new(DecoderOptions.Default); + using JxlDecoderInternal decoder = new(DecoderOptions.Default); decoder.SubscribeEvents(JxlDecoderStatus.BasicInfo | JxlDecoderStatus.FullImage); if (renderSpot == 0) @@ -882,7 +882,7 @@ public void TestSpotColors() public void TestDcNotGettable() { ReadOnlySpan compressed = "\xFF\n\0\x10\xB0\x13\0H\x80(\0\xDC\0U\x0F\0\0\xA8P\x19e\xDC\xE0\xE5\\\xCF\x97\x1F:,\xA6m\\gh\xABm\vK\x12E\xC6\xB1I\xAAC&pH\x12\xEB \xF3\x06\x12\bp\0\x80\x9F\x1C\x99W2d\xAD$\x01"u8; - using JxlDecoderCore decoder = new(DecoderOptions.Default); + using JxlDecoderInternal decoder = new(DecoderOptions.Default); decoder.SubscribeEvents(JxlDecoderStatus.BasicInfo); using MemoryStream ms = new(compressed.ToArray()); From 5c1dd400a7f3dd1242ce957f8a853ed48a242e25 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 19 Oct 2026 13:17:01 +0400 Subject: [PATCH 224/328] Complete noise encoder, add methods for decoder internal tests --- .../Processing/Decoder/JxlDecoderInternal.cs | 18 +- .../Encoder/Noise/JxlLossFunction.cs | 11 +- .../Encoder/Noise/JxlNoiseEncoder.cs | 266 ++++++++++++++++++ .../Encoder/Noise/JxlNoiseHistogram.cs | 2 +- .../Encoder/Noise/PhotonNoiseTests.cs | 1 - 5 files changed, 292 insertions(+), 6 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderInternal.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderInternal.cs index 2c1797cf9f..779730875e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderInternal.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderInternal.cs @@ -2125,7 +2125,7 @@ public int ProcessBoxes(Stream stream) { if (this.boxStage != JxlBoxStage.Header) { - // this.AdvanceInput(this.headerSize); + this.AdvanceInput(this.headerSize); this.headerSize = 0; if ((this.eventsWanted & JxlDecoderStatus.Box) != 0 && this.boxEvent && !this.boxOutBufferSetCurrentBox) @@ -2668,7 +2668,7 @@ public int ProcessBoxes(Stream stream) /// /// Stream to decode from. /// Thrown when data or input parameters are invalid. - public void DecodeInput(Stream stream) + public int DecodeInput(Stream stream) { this.Reset(); this.decoderStage = JxlDecoderStage.Started; @@ -2717,8 +2717,22 @@ public void DecodeInput(Stream stream) throw new InvalidOperationException("Missing metadata boxes for JPEG reconstruction"); } } + + return status; } + /// + /// Sets the event mask. + /// + /// The event that we want to subscribe to. + public void SubscribeEvents(int events) => this.eventsWanted |= events; + + /// + /// Removes the event mask. + /// + /// The event that we want to unsubscribe from. + public void UnsubscribeEvents(int events) => this.eventsWanted &= ~events; + private static void ThrowNotEnoughData() => throw new InvalidOperationException("Not enough data"); protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) => throw new NotImplementedException(); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlLossFunction.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlLossFunction.cs index e338862bb0..a08891d0e4 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlLossFunction.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlLossFunction.cs @@ -1,11 +1,13 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Optimize; using SixLabors.ImageSharp.Formats.Jxl.Processing.Noise; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Noise; -internal sealed class JxlLossFunction(ReadOnlyMemory noiseLevels) +internal sealed class JxlLossFunction(List noiseLevels) { public double Compute(Span w, Span df, bool skipRegularization = false) { @@ -16,7 +18,7 @@ public double Compute(Span w, Span df, bool skipRegularization = w.Clear(); - ReadOnlySpan levels = noiseLevels.Span; + ReadOnlySpan levels = CollectionsMarshal.AsSpan(noiseLevels); for (int i = 0; i < levels.Length; i++) { @@ -60,4 +62,9 @@ public double Compute(Span w, Span df, bool skipRegularization = return lossFunction; } + + public double OptimizeFunction(JxlOptimizeArray a, ref JxlOptimizeArray b) => + this.Compute( + a.Span, + b.Span); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseEncoder.cs index 026ee73f2f..519c07e304 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseEncoder.cs @@ -3,7 +3,12 @@ using System.Buffers; using System.Numerics.Tensors; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Optimize; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Noise; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Noise; @@ -12,6 +17,13 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Noise; /// internal static class JxlNoiseEncoder { + private static readonly float[][] LaplacianFilter = + [ + [-0.25f, -1.0f, -0.25f], + [-1.0f, 5.0f, -1.0f], + [-0.25f, -1.0f, -0.25f], + ]; + public static float GetScoreSumsOfAbsoluteDifferences(JxlImage3F opsin, int x, int y, int blockSize) { const int smallBlockSizeX = 3; @@ -63,4 +75,258 @@ public static float GetScoreSumsOfAbsoluteDifferences(JxlImage3F opsin, int x, i return totalSadSum / samples; } + + public static IMemoryOwner GetSadScoresForPatches( + Configuration configuration, + JxlImage3F opsin, + int blockSize, + int binCount, + JxlNoiseHistogram sadHistogram) + { + IMemoryOwner sadScores = configuration.MemoryAllocator.Allocate((opsin.YSize / blockSize) * (opsin.XSize / blockSize)); + Span sadScoresData = sadScores.GetSpan(); + int blockIndex = 0; + + for (int y = 0; y + blockSize <= opsin.YSize; y += blockSize) + { + for (int x = 0; x + blockSize <= opsin.XSize; x += blockSize) + { + float score = GetScoreSumsOfAbsoluteDifferences(opsin, x, y, blockSize); + sadScoresData[blockIndex++] = score; + sadHistogram.Increment(score * binCount); + } + } + + return sadScores; + } + + public static void OptimizeNoiseParameters(Configuration configuration, List noiseLevel, JxlNoiseParameters noiseParameters, float multiplier) + { + const double maxError = 1e-3; + const double precision = 1e-8; + const int maxIterations = 40; + + Span noiseLevelSpan = CollectionsMarshal.AsSpan(noiseLevel); + + // TODO: this is equivalent to .Average(x => x.NoiseLevel). + // Unfortunately we can't use LINQ on a Span or Memory though. + float avg = 0; + + for (int i = 0; i < noiseLevelSpan.Length; i++) + { + avg += noiseLevelSpan[i].NoiseLevel; + } + + avg /= noiseLevelSpan.Length; + + JxlLossFunction lossFunction = new(noiseLevel); + JxlOptimizeArray parameterVector = new(configuration, OptimizeArrayWidth, avg); + + parameterVector = JxlOptimizeArray.OptimizeWithScaledConjugateGradientMethod( + new Factory(configuration), + lossFunction.OptimizeFunction, + parameterVector, + precision, + maxIterations); + + // Clamp to account codestream limits + // TODO: TensorPrimitives + for (int i = 0; i < parameterVector.Span.Length; i++) + { + parameterVector[i] = Math.Clamp(parameterVector[i] * multiplier, 0.0f, JxlNoiseConstants.NoiseLutMax); + } + + Span unused = stackalloc double[parameterVector.Span.Length]; + double loss = lossFunction.Compute(parameterVector.Span, unused, skipRegularization: true) / noiseLevel.Count; + + if (loss > maxError) + { + noiseParameters.Clear(); + return; + } + + for (int i = 0; i < parameterVector.Span.Length; i++) + { + noiseParameters.Lookup[i] = (float)parameterVector.Span[i]; + } + } + + public static List GetNoiseLevel( + JxlImage3F opsin, + Span textureStrength, + float threshold, + int blockSize) + { + List noiseLevelPerIntensity = []; + const int filterSize = 1; + int patchIndex = 0; + + for (int y = 0; y + blockSize <= opsin.YSize; y += blockSize) + { + for (int x = 0; x + blockSize <= opsin.XSize; x += blockSize) + { + if (textureStrength[patchIndex] <= threshold) + { + float meanInt = 0; + + for (int yBlock = 0; yBlock < blockSize; ++yBlock) + { + for (int xBlock = 0; xBlock < blockSize; ++xBlock) + { + meanInt += 0.5f * (opsin.PlaneRow(1, y + yBlock)[x + xBlock] + + opsin.PlaneRow(0, y + yBlock)[x + xBlock]); + } + } + + meanInt /= blockSize * blockSize; + + float noiseLevel = 0; + int count = 0; + + for (int yBlock = 0; yBlock < blockSize; ++yBlock) + { + for (int xBlock = 0; xBlock < blockSize; ++xBlock) + { + float filteredValue = 0; + + for (int yFilter = -1 * filterSize; yFilter <= filterSize; ++yFilter) + { + if (yBlock + yFilter >= 0 && (yBlock + yFilter) < blockSize) + { + for (int xFilter = -1 * filterSize; xFilter <= filterSize; ++xFilter) + { + if ((xBlock + xFilter) >= 0 && (xBlock + xFilter) < blockSize) + { + filteredValue += + 0.5f * + (opsin.PlaneRow(1, y + yBlock + yFilter)[x + xBlock + xFilter] + + opsin.PlaneRow(0, y + yBlock + yFilter)[x + xBlock + xFilter]) * + LaplacianFilter[yFilter + filterSize][xFilter + filterSize]; + } + else + { + filteredValue += + 0.5f * + (opsin.PlaneRow(1, y + yBlock + yFilter)[x + xBlock - xFilter] + + opsin.PlaneRow(0, y + yBlock + yFilter)[x + xBlock - xFilter]) * + LaplacianFilter[yFilter + filterSize][xFilter + filterSize]; + } + } + } + else + { + for (int xFilter = -1 * filterSize; xFilter <= filterSize; ++xFilter) + { + if ((xBlock + xFilter) >= 0 && + (xBlock + xFilter) < blockSize) + { + filteredValue += + 0.5f * + (opsin.PlaneRow(1, y + yBlock - yFilter)[x + xBlock + xFilter] + + opsin.PlaneRow(0, y + yBlock - yFilter)[x + xBlock + xFilter]) * + LaplacianFilter[yFilter + filterSize][xFilter + filterSize]; + } + else + { + filteredValue += + 0.5f * + (opsin.PlaneRow(1, y + yBlock - yFilter)[x + xBlock - xFilter] + + opsin.PlaneRow(0, y + yBlock - yFilter)[x + xBlock - xFilter]) * + LaplacianFilter[yFilter + filterSize][xFilter + filterSize]; + } + } + } + } + + noiseLevel += MathF.Abs(filteredValue); + count++; + } + } + + noiseLevel /= count; + + JxlNoiseLevel nl = default; + nl.Intensity = meanInt; + nl.NoiseLevel = noiseLevel; + + noiseLevelPerIntensity.Add(nl); + } + + patchIndex++; + } + } + + return noiseLevelPerIntensity; + } + + public static float GetSadThreshold(JxlNoiseHistogram histogram) => (float)histogram.Mode / JxlNoiseHistogram.Bins; + + public static void EncodeFloatParameter(float value, float precision, JxlBitWriter writer) + { + if (value < 0) + { + throw new InvalidOperationException("Float parameter is negative"); + } + + int absValQuant = (int)MathF.Round(value * precision, MidpointRounding.AwayFromZero); + + if (absValQuant >= 1 << 10) + { + throw new InvalidOperationException("Absolute value quant overflows"); + } + + writer.Write(10, absValQuant); + } + + public static bool GetNoiseParameter(Configuration configuration, JxlImage3F opsin, JxlNoiseParameters noiseParams, float qualityCoefficient) + { + const int blockSize = 8; + const int binCount = 256; + + JxlNoiseHistogram sadHistogram = new(); + using IMemoryOwner sadScores = GetSadScoresForPatches(configuration, opsin, blockSize, binCount, sadHistogram); + float sadThreshold = GetSadThreshold(sadHistogram); + + if (sadThreshold is > 0.15f or <= 0.0f) + { + noiseParams.Clear(); + return false; + } + + List nl = GetNoiseLevel(opsin, sadScores.GetSpan(), sadThreshold, blockSize); + OptimizeNoiseParameters(configuration, nl, noiseParams, qualityCoefficient * 1.4f); + + return noiseParams.ContainsAny; + } + + public static bool EncodeNoise(JxlNoiseParameters noiseParams, JxlBitWriter writer, JxlLayerType layer, JxlAuxiliaryOutput auxOut) + { + if (!noiseParams.ContainsAny) + { + throw new InvalidOperationException("Noise parameters are empty"); + } + + return writer.WithMaxBits( + JxlNoiseParameters.NoisePoints * 16, layer, auxOut, () => + { + // Fast iteration without having to enumerate + // through the generic IEnumerable + Span fastLookup = noiseParams.Lookup; + for (int i = 0; i < fastLookup.Length; i++) + { + float item = fastLookup[i]; + EncodeFloatParameter(item, JxlNoiseConstants.Precision, writer); + } + + return true; + }); + } + + private sealed class Factory : JxlOptimizeArrayFactory + { + public Factory(Configuration configuration) + : base(configuration, OptimizeArrayWidth) + { + } + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseHistogram.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseHistogram.cs index 396021a55d..15fba46db4 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseHistogram.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseHistogram.cs @@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Noise; internal sealed class JxlNoiseHistogram { - private const int Bins = 256; + public const int Bins = 256; private readonly uint[] bins = new uint[Bins]; diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/Noise/PhotonNoiseTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/Noise/PhotonNoiseTests.cs index 4f1eb5e5a9..43aeae2b00 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/Noise/PhotonNoiseTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/Noise/PhotonNoiseTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Diagnostics.CodeAnalysis; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Noise; namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing.Encoder.Noise; From 44021f6570641cdffb1ae6a56f3b637e053494e4 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Fri, 23 Oct 2026 18:35:17 +0400 Subject: [PATCH 225/328] Document many components, add a new Modular decoder method, incomplete UndoTransforms in external image decoder, XML documentation for StoreVarLenUintX in ANS encoder, documentation and 2 new methods in JxlHeuristics, and introduce JxlImageOperations.ForceCopyImage to remove manual throws when copying fails There are still many components left to document. --- .../Formats/Jxl/IO/Jpeg/JpegDebug.cs | 13 + .../Formats/Jxl/IO/Jpeg/JpegReader.cs | 10 + .../Formats/Jxl/Memory/JxlImage3{T}.cs | 130 ++++++- .../Decoder/JxlExternalImageDecoder.cs | 47 +++ .../Decoder/Modular/JxlModularFrameDecoder.cs | 325 +++++++++++++++++- .../Processing/Encoder/Ans/JxlAnsEncoder.cs | 165 +++++++-- .../Encoder/Heuristics/JxlHeuristics.cs | 265 ++++++++++++++ .../Jxl/Processing/Encoder/JxlCluster.cs | 4 + .../Encoder/JxlCoefficientOrderEncoder.cs | 25 +- .../Encoder/JxlExternalImageEncoder.cs | 11 +- .../Jxl/Processing/Encoder/JxlGaborish.cs | 5 +- .../Jxl/Processing/Encoder/JxlTocEncoder.cs | 14 + .../Encoder/JxlTransformsEncoder.cs | 3 + .../Jxl/Processing/Encoder/JxlXybEncoder.cs | 87 ++++- .../Processing/Image/JxlImageOperations.cs | 32 ++ .../Jxl/Processing/Modular/JxlModularImage.cs | 15 + .../Processing/Modular/JxlModularOptions.cs | 6 + .../Processing/Modular/Transforms/JxlRct.cs | 14 +- .../Modular/Transforms/JxlSqueeze.cs | 35 ++ .../Primitives/JxlAspectRatioHelpers.cs | 11 + .../Processing/Primitives/JxlInverseMtf.cs | 14 +- .../Jxl/Processing/Primitives/JxlXorShift.cs | 12 + .../Formats/Jxl/Processing/OpsinImageTests.cs | 7 + .../Jxl/Processing/OpsinInverseTests.cs | 76 ++++ 24 files changed, 1237 insertions(+), 89 deletions(-) create mode 100644 tests/ImageSharp.Tests/Formats/Jxl/Processing/OpsinInverseTests.cs diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegDebug.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegDebug.cs index 21361b694c..6851d35e4e 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegDebug.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegDebug.cs @@ -5,8 +5,21 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg; +/// +/// Debug-related utilities for the JPEG parser. +/// internal static class JpegDebug { + /// + /// Logs a warning message, when the JPEG parser detects + /// invalid state. + /// + /// + /// Message to log. + /// + /// + /// This function in only available in DEBUG mode. + /// [Conditional("DEBUG")] public static void LogWarning(string message) => // TODO: do we allow Debug.WriteLine in this codebase? diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegReader.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegReader.cs index 395d283e83..2f4de01331 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegReader.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegReader.cs @@ -32,6 +32,16 @@ internal static class JpegReader 0, 0, 0, 0, 0, 0, 1, 0, ]; + /// + /// Within , returns a byte whose offset is + /// represented by the variable , then increments + /// . + /// + /// Data to read the byte from. + /// The offset of the byte. + /// + /// The byte at offset before it was incremented. + /// private static int ReadUint8(ReadOnlySpan data, ref int pos) => data[pos++]; private static int ReadUint16(ReadOnlySpan data, ref int pos) diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs index 882c1098bb..55d6fbcf71 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs @@ -2,36 +2,47 @@ // Licensed under the Six Labors Split License. using System.Buffers; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; namespace SixLabors.ImageSharp.Formats.Jxl.Memory; -// NOTE: Do not seal this class. +/// +/// A wrapper over 3 planes of type . See +/// also . +/// +/// +/// The kind of each item in each of the 3 planes. +/// +/// +/// This method should not be marked as sealed. Types such as +/// are commonly used across the JPEG-XL implementation, +/// which derive from this class. +/// internal class JxlImage3 : IDisposable where T : unmanaged { - private sealed class TypeChangingMemoryManager(Memory memory) : MemoryManager - where TTarget : unmanaged - { - public override Span GetSpan() => MemoryMarshal.Cast(memory.Span); - - // we don't use these - public override MemoryHandle Pin(int elementIndex = 0) => throw new NotImplementedException(); - - public override void Unpin() => throw new NotImplementedException(); - - protected override void Dispose(bool disposing) - { - } - } - + /// + /// A contains 3 planes. + /// private const int PlaneCount = 3; + /// + /// All 3 underlying planes are stored here. + /// private JxlPlane[] planes = new JxlPlane[3]; + /// + /// Initializes a new instance of the class + /// with 3 planes that are empty and have a width and height of 0 pixels. + /// public JxlImage3() { + // We want to initialize planes, so they're at least empty + // planes rather than null. + this.planes[0] = new(); + this.planes[1] = new(); + this.planes[2] = new(); } public JxlImage3(Configuration configuration, int xSize, int ySize) @@ -45,14 +56,40 @@ public JxlImage3(JxlImage3 other) } } + /// + /// Gets the width of this image. + /// public int XSize => this.planes[0].XSize; + /// + /// Gets the height of this image. + /// public int YSize => this.planes[0].YSize; + /// + /// Gets the number of bytes per row in this image. + /// public int BytesPerRow => this.planes[0].BytesPerRow; + /// + /// Gets the number of pixels per row in this image. + /// public int PixelsPerRow => this.planes[0].PixelsPerRow; + /// + /// Shorthand for image3.Plane().GetRow(). + /// + /// + /// Index of the plane. This can be 0, 1, or 2. + /// + /// + /// Index of the row within the plane whose index is specified by the + /// parameter. + /// + /// + /// A Span view for the row of the plane. Modifying values of this span + /// directly modifies the data for that row in that plane. + /// public Span PlaneRow(int plane, int row) { this.PlaneRowBoundsCheck(plane, row); @@ -86,6 +123,16 @@ public Span PlaneRow(Rectangle rectangle, int c, int y) return this.PlaneRow(c, y + rectangle.Top)[rectangle.Left..]; } + /// + /// Returns a plane specified by index . There are + /// 3 planes in a instance. + /// + /// + /// The index of the plane. This can only be 0, 1 or 2. + /// + /// + /// A plane whose index is specified by the parameter. + /// public JxlPlane Plane(int index) => this.planes[index]; public void Swap(JxlImage3 other) @@ -133,6 +180,7 @@ private void PlaneRowBoundsCheck(int c, int y) /// A rectangle with x,y=0,0 width,height=XSize,YSize. public Rectangle GetRectangle() => new(0, 0, this.XSize, this.YSize); + /// public void Dispose() { foreach (JxlPlane plane in this.planes) @@ -142,4 +190,52 @@ public void Dispose() GC.SuppressFinalize(this); } + + /// + /// + /// This is used by . The underlying data + /// of a plane is stored as Memory<byte>. Unlike a + /// where Span<byte> can be directly changed to Span<T> + /// using , changing the + /// type of a cannot be done as simply. Instead, we have + /// to create a new instance of a that implements + /// the method. Therefore the following code snippet can be + /// used with this class to reinterpret the type of a Memory. + /// + /// Memory<T> ConvertMemory<T>(Memory<byte> src) + /// { + /// TypeChangingMemoryManager<T> mm = new(src); + /// return mm.Memory; + /// } + /// + /// + /// + /// This is the only option so far, but it allocates on the heap. + /// + /// + /// + /// The target type that a Memory<byte> will be reinterpreted into. + /// + /// + /// Input Memory<byte> to reinterpret into Memory<T>. + /// + private sealed class TypeChangingMemoryManager(Memory memory) : MemoryManager + where TTarget : unmanaged + { + /// + public override Span GetSpan() => MemoryMarshal.Cast(memory.Span); + + /// + // NOTE: We only need the .GetSpan() method. We do not need + // any of the methods below. + public override MemoryHandle Pin(int elementIndex = 0) => throw new NotImplementedException(); + + /// + public override void Unpin() => throw new NotImplementedException(); + + /// + protected override void Dispose(bool disposing) + { + } + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlExternalImageDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlExternalImageDecoder.cs index efaf292a38..20b6def5fd 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlExternalImageDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlExternalImageDecoder.cs @@ -1,7 +1,14 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; +using System.Drawing; +using System.Net.NetworkInformation; using System.Numerics; +using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Memory; +using static System.Net.Mime.MediaTypeNames; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; @@ -25,4 +32,44 @@ public static void FloatToU32(Span input, Span output, int num, flo i.As().CopyTo(output[x..]); } } + + public static JxlPlane UndoOrientation(Configuration configuration, JxlExifOrientation undoOrientation, JxlPlane image) + where T : unmanaged + { + JxlPlane output; + int xSize = image.XSize; + int ySize = image.YSize; + + if (undoOrientation == JxlExifOrientation.FlipHorizontal) + { + output = JxlPlane.Create(configuration, xSize, ySize); + + _ = Parallel.For(0, ySize, configuration.GetParallelOptions(), (y) => + { + Span rowIn = image.GetRow(y); + Span rowOut = output.GetRow(y); + + for (int x = 0; x < xSize; ++x) + { + rowOut[xSize - x - 1] = rowIn[x]; + } + }); + } + else if (undoOrientation == JxlExifOrientation.Rotate180) + { + output = JxlPlane.Create(configuration, xSize, ySize); + + _ = Parallel.For(0, ySize, configuration.GetParallelOptions(), (y) => + { + Span rowIn = image.GetRow(y); + Span rowOut = output.GetRow(ySize - y - 1); + + for (int x = 0; x < xSize; ++x) + { + rowOut[xSize - x - 1] = rowIn[x]; + } + }); + } + // TODO + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Modular/JxlModularFrameDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Modular/JxlModularFrameDecoder.cs index ec6c02d0d6..26976060cd 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Modular/JxlModularFrameDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Modular/JxlModularFrameDecoder.cs @@ -2,7 +2,10 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; +using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular; using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding; @@ -13,13 +16,18 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Modular; +/// +/// Decodes JPEG-XL images coded in Modular mode. +/// +/// Global configuration +/// Frame dimensions for the image. internal sealed class JxlModularFrameDecoder(Configuration configuration, JxlFrameDimensions fd) { private const float AlmostZero = 1e-8f; private readonly Configuration configuration = configuration; - private readonly JxlModularImage fullImage = new(); - private readonly List globalTransform = []; + private JxlModularImage fullImage = new(); + private List globalTransform = []; private readonly JxlFrameDimensions frameDimensions = fd; private bool doColor; private bool haveSomething; @@ -179,6 +187,9 @@ private static void IntToFloat(ReadOnlySpan rowIn, Span rowOut, in public void DecodeQuantizerTable(int requiredSizeX, int requiredSizeY, JxlBitReader reader, JxlQuantizerEncoding encoding, int index, JxlModularFrameDecoder? decoder) { float qtableDen = 0; + + // When we decode a quantizer table, there's an F16 + // indicating quantizer table denominator. if (!JxlF16Coder.Read(reader, ref qtableDen)) { throw new InvalidOperationException("Quantizer table denominator is too small"); @@ -187,16 +198,19 @@ public void DecodeQuantizerTable(int requiredSizeX, int requiredSizeY, JxlBitRea encoding.QuantizationTableDenominator = qtableDen; using JxlModularImage img = JxlModularImage.Create(this.configuration, requiredSizeX, requiredSizeY, 8, 3); + + // Using default modular options. JxlModularOptions options = new(); if (decoder is not null) { - JxlModularStreamId qt = JxlModularStreamId.CreateQuantizerTable(index); - ModularGenericDecompress( + JxlModularStreamId quantizerTable = JxlModularStreamId.CreateQuantizerTable(index); + JxlModularEncoding.ModularGenericDecompress( + this.configuration, reader, img, null, - qt.GetId(decoder.frameDimensions), + quantizerTable.GetId(decoder.frameDimensions), options, true, decoder.tree, @@ -205,7 +219,8 @@ public void DecodeQuantizerTable(int requiredSizeX, int requiredSizeY, JxlBitRea } else { - ModularGenericDecompress( + JxlModularEncoding.ModularGenericDecompress( + this.configuration, reader, img, null, @@ -230,14 +245,17 @@ public void DecodeQuantizerTable(int requiredSizeX, int requiredSizeY, JxlBitRea for (int c = 0; c < 3; c++) { + JxlModularChannel currentChannel = img.Channels[c]; + for (int y = 0; y < requiredSizeY; y++) { - Span row = img.Channels[c].GetRow(y); + Span row = currentChannel.GetRow(y); for (int x = 0; x < requiredSizeX; x++) { qtable[(c * requiredSizeX * requiredSizeY) + (y * requiredSizeX) + x] = row[x]; + // The raw quantization table should never have a negative or zero value. if (row[x] <= 0) { throw new InvalidOperationException("Invalid raw quantization table"); @@ -246,4 +264,297 @@ public void DecodeQuantizerTable(int requiredSizeX, int requiredSizeY, JxlBitRea } } } + + public void DecodeGlobalInfo(JxlBitReader reader, JxlFrameHeader frameHeader, bool allowTruncatedGroup) + { + bool decodeColor = frameHeader.Encoding == JxlFrameEncoding.Modular; + JxlImageMetadata metadata = frameHeader.Metadata!.ImageMetadata!; + bool isGray = metadata.ColorEncoding!.IsGray; + + // We initially assume we have three color channels. + // It could be XYB, RGB, etc. See the if statement + // that follows immediately after this variable. + int numberOfChannels = 3; + + if (isGray && frameHeader.ColorTransform == JxlColorTransform.None) + { + // A grayscale channel can only adjust light, so it has + // no other colors. Meanwhile, having no additional color transforms + // means the entire color system is limited to just one + // color. That color is how bright or dark each pixel is: grayscale. + // So set number of channels to 1. + numberOfChannels = 1; + } + + this.doColor = decodeColor; + + int extraChannelCount = metadata.ExtraChannels.Count; + + // When we decode global info, the first bit indicates whether + // the MA tree is present. + bool hasTree = reader.ReadBoolean(); + + if (!allowTruncatedGroup || + + // This condition checks whether the total bits + // consumed are not byte-aligned. + // For example, 12 bits. An aligned variant would be + // 16 bits or 8 bits. + reader.TotalBitsConsumed < reader.TotalBytes * JxlMath.BitsPerByte) + { + if (hasTree) + { + // Trees cannot be larger than the tree size limit. + // The absolute maximum size of a tree is 1 << 22, + // or 4,194,304. + int treeSizeLimit = Math.Min( + 1 << 22, + 1024 + (this.frameDimensions.XSize * this.frameDimensions.YSize * (numberOfChannels + extraChannelCount) / 16)); + + // Decoding the actual MA tree now, followed by the ANS histograms. + JxlMaDecoder.DecodeTree(this.configuration, reader, this.tree, treeSizeLimit); + JxlAnsReader.DecodeHistograms(this.configuration, reader, (this.tree.Count + 1) / 2, this.code, this.contextMap); + } + } + + if (!this.doColor) + { + // When we don't have to process any colors, + // we effectively have 0 channels. + // + // In this function, this can only happen if + // the frame header says the coding mode for the + // input JPEG-XL file is VarDCT, which is different + // from Modular. + numberOfChannels = 0; + } + + // Does the file use floating-point pixels? + bool fp = metadata.BitDepth!.FloatingPointSample; + + // This statement only does validation. + if (metadata.BitDepth.BitsPerSample >= 32 && + this.doColor && + frameHeader.ColorTransform != JxlColorTransform.Xyb) + { + // We use more than 32 bits per sample, we process + // at least one or more colors, and the color + // transform is either Y'Cb'Cr or none. + if (metadata.BitDepth.BitsPerSample == 32 && !fp) + { + // We use exactly 32 bits per sample, but we + // do not use floating-point samples. + // + // This would have the effect of uint32 samples, + // but the Modular coding mode does not officially + // support this. + throw new NotSupportedException("JPEG-XL does not support uint32 samples in Modular"); + } + + if (metadata.BitDepth.BitsPerSample > 32) + { + // Additionally, the Modular coding mode + // does not support more than 32 bits per + // sample. + throw new NotSupportedException("Too many bits per sample"); + } + } + + // This is the image that will be returned. + // The amount of channels is the number of channels + // we have in the frame header, plus extra channels + // that might be present. + JxlModularImage img = JxlModularImage.Create( + this.configuration, + this.frameDimensions.XSize, + this.frameDimensions.YSize, + (int)metadata.BitDepth.BitsPerSample, + numberOfChannels + extraChannelCount); + + this.allSameShift = true; + + if (frameHeader.ColorTransform == JxlColorTransform.YCbCr) + { + JxlYCbCrChromaSubsampling subsampling = frameHeader.ChromaSubsampling!; + + // For Y'Cb'Cr color transforms, we adjust our image + // for chroma subsampling. + for (int c = 0; c < numberOfChannels; c++) + { + // Reference to the channel of the current index. + JxlModularChannel channel = img.Channels[c]; + + // First, we compute the shifts, which are for the + // chroma subsampling and therefore specific to the + // Y'Cb'Cr chroma transform. + channel.HorizontalShift = subsampling.HShift(c); + channel.VerticalShift = subsampling.VShift(c); + + // Then we compute the widths and heights that are + // divided by the shifts. That's because in Y'Cb'Cr + // chroma subsampling, the luma (Y) part is usually + // the same, while the Chroma (Cb, Cr) part is downsampled. + // So the size for the chroma channel is not the same + // as the original image. + // See https://en.wikipedia.org/wiki/Chroma_subsampling + int xSizeShifted = JxlMath.DivCeil(this.frameDimensions.XSize, 1 << channel.HorizontalShift); + int ySizeShifted = JxlMath.DivCeil(this.frameDimensions.YSize, 1 << channel.VerticalShift); + + // The chroma plane should be shrinked down to its downsampled + // size, as part of chroma subsampling. + // + // Note that if this is channel 0, which is a Luma channel, + // xSizeShifted and ySizeShifted will be the same as the channel's + // original width or height. There's no shift as Luma channels + // do not have downsampling; it's the Chroma channels that are + // downsampled. Meaning, this .Shrink() call wouldn't really do + // anything. + channel.Shrink(this.configuration, xSizeShifted, ySizeShifted); + + // Now we check if the shift is the same for all channels. + // If they are not, then we know that the shift isn't + // the same for all channels. + // + // This wouldn't really work for the 0 channel, but that's + // expected as the 0 channel is the Luma channel which isn't + // really downsampled. For channels 1 and 2 which are chroma + // channels, this should work. + if (channel.HorizontalShift != img.Channels[0].HorizontalShift || + channel.VerticalShift != img.Channels[0].VerticalShift) + { + this.allSameShift = false; + } + } + } + + // For the extra channels, we do almost the same as the + // chroma subsampling for Y'Cb'Cr. Except, extra channels have + // their own upsampling matrix, which we use. + // + // The upsampling matrix is a List, which can be + // slow. So we can use a Span wrapper which is more + // lightweight and faster. + Span extraChannelUpsampling = CollectionsMarshal.AsSpan(frameHeader.ExtraChannelUpsampling); + + // We have two loop variables. + // + // 'ec', short for Extra Channel, is the index of the extra channel. + // We need this to access the upsampling matrix which is specific for extra channels. + // + // 'c', on the other hand, just means channel. It is initially set to + // numberOfChannels, because that's where the first extra channel starts. + // + // Suppose our image is coded using RGB and we have two extra channels, one + // alpha channel and one black channel. In such a case: + // - numberOfChannels would be 3. We have 3 channels - R, G, and B. + // - That also means the first index of the extra channel is the last index + // of the RGB channel (2), plus 1. So that's 3. + // + // From then on, 'c' would keep incrementing, to access a generic list of channels. + for (int ec = 0, c = numberOfChannels; ec < extraChannelCount; ec++, c++) + { + // Upsampling for this specific extra channel + int upsampling = (int)extraChannelUpsampling[ec]; + + // Reference to the channel of the current index. + JxlModularChannel channel = img.Channels[c]; + + // We shrink the channel, so it becomes 'upsampling' times smaller + // than its upsampled size. + channel.Shrink( + this.configuration, + JxlMath.DivCeil(this.frameDimensions.XSizeUpsampled, upsampling), + JxlMath.DivCeil(this.frameDimensions.YSizeUpsampled, upsampling)); + + // Then we have to adjust the shifts. + // Both the horizontal shift and the vertical shift are + // equal to Ceil(Log2(upsampling for this channel)) - Ceil(Log2(upsampling for the frame sizes)). + channel.HorizontalShift = channel.VerticalShift = + JxlMath.CeilLog2Nonzero(upsampling) - JxlMath.CeilLog2Nonzero((int)frameHeader.Upsampling); + + // Now we want to make sure whether or not are shifts different + // for each channel. + if (channel.HorizontalShift != img.Channels[0].HorizontalShift || + channel.VerticalShift != img.Channels[0].VerticalShift) + { + this.allSameShift = false; + } + } + + // Main Modular decoding logic here. The result is stored + // into img. + JxlModularOptions options = new() + { + // Both the maximum channel size and the group dimension + // is set equal to the group dimension specified in the + // frame dimensions. + MaxChannelSize = this.frameDimensions.GroupDimension, + GroupDimension = this.frameDimensions.GroupDimension + }; + + // Do the actual Modular decode using the JxlModularOptions. + bool decompressedSuccessfully = JxlModularEncoding.ModularGenericDecompress( + this.configuration, + reader, + img, + this.groupHeader, + JxlModularStreamId.Global.GetId(this.frameDimensions), + options, + undoTransforms: false, + this.tree, + this.code, + this.contextMap, + allowTruncatedGroup); + + // The ModularGenericDecompress method returns a boolean indicating + // whether or not has the modular decoding process completed successfully. + // When this is false, we know that the decoding process failed, so + // throw an exception. + if (!decompressedSuccessfully) + { + throw new InvalidOperationException("Could not decode global Modular info"); + } + + // Initially we mark this Modular decoder as empty. + // Then, go through each channel. If each channel is + // within the frame dimensions, then at least one channel is + // therefore usable, so we will mark this Modular + // decoder as not empty. + this.haveSomething = false; + for (int c = 0; c < img.Channels.Count; c++) + { + JxlModularChannel channel = img.Channels[c]; + + // We want to make sure that the number of channels are >= the number + // of meta channels, and that each dimension fits within group dimensions. + if (c >= img.MetaChannels && + channel.Width <= this.frameDimensions.GroupDimension && + channel.Height <= this.frameDimensions.GroupDimension) + { + this.haveSomething = true; + break; + } + } + + // We have to move global transforms to groups, if both of these things happen: + // - Number of channels are < number of meta channels, + // or some channels are larger than group dimensions + // - Chroma subsampling is 4:4:4 or 4:0:0 (if the image uses Y'Cb'Cr), + // there are no extra channels, OR there are extra channels but they + // aren't upsampled - therefore, every channel has the same exact shift. + if (!this.haveSomething && this.allSameShift) + { + // If we only have one transform and that transform is the Reversible + // Color Transform (RCT), then we set the global transform to the RCT, + // and we remove it from 'img'. + if (img.Transforms.Count == 1 && img.Transforms[0].TransformType == JxlTransformType.Rct) + { + this.globalTransform = img.Transforms[0]; + img.Transforms.Clear(); + } + } + + // Finally, set the full image. + this.fullImage = img; + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlAnsEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlAnsEncoder.cs index 0a50606de3..c70841f590 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlAnsEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlAnsEncoder.cs @@ -1,26 +1,67 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; + namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; internal static class JxlAnsEncoder { + /// + /// Encodes a variable-length byte. The larger the number, + /// the more bits it encodes, and the smaller the number, + /// the lower bits it encodes, automatically. It looks + /// like this: + /// + /// is_nonzero_flag = Bits(1) + /// if (is_nonzero_flag == 0) { + /// return 0; + /// } else { + /// bits = Bits(3) + /// suffix = Bits(bits) + /// return suffix + /// } + /// + /// + /// This simple variable-length coding scheme encodes the + /// structure into multiple bits. The first bit specifies + /// whether the coefficient is NOT equal to 0. If this first + /// bit is equal to 0, then that means the variable-length + /// number value is 0, and therefore the result is immediately + /// 0. This means 0 requires the lowest amount of bits; just + /// an indicator that the value is equal to 0, which is equal + /// to just 1 bit. However, if this value is equal to 1, that + /// means the value is greater than 0. Following this, 3 + /// bits are read from the stream, which indicate the length + /// of the suffix. This variable will be referred to as "nbits". + /// Following that, the next "nbits" number of bits are read + /// and is referred to as the suffix. Therefore, the result + /// is (1 << nbits) + suffix. + /// + /// + /// As an example, to encode 20, we first encode a 1 bit, because + /// our input is not equal to 0. Then we compute Floor(Log2(20)) to + /// derive the number of suffix bits. We get 4 - the length of + /// the suffix. This narrows the interval to (1 << 4) .. ((1 < < (4 + 1)) - 1), + /// meaning 16 to 31 can be encoded with this suffix length. Note + /// that nbits adds (1 << nbits) to the result automatically. So, + /// subtract 1 << nbits from the input value to get 4, + /// and encode 0b0100 as the suffix. + /// + /// + /// + /// The number to encode. + /// + /// + /// The bit-writer where the number is encoded into. + /// + /// + /// The byte must have a 0..255 range. + /// public static void StoreVarLenUInt8(int n, JxlBitWriter writer) { DebugGuard.MustBeLessThanOrEqualTo(n, 255, nameof(n)); - // vl_uint8(n) { - // /* n limited to 0..255 */ - // is_nonzero_flag == B(1) - // if (is_nonzero_flag == 0) { - // /* Value is zero. */ - // return 0; - // } else { - // vl_suffix_length = B(3) - // vl_suffix = B(vl_suffix_length) - // return vl_suffix_length - // } - // } if (n == 0) { writer.Write(1, 0); @@ -34,22 +75,61 @@ public static void StoreVarLenUInt8(int n, JxlBitWriter writer) } } + /// + /// Encodes a variable-length word. The larger the number, + /// the more bits it encodes, and the smaller the number, + /// the lower bits it encodes, automatically. It looks + /// like this: + /// + /// is_nonzero_flag = Bits(1) + /// if (is_nonzero_flag == 0) { + /// return 0; + /// } else { + /// bits = Bits(4) + /// suffix = Bits(bits) + /// return suffix + /// } + /// + /// + /// This simple variable-length coding scheme encodes the + /// structure into multiple bits. The first bit specifies + /// whether the coefficient is NOT equal to 0. If this first + /// bit is equal to 0, then that means the variable-length + /// number value is 0, and therefore the result is immediately + /// 0. This means 0 requires the lowest amount of bits; just + /// an indicator that the value is equal to 0, which is equal + /// to just 1 bit. However, if this value is equal to 1, that + /// means the value is greater than 0. Following this, 3 + /// bits are read from the stream, which indicate the length + /// of the suffix. This variable will be referred to as "nbits". + /// Following that, the next "nbits" number of bits are read + /// and is referred to as the suffix. Therefore, the result + /// is (1 << nbits) + suffix. + /// + /// + /// As an example, to encode 20, we first encode a 1 bit, because + /// our input is not equal to 0. Then we compute Floor(Log2(20)) to + /// derive the number of suffix bits. We get 4 - the length of + /// the suffix. This narrows the interval to (1 << 4) .. ((1 < < (4 + 1)) - 1), + /// meaning 16 to 31 can be encoded with this suffix length. Note + /// that nbits adds (1 << nbits) to the result automatically. So, + /// subtract 1 << nbits from the input value to get 4, + /// and encode 0b0100 as the suffix. + /// + /// + /// + /// The number to encode. + /// + /// + /// The bit-writer where the number is encoded into. + /// + /// + /// The byte must have a 0..65535 range. + /// public static void StoreVarLenUInt16(int n, JxlBitWriter writer) { DebugGuard.MustBeLessThanOrEqualTo(n, 65535, nameof(n)); - // vl_uint8(n) { - // /* n limited to 0..65535 */ - // is_nonzero_flag == B(1) - // if (is_nonzero_flag == 0) { - // /* Value is zero. */ - // return 0; - // } else { - // vl_suffix_length = B(4) - // vl_suffix = B(vl_suffix_length) - // return vl_suffix_length - // } - // } if (n == 0) { writer.Write(1, 0); @@ -62,4 +142,41 @@ public static void StoreVarLenUInt16(int n, JxlBitWriter writer) writer.Write(nbits, n - (1 << nbits)); } } + + /// + /// Encodes an ANS Hybrid uint config. + /// + /// The hybrid uint config to be encoded. + /// Bit-stream where the configuration will be written to. + /// For MSB/LSB + public static void EncodeUIntConfiguration(JxlAnsHybridUIntConfiguration uintConfig, JxlBitWriter writer, int logAlphaSize) + { + writer.Write(JxlMath.CeilLog2Nonzero(logAlphaSize + 1), uintConfig.SplitExponent); + + if (uintConfig.SplitExponent == logAlphaSize) + { + // The MSB/LSB do not matter + return; + } + + int numBits = (int)JxlMath.CeilLog2Nonzero(uintConfig.SplitExponent + 1); + writer.Write(numBits, uintConfig.MsbInToken); + + numBits = (int)JxlMath.CeilLog2Nonzero(uintConfig.SplitExponent - uintConfig.MsbInToken + 1); + writer.Write(numBits, uintConfig.LsbInToken); + } + + /// + /// Encodes multiple hybrid UInt configurations. + /// + /// The configurations to encode. + /// Bit-stream where the configurations are written to. + /// For MSB/LSB. + public static void EncodeUIntConfigurations(Span configs, JxlBitWriter writer, int logAlphaSize) + { + foreach (JxlAnsHybridUIntConfiguration config in configs) + { + EncodeUIntConfiguration(config, writer, logAlphaSize); + } + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs index 25d2a6aeb8..d5bebfb3db 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs @@ -4,10 +4,15 @@ using System.Buffers; using System.Numerics; using System.Numerics.Tensors; +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Modular; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Heuristics; @@ -18,6 +23,9 @@ internal static class JxlHeuristics /// private const int Size = 5; + /// + /// Gets a very simple table that maps contexts to integers. + /// private static ReadOnlySpan SimpleContextMap => [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, @@ -25,6 +33,9 @@ internal static class JxlHeuristics 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, ]; + /// + /// Gets the kernels for sharper 2x downsampling. + /// private static ReadOnlySpan Downsample2SharperKernel => [ -0.000314256996835f, -0.000314256996835f, -0.000897597057705f, @@ -622,8 +633,262 @@ public static void UpsampleImage(JxlImageF input, JxlImageF output) } } + public static void FindBestDequantMatrices(JxlCompressParameters cparams, JxlModularFrameEncoder modularFrameEncoder, out JxlDequantMatrices dequantMatrices) + { + dequantMatrices = new(); + if (cparams.MaxErrorMode || cparams.DisablePerceptualOptimizations) + { + // Weights for the Mean Squared Error (MSE) + ReadOnlySpan mseWeights = [0.001f, 0.001f, 0.001f]; + + // When perceptual options are enabled, these are MSE weights, + // otherwise these are max errors specified by user compression parameters. + ReadOnlySpan wp = cparams.DisablePerceptualOptimizations ? mseWeights : cparams.MaxError; + + float[][] weights = [[1.0f / wp[0]], [1.0f / wp[1]], [1.0f / wp[2]]]; + + JxlDctQuantWeightParameters dctParameters = new(weights); + JxlQuantizerEncoding[] encodings = ArrayHelpers.CreateAndInitialize(JxlQuantizerConstants.NumberOfQuantizerTables, () => JxlQuantizerEncoding.Dct(dctParameters)); + + dequantMatrices.SetCustom(encodings, modularFrameEncoder); + Span dcWeights = [1.0f / wp[0], 1.0f / wp[1], 1.0f / wp[2]]; + + dequantMatrices.SetCustomDc(dequantMatrices, dcWeights); + } + } + + public static void DownsampleImage2Sharper(Configuration configuration, JxlImageF input, JxlImageF output) + { + const int kernelX = 12; + const int kernelY = 12; + + // A buffer used by the downsampling iteration + // ⚠️ IMPORTANT ⚠️ The length of the inline array + // MUST be equal to kernelY. When we update kernelY, + // please make sure to update the width of this + // inline array as well. + InlineArray12> rowIn = default; + + // Retrieving image dimensions + int xSize = input.XSize; + int ySize = input.YSize; + + JxlImageF tempDownsample = new(configuration, xSize, ySize); + JxlImageOperations.ForceCopyImage(input, tempDownsample); + + using JxlImageF boxDownsample = DownsampleImage(tempDownsample, 2); + tempDownsample.Dispose(); + + using JxlImageF mask = new(configuration, boxDownsample.XSize, boxDownsample.YSize); + CreateMask(boxDownsample, mask); + + for (int y = 0; y < output.YSize; y++) + { + Span rowOut = output.GetRow(y); + Span rowMask = mask.GetRow(y); + + for (int ky = 0; ky < kernelY; ky++) + { + int iy = (y * 2) + ky - ((kernelY - 1) / 2); + + if (iy < 0) + { + iy = 0; + } + else if (iy >= ySize) + { + iy = ySize - 1; + } + + rowIn[ky] = input.GetRowMemory(iy); + } + + for (int x = 0; x < output.XSize; x++) + { + // Processing of min and max values of the original image + float min = float.MaxValue; + float max = float.MinValue; + + // Radius + const int R = 5; + + for (int ky = R; ky + R < kernelY; ky++) + { + // Reusing this span as a performance improvement. + // Memory.Span seems to be expensive to compute. + Span currentRowIn = rowIn[ky].Span; + + for (int kx = R; kx + R < kernelX; kx++) + { + int ix = (x * 2) + kx - ((kernelX - 1) / 2); + + // Making sure x is not negative. If it is, set it to 0. + if (ix < 0) + { + ix = 0; + } + + // ix should not exceed image width. If it does, limit it. + else if (ix >= xSize) + { + ix = xSize - 1; + } + + min = float.Min(min, currentRowIn[ix]); + max = float.Max(max, currentRowIn[ix]); + } + } + + float sum = 0; + + for (int ky = 0; ky < kernelY; ky++) + { + // Reusing this span as a performance improvement. + // Memory.Span seems to be expensive to compute. + Span currentRowIn = rowIn[ky].Span; + + // ky * kernelX to prevent repeated multiplications + int kyKernelX = ky * kernelX; + + for (int kx = 0; kx < kernelX; kx++) + { + int ix = (x * 2) + kx - ((kernelX - 1) / 2); + + // Making sure x is not negative. If it is, set it to 0. + if (ix < 0) + { + ix = 0; + } + + // ix should not exceed image width. If it does, limit it. + else if (ix >= xSize) + { + ix = xSize - 1; + } + + sum += currentRowIn[ix] * Downsample2SharperKernel[kyKernelX + kx]; + } + } + + rowOut[x] = sum; + + const float maskMultiplier = 1; + + float a = rowMask[x] * maskMultiplier; + + // clipMin specifies the minimum bound. + // clipMax specifies the maximum bound. + float clipMin = min - a; + float clipMax = max + a; + + // To prevent repeated bounds checks, + // this is a reference to rowOut[x]. + ref float rowOutX = ref rowOut[x]; + + // Clamping to stay within the bounds + if (rowOutX < clipMin) + { + rowOutX = clipMin; + } + else if (rowOutX > clipMax) + { + rowOutX = clipMax; + } + } + } + } + + /// + /// Computes the weighted, mask-adjusted L2 difference between + /// two image blocks independently for each channel. + /// + /// First image + /// Second image + /// Mask for each channel + /// Y block coordinate + /// X block coordinate + /// + /// The L2 distance for the images. + /// + public static float ComputeBlockL2Distance(JxlImage3F a, JxlImage3F b, JxlImageF mask1x1, int by, int bx) + { + Rectangle rect = RectangleUtils.CreateRectangle( + bx * JxlFrameDimensions.BlockDimensions, + by * JxlFrameDimensions.BlockDimensions, + JxlFrameDimensions.BlockDimensions, + JxlFrameDimensions.BlockDimensions, + a.XSize, + a.YSize); + + // Squared errors per channel. These are initially 0. + InlineArray3 squaredError = default; + + for (int y = 0; y < rect.Height; y++) + { + // We precompute repeated PlaneRow calls for performance. + // The result is wrapped in an InlineArray3, so we can + // retrieve the rows using an array-like access, which + // lets us use loops for each of the 3 channels. + // + // Note that we have to use Memory here, as Span + // cannot be wrapped in an InlineArray. + InlineArray3> rowA = default; + InlineArray3> rowB = default; + + // Computing for the 'a' image first + rowA[0] = a.Plane(0).GetRowMemory(rect, y); + rowA[1] = a.Plane(1).GetRowMemory(rect, y); + rowA[2] = a.Plane(2).GetRowMemory(rect, y); + + // And for the 'b' image here + rowB[0] = a.Plane(0).GetRowMemory(rect, y); + rowB[1] = a.Plane(1).GetRowMemory(rect, y); + rowB[2] = a.Plane(2).GetRowMemory(rect, y); + + // We store a row for the mask directly. + // It's only one channel, so it can be stored + // as a single Span. + Span rowMask = mask1x1.GetRow(rect, y); + + // Actual computation logic here. + for (int x = 0; x < rect.Width; x++) + { + // First we retrieve the mask for the current row and column. + // And then we compute the square of it. + float mask = rowMask[x]; + float mask2 = mask * mask; + + for (int i = 0; i < 3; ++i) + { + // Here we compute the difference of the + // two rows at the current channel and location. + // We then compute the square of the difference. + float diff = rowA[i].Span[x] - rowB[i].Span[x]; + float diff2 = diff * diff; + + // Add the squared mask multiplied by the squared + // difference to the error for this channel. + squaredError[i] += mask2 * diff2; + } + } + } + + // These are weights for each channel. They're read-only. + ReadOnlySpan weights = [12.339445295782363f, 1.0f, 0.2f]; + + // The result is a dot product of squared errors for each + // channel multiplied by the channel's respective weights. + float result = + (weights[0] * squaredError[0]) + + (weights[1] * squaredError[1]) + + (weights[2] * squaredError[2]); + + return result; + } + private sealed class OccCounters : IDisposable { + // These are pooled using Array.Shared.Rent. private readonly int[] qfCounts; private readonly int[] dataForQfOrdCounts; private readonly int[] ordCounts; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlCluster.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlCluster.cs index 52145e4ba0..690e57a0ad 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlCluster.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlCluster.cs @@ -50,6 +50,10 @@ public static void HistogramCondition(JxlHistogram histogram) histogram.TotalCount = Vector.Sum(total); } + /// + /// Computes the Shannon entropy for a histogram. + /// + /// The histogram to compute the entropy for. public static void HistogramEntropy(JxlHistogram a) { a.Entropy = 0.0f; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlCoefficientOrderEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlCoefficientOrderEncoder.cs index 650717ee7d..484eac6577 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlCoefficientOrderEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlCoefficientOrderEncoder.cs @@ -12,16 +12,12 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; internal static class JxlCoefficientOrderEncoder { - private struct PosAndCount - { - public uint Pos; - public ulong CountAndIdx; - } - private static (uint UsedOrders, uint CustomizeOrders) ComputeUsedOrders(JxlSpeedTier speed, JxlAcStrategyImage acStrategy, Rectangle rect) { if (speed >= JxlSpeedTier.Falcon) { + // This can be slow. So avoid computing used orders when we + // want high performance. return (1, 1); } @@ -57,7 +53,7 @@ private static (uint UsedOrders, uint CustomizeOrders) ComputeUsedOrders(JxlSpee return (ret, retCustomize); } - private static bool ComputeCoeffOrder(JxlSpeedTier speed, IJxlDctAcImage acImage, JxlAcStrategyImage acStrategy, JxlFrameDimensions frameDim, ref uint allUsedOrders, uint prevUsedAcs, uint currentUsedAcs, uint currentUsedOrders, Span order) + private static bool ComputeCoefficientOrder(JxlSpeedTier speed, IJxlDctAcImage acImage, JxlAcStrategyImage acStrategy, JxlFrameDimensions frameDim, ref uint allUsedOrders, uint prevUsedAcs, uint currentUsedAcs, uint currentUsedOrders, Span order) { int[] numZeros = new int[JxlCoefficientOrder.CoefficientOrderMaxSize]; double blockFraction = 1.0; @@ -71,11 +67,13 @@ private static bool ComputeCoeffOrder(JxlSpeedTier speed, IJxlDctAcImage acImage { ulong threshold = uint.MaxValue * (ulong)blockFraction; + // Adapted from XorShift128Plus ulong s0 = 0x94D049BB133111EBUL; ulong s1 = 0xBF58476D1CE4E5B9UL; bool UseSample() { + // Adapted from XorShift128Plus ulong s = s0; ulong s0Local = s1; ulong bits = s + s0Local; @@ -194,7 +192,8 @@ bool UseSample() if (sz > (1 << 16)) { - throw new InvalidOperationException("Inavlid size"); + // The size is too large. 1 << 16 is the strict limit. + throw new InvalidOperationException("Invalid size"); } if (((1u << ord) & ~currentUsedAcs) != 0) @@ -248,6 +247,7 @@ bool UseSample() throw new InvalidOperationException("Invalid coefficient order"); } + // 1÷√sz float invSqrtSz = 1.0f / MathF.Sqrt(sz); for (int i = 0; i < sz; ++i) @@ -259,6 +259,7 @@ bool UseSample() if (count >= 1uL << 48) { + // Up to 1 << 48 counts are allowed. throw new InvalidOperationException("Count is too large"); } @@ -270,7 +271,6 @@ bool UseSample() for (int i = 0; i < sz; ++i) { order[offset + i] = (int)posAndVal[i].Pos; - isNonDefault |= naturalOrderBuffer[i] != posAndVal[i].Pos; } } @@ -304,6 +304,7 @@ private static bool TokenizePermutation(ReadOnlySpan order, int skip, int tokens.Add(new JxlToken((JxlMaTreeContext)JxlCoefficientOrder.CoeffOrderContext((uint)size), (uint)(end - skip))); uint last = 0; + for (int i = skip; i < end; ++i) { tokens.Add(new JxlToken((JxlMaTreeContext)JxlCoefficientOrder.CoeffOrderContext(last), lehmer[i])); @@ -413,6 +414,12 @@ public static bool EncodeCoeffOrders(ushort usedOrders, ReadOnlySpan order return true; } + private struct PosAndCount + { + public uint Pos; + public ulong CountAndIdx; + } + private sealed class PosAndCountComparer : IComparer { public static readonly PosAndCountComparer Instance = new(); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlExternalImageEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlExternalImageEncoder.cs index ac2651d71a..fdf7250e53 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlExternalImageEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlExternalImageEncoder.cs @@ -153,15 +153,8 @@ public static bool ConvertFromExternal(Configuration configuration, Memory if (colorChannels == 1) { - if (!JxlImageOperations.CopyImage(color.Plane(0), color.Plane(1))) - { - throw new InvalidOperationException("Failed to copy plane"); - } - - if (!JxlImageOperations.CopyImage(color.Plane(0), color.Plane(2))) - { - throw new InvalidOperationException("Failed to copy plane"); - } + JxlImageOperations.ForceCopyImage(color.Plane(0), color.Plane(1)); + JxlImageOperations.ForceCopyImage(color.Plane(0), color.Plane(2)); } if (!imageBundle.SetFromImage(color, currentColorEncoding)) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGaborish.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGaborish.cs index 4fddd68873..59faeb18a4 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGaborish.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGaborish.cs @@ -42,10 +42,7 @@ public static void InverseGaborish(Configuration configuration, JxlImage3F inOut using JxlImageF temp = new(configuration, inOut.Plane(2).XSize, inOut.Plane(2).YSize); - if (!JxlImageOperations.CopyImage(inOut.Plane(2), temp)) - { - throw new InvalidOperationException("Image copying failed"); - } + JxlImageOperations.ForceCopyImage(inOut.Plane(2), temp); Rectangle xRect = RectangleUtils.Extend(rect, 3, inOut.GetRectangle()); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlTocEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlTocEncoder.cs index c0c38df9fb..716217899e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlTocEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlTocEncoder.cs @@ -6,8 +6,22 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; +/// +/// Builds and writes TOC permutations. +/// internal static class JxlTocEncoder { + /// + /// Wraps over + /// to encode permutations into the bit-stream, while also writing a bit indicating + /// whether or not are permutations present. + /// + /// + /// Input permutations to write to the bit-stream. + /// + /// + /// The bit-stream writer where permutations are encoded into. + /// public static void WriteTocPermutation(Memory permutation, JxlBitWriter writer) { int nPermutation = permutation.Length; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlTransformsEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlTransformsEncoder.cs index a915b1618e..32fbaed684 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlTransformsEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlTransformsEncoder.cs @@ -8,6 +8,9 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; +/// +/// Performs forward DCT and AFV transforms. +/// internal static class JxlTransformsEncoder { private const int MaxBlocks = 32; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlXybEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlXybEncoder.cs index 037eb46c1f..0005d9df2a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlXybEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlXybEncoder.cs @@ -3,6 +3,7 @@ using System.Numerics; using System.Numerics.Tensors; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; using SixLabors.ImageSharp.Formats.Jxl.Cms; @@ -13,8 +14,22 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; +/// +/// When the color transform is set to XYB, this class lets the encoder +/// convert input RGB color spaces to output XYB. +/// internal static class JxlXybEncoder { + /// + /// Computes the Opsin absorbance for the input R, G, and B colors. + /// + /// The input Red color + /// The input Green color + /// The input Blue color + /// The premultiplied absorbance. + /// The output X absorbance + /// The output Y absorbance + /// The output B absorbance private static void OpsinAbsorbance( Vector r, Vector g, @@ -60,6 +75,16 @@ private static void OpsinAbsorbance( Vector.Create(bias[2])))); } + /// + /// Stores the input RGB color spaces into XYB spans. + /// + /// Input Red components + /// Input Green components + /// Input Blue components + /// Output X components + /// Output Y components + /// Output B components + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void StoreXYB( Vector r, Vector g, @@ -75,6 +100,16 @@ private static void StoreXYB( b.CopyTo(valZ); } + /// + /// Converts RGB to XYB. The conversion happens one SIMD vector at a time. + /// + /// Input Red values + /// Input Green values + /// Input Blue values + /// Premultiplied Opsin absorbance. + /// Converted R->X values are stored here. + /// Converted G->Y values are stored here. + /// Converted B->B values are stored here. private static void LinearRgbToXyb( Vector r, Vector g, @@ -114,6 +149,35 @@ private static void LinearRgbToXyb( StoreXYB(mixed0, mixed1, mixed2, valX, valY, valZ); } + /// + /// Performs an in-place conversion from RGB to XYB. This function + /// is meant to be invoked on each row. Parameters , + /// and initially contain + /// input RGB values. When this function returns, every , + /// and value will + /// contain the converted X, Y, and B, respectively. + /// + /// + /// This contains input values for the R (Red) component (RGB). + /// When this method returns, each value in this span will have + /// the converted X component (XYB). + /// + /// + /// This contains input values for the G (Green) component (RGB). + /// When this method returns, each value in this span will have + /// the converted Y component (XYB). + /// + /// + /// This contains input values for the B (Blue) component (RGB). + /// When this method returns, each value in this span will have + /// the converted B component (XYB). + /// + /// + /// Contains the Opsin absorbance. + /// + /// + /// Represents the number of items within each row. + /// public static void LinearRgbRowToXyb( Span row0, Span row1, @@ -163,7 +227,7 @@ private static void LinearSrgbToXyb( Memory premulAbsorb, JxlImage3F image) { - int xSize = image.Width; + int xSize = image.XSize; void ProcessRow(int y, ReadOnlySpan premulAbsorb) { @@ -190,7 +254,7 @@ void ProcessRow(int y, ReadOnlySpan premulAbsorb) } } - _ = Parallel.For(0, image.Height, configuration.GetParallelOptions(), x => + _ = Parallel.For(0, image.YSize, configuration.GetParallelOptions(), x => { ProcessRow(x, premulAbsorb.Span); }); @@ -201,7 +265,7 @@ private static void SRgbToXyb( Memory premulAbsorb, JxlImage3F image) { - int xSize = image.Width; + int xSize = image.XSize; void ProcessRow(int y, ReadOnlySpan premulAbsorb) { @@ -228,7 +292,7 @@ void ProcessRow(int y, ReadOnlySpan premulAbsorb) } } - _ = Parallel.For(0, image.Height, configuration.GetParallelOptions(), x => + _ = Parallel.For(0, image.YSize, configuration.GetParallelOptions(), x => { ProcessRow(x, premulAbsorb.Span); }); @@ -240,7 +304,7 @@ private static void SrgbToXybAndLinear( JxlImage3F image, JxlImage3F linear) { - int xSize = image.Width; + int xSize = image.XSize; void ProcessRow(int y, ReadOnlySpan premulAbsorb) { @@ -275,10 +339,7 @@ void ProcessRow(int y, ReadOnlySpan premulAbsorb) } } - _ = Parallel.For(0, image.Height, configuration.GetParallelOptions(), x => - { - ProcessRow(x, premulAbsorb.Span); - }); + _ = Parallel.For(0, image.YSize, configuration.GetParallelOptions(), x => ProcessRow(x, premulAbsorb.Span)); } private static void ComputePremulAbsorb(float intensityTarget, Span premulAbsorb) @@ -346,7 +407,7 @@ public static bool ToXyb( { if (wantLinear) { - JxlImageOperations.CopyImageTo(image, linear!); + JxlImageOperations.ForceCopyImage(image, linear!); } LinearSrgbToXyb(configuration, premulAbsorb, image); @@ -379,7 +440,7 @@ public static bool ToXyb( if (wantLinear) { - JxlImageOperations.CopyImageTo(linear!, image); + JxlImageOperations.ForceCopyImage(linear!, image); } LinearSrgbToXyb(configuration, premulAbsorb, image); @@ -395,8 +456,8 @@ public static void RgbToYcbcr( JxlImageF cbPlane, JxlImageF crPlane) { - int xSize = rPlane.Width; - int ySize = rPlane.Height; + int xSize = rPlane.XSize; + int ySize = rPlane.YSize; if (xSize == 0 || ySize == 0) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs index 6ece90cb35..60d9b56fba 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs @@ -64,6 +64,22 @@ public static bool CopyImage(JxlPlane from, JxlPlane to) return true; } + /// + /// Copies everything from one plane to another. If the copy + /// fails, an exception is thrown. + /// + /// The type of planes to copy. + /// Source plane (read-only) + /// Destination plane (write-only) + public static void ForceCopyImage(JxlPlane from, JxlPlane to) + where T : unmanaged + { + if (!CopyImage(from, to)) + { + throw new InvalidOperationException("Could not copy an image"); + } + } + /// /// Copies everything from one image to another. /// @@ -99,6 +115,22 @@ public static bool CopyImage(JxlImage3 from, JxlImage3 to) return true; } + /// + /// Copies everything from one image to another. If the copy + /// fails, an exception is thrown. + /// + /// The type of image to copy. + /// Source image (read-only) + /// Destination image (write-only) + public static void ForceCopyImage(JxlImage3 from, JxlImage3 to) + where T : unmanaged + { + if (!CopyImage(from, to)) + { + throw new InvalidOperationException("Could not copy an image"); + } + } + /// /// Within bounds specified by input rectangles, copies everything from one plane to another. /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularImage.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularImage.cs index 9b3ce7e02f..9d568cef79 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularImage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularImage.cs @@ -17,13 +17,24 @@ public JxlModularImage(int width, int height, int bitDepth) this.IsInvalid = false; } + /// + /// Initializes a new instance of the class, + /// representing an empty image with 0x0 width/height and a bit-depth + /// of 8 bits. + /// public JxlModularImage() : this(0, 0, 8) { } + /// + /// Gets or sets all channels within this image. + /// public List Channels { get; set; } = []; + /// + /// Gets or sets all transforms within this image. + /// public List Transforms { get; set; } = []; /// @@ -78,6 +89,10 @@ public static JxlModularImage Create(Configuration configuration, int width, int return result; } + /// + /// Disposes each channel within this modular image, then sets + /// to an empty list. + /// public void Dispose() { foreach (JxlModularChannel channel in this.Channels) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularOptions.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularOptions.cs index 5346b6a3d0..7b0c5cbe65 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularOptions.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularOptions.cs @@ -6,6 +6,9 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular; +/// +/// Configures the Modular decoder and encoder. +/// internal sealed class JxlModularOptions { /// @@ -44,6 +47,9 @@ internal sealed class JxlModularOptions public JxlPredictor Predictor { get; set; } = JxlPredictor.Undefined; + /// + /// Gets or sets the mode of the weighted prediction. + /// public int WpMode { get; set; } public float FastDecodeMultiplier { get; set; } = 1.01f; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlRct.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlRct.cs index 315fcf2b4e..66f0774865 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlRct.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlRct.cs @@ -7,7 +7,10 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; /// -/// Reversible Color Transform (RCT) +/// Implements the inverse and forward Reversible Color Transform (RCT) +/// with SIMD acceleration if possible. This class implements the mathematical +/// conversion of the RGB color space to the Y'Co'Cg color space (forward), +/// and Y'Co'Cg to RGB (inverse), losslessly. /// internal static class JxlRct { @@ -28,10 +31,8 @@ private static void InverseRctRow(int transformType, Span in0, Span in DebugGuard.MustBeBetweenOrEqualTo(transformType, 0, 6, nameof(transformType)); int width = in0.Length; // All input & output channels have equal widths - int second = transformType >> 1; int third = transformType & 1; - int n = Vector.Count; if (transformType == 6) @@ -185,10 +186,12 @@ public static void InverseRct(Configuration configuration, JxlModularImage img, _ = Parallel.For(0, h, configuration.GetParallelOptions(), y => { + // Get input channels Span in0 = img.Channels[m].GetRow(y); Span in1 = img.Channels[m + 1].GetRow(y); Span in2 = img.Channels[m + 2].GetRow(y); + // Get output channels Span out0 = img.Channels[m + (permutation % 3)].GetRow(y); Span out1 = img.Channels[m + ((permutation + 1 + (permutation / 3)) % 3)].GetRow(y); Span out2 = img.Channels[m + ((permutation + 2 - (permutation / 3)) % 3)].GetRow(y); @@ -215,7 +218,6 @@ private static void ForwardRctRow(int transform, Span in0, Span in1, S int second = transform >> 1; int third = transform & 1; - int width = in0.Length; // All input & output channels have equal widths if (!Vector.IsHardwareAccelerated || Vector.Count == 1) @@ -296,7 +298,7 @@ private static void ForwardRctRow(int transform, Span in0, Span in1, S // TODO: duplicate loops for second == 1, second == 2 // and otherwise? We should reduce the number of branches in - // loops. + // hot loops. if (second == 1) { o1 -= i0; @@ -356,10 +358,12 @@ private static void ForwardRctCore(Configuration configuration, InlineArray3 { + // Get input channels Span i0 = inp[0].GetRow(y); Span i1 = inp[1].GetRow(y); Span i2 = inp[2].GetRow(y); + // Get output channels Span o0 = output[0].GetRow(y); Span o1 = output[1].GetRow(y); Span o2 = output[2].GetRow(y); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs index 11752a92c9..8030b1c0d7 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs @@ -40,10 +40,45 @@ internal static class JxlSqueeze [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int Average(int x, int y) => (x + y + ((x > y) ? 1 : 0)) >> 1; + /// + /// Computes a smoothed prediction tendency from three neighboring values. + /// + /// + /// The value preceding . + /// + /// + /// The central value used to determine the local gradient. + /// + /// + /// The value following . + /// + /// + /// The smoothed prediction tendency, constrained according to the local + /// gradients when the three values are monotonically ordered; otherwise, + /// zero. + /// + /// + /// + /// When the input values are monotonically non-increasing or non-decreasing, + /// this method estimates a tendency using a weighted combination of the + /// neighboring values. + /// + /// + /// The result is adjusted to respect the gradients between + /// and , and between and + /// . These adjustments use parity-sensitive bounds + /// to constrain the tendency. + /// + /// + /// If the values are not monotonically ordered, the method returns zero, + /// indicating no prediction tendency. + /// + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int SmoothTendency(int b, int a, int n) { int diff = 0; + if (b >= a && a >= n) { diff = ((4 * b) - (3 * n) - a + 6) / 12; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlAspectRatioHelpers.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlAspectRatioHelpers.cs index 6c69a8fff7..1380f3d03b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlAspectRatioHelpers.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlAspectRatioHelpers.cs @@ -5,8 +5,14 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +/// +/// Aspect ratio utilities +/// internal static class JxlAspectRatioHelpers { + /// + /// Well-known aspect ratios. + /// private static readonly SignedRational[] Ratios = [ new(1, 1), @@ -18,6 +24,11 @@ internal static class JxlAspectRatioHelpers new(2, 1) ]; + /// + /// Returns a built-in aspect ratio. + /// + /// The index of the aspect ratio. + /// A well-known aspect ratio. public static SignedRational FixedAspectRatios(int ratio) => Ratios[ratio - 1]; public static int FindAspectRatio(int x, int y) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlInverseMtf.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlInverseMtf.cs index 18f9a55bdb..733e1e08c8 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlInverseMtf.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlInverseMtf.cs @@ -4,10 +4,15 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; /// -/// Inverse Move to Front implementation +/// Represents the Inverse Move-to-Front (MTF) Transform. /// internal static class JxlInverseMtf { + /// + /// Moves bytes to the front starting at index . + /// + /// The MTF bytes. + /// The MTF index. public static void MoveToFront(Span values, byte index) { byte value = values[index]; @@ -17,15 +22,22 @@ public static void MoveToFront(Span values, byte index) values[0] = value; } + /// + /// Performs the Inverse Move-to-Front Transform on the span of bytes. + /// + /// The MTF bytes. public static void InverseMoveToFrontTransform(Span values) { Span table = stackalloc byte[256]; + // First, we populate the table with indices. + // TODO: JxlSimdUtils.Iota? for (int i = 0; i < table.Length; i++) { table[i] = (byte)i; } + // Then we do the actual inverse transform here. for (int i = 0; i < values.Length; i++) { byte index = values[i]; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlXorShift.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlXorShift.cs index e5b45f7344..8df7f47999 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlXorShift.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlXorShift.cs @@ -5,8 +5,20 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +/// +/// XorShift is a kind of Random Number Generator (RNG) used by the +/// JPEG-XL codec. Specifically, this is a XorShift128Plus version +/// of the XorShift. +/// +/// +/// https://github.com/vpxyz/xorshift +/// More precisely: https://github.com/vpxyz/xorshift/tree/master/xorshift128plus +/// internal sealed class JxlXorShift { + /// + /// Number of generators. + /// public const int Generators = 8; private readonly ulong[] s0 = new ulong[8]; diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/OpsinImageTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/OpsinImageTests.cs index b67f6f4c22..a9603c8cc2 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/OpsinImageTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/OpsinImageTests.cs @@ -10,6 +10,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; +/// +/// Tests the sRGB to XYB and vice versa color conversion. +/// public class OpsinImageTests { /// @@ -128,6 +131,10 @@ public void OpsinRoundtrip() OpsinRoundtripTestRgb(1, 0, 0); } + /// + /// Tests that Linear sRGB { 0, 0, 0 } to Opsin produces + /// values equal to or extremely close to { 0, 0, 0 }. + /// [Fact] public void VerifyZero() { diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/OpsinInverseTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/OpsinInverseTests.cs new file mode 100644 index 0000000000..5083e0d2f4 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/OpsinInverseTests.cs @@ -0,0 +1,76 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; + +public class OpsinInverseTests +{ + /// + /// Converts Y'Cb'Cr to RGB without any SIMD/parallel acceleration. This is + /// a reference, so it's meant to be stable. The conversion is performed + /// as specified in ITU-R BT.601. + /// + /// Input Y'Cb'Cr source. + /// Output RGB. + /// Region in which conversion will happen. + private static void SlowYCbCrToRgb(JxlImage3F yCbCr, JxlImage3F rgb, Rectangle rect) + { + int width = rect.Width; + int height = rect.Height; + + if (width == 0 || height == 0) + { + // When either dimension has 0 pixels, there's really + // nothing to process. So return immediately. + return; + } + + // Defining constants used by the conversion process + // as specified by ITU-R BT.601 + const float c128 = 128.0f / 255; + const float crcr = 1.402f; + const float cgcb = -0.114f * 1.772f / 0.587f; + const float cgcr = -0.299f * 1.402f / 0.587f; + const float cbcb = 1.772f; + + // Main conversion loop here + for (int y = 0; y < height; y++) + { + // First we retrieve the input Y'Cb'Cr rows + // for each channel. + // NOTE: The Y'Cb'Cr planes are loaded in the + // { 1, 0, 2 } order instead of the traditional + // { 0, 1, 2 } order. See + // https://github.com/libjxl/libjxl/blob/777e88f46a6b35948d1967afd9ac5c7fb5595d95/lib/jxl/opsin_inverse_test.cc#L69-L71. + Span yRow = yCbCr.Plane(1).GetRow(rect, y); + Span cbRow = yCbCr.Plane(0).GetRow(rect, y); + Span crRow = yCbCr.Plane(2).GetRow(rect, y); + + // Similarly, we retrieve the output rows. Conversion + // outputs go here. + Span rRow = rgb.Plane(0).GetRow(rect, y); + Span gRow = rgb.Plane(1).GetRow(rect, y); + Span bRow = rgb.Plane(2).GetRow(rect, y); + + for (int x = 0; x < width; x++) + { + // Input Y'Cb'Cr pixels + float yValue = yRow[x] + c128; + float cbValue = cbRow[x]; + float crValue = crRow[x]; + + // The pixels are converted here. + float rVec = (crcr * crValue) + yValue; + float gVec = (cgcr * crValue) + (cgcb * cbValue) + yValue; + float bVec = (cbcb * cbValue) + yValue; + + // Storing them back to the output rows + rRow[x] = rVec; + gRow[x] = gVec; + bRow[x] = bVec; + } + } + } +} From 30cd712690f1c793bc4208a0c4ff44893461fb1a Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Fri, 23 Oct 2026 19:18:09 +0400 Subject: [PATCH 226/328] Optimize --- .../Decoder/FrameDecoder/JxlFrameDecoder.cs | 7 +- .../Processing/Decoder/JxlHuffmanDecoder.cs | 4 - .../AcStrategy/JxlAcStrategyEncoder.cs | 10 +- .../JxlChromaFromLumaEncoder.cs | 2 +- .../Encoder/Heuristics/JxlHeuristics.cs | 6 +- .../Jxl/Processing/Encoder/JxlFrameEncoder.cs | 136 ++++++++---------- .../Jxl/Processing/Encoder/JxlGroupEncoder.cs | 34 +++-- .../Jxl/Processing/Encoder/Lz77/JxlLz77.cs | 65 ++++++--- .../Encoder/Lz77/JxlLz77HashChain.cs | 10 +- .../Encoder/Noise/JxlNoiseEncoder.cs | 4 +- .../PatchDictionary/JxlPatchColorspaceInfo.cs | 23 +-- 11 files changed, 150 insertions(+), 151 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/FrameDecoder/JxlFrameDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/FrameDecoder/JxlFrameDecoder.cs index 69cea35289..bf25a385d5 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/FrameDecoder/JxlFrameDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/FrameDecoder/JxlFrameDecoder.cs @@ -45,12 +45,7 @@ public int References { get { - if (!this.isFinalized) - { - return 0; - } - - if (!this.HasEverything) + if (!this.isFinalized || !this.HasEverything) { return 0; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlHuffmanDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlHuffmanDecoder.cs index b42bd79282..7490ee9c3e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlHuffmanDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlHuffmanDecoder.cs @@ -34,10 +34,6 @@ internal sealed class JxlHuffmanDecoder new(2, 0), new(2, 4), new(2, 3), new(3, 2), new(2, 0), new(2, 4), new(2, 3), new(4, 5), ]; - public JxlHuffmanDecoder() - { - } - private static ReadOnlySpan CodeLengthCodeOrder => [ 1, 2, 3, 4, 0, 5, 17, 6, 16, 7, 8, 9, 10, 11, 12, 13, 14, 15, diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyEncoder.cs index fce68912fb..2afa749350 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyEncoder.cs @@ -968,7 +968,7 @@ public static bool ProcessAcStrategyRectangle( { for (int cx = 0; cx + acs.CoveredBlocksX - 1 < rect.Width; cx += acs.CoveredBlocksX) { - if (cy + 7 < rect.Height && cx + 7 < rect.Width) + if (cy < rect.Height - 7 && cx < rect.Width - 7) { if (cparams.DecodingSpeedTier < 4 && mt.Type == JxlAcStrategyType.DCT32X64) { @@ -1008,7 +1008,7 @@ public static bool ProcessAcStrategyRectangle( continue; } - if (cy + 3 < rect.Height && cx + 3 < rect.Width) + if (cy < rect.Height - 3 && cx < rect.Width - 3) { if (mt.Type == JxlAcStrategyType.DCT16X32) { @@ -1049,7 +1049,7 @@ public static bool ProcessAcStrategyRectangle( continue; } - if (cy + 1 < rect.Height && cx + 1 < rect.Width) + if (cy < rect.Height - 1 && cx < rect.Width - 1) { if (mt.Type == JxlAcStrategyType.DCT8X16) { @@ -1148,9 +1148,9 @@ public static bool ProcessAcStrategyRectangle( int step = cparams.SpeedTier >= JxlSpeedTier.Tortoise ? 2 : 1; - for (int cy = 0; cy + 3 < rect.Height; cy += step) + for (int cy = 0; cy < rect.Height - 3; cy += step) { - for (int cx = 0; cx + 3 < rect.Width; cx += step) + for (int cx = 0; cx < rect.Width - 3; cx += step) { if ((cy | cx) % 4 == 0) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlChromaFromLumaEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlChromaFromLumaEncoder.cs index e1b98b45c9..f6cd7b609b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlChromaFromLumaEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlChromaFromLumaEncoder.cs @@ -252,7 +252,7 @@ public static void ComputeTile(JxlImage3F opsin, Rectangle opsinRect, JxlDequant float q = useDct8 ? 1 : quantizer.Scale * strangeMultiplier * qq; Vector qv = Vector.Create(q); - for (int i = 0; i < cx * cy * 64; i += Vector.Count) + for (int i = 0; i <= (cx * cy * 64) - Vector.Count; i += Vector.Count) { Vector bY = Vector.Create(blockY[i..]); Vector bX = Vector.Create(blockX[i..]); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs index d5bebfb3db..f04b9ce9ff 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs @@ -178,7 +178,7 @@ public static void FindBestBlockEntropyModel(JxlCompressParameters cparameters, } else if (next > qft.Count + 1) { - if (j - 1 == lastCut && j != 0) + if (j == lastCut + 1 && j != 0) { qft.Add((uint)j); } @@ -356,7 +356,7 @@ public static void ReduceRinging(JxlImageF initial, JxlImageF mask, JxlImageF do int x2 = x + xi; int y2 = y + yi; - if (x2 < 0 || y2 < 0 || x2 >= xsize2 || y2 >= ysize2) + if ((uint)x2 >= xsize2 || (uint)y2 >= ysize2) { continue; } @@ -408,7 +408,7 @@ public static float UpsamplerDerivative(int x2, int y2, int x, int y) int kx = x2 - ix + (Size / 2); int ky = y2 - iy + (Size / 2); - if (kx < 0 || kx >= Size || ky < 0 || ky >= Size) + if ((uint)kx >= Size || (uint)ky >= Size) { // Shouldn't happen. return 0; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFrameEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFrameEncoder.cs index 3a9979e684..223b21d4f3 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFrameEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFrameEncoder.cs @@ -25,39 +25,23 @@ public static uint GetGroupSizeShift(int width, int height, JxlCompressParameter return 0; } - // No point using groups when only one group is full and the others are - // less than half full: multithreading will not really help much, while - // compression does suffer; but no reason to have group larger than image. - if (width <= 128 && height <= 128) + // Suppress warnings here, as: + // (<= 128, <= 128) - produces warning to fix formatting + // Applying code fix -> ( <= 128, <= 128) - produces warning, open parentheses must not have whitespace + // Applying code fix - produces (<= 128, <= 128). Then first warning again. +#pragma warning disable format + return (width, height) switch { - return 0; - } - - if (width <= 256 && height <= 256) - { - return 1; - } - - if (width <= 400 && height <= 400) - { - return 2; - } - - return 1; + (<= 128, <= 128) => 0, + (<= 256, <= 256) => 1, + (<= 400, <= 400) => 2, + _ => 1 + }; +#pragma warning restore format } - public static void SimplifyInvisible(JxlImage3F image, JxlImageF alpha, bool lossless) + public static unsafe void SimplifyInvisible(JxlImage3F image, JxlImageF alpha, bool lossless) { - // Invisible (alpha = 0) pixels tend to be a mess in optimized PNGs. - // Since they have no visual impact, we can replace them with - // something that compresses better and reduces artifacts near the edges. This - // does some kind of smoothing that seems to work. - // Replace invisible pixels with a weighted average of the pixel to the left, - // the pixel to the topright, and non-invisible neighbours. - // Produces downward-blurry smears, with in the upwards direction only a 1px - // edge duplication but not more. It would probably be better to smear in all - // directions. That requires an alpha-weighed convolution with a large enough - // kernel though, which might be too much. for (int c = 0; c < 3; c++) { for (int y = 0; y < image.YSize; y++) @@ -69,74 +53,80 @@ public static void SimplifyInvisible(JxlImage3F image, JxlImageF alpha, bool los Span pa = y > 0 ? alpha.GetRow(y - 1) : []; Span na = y + 1 < image.YSize ? alpha.GetRow(y + 1) : []; - for (int x = 0; x < image.XSize; ++x) + // We create a pointer to 'a', to remove bounds checks in a hot loop. + // We also use a debug guard, so the range is valid. + DebugGuard.MustBeGreaterThanOrEqualTo(a.Length, image.XSize, nameof(a)); + fixed (float* unsafeA = a) { - if (a[x] == 0) + for (int x = 0; x < image.XSize; ++x) { - if (lossless) + if (unsafeA[x] == 0) { - row[x] = 0; - continue; - } - - float d = 0.0f; - row[x] = 0; + if (lossless) + { + row[x] = 0; + continue; + } - if (x > 0) - { - row[x] += row[x - 1]; - d++; + float d = 0.0f; + row[x] = 0; - if (a[x - 1] > 0.0f) + if (x > 0) { row[x] += row[x - 1]; d++; + + if (unsafeA[x - 1] > 0.0f) + { + row[x] += row[x - 1]; + d++; + } } - } - if (x + 1 < image.XSize) - { - if (y > 0) + if (x < image.XSize - 1) { - row[x] += prow[x + 1]; - d++; + if (y > 0) + { + row[x] += prow[x + 1]; + d++; + } + + if (unsafeA[x + 1] > 0.0f) + { + row[x] += 2.0f * row[x + 1]; + d += 2.0f; + } + + if (y > 0 && pa[x + 1] > 0.0f) + { + row[x] += 2.0f * prow[x + 1]; + d += 2.0f; + } + + if (y + 1 < image.YSize && na[x + 1] > 0.0f) + { + row[x] += 2.0f * nrow[x + 1]; + d += 2.0f; + } } - if (a[x + 1] > 0.0f) + if (y > 0 && pa[x] > 0.0f) { - row[x] += 2.0f * row[x + 1]; + row[x] += 2.0f * prow[x]; d += 2.0f; } - if (y > 0 && pa[x + 1] > 0.0f) + if (y + 1 < image.YSize && na[x] > 0.0f) { - row[x] += 2.0f * prow[x + 1]; + row[x] += 2.0f * nrow[x]; d += 2.0f; } - if (y + 1 < image.YSize && na[x + 1] > 0.0f) + if (d > 1.0f) { - row[x] += 2.0f * nrow[x + 1]; - d += 2.0f; + row[x] /= d; } } - - if (y > 0 && pa[x] > 0.0f) - { - row[x] += 2.0f * prow[x]; - d += 2.0f; - } - - if (y + 1 < image.YSize && na[x] > 0.0f) - { - row[x] += 2.0f * nrow[x]; - d += 2.0f; - } - - if (d > 1.0f) - { - row[x] /= d; - } } } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs index a9dfe03665..7f8adc1a17 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs @@ -4,6 +4,7 @@ using System.Buffers; using System.Numerics.Tensors; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Runtime.Intrinsics; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; @@ -292,7 +293,7 @@ public static void AdjustQuantBlockAC( { // If this 8x8 block is too flat, increase the adaptive quantization level // a bit to reduce visible block boundaries and requantize the block. - if (hfNonZeros[0] + hfNonZeros[1] + hfNonZeros[2] + hfNonZeros[3] < 11) + if (hfNonZeros[3] + hfNonZeros[2] + hfNonZeros[1] + hfNonZeros[0] < 11) { quant++; @@ -472,13 +473,15 @@ process at 64 lanes at a time. */ // Don't call .Clear(). All items will be populated. - Span inverseQAc = stackalloc float[64]; - Span oquant = stackalloc int[64]; - Span adjQuant = stackalloc float[64]; - Span dequantm = stackalloc float[64]; + Span shared = stackalloc float[64 * 6]; - Span mulAdjQuantDequantm = stackalloc float[64]; - Span mulAdjQuantDequantmInverseQAc = stackalloc float[64]; + Span inverseQAc = shared.Slice(64 * 0, 64); + Span oquant = MemoryMarshal.Cast(shared.Slice(64 * 1, 64)); + Span adjQuant = shared.Slice(64 * 2, 64); + Span dequantm = shared.Slice(64 * 3, 64); + + Span mulAdjQuantDequantm = shared.Slice(64 * 4, 64); + Span mulAdjQuantDequantmInverseQAc = shared.Slice(64 * 5, 64); inverseQAc.Fill(quantizer.InverseQuantAc(quant)); @@ -552,14 +555,15 @@ public static void ComputeCoefficients(Configuration configuration, int groupInd Span quantized = mem.Memory.Span; // SIMD on 64-bit lanes equivalent - // No need to call .Clear(). All values will zero-init. - Span xFactor = stackalloc float[64]; - Span bFactor = stackalloc float[64]; - Span inX = stackalloc float[64]; - Span inY = stackalloc float[64]; - Span inB = stackalloc float[64]; - Span outX = stackalloc float[64]; - Span outB = stackalloc float[64]; + // No need to call .Clear(). All values will be populated. + Span shared = stackalloc float[64 * 7]; + Span xFactor = shared.Slice(64 * 0, 64); + Span bFactor = shared.Slice(64 * 1, 64); + Span inX = shared.Slice(64 * 2, 64); + Span inY = shared.Slice(64 * 3, 64); + Span inB = shared.Slice(64 * 4, 64); + Span outX = shared.Slice(64 * 5, 64); + Span outB = shared.Slice(64 * 6, 64); for (int by = 0; by < heightBlocks; by++) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77.cs index 05e5f0eefc..1ce753ecaf 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77.cs @@ -75,6 +75,10 @@ public static List[] ApplyRunLengthEncoding( symbolCostSpan[i + 1] = sce.GetBits((int)input[i].Context, (int)token) + numBits + symbolCostSpan[i]; } + // We access items from the 'input' List very frequently, + // so creating a Span wrapper can be much faster. + Span inputSpan = CollectionsMarshal.AsSpan(input); + for (int i = 0; i < input.Count; i++) { int numToCopy = 0; @@ -87,9 +91,9 @@ public static List[] ApplyRunLengthEncoding( if (i > 0) { - for (; i + numToCopy < input.Count; numToCopy++) + for (; numToCopy < input.Count - i; numToCopy++) { - if (input[i + numToCopy].Value != input[i - 1].Value) + if (inputSpan[i + numToCopy].Value != inputSpan[i - 1].Value) { break; } @@ -98,7 +102,7 @@ public static List[] ApplyRunLengthEncoding( if (numToCopy == 0) { - output.Add(input[i]); + output.Add(inputSpan[i]); continue; } @@ -114,14 +118,14 @@ public static List[] ApplyRunLengthEncoding( { for (int j = 0; j < numToCopy; j++) { - output.Add(input[i + j]); + output.Add(inputSpan[i + j]); } i += numToCopy - 1; continue; } - output.Add(new JxlToken(input[i].Context, (uint)lz77Length)); + output.Add(new JxlToken(inputSpan[i].Context, (uint)lz77Length)); output[^1] = output[^1] with { IsLz77Length = true @@ -160,7 +164,7 @@ private static List[] ApplyLz77Default( float bitDecrease = 0; int totalSymbols = 0; JxlAnsHybridUIntConfiguration uintConfig = new(); - List symCost = []; + List symbolCost = []; for (int stream = 0; stream < tokens.Length; stream++) { @@ -171,12 +175,19 @@ private static List[] ApplyLz77Default( totalSymbols += input.Count; List data = new(input.Count); - symCost.Resize(input.Count + 1); + symbolCost.Resize(input.Count + 1); + + // For symbolCost, we should create a Span wrapper over the List with every + // iteration, as the symbolCost list can be resized. + Span symbolCostSpan = CollectionsMarshal.AsSpan(symbolCost); + Span inputSpan = CollectionsMarshal.AsSpan(input); + Span dataSpan = CollectionsMarshal.AsSpan(data); + for (int pos = 0; pos < input.Count; pos++) { - uintConfig.Encode(input[pos].Value, out uint tok, out uint nbits, out _); - symCost[pos + 1] = sce.GetBits((int)input[pos].Context, (int)tok) + nbits + symCost[pos]; - data[pos] = input[pos].Value; + uintConfig.Encode(inputSpan[pos].Value, out uint tok, out uint nbits, out _); + symbolCostSpan[pos + 1] = sce.GetBits((int)inputSpan[pos].Context, (int)tok) + nbits + symbolCost[pos]; + dataSpan[pos] = inputSpan[pos].Value; } _ = output.EnsureCapacity(input.Count); @@ -191,7 +202,7 @@ private static List[] ApplyLz77Default( for (int pos = 0; pos < input.Count; pos++) { - output.Add(input[pos]); + output.Add(inputSpan[pos]); // Guard against reading past input bounds if (pos + hashSize >= input.Count) @@ -207,7 +218,7 @@ private static List[] ApplyLz77Default( } // Bit cost comparison. Raw tokens vs LZ77 pair - float cost = symCost[pos + len] - symCost[pos]; + float cost = symbolCostSpan[pos + len] - symbolCostSpan[pos]; int lz77Length = len - (int)lz77.MinimumLength; float lz77Cost = JxlLz77Costs.GetLengthCost(lz77Length) + JxlLz77Costs.GetDistanceCost(dist_symbol); @@ -215,7 +226,7 @@ private static List[] ApplyLz77Default( { for (int offset = 1; offset < len; offset++) { - output.Add(input[pos + offset]); + output.Add(inputSpan[pos + offset]); hashMap.Update(pos + offset); } @@ -338,12 +349,16 @@ public static List[] ApplyOptimal( float litCost = prefixCosts[i].TotalCost + symbolCost[i + 1] - symCost[i]; - if (prefixCosts[i + 1].TotalCost > litCost) + // Create a reference to prefixCosts[i + 1] to reduce the + // number of bounds checks. + ref MatchInfo prefixCostItem = ref prefixCosts[i + 1]; + + if (prefixCostItem.TotalCost > litCost) { - prefixCosts[i + 1].DistanceSymbol = 0; - prefixCosts[i + 1].Length = 1; - prefixCosts[i + 1].Context = (int)input[i].Context; - prefixCosts[i + 1].TotalCost = litCost; + prefixCostItem.DistanceSymbol = 0; + prefixCostItem.Length = 1; + prefixCostItem.Context = (int)input[i].Context; + prefixCostItem.TotalCost = litCost; } if (skipLz77 > 0) @@ -398,12 +413,16 @@ public static List[] ApplyOptimal( float lz77Cost = sce.GetLengthCost((int)input[i].Context, j - minLength, lz77) + sce.GetDistanceCost(distanceSymbols[j], lz77); float cost = prefixCosts[i].TotalCost + lz77Cost; - if (prefixCosts[i + j].TotalCost > cost) + // Create a reference to prefixCosts[i + j] to reduce the + // number of bounds checks. + ref MatchInfo prefixCost = ref prefixCosts[i + j]; + + if (prefixCost.TotalCost > cost) { - prefixCosts[i + j].Length = j; - prefixCosts[i + j].DistanceSymbol = distanceSymbols[j] + 1; - prefixCosts[i + j].Context = (int)input[i].Context; - prefixCosts[i + j].TotalCost = cost; + prefixCost.Length = j; + prefixCost.DistanceSymbol = distanceSymbols[j] + 1; + prefixCost.Context = (int)input[i].Context; + prefixCost.TotalCost = cost; } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77HashChain.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77HashChain.cs index 6f230b551e..0e70d3863c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77HashChain.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77HashChain.cs @@ -337,8 +337,12 @@ public unsafe void FindMatch(int pos, int maxDist, int* resultDistSymbol, int* r public unsafe void FindMatch(int pos, int maxDist, ref int resultDistSymbol, ref int resultLength) { - int* pDistSymbol = (int*)Unsafe.AsPointer(ref resultDistSymbol); - int* pLength = (int*)Unsafe.AsPointer(ref resultLength); - this.FindMatch(pos, maxDist, pDistSymbol, pLength); + fixed (int* pDistSymbol = &resultDistSymbol) + { + fixed (int* pLength = &resultLength) + { + this.FindMatch(pos, maxDist, pDistSymbol, pLength); + } + } } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseEncoder.cs index 519c07e304..aa1a7f975f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseEncoder.cs @@ -87,9 +87,9 @@ public static IMemoryOwner GetSadScoresForPatches( Span sadScoresData = sadScores.GetSpan(); int blockIndex = 0; - for (int y = 0; y + blockSize <= opsin.YSize; y += blockSize) + for (int y = 0; y <= opsin.YSize - blockSize; y += blockSize) { - for (int x = 0; x + blockSize <= opsin.XSize; x += blockSize) + for (int x = 0; x <= opsin.XSize - blockSize; x += blockSize) { float score = GetScoreSumsOfAbsoluteDifferences(opsin, x, y, blockSize); sadScoresData[blockIndex++] = score; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/PatchDictionary/JxlPatchColorspaceInfo.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/PatchDictionary/JxlPatchColorspaceInfo.cs index 6538478cd6..0bf9dedf16 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/PatchDictionary/JxlPatchColorspaceInfo.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/PatchDictionary/JxlPatchColorspaceInfo.cs @@ -14,21 +14,13 @@ public JxlPatchColorspaceInfo(bool isXyb) { if (isXyb) { - this.ChannelDequant[0] = 0.01615f; - this.ChannelDequant[1] = 0.08875f; - this.ChannelDequant[2] = 0.1922f; - this.ChannelWeights[0] = 30.0f; - this.ChannelWeights[1] = 3.0f; - this.ChannelWeights[2] = 1.0f; + ((ReadOnlySpan)[0.01615f, 0.08875f, 0.1922f]).CopyTo(this.ChannelDequant); + ((ReadOnlySpan)[30.0f, 3.0f, 1.0f]).CopyTo(this.ChannelWeights); } else { - this.ChannelDequant[0] = 20.0f / 255; - this.ChannelDequant[1] = 22.0f / 255; - this.ChannelDequant[2] = 20.0f / 255; - this.ChannelWeights[0] = 0.017f * 255; - this.ChannelWeights[1] = 0.02f * 255; - this.ChannelWeights[2] = 0.017f * 255; + ((ReadOnlySpan)[20.0f / 255, 22.0f / 255, 20.0f / 255]).CopyTo(this.ChannelDequant); + ((ReadOnlySpan)[0.017f * 255, 0.02f * 255, 0.017f * 255]).CopyTo(this.ChannelWeights); } } @@ -50,10 +42,9 @@ public readonly bool IsSimilar(Span v1, Span v2, float threshold) { float distance = 0; - for (int c = 0; c < 3; c++) - { - distance += MathF.Abs(v1[c] - v2[c]) * this.ChannelWeights[c]; - } + distance += MathF.Abs(v1[2] - v2[2]) * this.ChannelWeights[2]; + distance += MathF.Abs(v1[1] - v2[1]) * this.ChannelWeights[1]; + distance += MathF.Abs(v1[0] - v2[0]) * this.ChannelWeights[0]; return distance <= threshold; } From a61b07faf6440b94353890fac9e1f2870f99376d Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 28 Oct 2026 22:28:34 +1000 Subject: [PATCH 227/328] Correct JPEG XL syntax and existing type references --- .../JxlPqTransferFunction.cs | 3 +- .../Jxl/IO/FrameHeader/JxlAnimationFrame.cs | 2 +- .../Formats/Jxl/Memory/JxlPlaneBase.cs | 2 +- .../Decoder/FrameDecoder/JxlFrameDecoder.cs | 1 + .../Decoder/Group/JxlGroupDecoder.cs | 2 +- .../Jxl/Processing/Encoder/JxlBitWriter.cs | 10 +- .../Encoder/Optimize/JxlOptimizeArray{T}.cs | 97 ++++++++++--------- .../Jxl/Processing/Image/JxlImageFeatures.cs | 1 + .../Formats/Jxl/Processing/JxlMath.cs | 14 --- .../Jxl/Processing/JxlPassesSharedState.cs | 1 + .../Modular/Encoding/JxlModularEncoding.cs | 15 ++- .../Encoding/JxlModularEncodingEncoder.cs | 1 + .../Quantization/JxlQuantWeights.cs | 1 + .../Processing/RenderPipeline/SplineStage.cs | 1 + 14 files changed, 86 insertions(+), 65 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlPqTransferFunction.cs b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlPqTransferFunction.cs index f0df5c00c7..5c1ffa21b3 100644 --- a/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlPqTransferFunction.cs +++ b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlPqTransferFunction.cs @@ -2,13 +2,14 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using SixLabors.ImageSharp.Formats.Jxl.Processing; namespace SixLabors.ImageSharp.Formats.Jxl.Cms.TransferFunctions; /// /// Perceptual Quantization transfer function. /// -internal struct JxlPqTransferFunction(float displayIntensityTarget = DefaultIntensityTarget) +internal struct JxlPqTransferFunction(float displayIntensityTarget = JxlMath.DefaultIntensityTarget) { private const double M1 = 2610.0 / 16384; private const double M2 = (2523.0 / 4096) * 128; diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlAnimationFrame.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlAnimationFrame.cs index 1854b4ab67..be422cc297 100644 --- a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlAnimationFrame.cs +++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlAnimationFrame.cs @@ -2,7 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Jxl.Fields; -using SixLabors.ImageSharp.Formats.Jxl.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs index f2e5235f9a..004d835aed 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs @@ -63,7 +63,7 @@ public JxlPlaneBase() #if DEBUG this.bytes?.Memory ?? throw new InvalidOperationException("Bytes are missing"); #else - return this.bytes!.Memory; + this.bytes!.Memory; #endif /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/FrameDecoder/JxlFrameDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/FrameDecoder/JxlFrameDecoder.cs index bf25a385d5..ccfaade87d 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/FrameDecoder/JxlFrameDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/FrameDecoder/JxlFrameDecoder.cs @@ -5,6 +5,7 @@ using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Group; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Modular; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs index 2c96040bd5..522553afb5 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs @@ -126,7 +126,7 @@ private static void DequantBlock( private static bool DecodeGroupCore( JxlFrameHeader frameHeader, - JxlGetBlock getBlock, + IJxlGetBlock getBlock, JxlGroupDecoderCache groupDecCache, JxlPassesDecoderState decState, int thread, diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlBitWriter.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlBitWriter.cs index 1096cf11b1..32960f5a9c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlBitWriter.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlBitWriter.cs @@ -71,7 +71,15 @@ public void ZeroPadToByte() this.Write((int)remainderBits, 0); } - public bool WithMaxBits(ulong maxBits, JxlLayerType layer, JxlAuxiliaryOutput, Func func) + /// + /// Runs an operation with a maximum number of output bits. + /// + /// The output bit budget for the operation. + /// The encoder layer associated with the operation. + /// The encoder statistics supplied by the caller. + /// The operation to run. + /// The result of the operation. + public bool WithMaxBits(ulong maxBits, JxlLayerType layer, JxlAuxiliaryOutput auxOut, Func func) => this.WithMaxBits(maxBits, func); public bool WithMaxBits(ulong maxBits, Func func) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Optimize/JxlOptimizeArray{T}.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Optimize/JxlOptimizeArray{T}.cs index 2a89b1cd4b..bd172350b3 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Optimize/JxlOptimizeArray{T}.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Optimize/JxlOptimizeArray{T}.cs @@ -57,50 +57,59 @@ public readonly ref T this[int index] public readonly T DotProduct(JxlOptimizeArray y) => TensorPrimitives.Dot(this.Span, y.Span); - public static JxlOptimizeArray OptimizeWithScaledConjugateGradientMethod( - JxlOptimizeArrayFactory factory, - JxlOptimizeFunction function, - JxlOptimizeArray w0, - T gradientNormalThreshold, + /// + /// Finds objective-function parameters with the scaled conjugate gradient method. + /// + /// The floating-point type used by the objective and the search. + /// The factory for search arrays. + /// The objective function. + /// The initial parameter values. + /// The gradient norm at which the search stops. + /// The maximum number of search iterations. + /// The parameter values at the end of the search. + public static JxlOptimizeArray OptimizeWithScaledConjugateGradientMethod( + JxlOptimizeArrayFactory factory, + JxlOptimizeFunction function, + JxlOptimizeArray w0, + TFloat gradientNormalThreshold, int maxIterations) - where T : unmanaged, INumber, - IBinaryFloatingPointIeee754 + where TFloat : unmanaged, INumber, IBinaryFloatingPointIeee754 { // These variable names look cryptic, but they're part of // the Scaled Conjugate Gradient method. See the reference // implementation: // https://github.com/libjxl/libjxl/blob/main/lib/jxl/enc_optimize.h#L81-L188 int n = w0.length; - T rsqThreshold = gradientNormalThreshold * gradientNormalThreshold; - - T sigma0 = T.CreateSaturating(0.0001); - T lMin = T.CreateSaturating(1.0e-15); - T lMax = T.CreateSaturating(1.0e15); - - JxlOptimizeArray w = w0; - - JxlOptimizeArray wp = factory.CreateArray(); - JxlOptimizeArray r = factory.CreateArray(); - JxlOptimizeArray rt = factory.CreateArray(); - JxlOptimizeArray e = factory.CreateArray(); - JxlOptimizeArray p = factory.CreateArray(); - - T psq = default; - T fp; - T D = default; - T d; - T m = default; - T a; - T b; - T s; - T t = default; - - T fw = function(w, ref r); - T rsq = r.DotProduct(r); + TFloat rsqThreshold = gradientNormalThreshold * gradientNormalThreshold; + + TFloat sigma0 = TFloat.CreateSaturating(0.0001); + TFloat lMin = TFloat.CreateSaturating(1.0e-15); + TFloat lMax = TFloat.CreateSaturating(1.0e15); + + JxlOptimizeArray w = w0; + + JxlOptimizeArray wp = factory.CreateArray(); + JxlOptimizeArray r = factory.CreateArray(); + JxlOptimizeArray rt = factory.CreateArray(); + JxlOptimizeArray e = factory.CreateArray(); + JxlOptimizeArray p = factory.CreateArray(); + + TFloat psq = default; + TFloat fp; + TFloat D = default; + TFloat d; + TFloat m = default; + TFloat a; + TFloat b; + TFloat s; + TFloat t = default; + + TFloat fw = function(w, ref r); + TFloat rsq = r.DotProduct(r); e = r; p = r; - T l = T.CreateSaturating(1.0); + TFloat l = TFloat.CreateSaturating(1.0); bool success = true; long nSuccess = 0; long k = 0; @@ -112,20 +121,20 @@ public static JxlOptimizeArray OptimizeWithScaledConjugateGradientMethod( { m = -p.DotProduct(r); - if (m >= T.Zero) + if (m >= TFloat.Zero) { p = r; m = -p.DotProduct(r); } psq = p.DotProduct(p); - s = sigma0 / T.Sqrt(psq); + s = sigma0 / TFloat.Sqrt(psq); _ = function(w + (s * p), ref rt); t = p.DotProduct(r - rt) / s; } d = t + (l * psq); - if (d <= T.Zero) + if (d <= TFloat.Zero) { d = l * psq; l -= t / psq; @@ -135,8 +144,8 @@ public static JxlOptimizeArray OptimizeWithScaledConjugateGradientMethod( wp = w + (a * p); fp = function(wp, ref rt); - D = T.CreateSaturating(2.0) * (fp - fw) / (a * m); - if (D >= T.Zero) + D = TFloat.CreateSaturating(2.0) * (fp - fw) / (a * m); + if (D >= TFloat.Zero) { success = true; nSuccess++; @@ -160,19 +169,19 @@ public static JxlOptimizeArray OptimizeWithScaledConjugateGradientMethod( } } - if (D < T.CreateSaturating(0.25)) + if (D < TFloat.CreateSaturating(0.25)) { - l = T.Min(T.CreateSaturating(4) * l, lMax); + l = TFloat.Min(TFloat.CreateSaturating(4) * l, lMax); } - else if (D > T.CreateSaturating(0.75)) + else if (D > TFloat.CreateSaturating(0.75)) { - l = T.Max(T.CreateSaturating(0.25) * l, lMax); + l = TFloat.Max(TFloat.CreateSaturating(0.25) * l, lMax); } if ((nSuccess % n) == 0) { p = r; - l = T.CreateSaturating(1.0); + l = TFloat.CreateSaturating(1.0); } else if (success) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageFeatures.cs b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageFeatures.cs index 2bbfeb2fdf..131ef5393e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageFeatures.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageFeatures.cs @@ -3,6 +3,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Patch; using SixLabors.ImageSharp.Formats.Jxl.Processing.Noise; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Image; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs index f0ab04104e..e8582d8cc0 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs @@ -279,20 +279,6 @@ public static bool SafeAdd(ulong a, ulong b, out ulong sum) } } - /// - /// Ensures that the value stays within the specified range. - /// - /// The input value - /// The lower bound - /// The upper bound - /// - /// if value is lower than . - /// if value is higher than . Otherwise, if it's within - /// the range. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int Clamp1(int value, int low, int high) => Numerics.Clamp(value, low, high); - /// /// Rounds the dimensions up to block dimensions. /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs index f56689382a..7e6a585146 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs @@ -7,6 +7,7 @@ using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Patch; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncoding.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncoding.cs index a63960f038..5cd4f530d8 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncoding.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncoding.cs @@ -759,7 +759,7 @@ public static bool ModularDecode(Configuration configuration, JxlBitReader br, J } maxTreeSize = Math.Min(1uL << 20, maxTreeSize); - DecodeTree(configuration, br, treeStorage, maxTreeSize)); + DecodeTree(configuration, br, treeStorage, maxTreeSize); DecodeHistograms( configuration, br, @@ -813,7 +813,18 @@ public static bool ModularDecode(Configuration configuration, JxlBitReader br, J return true; } - public static bool ModularGenericDecompress(Configuration configuration. JxlBitReader br, JxlModularImage image, JxlGroupHeader header, int groupId, JxlModularOptions options, bool undoTransforms, Tree tree, JxlAnsCode code, List contextMap, bool allowTruncatedGroup) + public static bool ModularGenericDecompress( + Configuration configuration, + JxlBitReader br, + JxlModularImage image, + JxlGroupHeader header, + int groupId, + JxlModularOptions options, + bool undoTransforms, + Tree tree, + JxlAnsCode code, + List contextMap, + bool allowTruncatedGroup) { List reqSizes = new(capacity: image.Channels.Count); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncodingEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncodingEncoder.cs index c69e2addf2..6753df9180 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncodingEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncodingEncoder.cs @@ -4,6 +4,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantWeights.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantWeights.cs index 94aab143af..ddbae3a08d 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantWeights.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantWeights.cs @@ -10,6 +10,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Modular; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/SplineStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/SplineStage.cs index 39864265d0..8cd3d6f43d 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/SplineStage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/SplineStage.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; From 7991f4c05758faa0109e762f5a02cc619abfe805 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 28 Oct 2026 22:54:27 +1000 Subject: [PATCH 228/328] Replace JPEG XL byte-backed planes with typed owned storage Keep logical extents separate from sample stride, remove casting memory managers and ownership-copy constructors, and bound row copies and fills. Initialize AC strategy storage at construction and correct byte decoding and rectangular block extents against libjxl. Add focused layout and ownership regression cases. Release compilation remains blocked by 48 declaration errors in unfinished codec components. Runtime tests have not run. --- .../Jxl/Memory/ImageTypes/JxlImage3B.cs | 19 -- .../Jxl/Memory/ImageTypes/JxlImage3F.cs | 19 -- .../Jxl/Memory/ImageTypes/JxlImage3I.cs | 19 -- .../Jxl/Memory/ImageTypes/JxlImage3S.cs | 19 -- .../Jxl/Memory/ImageTypes/JxlImage3U.cs | 19 -- .../Jxl/Memory/ImageTypes/JxlImageB.cs | 32 --- .../Jxl/Memory/ImageTypes/JxlImageF.cs | 23 -- .../Jxl/Memory/ImageTypes/JxlImageI.cs | 23 -- .../Jxl/Memory/ImageTypes/JxlImageS.cs | 23 -- .../Jxl/Memory/ImageTypes/JxlImageSB.cs | 23 -- .../Jxl/Memory/ImageTypes/JxlImageU.cs | 23 -- .../Formats/Jxl/Memory/JxlImage3{T}.cs | 250 ++++++------------ .../Formats/Jxl/Memory/JxlPlaneBase.cs | 209 --------------- .../Formats/Jxl/Memory/JxlPlane{T}.cs | 243 ++++++++++++----- .../AcStrategy/JxlAcStrategyImage.cs | 158 ++++++++--- .../Processing/AcStrategy/JxlAcStrategyRow.cs | 27 +- .../Jxl/Processing/Blending/JxlBlending.cs | 4 +- .../Jxl/Processing/Butteraugli/Butteraugli.cs | 179 ++++++++----- .../Butteraugli/ButteraugliComparator.cs | 42 +-- .../Butteraugli/ButteraugliPsychoImage.cs | 5 +- .../BlockLoader/JxlBitStreamBlockLoader.cs | 10 +- .../Decoder/Group/JxlGroupDecoder.cs | 3 +- .../Decoder/Group/JxlGroupDecoderCache.cs | 9 +- .../Jxl/Processing/Decoder/JxlNoiseDecoder.cs | 12 +- .../Decoder/JxlPassesDecoderState.cs | 6 +- .../Jxl/Processing/Decoder/JxlXybDecoder.cs | 4 +- .../JxlChromaFromLumaEncoder.cs | 21 +- .../Comparator/ButteraugliJxlComparator.cs | 10 +- .../Encoder/Comparator/JxlComparator.cs | 4 +- .../JxlComparatorScoreComputation.cs | 20 +- .../Encoder/Comparator/NullComparator.cs | 4 +- .../DotDetection/JxlConnectedComponent.cs | 4 +- .../DotDetection/JxlDotDetectionUtils.cs | 40 ++- .../Encoder/DotDetection/JxlDotDictionary.cs | 4 +- .../Heuristics/JxlAcStrategyHeuristics.cs | 10 +- .../Encoder/Heuristics/JxlCflHeuristics.cs | 8 +- .../Encoder/Heuristics/JxlHeuristics.cs | 32 ++- .../Encoder/JxlAdaptiveQuantization.cs | 119 ++++++--- .../Jxl/Processing/Encoder/JxlEntropyCoder.cs | 8 +- .../Encoder/JxlExternalImageEncoder.cs | 55 +++- .../Jxl/Processing/Encoder/JxlFrameEncoder.cs | 4 +- .../Jxl/Processing/Encoder/JxlGaborish.cs | 6 +- .../Jxl/Processing/Encoder/JxlGroupEncoder.cs | 20 +- .../Encoder/JxlImageBundleEncoder.cs | 20 +- .../Jxl/Processing/Encoder/JxlXybEncoder.cs | 28 +- .../Encoder/Noise/JxlNoiseEncoder.cs | 16 +- .../Jxl/Processing/Image/JxlImageBundle.cs | 42 +-- .../Processing/Image/JxlImageOperations.cs | 37 +-- .../Jxl/Processing/JxlColorCorrelationMap.cs | 10 +- .../Formats/Jxl/Processing/JxlCompressedDc.cs | 27 +- .../Formats/Jxl/Processing/JxlConvolve.cs | 25 +- .../Jxl/Processing/JxlPassesSharedState.cs | 24 +- .../Processing/Modular/JxlModularChannel.cs | 10 +- .../Modular/Transforms/JxlPalette.cs | 17 +- .../Processing/Quantization/JxlQuantizer.cs | 7 +- .../Processing/RenderPipeline/Epf0Stage.cs | 6 +- .../Processing/RenderPipeline/Epf1Stage.cs | 6 +- .../Processing/RenderPipeline/Epf2Stage.cs | 6 +- .../RenderPipeline/WriteToImage3FStage.cs | 4 +- .../RenderPipeline/WriteToImageBundleStage.cs | 10 +- .../Jxl/Processing/Splines/JxlSplines.cs | 8 +- .../Jxl/Processing/ButteraugliTests.cs | 26 +- .../Processing/Decoder/JxlDecoderCoreTests.cs | 23 +- .../Formats/Jxl/Processing/GaborishTests.cs | 9 +- .../Jxl/Processing/ImageOperationsTests.cs | 190 +++++++++++++ .../Formats/Jxl/Processing/OpsinImageTests.cs | 10 +- .../Jxl/Processing/OpsinInverseTests.cs | 4 +- 67 files changed, 1208 insertions(+), 1129 deletions(-) delete mode 100644 src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3B.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3F.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3I.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3S.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3U.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageB.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageF.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageI.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageS.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageSB.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageU.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3B.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3B.cs deleted file mode 100644 index 8c3b2ae36a..0000000000 --- a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3B.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; - -/// -/// Represents a three-plane, 2D raster image of type . -/// -internal sealed class JxlImage3B : JxlImage3 -{ - public JxlImage3B() - { - } - - public JxlImage3B(Configuration configuration, int xSize, int ySize) - : base(configuration, xSize, ySize) - { - } -} diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3F.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3F.cs deleted file mode 100644 index 8387f63194..0000000000 --- a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3F.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; - -/// -/// Represents a three-plane, 2D raster image of type . -/// -internal sealed class JxlImage3F : JxlImage3 -{ - public JxlImage3F() - { - } - - public JxlImage3F(Configuration configuration, int xSize, int ySize) - : base(configuration, xSize, ySize) - { - } -} diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3I.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3I.cs deleted file mode 100644 index f3486b95a7..0000000000 --- a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3I.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; - -/// -/// Represents a three-plane, 2D raster image of type . -/// -internal sealed class JxlImage3I : JxlImage3 -{ - public JxlImage3I() - { - } - - public JxlImage3I(Configuration configuration, int xSize, int ySize) - : base(configuration, xSize, ySize) - { - } -} diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3S.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3S.cs deleted file mode 100644 index 94b88056a1..0000000000 --- a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3S.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; - -/// -/// Represents a three-plane, 2D raster image of type . -/// -internal sealed class JxlImage3S : JxlImage3 -{ - public JxlImage3S() - { - } - - public JxlImage3S(Configuration configuration, int xSize, int ySize) - : base(configuration, xSize, ySize) - { - } -} diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3U.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3U.cs deleted file mode 100644 index 8d118c048c..0000000000 --- a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3U.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; - -/// -/// Represents a three-plane, 2D raster image of type . -/// -internal sealed class JxlImage3U : JxlImage3 -{ - public JxlImage3U() - { - } - - public JxlImage3U(Configuration configuration, int xSize, int ySize) - : base(configuration, xSize, ySize) - { - } -} diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageB.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageB.cs deleted file mode 100644 index 2d36f5b668..0000000000 --- a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageB.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; - -/// -/// Represents a single-plane, 2D raster image of type . -/// -internal sealed class JxlImageB : JxlPlane -{ - public JxlImageB() - { - } - - public JxlImageB(int width, int height) - : base(width, height) - { - } - - public JxlImageB(Configuration configuration, int xSize, int ySize, int prePadding = 0) - : base(xSize, ySize) - => this.Allocate(configuration, prePadding); - - public Memory GetRowBytesMemory(int y) - { - DebugGuard.MustBeLessThan(y, this.YSize, nameof(y)); - - Memory row = this.Bytes[(y * this.BytesPerRow)..]; - - return row; - } -} diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageF.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageF.cs deleted file mode 100644 index 6810e87df9..0000000000 --- a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageF.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; - -/// -/// Represents a single-plane, 2D raster image of type . -/// -internal sealed class JxlImageF : JxlPlane -{ - public JxlImageF() - { - } - - public JxlImageF(int width, int height) - : base(width, height) - { - } - - public JxlImageF(Configuration configuration, int xSize, int ySize, int prePadding = 0) - : base(xSize, ySize) - => this.Allocate(configuration, prePadding); -} diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageI.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageI.cs deleted file mode 100644 index 0261ffd63c..0000000000 --- a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageI.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; - -/// -/// Represents a single-plane, 2D raster image of type . -/// -internal sealed class JxlImageI : JxlPlane -{ - public JxlImageI() - { - } - - public JxlImageI(int width, int height) - : base(width, height) - { - } - - public JxlImageI(Configuration configuration, int xSize, int ySize, int prePadding = 0) - : base(xSize, ySize) - => this.Allocate(configuration, prePadding); -} diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageS.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageS.cs deleted file mode 100644 index b53716f12f..0000000000 --- a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageS.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; - -/// -/// Represents a single-plane, 2D raster image of type . -/// -internal sealed class JxlImageS : JxlPlane -{ - public JxlImageS() - { - } - - public JxlImageS(int width, int height) - : base(width, height) - { - } - - public JxlImageS(Configuration configuration, int xSize, int ySize, int prePadding = 0) - : base(xSize, ySize) - => this.Allocate(configuration, prePadding); -} diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageSB.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageSB.cs deleted file mode 100644 index 355c50785c..0000000000 --- a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageSB.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; - -/// -/// Represents a single-plane, 2D raster image of type . -/// -internal sealed class JxlImageSB : JxlPlane -{ - public JxlImageSB() - { - } - - public JxlImageSB(int width, int height) - : base(width, height) - { - } - - public JxlImageSB(Configuration configuration, int xSize, int ySize, int prePadding = 0) - : base(xSize, ySize) - => this.Allocate(configuration, prePadding); -} diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageU.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageU.cs deleted file mode 100644 index d41669e4d1..0000000000 --- a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageU.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; - -/// -/// Represents a single-plane, 2D raster image of type . -/// -internal sealed class JxlImageU : JxlPlane -{ - public JxlImageU() - { - } - - public JxlImageU(int width, int height) - : base(width, height) - { - } - - public JxlImageU(Configuration configuration, int xSize, int ySize, int prePadding = 0) - : base(xSize, ySize) - => this.Allocate(configuration, prePadding); -} diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs index 55d6fbcf71..72aea3fcf9 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs @@ -1,183 +1,151 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Buffers; -using System.Runtime.InteropServices; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; - namespace SixLabors.ImageSharp.Formats.Jxl.Memory; /// -/// A wrapper over 3 planes of type . See -/// also . +/// Owns three equally sized JPEG XL sample planes. /// -/// -/// The kind of each item in each of the 3 planes. -/// +/// The sample type. /// -/// This method should not be marked as sealed. Types such as -/// are commonly used across the JPEG-XL implementation, -/// which derive from this class. +/// Each plane has one owner. Use the image-copy operations to copy samples; copying ownership is not supported. +/// Row views borrow storage from the selected plane and must not outlive it. /// -internal class JxlImage3 : IDisposable +internal sealed class JxlImage3 : IDisposable where T : unmanaged { /// - /// A contains 3 planes. + /// The three owned planes. /// - private const int PlaneCount = 3; + private readonly JxlPlane[] planes; /// - /// All 3 underlying planes are stored here. + /// Initializes three empty planes without allocating sample storage. /// - private JxlPlane[] planes = new JxlPlane[3]; + public JxlImage3() => this.planes = [new(), new(), new()]; /// - /// Initializes a new instance of the class - /// with 3 planes that are empty and have a width and height of 0 pixels. + /// Initializes three allocated sample planes. /// - public JxlImage3() - { - // We want to initialize planes, so they're at least empty - // planes rather than null. - this.planes[0] = new(); - this.planes[1] = new(); - this.planes[2] = new(); - } - + /// The configuration that owns the allocator. + /// The logical width in samples. + /// The logical height in rows. public JxlImage3(Configuration configuration, int xSize, int ySize) - => this.Allocate(configuration, xSize, ySize); - - public JxlImage3(JxlImage3 other) { - for (int i = 0; i < PlaneCount; i++) + this.planes = new JxlPlane[3]; + int allocated = 0; + + try + { + for (; allocated < this.planes.Length; allocated++) + { + this.planes[allocated] = new(configuration, xSize, ySize); + } + } + catch { - this.planes[i] = other.planes[i]; + // Construction does not transfer ownership until all planes exist. Release earlier + // planes if a later allocation fails, then preserve the allocator's original exception. + for (int i = 0; i < allocated; i++) + { + this.planes[i].Dispose(); + } + + throw; } } /// - /// Gets the width of this image. + /// Gets the logical width in samples. /// public int XSize => this.planes[0].XSize; /// - /// Gets the height of this image. + /// Gets the logical height in rows. /// public int YSize => this.planes[0].YSize; /// - /// Gets the number of bytes per row in this image. + /// Gets the physical row stride in samples. /// - public int BytesPerRow => this.planes[0].BytesPerRow; + public int PixelsPerRow => this.planes[0].PixelsPerRow; /// - /// Gets the number of pixels per row in this image. + /// Borrows storage from a plane row through the end of that plane's allocation. /// - public int PixelsPerRow => this.planes[0].PixelsPerRow; + /// The plane index: zero, one, or two. + /// The row index. + /// The row, its padding, and following rows. + public Span PlaneRow(int plane, int row) => this.planes[plane].GetRow(row); /// - /// Shorthand for image3.Plane().GetRow(). + /// Borrows memory from a plane row through the end of that plane's allocation. /// - /// - /// Index of the plane. This can be 0, 1, or 2. - /// - /// - /// Index of the row within the plane whose index is specified by the - /// parameter. - /// - /// - /// A Span view for the row of the plane. Modifying values of this span - /// directly modifies the data for that row in that plane. - /// - public Span PlaneRow(int plane, int row) - { - this.PlaneRowBoundsCheck(plane, row); - - int rowOffset = row * this.planes[0].BytesPerRow; - Span rowSpan = MemoryMarshal.Cast(this.planes[plane].BytesSpan[rowOffset..]); - - return rowSpan; - } - - // This method performs minor allocations! - public Memory PlaneRowMemory(int plane, int row) - { - this.PlaneRowBoundsCheck(plane, row); + /// The plane index: zero, one, or two. + /// The row index. + /// The row, its padding, and following rows. + public Memory PlaneRowMemory(int plane, int row) => this.planes[plane].GetRowMemory(row); - int rowOffset = row * this.planes[0].BytesPerRow; - Memory rowMemoryBytes = this.planes[plane].Bytes[rowOffset..]; - - // we have to allocate a utility class so we can reinterpret - // a Memory. - // Unsafe.As is truly unsafe because, f.e. what if there are - // 400 bytes but T is 4 bytes? the length will remain as 400. - TypeChangingMemoryManager reinterpreter = new(rowMemoryBytes); - return reinterpreter.Memory; - } - - public Span PlaneRow(Rectangle rectangle, int c, int y) - { - DebugGuard.MustBeGreaterThanOrEqualTo(y + rectangle.Top, 0, nameof(y)); - - return this.PlaneRow(c, y + rectangle.Top)[rectangle.Left..]; - } + /// + /// Borrows storage from a row within a rectangle. + /// + /// The rectangle within the logical plane. + /// The plane index: zero, one, or two. + /// The row index relative to the rectangle. + /// The remaining allocated samples from the requested position. + public Span PlaneRow(Rectangle rectangle, int c, int y) => this.planes[c].GetRow(rectangle, y); /// - /// Returns a plane specified by index . There are - /// 3 planes in a instance. + /// Gets an owned plane without transferring its ownership. /// - /// - /// The index of the plane. This can only be 0, 1 or 2. - /// - /// - /// A plane whose index is specified by the parameter. - /// + /// The plane index: zero, one, or two. + /// The selected plane. public JxlPlane Plane(int index) => this.planes[index]; + /// + /// Exchanges the storage and dimensions of corresponding planes. + /// + /// The image that receives this image's previous state. public void Swap(JxlImage3 other) { - for (int i = 0; i < PlaneCount; i++) + for (int i = 0; i < this.planes.Length; i++) { - other.planes[i].Swap(this.planes[i]); + this.planes[i].Swap(other.planes[i]); } } - public static JxlImage3 Create(Configuration configuration, int xSize, int ySize) - => new(configuration, xSize, ySize); - - public void Allocate(Configuration configuration, int xSize, int ySize) - { - JxlPlane plane0 = JxlPlane.Create(configuration, xSize, ySize); - JxlPlane plane1 = JxlPlane.Create(configuration, xSize, ySize); - JxlPlane plane2 = JxlPlane.Create(configuration, xSize, ySize); - - this.planes = [plane0, plane1, plane2]; - } + /// + /// Creates three allocated sample planes. + /// + /// The configuration that owns the allocator. + /// The logical width in samples. + /// The logical height in rows. + /// The image that owns the new planes. + public static JxlImage3 Create(Configuration configuration, int xSize, int ySize) => new(configuration, xSize, ySize); + /// + /// Changes the logical dimensions of all three planes without changing their strides. + /// + /// The requested width. + /// The requested height. + /// Whether the dimensions fit within the original allocation. public bool ShrinkTo(int x, int y) { - for (int i = 0; i < PlaneCount; i++) + // The planes are allocated and exchanged as a set, so they have the same capacity. + if (!this.planes[0].ShrinkTo(x, y)) { - if (!this.planes[i].ShrinkTo(x, y)) - { - return false; - } + return false; } + this.planes[1].ShrinkTo(x, y); + this.planes[2].ShrinkTo(x, y); return true; } - private void PlaneRowBoundsCheck(int c, int y) - { - DebugGuard.MustBeLessThan(c, PlaneCount, nameof(c)); - DebugGuard.MustBeLessThan(y, this.YSize, nameof(y)); - } - /// - /// Returns the rectangle for this image bounds. + /// Gets the logical bounds of the image. /// - /// A rectangle with x,y=0,0 width,height=XSize,YSize. + /// The rectangle at the origin with the logical width and height. public Rectangle GetRectangle() => new(0, 0, this.XSize, this.YSize); /// @@ -187,55 +155,5 @@ public void Dispose() { plane.Dispose(); } - - GC.SuppressFinalize(this); - } - - /// - /// - /// This is used by . The underlying data - /// of a plane is stored as Memory<byte>. Unlike a - /// where Span<byte> can be directly changed to Span<T> - /// using , changing the - /// type of a cannot be done as simply. Instead, we have - /// to create a new instance of a that implements - /// the method. Therefore the following code snippet can be - /// used with this class to reinterpret the type of a Memory. - /// - /// Memory<T> ConvertMemory<T>(Memory<byte> src) - /// { - /// TypeChangingMemoryManager<T> mm = new(src); - /// return mm.Memory; - /// } - /// - /// - /// - /// This is the only option so far, but it allocates on the heap. - /// - /// - /// - /// The target type that a Memory<byte> will be reinterpreted into. - /// - /// - /// Input Memory<byte> to reinterpret into Memory<T>. - /// - private sealed class TypeChangingMemoryManager(Memory memory) : MemoryManager - where TTarget : unmanaged - { - /// - public override Span GetSpan() => MemoryMarshal.Cast(memory.Span); - - /// - // NOTE: We only need the .GetSpan() method. We do not need - // any of the methods below. - public override MemoryHandle Pin(int elementIndex = 0) => throw new NotImplementedException(); - - /// - public override void Unpin() => throw new NotImplementedException(); - - /// - protected override void Dispose(bool disposing) - { - } } } diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs deleted file mode 100644 index 004d835aed..0000000000 --- a/src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs +++ /dev/null @@ -1,209 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace SixLabors.ImageSharp.Formats.Jxl.Memory; - -/// -/// Base class for a single-plane image. -/// -internal class JxlPlaneBase : IDisposable -{ - /// - /// Underlying bytes - /// - private IMemoryOwner? bytes; - - /// - /// Initializes a new instance of the class. - /// - /// Plane width - /// Plane height - /// The size of each pixel in bytes. - public JxlPlaneBase(int xSize, int ySize, int sizeOfT) - { - this.XSize = xSize; - this.YSize = ySize; - this.OriginalXSize = xSize; - this.OriginalYSize = ySize; - this.BytesPerRow = 0; - this.Size = sizeOfT; - } - - /// - /// Initializes a new instance of the class with empty values. - /// - public JxlPlaneBase() - : this(0, 0, 0) - { - } - - /// - /// Gets the number of bytes per row. - /// - public int BytesPerRow { get; private set; } - - /// - /// Gets the width of the image. - /// - public int XSize { get; private set; } - - /// - /// Gets the height of the image. - /// - public int YSize { get; private set; } - - /// - /// Gets the underlying bytes of this image as a Memory<T>. - /// - public Memory Bytes => -#if DEBUG - this.bytes?.Memory ?? throw new InvalidOperationException("Bytes are missing"); -#else - this.bytes!.Memory; -#endif - - /// - /// Gets the underlying bytes of this image as a Span<T>. - /// - public Span BytesSpan => this.Bytes.Span; - - protected int Size { get; set; } - - /// - /// Gets or sets the width that was initially assigned. For example, if the image gets shrinked, - /// the XSize YSize properties get changed while this property will stay same. - /// - protected int OriginalXSize { get; set; } - - /// - /// Gets or sets the height that was initially assigned. For example, if the image gets shrinked, - /// the XSize YSize properties get changed while this property will stay same. - /// - protected int OriginalYSize { get; set; } - - /// - /// Allocates the underlying memory for the plane. - /// - /// The configuration which has a memory allocator used to allocate memory. - /// Padding - /// Status of allocation. - public bool Allocate(Configuration configuration, int prePadding) - { - if (this.bytes != null || this.BytesPerRow != 0) - { - return false; - } - - if (this.XSize == 0 || this.YSize == 0) - { - return true; - } - - int totalBytes = unchecked(this.YSize * this.BytesPerRow); - - this.bytes = configuration.MemoryAllocator.Allocate(totalBytes + (prePadding * this.Size)); - - return true; - } - - /// - /// Shrinks the image so its width is equal to and its height is - /// equal to . - /// - /// The output width - /// The output height - /// Status of the shrinking operation. - /// - /// - /// This method can only shrink memory. It cannot expand it. - /// - /// - /// When shrinking, the underlying memory does not get resized. - /// - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool ShrinkTo(int x, int y) - { - if (x <= this.OriginalXSize || y <= this.OriginalYSize) - { - return false; - } - - DebugGuard.MustBeLessThanOrEqualTo(x, this.OriginalXSize, nameof(x)); - DebugGuard.MustBeLessThanOrEqualTo(y, this.OriginalYSize, nameof(y)); - - this.XSize = x; - this.YSize = y; - - return true; - } - - /// - /// Base function to return the span for a specified row as a generic <T>. - /// - /// The type of the row. - /// The index of the row to get the span for. - /// A span which covers the row memory. - protected Span GetRowBase(int y) - where T : unmanaged - { - DebugGuard.MustBeLessThan(y, this.YSize, nameof(y)); - - Span row = this.Bytes.Span[(y * this.BytesPerRow)..]; - return MemoryMarshal.Cast(row); - } - - protected Span GetRowMinusBase(int y, int minus) - where T : unmanaged - { - DebugGuard.MustBeLessThan(y, this.YSize, nameof(y)); - - Span row = this.Bytes.Span[((y * this.BytesPerRow) - minus)..]; - return MemoryMarshal.Cast(row); - } - - protected Span GetRowPlusBase(int y, int plus) - where T : unmanaged - { - DebugGuard.MustBeLessThan(y, this.YSize, nameof(y)); - - Span row = this.Bytes.Span[((y * this.BytesPerRow) + plus)..]; - return MemoryMarshal.Cast(row); - } - - protected Memory GetRowMemoryBase(int y) - where T : unmanaged - { - DebugGuard.MustBeLessThan(y, this.YSize, nameof(y)); - - Memory row = this.Bytes[(y * this.BytesPerRow)..]; - return JxlMemoryHelpers.CastMemory(row); - } - - /// - /// Swaps properties & data of this image with the specified image. - /// - /// The other image to swap with. - public void Swap(JxlPlaneBase other) - { - (this.XSize, other.XSize) = (other.XSize, this.XSize); - (this.YSize, other.YSize) = (other.YSize, this.YSize); - (this.OriginalXSize, other.OriginalXSize) = (other.OriginalXSize, this.OriginalXSize); - (this.OriginalYSize, other.OriginalYSize) = (other.OriginalYSize, this.OriginalYSize); - (this.BytesPerRow, other.BytesPerRow) = (other.BytesPerRow, this.BytesPerRow); - (this.bytes, other.bytes) = (other.bytes, this.bytes); - } - - /// - /// Releases all underlying memory used by this plane. - /// - public void Dispose() - { - this.bytes?.Dispose(); - GC.SuppressFinalize(this); - } -} diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs index 956c043eff..dd21ce00ac 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs @@ -1,133 +1,246 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; namespace SixLabors.ImageSharp.Formats.Jxl.Memory; /// -/// A generic version of a 2D single-plane JPEG XL image. +/// Owns a contiguous JPEG XL sample plane with padded rows. /// -/// The type of each pixel. -internal class JxlPlane : JxlPlaneBase +/// The sample type. +/// +/// Logical dimensions exclude padding. Changing them does not change the physical stride. +/// Borrowed row views must not outlive the plane. +/// +internal sealed class JxlPlane : IDisposable where T : unmanaged { /// - /// Initializes a new instance of the class. + /// The allocator-owned storage, including prefix and row padding. /// - public JxlPlane() - { - } + private IMemoryOwner? owner; + + /// + /// The borrowed view used by row access. An empty plane has no allocation. + /// + private Memory memory; + + /// + /// The number of samples before the first row. + /// + private int origin; + + /// + /// The original allocation width. + /// + private int allocatedWidth; + + /// + /// The original allocation height. + /// + private int allocatedHeight; /// - /// Initializes a new instance of the class with the specified width and height. + /// Initializes an empty plane without allocating sample storage. /// - /// Plane width. - /// Plane height - public unsafe JxlPlane(int width, int height) - : base(width, height, sizeof(T)) + public JxlPlane() { } /// - /// Gets the number of pixels per row. + /// Initializes an allocated sample plane. /// - public unsafe int PixelsPerRow => this.BytesPerRow / sizeof(T); + /// The configuration that owns the allocator. + /// The logical width in samples. + /// The logical height in rows. + public JxlPlane(Configuration configuration, int width, int height) + : this(configuration, width, height, 0) + { + } /// - /// Allocates a new plane. + /// Initializes an allocated sample plane with a prefix before its first row. /// - /// The configuration which contains a memory allocator. - /// Plane width - /// Plane height - /// Padding - /// A new allocated plane - /// Thrown when allocation fails. - public static JxlPlane Create(Configuration configuration, int xSize, int ySize, int prePadding = 0) + /// The configuration that owns the allocator. + /// The logical width in samples. + /// The logical height in rows. + /// The prefix length in samples. + public JxlPlane(Configuration configuration, int width, int height, int prePadding) { - JxlPlane plane = new(xSize, ySize); - - bool allocated = plane.Allocate(configuration, prePadding); + Guard.MustBeGreaterThanOrEqualTo(width, 0, nameof(width)); + Guard.MustBeGreaterThanOrEqualTo(height, 0, nameof(height)); + Guard.MustBeGreaterThanOrEqualTo(prePadding, 0, nameof(prePadding)); - if (!allocated) + this.XSize = this.allocatedWidth = width; + this.YSize = this.allocatedHeight = height; + if (width == 0 || height == 0) { - throw new InvalidOperationException("Failed to allocate a JPEG XL plane"); + return; } - return plane; + this.origin = prePadding; + + // Round the physical width up to a complete vector. A final vector can include padding, + // but cannot read past the allocation. This does not promise an aligned base address. + int samplesPerVector = Vector512.Count / Unsafe.SizeOf(); + this.PixelsPerRow = checked((int)((((long)width + samplesPerVector - 1) / samplesPerVector) * samplesPerVector)); + int length = checked((this.PixelsPerRow * height) + prePadding); + + // Existing predictors and transforms address neighboring rows by stride. They require + // contiguous storage. Typed memory avoids a casting memory-manager allocation per row. + this.owner = configuration.MemoryAllocator.Allocate(length); + this.memory = this.owner.Memory; + + // Only padding is initialized here. Callers must write the logical samples before use. + // Zero padding keeps unused vector lanes finite without clearing the entire image. + Span storage = this.memory.Span; + storage[..prePadding].Clear(); + + for (int y = 0; y < height; y++) + { + storage.Slice(prePadding + (y * this.PixelsPerRow) + width, this.PixelsPerRow - width).Clear(); + } } /// - /// Returns a span for the specified row. + /// Gets the logical width in samples. /// - /// The row index. - /// A span which covers memory for the specified row. - public Span GetRow(int y) => this.GetRowBase(y); + public int XSize { get; private set; } /// - /// Returns a span for the specified row, but backwards by specified number of elements. + /// Gets the logical height in rows. + /// + public int YSize { get; private set; } + + /// + /// Gets the physical row stride in samples, including padding. + /// + public int PixelsPerRow { get; private set; } + + /// + /// Creates an allocated plane. + /// + /// The configuration that owns the allocator. + /// The logical width in samples. + /// The logical height in rows. + /// The prefix length in samples. + /// The plane that owns the new storage. + public static JxlPlane Create(Configuration configuration, int xSize, int ySize, int prePadding = 0) + => new(configuration, xSize, ySize, prePadding); + + /// + /// Borrows storage from a row's start through the end of the allocation. /// /// The row index. - /// Once the offset of the row was derived, this is how much to subtract the offset. - /// A span which covers memory for the specified row. - public Span GetRowMinus(int y, int minus) => this.GetRowMinusBase(y, minus); + /// The row, its padding, and following rows. + /// + /// Stride-based kernels access following rows. Copies and fills must select their logical width explicitly. + /// + public Span GetRow(int y) => this.memory.Span[(this.origin + (y * this.PixelsPerRow))..]; /// - /// Returns a span for the specified row, but with offset by specified number of elements. + /// Borrows storage starting before a row. /// /// The row index. - /// Once the offset of the row was derived, this is the additional offset. - /// A span which covers memory for the specified row. - public Span GetRowPlus(int y, int plus) => this.GetRowMinusBase(y, plus); + /// The offset to subtract, in samples. + /// The remaining allocated samples from the requested position. + public Span GetRowMinus(int y, int minus) => this.memory.Span[(this.origin + (y * this.PixelsPerRow) - minus)..]; /// - /// Returns a memory for the specified row. + /// Borrows storage starting at a signed offset from a row. /// /// The row index. - /// A memory which covers data for the specified row. - public Memory GetRowMemory(int y) => this.GetRowMemoryBase(y); + /// The offset to add, in samples. + /// The remaining allocated samples from the requested position. + public Span GetRowPlus(int y, int plus) => this.memory.Span[(this.origin + (y * this.PixelsPerRow) + plus)..]; /// - /// Returns a span for the specified row within the specified rectangle bounds. + /// Borrows memory from a row's start through the end of the allocation. /// - /// The bounds. /// The row index. - /// A span which covers memory for the specified row with the rectangle offsets. - public Span GetRow(Rectangle rectangle, int y) - { - DebugGuard.MustBeGreaterThanOrEqualTo(y + rectangle.Top, 0, nameof(y)); + /// The row, its padding, and following rows. + public Memory GetRowMemory(int y) => this.memory[(this.origin + (y * this.PixelsPerRow))..]; - return this.GetRow(y + rectangle.Top)[rectangle.Left..]; - } + /// + /// Borrows storage from the start of a row within a rectangle. + /// + /// The rectangle within the logical plane. + /// The row index relative to the rectangle. + /// The remaining allocated samples from the requested position. + public Span GetRow(Rectangle rectangle, int y) => this.GetRow(y + rectangle.Top)[rectangle.Left..]; /// - /// Returns a span for the specified row within the specified rectangle bounds. + /// Borrows memory from the start of a row within a rectangle. /// - /// The bounds. - /// The row index. - /// A span which covers memory for the specified row with the rectangle offsets. - public Memory GetRowMemory(Rectangle rectangle, int y) + /// The rectangle within the logical plane. + /// The row index relative to the rectangle. + /// The remaining allocated samples from the requested position. + public Memory GetRowMemory(Rectangle rectangle, int y) => this.GetRowMemory(y + rectangle.Top)[rectangle.Left..]; + + /// + /// Changes logical dimensions without reallocating or changing the stride. + /// + /// The requested width. + /// The requested height. + /// Whether both dimensions fit within the original allocation. + /// + /// A previously reduced plane can grow back within its original dimensions. + /// + public bool ShrinkTo(int x, int y) { - DebugGuard.MustBeGreaterThanOrEqualTo(y + rectangle.Top, 0, nameof(y)); + if ((uint)x > (uint)this.allocatedWidth || (uint)y > (uint)this.allocatedHeight) + { + return false; + } - return this.GetRowMemory(y + rectangle.Top)[rectangle.Left..]; + this.XSize = x; + this.YSize = y; + return true; } /// - /// Checks if the specified rectangle is within the bounds image. + /// Checks whether a rectangle fits within the logical plane. /// - /// The input rectangle. - /// Boolean indicating whether the rectangle is inside. - public bool IsRectangleInside(Rectangle rectangle) => rectangle.Contains(this.GetRectangle()); + /// The rectangle to check. + /// Whether the plane contains the rectangle. + public bool IsRectangleInside(Rectangle rectangle) => this.GetRectangle().Contains(rectangle); /// - /// Returns the rectangle for this image bounds. + /// Gets the logical bounds of the plane. /// - /// A rectangle with x,y=0,0 width,height=XSize,YSize. + /// The rectangle at the origin with the logical width and height. public Rectangle GetRectangle() => new(0, 0, this.XSize, this.YSize); /// - /// Fills everything in this image with 0. + /// Clears the logical samples. /// public void Clear() => JxlImageOperations.ZeroFillImage(this); + + /// + /// Exchanges storage ownership, logical dimensions, and allocation layout. + /// + /// The plane that receives this plane's previous state. + public void Swap(JxlPlane other) + { + (this.owner, other.owner) = (other.owner, this.owner); + (this.memory, other.memory) = (other.memory, this.memory); + (this.origin, other.origin) = (other.origin, this.origin); + (this.allocatedWidth, other.allocatedWidth) = (other.allocatedWidth, this.allocatedWidth); + (this.allocatedHeight, other.allocatedHeight) = (other.allocatedHeight, this.allocatedHeight); + (this.XSize, other.XSize) = (other.XSize, this.XSize); + (this.YSize, other.YSize) = (other.YSize, this.YSize); + (this.PixelsPerRow, other.PixelsPerRow) = (other.PixelsPerRow, this.PixelsPerRow); + } + + /// + public void Dispose() + { + this.owner?.Dispose(); + this.owner = null; + this.memory = default; + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyImage.cs b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyImage.cs index 4c751e290c..82e85faca5 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyImage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyImage.cs @@ -1,56 +1,100 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; +/// +/// Owns the transform selection and first-block flag for each block of 8 by 8 pixels. +/// internal sealed class JxlAcStrategyImage : IDisposable { - private const byte Invalid = byte.MaxValue; // 255 - - private JxlImageB? layers; - private Memory row; - private int stride; - - public int XSize => this.layers!.XSize; - - public int YSize => this.layers!.YSize; - - public int PixelsPerRow => this.layers!.PixelsPerRow; - - public JxlAcStrategyRow GetRow(int y, int xPrefix = 0) + /// + /// Marks a block that has no assigned transform. + /// + private const byte Invalid = byte.MaxValue; + + /// + /// The owned transform-selection plane. + /// + private readonly JxlPlane layers; + + /// + /// The borrowed storage used to address multi-block transforms by stride. + /// + private readonly Memory row; + + /// + /// The physical row stride in blocks. + /// + private readonly int stride; + + /// + /// Initializes an allocated transform-selection plane. + /// + /// The configuration that owns the allocator. + /// The width in blocks of 8 by 8 pixels. + /// The height in blocks of 8 by 8 pixels. + private JxlAcStrategyImage(Configuration configuration, int xSize, int ySize) { - ReadOnlyMemory layerRow = this.layers!.GetRowBytesMemory(y); - ReadOnlyMemory row = layerRow[xPrefix..]; - - return new JxlAcStrategyRow(row); + this.layers = new(configuration, xSize, ySize); + this.row = this.layers.GetRowMemory(0); + this.stride = this.layers.PixelsPerRow; } - public static JxlAcStrategyImage Create(Configuration memoryManager, int xSize, int ySize) + /// + /// Gets the logical width in blocks of 8 by 8 pixels. + /// + public int XSize => this.layers.XSize; + + /// + /// Gets the logical height in blocks of 8 by 8 pixels. + /// + public int YSize => this.layers.YSize; + + /// + /// Gets the physical row stride in blocks. + /// + public int PixelsPerRow => this.stride; + + /// + /// Borrows a row of transform selections. + /// + /// The row index in blocks. + /// The starting column in blocks. + /// The selections from the starting column to the logical row end. + public JxlAcStrategyRow GetRow(int y, int xPrefix = 0) { - JxlAcStrategyImage image = new() - { - layers = new JxlImageB(memoryManager, xSize, ySize) - }; - - image.row = image.layers.GetRowBytesMemory(0); - image.stride = image.layers.PixelsPerRow; - - return image; + ReadOnlySpan layerRow = this.layers.GetRow(y)[..this.XSize]; + return new JxlAcStrategyRow(layerRow[xPrefix..]); } + /// + /// Creates an allocated transform-selection plane. + /// + /// The configuration that owns the allocator. + /// The width in blocks of 8 by 8 pixels. + /// The height in blocks of 8 by 8 pixels. + /// The owner of the transform selections. + public static JxlAcStrategyImage Create(Configuration configuration, int xSize, int ySize) => new(configuration, xSize, ySize); + + /// + /// Counts the first blocks of transforms with the selected type. + /// + /// The transform type to count. + /// The number of transforms, excluding their continuation blocks. public int CountBlocks(JxlAcStrategyType type) { int value = 0; int compare = ((int)type << 1) | 1; - for (int y = 0; y < this.layers!.YSize; y++) + for (int y = 0; y < this.layers.YSize; y++) { - ReadOnlySpan row = this.layers!.GetRow(y); + ReadOnlySpan row = this.layers.GetRow(y); - for (int x = 0; x < this.layers!.XSize; x++) + for (int x = 0; x < this.layers.XSize; x++) { if (row[x] == compare) { @@ -62,10 +106,30 @@ public int CountBlocks(JxlAcStrategyType type) return value; } + /// + /// Borrows transform selections from a row within a rectangle. + /// + /// The rectangle in blocks of 8 by 8 pixels. + /// The row index relative to the rectangle. + /// The selections from the rectangle's left edge. public JxlAcStrategyRow GetRow(in Rectangle rect, int y) => this.GetRow(rect.Y + y, rect.X); + /// + /// Checks whether a block has an assigned transform. + /// + /// The block column. + /// The block row. + /// Whether the block has an assigned transform. public bool IsValid(int x, int y) => this.row.Span[(y * this.stride) + x] != Invalid; + /// + /// Assigns a transform to blocks whose extent has already been validated. + /// + /// The first block column. + /// The first block row. + /// The transform type. + /// Whether to reject an overlapping transform. + /// Whether the assignment succeeded. public bool SetNoBoundsChecks(int x, int y, JxlAcStrategyType type, bool check = true) { JxlAcStrategy strategy = new(type); @@ -73,7 +137,9 @@ public bool SetNoBoundsChecks(int x, int y, JxlAcStrategyType type, bool check = int rawType = (int)type; int rawTypeTimes2 = rawType << 1; - for (int iy = 0; iy < strategy.CoveredBlocksX; iy++) + // Every covered block stores the transform type in the upper bits. Only the top-left + // block sets bit zero, so readers can distinguish a transform from its continuation blocks. + for (int iy = 0; iy < strategy.CoveredBlocksY; iy++) { for (int ix = 0; ix < strategy.CoveredBlocksX; ix++) { @@ -91,11 +157,18 @@ public bool SetNoBoundsChecks(int x, int y, JxlAcStrategyType type, bool check = return true; } + /// + /// Assigns a transform that fits within the logical plane. + /// + /// The first block column. + /// The first block row. + /// The transform type. + /// Whether the assignment succeeded. public bool Set(int x, int y, JxlAcStrategyType type) { JxlAcStrategy strategy = new(type); - if (y + strategy.CoveredBlocksX > this.layers!.YSize || + if (y + strategy.CoveredBlocksY > this.layers.YSize || x + strategy.CoveredBlocksX > this.layers.XSize) { throw new InvalidOperationException("Invalid range"); @@ -104,15 +177,26 @@ public bool Set(int x, int y, JxlAcStrategyType type) return this.SetNoBoundsChecks(x, y, type, check: false); } - public void FillDct8(in Rectangle rect) => JxlImageOperations.FillPlane((byte)(((int)JxlAcStrategyType.DCT << 1) | 1), this.layers ?? throw new InvalidOperationException("Image is missing"), rect); + /// + /// Assigns independent eight-by-eight transforms within a rectangle. + /// + /// The rectangle in blocks of 8 by 8 pixels. + public void FillDct8(in Rectangle rect) => JxlImageOperations.FillPlane((byte)(((int)JxlAcStrategyType.DCT << 1) | 1), this.layers, rect); - public void FillDct8() => this.FillDct8(this.layers!.GetRectangle()); + /// + /// Assigns independent eight-by-eight transforms to every logical block. + /// + public void FillDct8() => this.FillDct8(this.layers.GetRectangle()); - public void FillInvalid() => JxlImageOperations.FillImage(Invalid, this.layers ?? throw new InvalidOperationException("Image is missing")); + /// + /// Marks every logical block as unassigned. + /// + public void FillInvalid() => JxlImageOperations.FillImage(Invalid, this.layers); + /// public void Dispose() { - this.layers?.Dispose(); + this.layers.Dispose(); GC.SuppressFinalize(this); } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyRow.cs b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyRow.cs index 60900dc578..4cb7c1bb9f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyRow.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyRow.cs @@ -1,25 +1,28 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - namespace SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; -internal readonly struct JxlAcStrategyRow(ReadOnlyMemory row) +/// +/// Borrows a row of JPEG XL transform selections. +/// +/// The encoded selections for one logical block row. +internal readonly ref struct JxlAcStrategyRow(ReadOnlySpan row) { + /// + /// Gets the transform type and first-block flag at a column. + /// + /// The column in blocks of 8 by 8 pixels. + /// The selected transform and its position within a multi-block transform. public readonly JxlAcStrategy this[int x] { get { - ReadOnlySpan span = row.Span; - - DebugGuard.MustBeLessThan(x * 8, span.Length, "x overflows"); - - ref byte first = ref MemoryMarshal.GetReference(span); - JxlAcStrategyType strategy = (JxlAcStrategyType)(Unsafe.Add(ref Unsafe.As(ref first), x) >> 1); - bool isFirst = Unsafe.Add(ref first, x) != 0; - + // One byte stores one block: bit zero marks the first block, and the remaining + // bits store its transform type. Continuation blocks retain that type with bit zero clear. + byte value = row[x]; + JxlAcStrategyType strategy = (JxlAcStrategyType)(value >> 1); + bool isFirst = (value & 1) != 0; return new JxlAcStrategy(strategy, isFirst); } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlending.cs b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlending.cs index 2da31b8e66..5d447c9e9d 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlending.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlending.cs @@ -3,7 +3,7 @@ using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Patch; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Blending; @@ -55,7 +55,7 @@ public static void PerformBlending( bool hasAlpha = extraChannelInfo.Any(x => x.Type == JxlExtraChannel.Alpha); int numEc = extraChannelInfo.Count; - using JxlImageF tmp = new(configuration, xsize, 3 + numEc); + using JxlPlane tmp = new(configuration, xsize, 3 + numEc); for (int i = 0; i < numEc; i++) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs index aad9e1972b..ddd93445d4 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs @@ -5,7 +5,6 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.Memory; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; @@ -433,7 +432,7 @@ public static void XybLowFrequencyToValues( valY = y * yMul; } - public static void XybLowFrequencyToValues(JxlImage3F xybLf) + public static void XybLowFrequencyToValues(JxlImage3 xybLf) { int lanes = Vector.Count; @@ -524,9 +523,9 @@ public static void Subtract(JxlPlane a, JxlPlane b, JxlPlane xyb, + JxlImage3 lf, + JxlImage3 mf, ButteraugliBlurTemp blurTemp) { const float sigmaLf = 7.15593339443f; @@ -556,7 +555,7 @@ public static bool SeparateLFAndMF( public static bool SeparateMfAndHf( Configuration configuration, - JxlImage3F mf, + JxlImage3 mf, ref InlineArray2> hf, ButteraugliBlurTemp blurTemp) { @@ -758,15 +757,15 @@ public static void DeallocateHFAndUHF(InlineArray2> hf, InlineAr public static bool SeparateFrequencies( Configuration configuration, ButteraugliBlurTemp blurTemp, - JxlImage3F xyb, + JxlImage3 xyb, ButteraugliPsychoImage ps) { - ps.Lf = new JxlImage3F( + ps.Lf = new JxlImage3( configuration, xyb.XSize, xyb.YSize); - ps.Mf = new JxlImage3F( + ps.Mf = new JxlImage3( configuration, xyb.XSize, xyb.YSize); @@ -1419,13 +1418,13 @@ public static bool MaltaDiffMap( } public static bool MaltaDiffMap( - JxlImageF lum0, - JxlImageF lum1, + JxlPlane lum0, + JxlPlane lum1, float w0Gt1, float w0Lt1, float norm1, - JxlImageF diffs, - JxlImageF blockDiffAc) + JxlPlane diffs, + JxlPlane blockDiffAc) { const float len = 3.75f; const float mulli = 0.39905817637f; @@ -1504,10 +1503,10 @@ public static void CombineChannelsForMasking( } public static void DiffPrecompute( - JxlImageF xyb, + JxlPlane xyb, float mul, float biasArg, - JxlImageF output) + JxlPlane output) { int width = xyb.XSize; int height = xyb.YSize; @@ -1557,7 +1556,7 @@ private static void StoreMin3( } } - public static void FuzzyErosion(JxlImageF from, JxlImageF to) + public static void FuzzyErosion(JxlPlane from, JxlPlane to) { int width = from.XSize; int height = from.YSize; @@ -1656,11 +1655,11 @@ public static void FuzzyErosion(JxlImageF from, JxlImageF to) public static bool Mask( Configuration configuration, - JxlImageF mask0, - JxlImageF mask1, + JxlPlane mask0, + JxlPlane mask1, ButteraugliBlurTemp blurTemp, - JxlImageF? diffAc, - out JxlImageF mask) + JxlPlane? diffAc, + out JxlPlane mask) { int width = mask0.XSize; int height = mask0.YSize; @@ -1671,10 +1670,10 @@ public static bool Mask( const float bias = 12.61050594197f; const float radius = 2.7f; - JxlImageF diff0 = new(configuration, width, height); - JxlImageF diff1 = new(configuration, width, height); - JxlImageF blurred0 = new(configuration, width, height); - JxlImageF blurred1 = new(configuration, width, height); + JxlPlane diff0 = new(configuration, width, height); + JxlPlane diff1 = new(configuration, width, height); + JxlPlane blurred0 = new(configuration, width, height); + JxlPlane blurred1 = new(configuration, width, height); DiffPrecompute(mask0, mul, bias, diff0); DiffPrecompute(mask1, mul, bias, diff1); @@ -1723,11 +1722,11 @@ public static bool MaskButteraugliPsychoImage( int width, int height, ButteraugliBlurTemp blurTemp, - JxlImageF mask, - out JxlImageF? diffAc) + JxlPlane mask, + out JxlPlane? diffAc) { - JxlImageF mask0 = new(configuration, width, height); - JxlImageF mask1 = new(configuration, width, height); + JxlPlane mask0 = new(configuration, width, height); + JxlPlane mask1 = new(configuration, width, height); CombineChannelsForMasking( pi0.Hf, @@ -1781,11 +1780,11 @@ public static float MaskColor( => (color[0] * mask) + (color[1] * mask) + (color[2] * mask); public static bool CombineChannelsToDiffmap( - JxlImageF mask, - JxlImage3F blockDiffDc, + JxlPlane mask, + JxlImage3 blockDiffDc, JxlImage3 blockDiffAc, float xmul, - JxlImageF result) + JxlPlane result) { if (!JxlImageOperations.SameSize(mask, result)) { @@ -2001,11 +2000,11 @@ private static Vector Gamma(Vector v) public static bool OpsinDynamicsImage( Configuration configuration, - JxlImage3F rgb, + JxlImage3 rgb, in ButteraugliParameters parameters, - JxlImage3F blurred, + JxlImage3 blurred, ButteraugliBlurTemp blurTemp, - JxlImage3F xyb) + JxlImage3 xyb) { if (blurred == null) { @@ -2108,17 +2107,17 @@ public static bool OpsinDynamicsImage( public static bool ButteraugliDiffmapInPlace( Configuration configuration, - JxlImage3F image0, - JxlImage3F image1, + JxlImage3 image0, + JxlImage3 image1, in ButteraugliParameters parameters, - JxlImageF diffmap) + JxlPlane diffmap) { int xSize = image0.XSize; int ySize = image0.YSize; using ButteraugliBlurTemp blurTemp = new(); - using (JxlImage3F temp = new(configuration, xSize, ySize)) + using (JxlImage3 temp = new(configuration, xSize, ySize)) { if (!OpsinDynamicsImage(configuration, image0, parameters, temp, blurTemp, image0)) { @@ -2131,12 +2130,12 @@ public static bool ButteraugliDiffmapInPlace( } } - using JxlPlane blockDiffDc = JxlImageF.Create(configuration, xSize, ySize); + using JxlPlane blockDiffDc = JxlPlane.Create(configuration, xSize, ySize); blockDiffDc.Clear(); // LF/DC - using (JxlImage3F lf0 = new(configuration, xSize, ySize)) - using (JxlImage3F lf1 = new(configuration, xSize, ySize)) + using (JxlImage3 lf0 = new(configuration, xSize, ySize)) + using (JxlImage3 lf1 = new(configuration, xSize, ySize)) { if (!SeparateLFAndMF(configuration, image0, lf0, image0, blurTemp)) { @@ -2171,10 +2170,10 @@ public static bool ButteraugliDiffmapInPlace( return false; } - using JxlImageF blockDiffAc = new(configuration, xSize, ySize); + using JxlPlane blockDiffAc = new(configuration, xSize, ySize); blockDiffAc.Clear(); - using (JxlImageF diffs = new(configuration, xSize, ySize)) + using (JxlPlane diffs = new(configuration, xSize, ySize)) { if (!MaltaDiffMap( true, @@ -2231,7 +2230,7 @@ public static bool ButteraugliDiffmapInPlace( float hfAsymmetry = parameters.HfAsymmetry; - using (JxlImageF diffs = new(configuration, xSize, ySize)) + using (JxlPlane diffs = new(configuration, xSize, ySize)) { _ = MaltaDiffMap( false, @@ -2287,9 +2286,9 @@ public static bool ButteraugliDiffmapInPlace( } // Mask - using JxlImageF mask = new(configuration, xSize, ySize); - using JxlImageF mask0 = new(configuration, xSize, ySize); - using JxlImageF mask1 = new(configuration, xSize, ySize); + using JxlPlane mask = new(configuration, xSize, ySize); + using JxlPlane mask0 = new(configuration, xSize, ySize); + using JxlPlane mask1 = new(configuration, xSize, ySize); CombineChannelsForMasking(hf0, uhf0, mask0); CombineChannelsForMasking(hf1, uhf1, mask1); @@ -2297,7 +2296,7 @@ public static bool ButteraugliDiffmapInPlace( DeallocateHFAndUHF(hf0, uhf0); DeallocateHFAndUHF(hf1, uhf1); - if (!Mask(configuration, mask0, mask1, blurTemp, mask, out JxlImageF newBlockDiffAc)) + if (!Mask(configuration, mask0, mask1, blurTemp, mask, out JxlPlane newBlockDiffAc)) { return false; } @@ -2322,12 +2321,12 @@ public static bool ButteraugliDiffmapInPlace( // Calculate a 2x2 subsampled image for purposes of recursive butteraugli at // multiresolution. - public static JxlImage3F SubSample2x(Configuration configuration, JxlImage3F input) + public static JxlImage3 SubSample2x(Configuration configuration, JxlImage3 input) { int xs = (input.XSize + 1) / 2; int ys = (input.YSize + 1) / 2; - JxlImage3F retval = new(configuration, xs, ys); + JxlImage3 retval = new(configuration, xs, ys); for (int c = 0; c < 3; ++c) { @@ -2374,7 +2373,7 @@ public static JxlImage3F SubSample2x(Configuration configuration, JxlImage3F inp return retval; } - public static void AddSupersampled2x(JxlImageF src, float w, JxlImageF dest) + public static void AddSupersampled2x(JxlPlane src, float w, JxlPlane dest) { const float heuristicMixingValue = 0.3f; @@ -2419,11 +2418,11 @@ private static void ScoreToRgb(float score, float goodThreshold, float badThresh } } - public static JxlImage3F CreateHeatMapImage(Configuration configuration, JxlImageF distmap, float goodThreshold, float badThreshold) + public static JxlImage3 CreateHeatMapImage(Configuration configuration, JxlPlane distmap, float goodThreshold, float badThreshold) { // Do not dispose (this is the return value; it is caller's responsibility to dispose this // or keep it) - JxlImage3F heatmap = new(configuration, distmap.XSize, distmap.YSize); + JxlImage3 heatmap = new(configuration, distmap.XSize, distmap.YSize); for (int y = 0; y < distmap.YSize; y++) { @@ -2494,7 +2493,13 @@ public static float ButteraugliFuzzyClass(float score) return val; } - public static bool ButteraugliInterfaceInPlace(Configuration configuration, JxlImage3F rgb0, JxlImage3F rgb1, ButteraugliParameters parameters, JxlImageF diffmap, out double diffvalue) + public static bool ButteraugliInterfaceInPlace( + Configuration configuration, + JxlImage3 rgb0, + JxlImage3 rgb1, + ButteraugliParameters parameters, + JxlPlane diffmap, + out double diffvalue) { diffvalue = 0; @@ -2520,12 +2525,12 @@ public static bool ButteraugliInterfaceInPlace(Configuration configuration, JxlI return ok; } - JxlImageF subDiffmap = new(); + JxlPlane subDiffmap = new(); if (xsize >= 15 && ysize >= 15) { - using JxlImage3F rgb0Sub = SubSample2x(configuration, rgb0); - using JxlImage3F rgb1Sub = SubSample2x(configuration, rgb1); + using JxlImage3 rgb0Sub = SubSample2x(configuration, rgb0); + using JxlImage3 rgb1Sub = SubSample2x(configuration, rgb1); if (!ButteraugliDiffmapInPlace(configuration, rgb0Sub, rgb1Sub, parameters, diffmap)) { @@ -2547,7 +2552,13 @@ public static bool ButteraugliInterfaceInPlace(Configuration configuration, JxlI return true; } - public static bool ButteraugliInterface(Configuration configuration, JxlImage3F rgb0, JxlImage3F rgb1, ButteraugliParameters parameters, JxlImageF diffmap, out double diffValue) + public static bool ButteraugliInterface( + Configuration configuration, + JxlImage3 rgb0, + JxlImage3 rgb1, + ButteraugliParameters parameters, + JxlPlane diffmap, + out double diffValue) { diffValue = 0; @@ -2560,7 +2571,14 @@ public static bool ButteraugliInterface(Configuration configuration, JxlImage3F return true; } - public static bool ButteraugliInterface(Configuration configuration, JxlImage3F rgb0, JxlImage3F rgb1, float hfAsymmetry, float xmul, JxlImageF diffmap, out double diffValue) + public static bool ButteraugliInterface( + Configuration configuration, + JxlImage3 rgb0, + JxlImage3 rgb1, + float hfAsymmetry, + float xmul, + JxlPlane diffmap, + out double diffValue) { ButteraugliParameters parameters = new() { @@ -2571,7 +2589,12 @@ public static bool ButteraugliInterface(Configuration configuration, JxlImage3F return ButteraugliInterface(configuration, rgb0, rgb1, parameters, diffmap, out diffValue); } - public static bool ButteraugliDiffmap(Configuration configuration, JxlImage3F rgb0, JxlImage3F rgb1, ButteraugliParameters parameters, out JxlImageF diffmap) + public static bool ButteraugliDiffmap( + Configuration configuration, + JxlImage3 rgb0, + JxlImage3 rgb1, + ButteraugliParameters parameters, + out JxlPlane diffmap) { diffmap = new(); @@ -2603,7 +2626,13 @@ public static bool ButteraugliDiffmap(Configuration configuration, JxlImage3F rg return butteraugli.Diffmap(configuration, rgb1, diffmap); } - public static bool ButteraugliDiffmapSmall(Configuration configuration, int max, JxlImage3F rgb0, JxlImage3F rgb1, ButteraugliParameters parameters, out JxlImageF diffmap) + public static bool ButteraugliDiffmapSmall( + Configuration configuration, + int max, + JxlImage3 rgb0, + JxlImage3 rgb1, + ButteraugliParameters parameters, + out JxlPlane diffmap) { int xsize = rgb0.XSize; int ysize = rgb0.YSize; @@ -2613,8 +2642,8 @@ public static bool ButteraugliDiffmapSmall(Configuration configuration, int max, int xscaled = Math.Max(max, xsize); int yscaled = Math.Max(max, ysize); - using JxlImage3F scaled0 = new(configuration, xscaled, yscaled); - using JxlImage3F scaled1 = new(configuration, xscaled, yscaled); + using JxlImage3 scaled0 = new(configuration, xscaled, yscaled); + using JxlImage3 scaled1 = new(configuration, xscaled, yscaled); for (int i = 0; i < 3; ++i) { @@ -2631,7 +2660,7 @@ public static bool ButteraugliDiffmapSmall(Configuration configuration, int max, } } - JxlImageF diffmapScaled = new(); + JxlPlane diffmapScaled = new(); bool ok = ButteraugliDiffmap(configuration, scaled0, scaled1, parameters, out diffmapScaled); diffmap = new(configuration, xsize, ysize); @@ -2650,7 +2679,7 @@ public static bool ButteraugliDiffmapSmall(Configuration configuration, int max, return ok; } - public static float ButteraugliScoreFromDiffmap(JxlImageF diffmap, ButteraugliParameters parameters) + public static float ButteraugliScoreFromDiffmap(JxlPlane diffmap, ButteraugliParameters parameters) { float retval = 0.0f; @@ -2666,9 +2695,25 @@ public static float ButteraugliScoreFromDiffmap(JxlImageF diffmap, ButteraugliPa return retval; } - public static bool MaltaDiffMap(JxlPlane lum0, JxlPlane lum1, float w0Gt1, float w0Lt1, float norm1, JxlPlane diffs, JxlImage3 blockDiffAc, int c) + public static bool MaltaDiffMap( + JxlPlane lum0, + JxlPlane lum1, + float w0Gt1, + float w0Lt1, + float norm1, + JxlPlane diffs, + JxlImage3 blockDiffAc, + int c) => MaltaDiffMap(isLf: false, lum0, lum1, w0Gt1, w0Lt1, norm1, diffs, blockDiffAc.Plane(c)); - public static bool MaltaDiffMapLf(JxlPlane lum0, JxlPlane lum1, float w0Gt1, float w0Lt1, float norm1, JxlPlane diffs, JxlImage3 blockDiffAc, int c) + public static bool MaltaDiffMapLf( + JxlPlane lum0, + JxlPlane lum1, + float w0Gt1, + float w0Lt1, + float norm1, + JxlPlane diffs, + JxlImage3 blockDiffAc, + int c) => MaltaDiffMap(isLf: true, lum0, lum1, w0Gt1, w0Lt1, norm1, diffs, blockDiffAc.Plane(c)); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs index d290268da6..3d07cb1d4a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Jxl.Memory; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; @@ -13,7 +12,7 @@ internal class ButteraugliComparator : IDisposable private readonly int ySize; private ButteraugliParameters parameters; private readonly ButteraugliPsychoImage pi0 = new(); - private JxlImage3F? temp; + private JxlImage3? temp; private readonly ButteraugliBlurTemp blurTemp = new(); private ButteraugliComparator? sub; @@ -24,16 +23,16 @@ public ButteraugliComparator(int xsize, int ysize, ButteraugliParameters paramet this.parameters = parameters; } - public JxlImage3F Temp => this.temp ??= new(); + public JxlImage3 Temp => this.temp ??= new(); - public static ButteraugliComparator Make(Configuration configuration, JxlImage3F rgb0, ButteraugliParameters parameters) + public static ButteraugliComparator Make(Configuration configuration, JxlImage3 rgb0, ButteraugliParameters parameters) { int xSize = rgb0.XSize; int ySize = rgb0.YSize; ButteraugliComparator result = new(xSize, ySize, parameters) { - temp = new JxlImage3F(configuration, xSize, ySize) + temp = new JxlImage3(configuration, xSize, ySize) }; if (xSize < 8 || ySize < 8) @@ -41,7 +40,7 @@ public static ButteraugliComparator Make(Configuration configuration, JxlImage3F return result; } - JxlImage3F xyb0 = new(configuration, xSize, ySize); + JxlImage3 xyb0 = new(configuration, xSize, ySize); if (!Butteraugli.OpsinDynamicsImage(configuration, rgb0, parameters, result.Temp, result.blurTemp, xyb0)) { @@ -55,7 +54,7 @@ public static ButteraugliComparator Make(Configuration configuration, JxlImage3F throw new InvalidOperationException("Could not separate frequencies"); } - JxlImage3F subsampledRgb0 = Butteraugli.SubSample2x(configuration, rgb0); + JxlImage3 subsampledRgb0 = Butteraugli.SubSample2x(configuration, rgb0); result.sub = Make(configuration, subsampledRgb0, parameters); return result; @@ -70,7 +69,7 @@ public void ReleaseTemp() /// /// Computes the butteraugli map between the original image given in the constructor and the distorted image given here. /// - public virtual bool Diffmap(Configuration configuration, JxlImage3F rgb1, JxlImageF result) + public virtual bool Diffmap(Configuration configuration, JxlImage3 rgb1, JxlPlane result) { if (this.xSize < 8 || this.ySize < 8) { @@ -78,7 +77,7 @@ public virtual bool Diffmap(Configuration configuration, JxlImage3F rgb1, JxlIma return true; } - JxlImage3F xyb1 = new(configuration, this.xSize, this.ySize); + JxlImage3 xyb1 = new(configuration, this.xSize, this.ySize); if (!Butteraugli.OpsinDynamicsImage(configuration, rgb1, this.parameters, this.Temp, this.blurTemp, xyb1)) { @@ -99,8 +98,8 @@ public virtual bool Diffmap(Configuration configuration, JxlImage3F rgb1, JxlIma return true; } - JxlImage3F subXyb = new(configuration, this.sub.xSize, this.sub.ySize); - JxlImage3F subsampledRgb1 = Butteraugli.SubSample2x(configuration, rgb1); + JxlImage3 subXyb = new(configuration, this.sub.xSize, this.sub.ySize); + JxlImage3 subsampledRgb1 = Butteraugli.SubSample2x(configuration, rgb1); if (!Butteraugli.OpsinDynamicsImage(configuration, subsampledRgb1, this.parameters, this.sub.Temp, this.sub.blurTemp, subXyb)) { @@ -109,7 +108,7 @@ public virtual bool Diffmap(Configuration configuration, JxlImage3F rgb1, JxlIma this.sub.ReleaseTemp(); - if (!this.DiffmapOpsinDynamicsImage(configuration, subXyb, out JxlImageF subResult)) + if (!this.DiffmapOpsinDynamicsImage(configuration, subXyb, out JxlPlane subResult)) { return false; } @@ -123,7 +122,7 @@ public virtual bool Diffmap(Configuration configuration, JxlImage3F rgb1, JxlIma /// /// Same as Diffmap but OpsinDynamicsImage() was already applied. /// - public bool DiffmapOpsinDynamicsImage(Configuration configuration, JxlImage3F xyb1, out JxlImageF result) + public bool DiffmapOpsinDynamicsImage(Configuration configuration, JxlImage3 xyb1, out JxlPlane result) { result = new(); @@ -147,7 +146,7 @@ public bool DiffmapOpsinDynamicsImage(Configuration configuration, JxlImage3F xy /// /// Same as above but the frequency decomposition was already applied. /// - public bool DiffmapPsychoImage(Configuration configuration, ButteraugliPsychoImage pi1, JxlImageF diffmap) + public bool DiffmapPsychoImage(Configuration configuration, ButteraugliPsychoImage pi1, JxlPlane diffmap) { if (this.xSize < 8 || this.ySize < 8) { @@ -158,7 +157,7 @@ public bool DiffmapPsychoImage(Configuration configuration, ButteraugliPsychoIma float hfAsymmetry = this.parameters.HfAsymmetry; float xmul = this.parameters.XMultiplier; - JxlImageF diffs = new(configuration, this.xSize, this.ySize); + JxlPlane diffs = new(configuration, this.xSize, this.ySize); JxlImage3 blockDiffAc = new(configuration, this.xSize, this.ySize); JxlImageOperations.ZeroFillImage(blockDiffAc); @@ -241,7 +240,7 @@ public bool DiffmapPsychoImage(Configuration configuration, ButteraugliPsychoIma return false; } - JxlImage3F blockDiffDc = new(configuration, this.xSize, this.ySize); + JxlImage3 blockDiffDc = new(configuration, this.xSize, this.ySize); for (int c = 0; c < 3; c++) { @@ -268,7 +267,7 @@ public bool DiffmapPsychoImage(Configuration configuration, ButteraugliPsychoIma blockDiffDc.Plane(c)); } - JxlImageF mask = new(); + JxlPlane mask = new(); if (!Butteraugli.MaskButteraugliPsychoImage( configuration, @@ -278,17 +277,20 @@ public bool DiffmapPsychoImage(Configuration configuration, ButteraugliPsychoIma this.ySize, this.blurTemp, mask, - out JxlImageF? diffAc)) + out JxlPlane? diffAc)) { return false; } - diffAc?.BytesSpan.CopyTo(blockDiffAc.Plane(1).BytesSpan); + if (diffAc is not null) + { + JxlImageOperations.ForceCopyImage(diffAc, blockDiffAc.Plane(1)); + } return Butteraugli.CombineChannelsToDiffmap(mask, blockDiffDc, blockDiffAc, xmul, diffmap); } - public virtual bool Mask(Configuration configuration, JxlImageF mask) + public virtual bool Mask(Configuration configuration, JxlPlane mask) => Butteraugli.MaskButteraugliPsychoImage(configuration, this.pi0, this.pi0, this.xSize, this.ySize, this.blurTemp, mask, out _); public void Dispose() diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliPsychoImage.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliPsychoImage.cs index de1166cf8e..d5840af082 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliPsychoImage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliPsychoImage.cs @@ -3,7 +3,6 @@ using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Memory; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; @@ -17,7 +16,7 @@ internal sealed class ButteraugliPsychoImage public InlineArray2> Hf; // XY - public JxlImage3F? Mf { get; set; } // XYB + public JxlImage3? Mf { get; set; } // XYB - public JxlImage3F? Lf { get; set; } // XYB + public JxlImage3? Lf { get; set; } // XYB } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/BlockLoader/JxlBitStreamBlockLoader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/BlockLoader/JxlBitStreamBlockLoader.cs index 6b52794d86..ae2e4b2b9a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/BlockLoader/JxlBitStreamBlockLoader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/BlockLoader/JxlBitStreamBlockLoader.cs @@ -5,7 +5,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; @@ -20,8 +20,8 @@ internal sealed class JxlBitStreamBlockLoader : IJxlGetBlock { private readonly JxlGroupDecoderCache groupDecCache; private readonly JxlBlockContextMap blockCtxMap; - private readonly JxlImageI rawQuantField; - private readonly JxlImageB quantDc; + private readonly JxlPlane rawQuantField; + private readonly JxlPlane quantDc; private readonly JxlAnsSymbolReader[] decoders; private readonly JxlBitReader[] readers; @@ -50,8 +50,8 @@ internal sealed class JxlBitStreamBlockLoader : IJxlGetBlock public JxlBitStreamBlockLoader( JxlGroupDecoderCache groupDecCache, JxlBlockContextMap blockCtxMap, - JxlImageI rawQuantField, - JxlImageB quantDc, + JxlPlane rawQuantField, + JxlPlane quantDc, JxlAnsSymbolReader[] decoders, JxlBitReader[] readers) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs index 522553afb5..24cf2fa7cb 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs @@ -7,7 +7,6 @@ using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; using SixLabors.ImageSharp.Formats.Jxl.Memory; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; @@ -698,7 +697,7 @@ public static bool DecodeGroup( JxlRenderPipelineBuffer buffer = renderPipelineInput.GetBuffer(c); Rectangle dstRect = buffer.Bounds; - JxlImageF upsamplingDst = buffer.Image; + JxlPlane upsamplingDst = buffer.Image; if (!dstRect.IsInside(upsamplingDst.GetRectangle())) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoderCache.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoderCache.cs index 1019219635..e64d3ab2b0 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoderCache.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoderCache.cs @@ -2,7 +2,7 @@ // Licensed under the Six Labors Split License. using System.Buffers; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; @@ -26,16 +26,17 @@ internal sealed class JxlGroupDecoderCache(Configuration configuration) public Memory DecodeGroupQBlock16ScratchSpace { get; set; } /// - /// Gets or sets the scratch-space for . + /// Gets or sets the scratch space for + /// . /// public Memory ScratchSpaceMemory { get; set; } /// /// Gets or sets the number of non-zero coefficients. /// - public JxlImage3I[] NumberOfNonZeroes { get; set; } = ArrayHelpers.CreateAndInitialize(JxlShared.MaximumNumberOfPasses, () => new JxlImage3I()); + public JxlImage3[] NumberOfNonZeroes { get; set; } = ArrayHelpers.CreateAndInitialize(JxlShared.MaximumNumberOfPasses, () => new JxlImage3()); - public JxlImageF DcBuffer { get; set; } = new(); + public JxlPlane DcBuffer { get; set; } = new(); public void InitializeDcBufferOnce() { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs index 9621d6d608..c02c156a40 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs @@ -5,7 +5,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Noise; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; @@ -21,7 +21,7 @@ private static void BitsToFloatingPoint(ReadOnlySpan randomBits, Span noise) { const int floatsPerBatch = JxlXorShift.Generators * sizeof(ulong) / sizeof(float); @@ -69,9 +69,9 @@ public static void Random3Planes( int nonVisibleFrameIndex, int x0, int y0, - (JxlImageF Image, Rectangle Bounds) plane0, - (JxlImageF Image, Rectangle Bounds) plane1, - (JxlImageF Image, Rectangle Bounds) plane2) + (JxlPlane Image, Rectangle Bounds) plane0, + (JxlPlane Image, Rectangle Bounds) plane1, + (JxlPlane Image, Rectangle Bounds) plane2) { JxlXorShift rng = new((uint)visibleFrameIndex, (uint)nonVisibleFrameIndex, (uint)x0, (uint)y0); @@ -95,7 +95,7 @@ private static void PrepareNoiseInput( JxlRenderPipelineInput input = decState.RenderPipeline.GetInputBuffers(groupIndex, thread); int noiseCStart = 3 + frameHeader.Metadata!.ImageMetadata!.ExtraChannelCount; - InlineArray3<(JxlImageF Image, Rectangle Rect)> rects = default; + InlineArray3<(JxlPlane Image, Rectangle Rect)> rects = default; for (int iy = 0; iy < frameHeader.Upsampling; iy++) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs index 53dc70d924..9a2bc7c470 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs @@ -3,7 +3,7 @@ using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; @@ -28,7 +28,7 @@ public JxlPassesDecoderState(JxlFrameHeader header, Configuration configuration) if (header.LoopFilter?.EpfIterations > 0) { - this.Sigma = new JxlImageF( + this.Sigma = new JxlPlane( configuration, (this.Shared.FrameDimensions.XSizeBlocks + 2) * SigmaPadding, (this.Shared.FrameDimensions.YSizeBlocks + 2) * SigmaPadding); @@ -52,7 +52,7 @@ public JxlPassesDecoderState(JxlFrameHeader header, Configuration configuration) public float BDmMultiplier { get; set; } - public JxlImageF? Sigma { get; set; } + public JxlPlane? Sigma { get; set; } public int Width { get; set; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs index 87f96e97eb..3fb298621a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs @@ -5,7 +5,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.Cms; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; @@ -65,7 +65,7 @@ public static void ConvertXybToRgb( linearB = (LoadDuplicate128(ref inverseMatrix[8 * 4]) * mixedB) + linearB; } - public static bool OpsinToLinear(JxlImage3F opsin, Rectangle rect, JxlImage3F linear, JxlOpsinParameters opsinParameters) + public static bool OpsinToLinear(JxlImage3 opsin, Rectangle rect, JxlImage3 linear, JxlOpsinParameters opsinParameters) { if (!JxlImageOperations.SameSize(rect, linear)) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlChromaFromLumaEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlChromaFromLumaEncoder.cs index f6cd7b609b..8ea341aa8a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlChromaFromLumaEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlChromaFromLumaEncoder.cs @@ -4,7 +4,7 @@ using System.Numerics; using SixLabors.ImageSharp.Formats.Jxl.Cms; using SixLabors.ImageSharp.Formats.Jxl.Fields; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; @@ -14,9 +14,9 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.ChromaFromLuma; internal static class JxlChromaFromLumaEncoder { - public static JxlImageF InitializeDcStorage(Configuration configuration, int numBlocks) + public static JxlPlane InitializeDcStorage(Configuration configuration, int numBlocks) { - JxlImageF dcValues = new(configuration, JxlMath.RoundUpTo(numBlocks, Vector.Count), 4); + JxlPlane dcValues = new(configuration, JxlMath.RoundUpTo(numBlocks, Vector.Count), 4); for (int y = 0; y < 4; y++) { @@ -126,7 +126,20 @@ public static int FindBestMultiplier(Span valuesM, Span valuesS, i return (int)Math.Clamp(MathF.Round(x), -128.0f, 127.0f); } - public static void ComputeTile(JxlImage3F opsin, Rectangle opsinRect, JxlDequantMatrices dequant, JxlAcStrategyImage? acStrategy, JxlImageI? rawQuantField, JxlQuantizer quantizer, Rectangle rect, bool fast, bool useDct8, JxlImageSB mapX, JxlImageSB mapB, JxlImageF dcValues, Span mem) + public static void ComputeTile( + JxlImage3 opsin, + Rectangle opsinRect, + JxlDequantMatrices dequant, + JxlAcStrategyImage? acStrategy, + JxlPlane? rawQuantField, + JxlQuantizer quantizer, + Rectangle rect, + bool fast, + bool useDct8, + JxlPlane mapX, + JxlPlane mapB, + JxlPlane dcValues, + Span mem) { int widthBlocks = opsinRect.Width / JxlFrameDimensions.BlockDimensions; const float distanceMultiplierAC = 1e-9f; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/ButteraugliJxlComparator.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/ButteraugliJxlComparator.cs index 919446bfca..0a34bef6b0 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/ButteraugliJxlComparator.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/ButteraugliJxlComparator.cs @@ -4,7 +4,7 @@ using System.Numerics.Tensors; using SixLabors.ImageSharp.Formats.Jxl.Cms; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; @@ -25,7 +25,7 @@ internal sealed class ButteraugliJxlComparator(ButteraugliParameters parameters, public override float BadQualityScore => Butteraugli.Butteraugli.ButteraugliFuzzyInverse(0.5f); - public override void CompareWith(Configuration configuration, JxlImageBundle actual, JxlImageF? diffMap, out float score) + public override void CompareWith(Configuration configuration, JxlImageBundle actual, JxlPlane? diffMap, out float score) { DebugGuard.NotNull(this.comparator, nameof(this.comparator)); @@ -39,12 +39,12 @@ public override void CompareWith(Configuration configuration, JxlImageBundle act JxlTransformsEncoder.TransformIfNeeded(configuration, actual, JxlColorEncoding.LinearSRgb(actual.IsGray), cms, store, out JxlImageBundle actualLinearSRgb); - JxlImageF tempDiffmap = new(configuration, this.width, this.height); - JxlImage3F scaledActualLinearSRgb = actualLinearSRgb.Color!; + JxlPlane tempDiffmap = new(configuration, this.width, this.height); + JxlImage3 scaledActualLinearSRgb = actualLinearSRgb.Color!; if (this.intensityTarget != 0 && actual.Metadata!.IntensityTarget != this.intensityTarget) { - JxlImage3F scaledActualLinearSRgbStore = new(configuration, this.width, this.height); + JxlImage3 scaledActualLinearSRgbStore = new(configuration, this.width, this.height); scaledActualLinearSRgb = scaledActualLinearSRgbStore; float scale = actual.Metadata!.IntensityTarget / this.intensityTarget; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparator.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparator.cs index 920e929099..8a8be68646 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparator.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparator.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Comparator; @@ -22,6 +22,6 @@ internal abstract class JxlComparator public abstract void CompareWith( Configuration configuration, JxlImageBundle actual, - JxlImageF? diffMap, + JxlPlane? diffMap, out float score); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparatorScoreComputation.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparatorScoreComputation.cs index 8dec22e152..b0530ee720 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparatorScoreComputation.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparatorScoreComputation.cs @@ -4,14 +4,14 @@ using System.Numerics.Tensors; using SixLabors.ImageSharp.Formats.Jxl.Cms; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Comparator; internal static class JxlComparatorScoreComputation { - private static void AlphaBlend(JxlImage3F input, int c, float backgroundLinear, JxlImageF alpha, JxlImage3F output) + private static void AlphaBlend(JxlImage3 input, int c, float backgroundLinear, JxlPlane alpha, JxlImage3 output) { float background = (float)JxlGammaCorrect.LinearToSRgb8Direct(backgroundLinear); @@ -59,7 +59,13 @@ private static void AlphaBlend(float backgroundLinear, JxlImageBundle image) } } - private static void ComputeScoreImpl(Configuration configuration, JxlImageBundle rgb0, JxlImageBundle rgb1, JxlComparator comparator, JxlImageF? diffMap, out float score) + private static void ComputeScoreImpl( + Configuration configuration, + JxlImageBundle rgb0, + JxlImageBundle rgb1, + JxlComparator comparator, + JxlPlane? diffMap, + out float score) { comparator.SetReferenceImage(rgb0); comparator.CompareWith(configuration, rgb1, diffMap, out score); @@ -72,7 +78,7 @@ public static void ComputeScore( JxlComparator comparator, JxlCmsInterface cms, out float score, - JxlImageF? diffMap = null, + JxlPlane? diffMap = null, bool ignoreAlpha = false) { JxlImageMetadata metadata0 = rgb0.Metadata.Copy(); @@ -110,7 +116,7 @@ public static void ComputeScore( out score); } - JxlImageF diffMapBlack = new(); + JxlPlane diffMapBlack = new(); float distBlack; { const float black = 0.0f; @@ -130,7 +136,7 @@ public static void ComputeScore( out distBlack); } - JxlImageF diffMapWhite = new(); + JxlPlane diffMapWhite = new(); float distWhite; { const float white = 1.0f; @@ -155,7 +161,7 @@ public static void ComputeScore( int xSize = rgb0.XSize; int ySize = rgb0.YSize; - JxlImageF outputDiffMap = new( + JxlPlane outputDiffMap = new( configuration, xSize, ySize); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/NullComparator.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/NullComparator.cs index 810c4c59a0..dc255e91b2 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/NullComparator.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/NullComparator.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Comparator; @@ -15,7 +15,7 @@ internal sealed class NullComparator : JxlComparator public override float BadQualityScore => 0; - public override void CompareWith(Configuration configuration, JxlImageBundle actual, JxlImageF? diffMap, out float score) => score = 0; + public override void CompareWith(Configuration configuration, JxlImageBundle actual, JxlPlane? diffMap, out float score) => score = 0; public override void SetReferenceImage(Configuration configuration, JxlImageBundle reference) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlConnectedComponent.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlConnectedComponent.cs index df1cf129e8..76ef93aa6e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlConnectedComponent.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlConnectedComponent.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.DotDetection; @@ -23,7 +23,7 @@ public JxlConnectedComponent(Rectangle bounds, List points) public float Score { get; private set; } - public void ComputeStats(JxlImageF energy, in Rectangle rect, int extra) + public void ComputeStats(JxlPlane energy, in Rectangle rect, int extra) { this.stats = default; int nIn = 0; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlDotDetectionUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlDotDetectionUtils.cs index fd548411a4..5f72b69b22 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlDotDetectionUtils.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlDotDetectionUtils.cs @@ -5,7 +5,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Common.Helpers; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using Matrix2x2 = System.Runtime.CompilerServices.InlineArray2>; @@ -31,13 +31,13 @@ internal static class JxlDotDetectionUtils /// Original image /// Smooth image /// Sum of Square Differences coefficients - public static JxlImageF Ssd(Configuration configuration, JxlImage3F original, JxlImage3F smooth) + public static JxlPlane Ssd(Configuration configuration, JxlImage3 original, JxlImage3 smooth) { Vector colorCoef0 = Vector.Zero; Vector colorCoef1 = Vector.Create(10.0f); Vector colorCoef2 = Vector.Zero; - JxlImageF sumOfSquares = new(configuration, original.XSize, original.YSize); + JxlPlane sumOfSquares = new(configuration, original.XSize, original.YSize); _ = Parallel.For(0, original.YSize, configuration.GetParallelOptions(), y => { @@ -177,7 +177,7 @@ public static Rectangle BoundingRectangle(Span pixels) highY - lowY + 1); } - public static JxlGaussianEllipse? FitGaussianFast(JxlConnectedComponent cc, Rectangle rect, JxlImage3F img, JxlImage3F background) + public static JxlGaussianEllipse? FitGaussianFast(JxlConnectedComponent cc, Rectangle rect, JxlImage3 img, JxlImage3 background) { const bool leastSqIntensity = true; const double epsilon = 1e-6; @@ -346,7 +346,12 @@ public static Rectangle BoundingRectangle(Span pixels) return ans; } - public static void ComputeDotLossless(ref JxlGaussianEllipse ellipse, JxlConnectedComponent cc, Rectangle rect, JxlImage3F img, JxlImage3F background) + public static void ComputeDotLossless( + ref JxlGaussianEllipse ellipse, + JxlConnectedComponent cc, + Rectangle rect, + JxlImage3 img, + JxlImage3 background) { const int rectBounds = 2; const double intensityR = 1.0; @@ -441,7 +446,7 @@ public static void ComputeDotLossless(ref JxlGaussianEllipse ellipse, JxlConnect ellipse.RidgeLoss = ellipse.L2Loss + ridgeTerm; } - public static JxlGaussianEllipse? FitGaussian(JxlConnectedComponent cc, Rectangle rect, JxlImage3F img, JxlImage3F background) + public static JxlGaussianEllipse? FitGaussian(JxlConnectedComponent cc, Rectangle rect, JxlImage3 img, JxlImage3 background) { JxlGaussianEllipse? ellipseOrNull = FitGaussianFast(cc, rect, img, background); @@ -473,7 +478,7 @@ public static void ComputeDotLossless(ref JxlGaussianEllipse ellipse, JxlConnect return ellipse; } - public static bool ExtractComponent(Rectangle rect, JxlImageF img, List pixels, Point seed, double threshold) + public static bool ExtractComponent(Rectangle rect, JxlPlane img, List pixels, Point seed, double threshold) { Span neighbors = [ @@ -522,9 +527,9 @@ public static bool ExtractComponent(Rectangle rect, JxlImageF img, List p return true; } - public static JxlImageF? ComputeEnergyImage(Configuration configuration, JxlImage3F original, ref JxlImage3F smooth) + public static JxlPlane? ComputeEnergyImage(Configuration configuration, JxlImage3 original, ref JxlImage3 smooth) { - using JxlImage3F forig = new(configuration, original.XSize, original.YSize); + using JxlImage3 forig = new(configuration, original.XSize, original.YSize); smooth = new(configuration, original.XSize, original.YSize); Rectangle rect = original.GetRectangle(); @@ -553,11 +558,18 @@ public static bool ExtractComponent(Rectangle rect, JxlImageF img, List p return Ssd(configuration, forig, smooth); } - public static List FindCC(Configuration configuration, JxlImageF energy, Rectangle rect, double tLow, double tHigh, int maxWindow, double minScore) + public static List FindCC( + Configuration configuration, + JxlPlane energy, + Rectangle rect, + double tLow, + double tHigh, + int maxWindow, + double minScore) { const int extraRect = 4; - using JxlImageF img = new(configuration, energy.XSize, energy.YSize); + using JxlPlane img = new(configuration, energy.XSize, energy.YSize); if (!JxlImageOperations.CopyImage(energy, img)) { @@ -609,15 +621,15 @@ public static List FindCC(Configuration configuration, Jx public static List DetectGaussianEllipses( Configuration configuration, - JxlImage3F opsin, + JxlImage3 opsin, Rectangle rect, JxlGaussianDetectParameters parameters, JxlEllipseQuantParameters qparameters) { List dots = []; - JxlImage3F smooth = new(configuration, opsin.XSize, opsin.YSize); - JxlImageF energy = ComputeEnergyImage(configuration, opsin, ref smooth) ?? throw new InvalidOperationException("Failed to compute energy image"); + JxlImage3 smooth = new(configuration, opsin.XSize, opsin.YSize); + JxlPlane energy = ComputeEnergyImage(configuration, opsin, ref smooth) ?? throw new InvalidOperationException("Failed to compute energy image"); List components = FindCC(configuration, energy, rect, parameters.TLow, parameters.THigh, parameters.MaxWindowSize, parameters.MinScore); int numCC = Math.Min(parameters.MaxCC, (components.Count * parameters.PercCC) / 100); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlDotDictionary.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlDotDictionary.cs index e4c249b324..9c321ce18e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlDotDictionary.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlDotDictionary.cs @@ -2,7 +2,7 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.DotDetection; @@ -37,7 +37,7 @@ internal static class JxlDotDictionary public static List FindDotDictionary( Configuration configuration, JxlCompressParameters cparams, - JxlImage3F opsin, + JxlImage3 opsin, Rectangle rect, JxlColorCorrelation colorCorrelation) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlAcStrategyHeuristics.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlAcStrategyHeuristics.cs index fb6dfffbfe..3c9741a1a8 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlAcStrategyHeuristics.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlAcStrategyHeuristics.cs @@ -2,7 +2,7 @@ // Licensed under the Six Labors Split License. using System.Buffers; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; @@ -36,7 +36,13 @@ public void Dispose() this.qMemoryOwner?.Dispose(); } - public bool Initialize(JxlImage3F src, Rectangle rectIn, JxlImageF quantField, JxlImageF mask, JxlImageF mask1x1, JxlDequantMatrices matrices) + public bool Initialize( + JxlImage3 src, + Rectangle rectIn, + JxlPlane quantField, + JxlPlane mask, + JxlPlane mask1x1, + JxlDequantMatrices matrices) { this.Config.Dequant = matrices; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlCflHeuristics.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlCflHeuristics.cs index 759418dcd2..f40545ffdc 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlCflHeuristics.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlCflHeuristics.cs @@ -3,7 +3,7 @@ using System.Buffers; using System.Runtime.InteropServices; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.ChromaFromLuma; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; @@ -18,7 +18,7 @@ internal sealed class JxlCflHeuristics(Configuration configuration) : IDisposabl { // TODO private Memory scratchSpace; - private JxlImageF? dcValues; + private JxlPlane? dcValues; private IMemoryOwner? scratchSpaceMemoryOwner; private static int ItemsPerThread @@ -53,11 +53,11 @@ public void Initialize(Rectangle rect) public void ComputeTile( Rectangle rectangle, - JxlImage3F opsin, + JxlImage3 opsin, Rectangle opsinRect, JxlDequantMatrices dequant, JxlAcStrategyImage? acStrategy, - JxlImageI rawQuantField, + JxlPlane rawQuantField, JxlQuantizer quantizer, bool fast, int thread, diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs index f04b9ce9ff..044858cb33 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs @@ -6,7 +6,6 @@ using System.Numerics.Tensors; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Memory; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Modular; @@ -124,7 +123,11 @@ internal static class JxlHeuristics -0.00624645f, -0.02921014f, -0.04022174f, -0.03452303f, -0.01716200f, ]; - public static void FindBestBlockEntropyModel(JxlCompressParameters cparameters, JxlImageI rqf, JxlAcStrategyImage acStrategy, JxlBlockContextMap blockCtxMap) + public static void FindBestBlockEntropyModel( + JxlCompressParameters cparameters, + JxlPlane rqf, + JxlAcStrategyImage acStrategy, + JxlBlockContextMap blockCtxMap) { if (cparameters.DecodingSpeedTier >= 1) { @@ -293,7 +296,7 @@ public static void StoreMin2(float v, ref float min1, ref float min2) } } - public static void CreateMask(JxlImageF image, JxlImageF mask) + public static void CreateMask(JxlPlane image, JxlPlane mask) { for (int y = 0; y < image.YSize; y++) { @@ -330,7 +333,7 @@ public static void CreateMask(JxlImageF image, JxlImageF mask) } } - public static void ReduceRinging(JxlImageF initial, JxlImageF mask, JxlImageF down) + public static void ReduceRinging(JxlPlane initial, JxlPlane mask, JxlPlane down) { int xsize2 = down.XSize; int ysize2 = down.YSize; @@ -465,7 +468,7 @@ private static void ElementwiseDivide(JxlPlane image1, JxlPlane image2, } } - public static void AntiUpsample(JxlImageF input, JxlImageF d) + public static void AntiUpsample(JxlPlane input, JxlPlane d) { int xsize = input.XSize; int ysize = input.YSize; @@ -521,7 +524,7 @@ public static void AntiUpsample(JxlImageF input, JxlImageF d) } } - public static void UpsampleImage(JxlImageF input, JxlImageF output) + public static void UpsampleImage(JxlPlane input, JxlPlane output) { int xsize = input.XSize; int ysize = input.YSize; @@ -633,7 +636,10 @@ public static void UpsampleImage(JxlImageF input, JxlImageF output) } } - public static void FindBestDequantMatrices(JxlCompressParameters cparams, JxlModularFrameEncoder modularFrameEncoder, out JxlDequantMatrices dequantMatrices) + public static void FindBestDequantMatrices( + JxlCompressParameters cparams, + JxlModularFrameEncoder modularFrameEncoder, + out JxlDequantMatrices dequantMatrices) { dequantMatrices = new(); if (cparams.MaxErrorMode || cparams.DisablePerceptualOptimizations) @@ -657,7 +663,7 @@ public static void FindBestDequantMatrices(JxlCompressParameters cparams, JxlMod } } - public static void DownsampleImage2Sharper(Configuration configuration, JxlImageF input, JxlImageF output) + public static void DownsampleImage2Sharper(Configuration configuration, JxlPlane input, JxlPlane output) { const int kernelX = 12; const int kernelY = 12; @@ -673,13 +679,13 @@ public static void DownsampleImage2Sharper(Configuration configuration, JxlImage int xSize = input.XSize; int ySize = input.YSize; - JxlImageF tempDownsample = new(configuration, xSize, ySize); + JxlPlane tempDownsample = new(configuration, xSize, ySize); JxlImageOperations.ForceCopyImage(input, tempDownsample); - using JxlImageF boxDownsample = DownsampleImage(tempDownsample, 2); + using JxlPlane boxDownsample = DownsampleImage(tempDownsample, 2); tempDownsample.Dispose(); - using JxlImageF mask = new(configuration, boxDownsample.XSize, boxDownsample.YSize); + using JxlPlane mask = new(configuration, boxDownsample.XSize, boxDownsample.YSize); CreateMask(boxDownsample, mask); for (int y = 0; y < output.YSize; y++) @@ -810,7 +816,7 @@ public static void DownsampleImage2Sharper(Configuration configuration, JxlImage /// /// The L2 distance for the images. /// - public static float ComputeBlockL2Distance(JxlImage3F a, JxlImage3F b, JxlImageF mask1x1, int by, int bx) + public static float ComputeBlockL2Distance(JxlImage3 a, JxlImage3 b, JxlPlane mask1x1, int by, int bx) { Rectangle rect = RectangleUtils.CreateRectangle( bx * JxlFrameDimensions.BlockDimensions, @@ -893,7 +899,7 @@ private sealed class OccCounters : IDisposable private readonly int[] dataForQfOrdCounts; private readonly int[] ordCounts; - public OccCounters(JxlImageI rqf, JxlAcStrategyImage acStrategy) + public OccCounters(JxlPlane rqf, JxlAcStrategyImage acStrategy) { this.qfCounts = ArrayPool.Shared.Rent(256); this.dataForQfOrdCounts = ArrayPool.Shared.Rent(256 * JxlForwardCoefficientOrder.OrderCount); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs index 8302b826da..7fa3460f8d 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs @@ -8,7 +8,6 @@ using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Memory; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; @@ -225,7 +224,7 @@ private static float HfModulationScalar(int x, int y, JxlPlane xybY, Rect return scalarSumY + outVal; } - private static Vector HfModulationVectorized(int x, int y, JxlImageF xybY, Rectangle rect, Vector outVal) + private static Vector HfModulationVectorized(int x, int y, JxlPlane xybY, Rectangle rect, Vector outVal) { const float valueMinY = 0.0206f; const float mulY = -0.38f; @@ -267,7 +266,7 @@ private static Vector HfModulationVectorized(int x, int y, JxlImageF xybY return new Vector(scalarSumY) + outVal; } - public static Vector HfModulation(int x, int y, JxlImageF xybY, Rectangle rect, Vector outVal) + public static Vector HfModulation(int x, int y, JxlPlane xybY, Rectangle rect, Vector outVal) { if (Vector.Count == 1) { @@ -285,7 +284,15 @@ public static Vector HfModulation(int x, int y, JxlImageF xybY, Rectangle } } - private static void PerBlockModulations(float butteraugliTarget, JxlPlane xybX, JxlPlane xybY, JxlPlane xybB, Rectangle rectIn, float scale, Rectangle rectOut, JxlImageF output) + private static void PerBlockModulations( + float butteraugliTarget, + JxlPlane xybX, + JxlPlane xybY, + JxlPlane xybB, + Rectangle rectIn, + float scale, + Rectangle rectOut, + JxlPlane output) { float baseLevel = 0.48f * scale; @@ -376,7 +383,7 @@ private static void StoreMin4(float v, ref float min0, ref float min1, ref float // Look for smooth areas near the area of degradation. // If the areas are generally smooth, don't do masking. // Output is downsampled by 2x. - public static bool FuzzyErosion(float butteraugliTarget, Rectangle fromRect, JxlImageF from, Rectangle toRect, JxlImageF to) + public static bool FuzzyErosion(float butteraugliTarget, Rectangle fromRect, JxlPlane from, Rectangle toRect, JxlPlane to) { int width = from.XSize; int height = from.YSize; @@ -494,7 +501,7 @@ public static bool FuzzyErosion(float butteraugliTarget, Rectangle fromRect, Jxl return true; } - private static void Blur1x1Masking(Configuration configuration, ref JxlImageF mask1x1, Rectangle rect) + private static void Blur1x1Masking(Configuration configuration, ref JxlPlane mask1x1, Rectangle rect) { const float filterMask1x1_0 = 0.364911248f; const float filterMask1x1_1 = 0.05f; @@ -522,7 +529,7 @@ private static void Blur1x1Masking(Configuration configuration, ref JxlImageF ma weights.D2 = JxlWeightsSymmetric5.CreateVector4(normalize * filterMask1x1_4); weights.L = JxlWeightsSymmetric5.CreateVector4(normalize * filterMask1x1_3); - JxlImageF temp = new(configuration, rect.Width, rect.Height); + JxlPlane temp = new(configuration, rect.Width, rect.Height); if (!JxlConvolve.Symmetric5(mask1x1, in rect, ref weights, temp, temp.GetRectangle())) { @@ -532,7 +539,14 @@ private static void Blur1x1Masking(Configuration configuration, ref JxlImageF ma mask1x1 = temp; } - public static JxlImageF AdaptiveQuantizationMap(Configuration configuration, float butteraugliTarget, JxlImageF xyb, Rectangle rect, float scale, out JxlImageF mask, out JxlImageF mask1x1) + public static JxlPlane AdaptiveQuantizationMap( + Configuration configuration, + float butteraugliTarget, + JxlPlane xyb, + Rectangle rect, + float scale, + out JxlPlane mask, + out JxlPlane mask1x1) { if ((rect.Width & 7) != 0) { @@ -549,13 +563,13 @@ public static JxlImageF AdaptiveQuantizationMap(Configuration configuration, flo int xSizeBlocks = rect.Width / 8; int ySizeBlocks = rect.Height / 8; - impl.AdaptiveQuantizationMap = new JxlImageF(configuration, xSizeBlocks, ySizeBlocks); - mask = new JxlImageF(configuration, xSizeBlocks, ySizeBlocks); - mask1x1 = new JxlImageF(configuration, xyb.XSize, xyb.YSize); + impl.AdaptiveQuantizationMap = new JxlPlane(configuration, xSizeBlocks, ySizeBlocks); + mask = new JxlPlane(configuration, xSizeBlocks, ySizeBlocks); + mask1x1 = new JxlPlane(configuration, xyb.XSize, xyb.YSize); // out variables cannot be captured in a local function - JxlImageF maskRef = mask; - JxlImageF mask1x1Ref = mask1x1; + JxlPlane maskRef = mask; + JxlPlane mask1x1Ref = mask1x1; impl.InitializeBuffer(configuration); @@ -589,12 +603,12 @@ void ProcessTile(int idx) return impl.AdaptiveQuantizationMap!; } - private static JxlImageF TileDistMap(Configuration configuration, JxlImageF distMap, int tileSize, int margin, JxlAcStrategyImage acStrategy) + private static JxlPlane TileDistMap(Configuration configuration, JxlPlane distMap, int tileSize, int margin, JxlAcStrategyImage acStrategy) { int tileXSize = (distMap.XSize + tileSize - 1) / tileSize; int tileYSize = (distMap.YSize + tileSize - 1) / tileSize; - JxlImageF tileDistMap = new(configuration, tileXSize, tileYSize); + JxlPlane tileDistMap = new(configuration, tileXSize, tileYSize); const float borderMul = 0.98f; const float cornerMul = 0.7f; @@ -687,7 +701,12 @@ private static JxlImageF TileDistMap(Configuration configuration, JxlImageF dist return tileDistMap; } - private static JxlImageBundle RoundtripImage(Configuration configuration, JxlFrameHeader frameHeader, JxlImage3F opsin, JxlPassesEncoderState encState, JxlCmsInterface cms) + private static JxlImageBundle RoundtripImage( + Configuration configuration, + JxlFrameHeader frameHeader, + JxlImage3 opsin, + JxlPassesEncoderState encState, + JxlCmsInterface cms) { JxlPassesDecoderState decState = new(frameHeader, configuration); @@ -718,7 +737,7 @@ private static JxlImageBundle RoundtripImage(Configuration configuration, JxlFra Origin = frameHeader.FrameOrigin }; - JxlImage3F tmp = new(configuration, opsin.XSize, opsin.YSize); + JxlImage3 tmp = new(configuration, opsin.XSize, opsin.YSize); if (!decoded.SetFromImage(tmp, decState.OutputEncodingInfo.ColorEncoding)) { @@ -765,7 +784,7 @@ void ProcessGroup(int groupIndex) for (int c = 0; c < metadata.ExtraChannelCount; c++) { - (JxlImageF plane, Rectangle rect) = input.GetBuffer(3 + c); + (JxlPlane plane, Rectangle rect) = input.GetBuffer(3 + c); JxlImageOperations.FillPlane(0.0f, plane, rect); } @@ -788,7 +807,7 @@ public static float InitialQuantDc(float butteraugliTarget) return MathF.Min(DcQuant / butteraugliTargetDc, 50f); } - public static bool AdjustQuantField(JxlAcStrategyImage acStrategy, Rectangle rect, float butteraugliTarget, JxlImageF quantField) + public static bool AdjustQuantField(JxlAcStrategyImage acStrategy, Rectangle rect, float butteraugliTarget, JxlPlane quantField) { int stride = quantField.PixelsPerRow; float meanMaxMixer = 1.0f; @@ -860,7 +879,15 @@ public static bool AdjustQuantField(JxlAcStrategyImage acStrategy, Rectangle rec return true; } - public static bool FindBestQuantization(Configuration configuration, JxlFrameHeader frameHeader, JxlImage3F linear, JxlImage3F opsin, JxlImageF quantField, JxlPassesEncoderState encState, JxlCmsInterface cms, JxlAuxiliaryOutput auxOut) + public static bool FindBestQuantization( + Configuration configuration, + JxlFrameHeader frameHeader, + JxlImage3 linear, + JxlImage3 opsin, + JxlPlane quantField, + JxlPassesEncoderState encState, + JxlCmsInterface cms, + JxlAuxiliaryOutput auxOut) { JxlCompressionParameters cparams = encState.CompressionParameters; @@ -873,7 +900,7 @@ public static bool FindBestQuantization(Configuration configuration, JxlFrameHea } JxlQuantizer quantizer = encState.Shared.Quantizer; - JxlImageI rawQuantField = encState.Shared.RawQuantField; + JxlPlane rawQuantField = encState.Shared.RawQuantField; float butteraugliTarget = cparams.ButteraugliDistance; float originalButteraugli = cparams.OriginalButteraugliDistance; @@ -893,7 +920,7 @@ public static bool FindBestQuantization(Configuration configuration, JxlFrameHea return false; } - using JxlImageF initialQuantField = new(configuration, quantField.XSize, quantField.YSize); + using JxlPlane initialQuantField = new(configuration, quantField.XSize, quantField.YSize); if (!JxlImageOperations.CopyImage(quantField, initialQuantField)) { @@ -927,7 +954,7 @@ public static bool FindBestQuantization(Configuration configuration, JxlFrameHea iters = MaxButteraugliIterations; } - JxlImageF tileDistmap = new(); + JxlPlane tileDistmap = new(); for (int i = 0; i < iters + 1; i++) { @@ -938,7 +965,7 @@ public static bool FindBestQuantization(Configuration configuration, JxlFrameHea JxlImageBundle decLinear = RoundtripImage(configuration, frameHeader, opsin, encState, cms); - JxlImageF diffmap = new(); + JxlPlane diffmap = new(); comparator.CompareWith(configuration, decLinear, diffmap, out float score); if (!lowerIsBetter) @@ -1088,11 +1115,18 @@ public static bool FindBestQuantization(Configuration configuration, JxlFrameHea return true; } - public static bool FindBestQuantizationMaxError(Configuration configuration, JxlFrameHeader frameHeader, JxlImage3F opsin, JxlImageF quantField, JxlPassesEncoderState encState, JxlCmsInterface cms, JxlAuxiliaryOutput auxOut) + public static bool FindBestQuantizationMaxError( + Configuration configuration, + JxlFrameHeader frameHeader, + JxlImage3 opsin, + JxlPlane quantField, + JxlPassesEncoderState encState, + JxlCmsInterface cms, + JxlAuxiliaryOutput auxOut) { JxlCompressionParameters cparams = encState.CompressionParameters; JxlQuantizer quantizer = encState.Shared.Quantizer; - JxlImageI rawQuantField = encState.Shared.RawQuantField; + JxlPlane rawQuantField = encState.Shared.RawQuantField; float initialQuantDc = 16f * MathF.Sqrt(0.1f / cparams.ButteraugliDistance); @@ -1187,7 +1221,14 @@ public static bool FindBestQuantizationMaxError(Configuration configuration, Jxl return true; } - public static JxlImageF InitialQuantField(Configuration configuration, float butteraugliTarget, JxlImage3F opsin, Rectangle rect, float rescale, JxlImageF mask, JxlImageF mask1x1) + public static JxlPlane InitialQuantField( + Configuration configuration, + float butteraugliTarget, + JxlImage3 opsin, + Rectangle rect, + float rescale, + JxlPlane mask, + JxlPlane mask1x1) { float quantAc = AcQuant / butteraugliTarget; @@ -1197,9 +1238,9 @@ public static JxlImageF InitialQuantField(Configuration configuration, float but public static bool FindBestQuantizer( Configuration configuration, JxlFrameHeader frameHeader, - JxlImage3F linear, - JxlImage3F opsin, - JxlImageF quantField, + JxlImage3 linear, + JxlImage3 opsin, + JxlPlane quantField, JxlPassesEncoderState encState, JxlCmsInterface cms, JxlAuxiliaryOutput auxOut, @@ -1222,11 +1263,11 @@ public static bool FindBestQuantizer( private sealed class AdaptiveQuantizationInternal { - public List PreErosion { get; set; } = []; + public List> PreErosion { get; set; } = []; - public JxlImageF? AdaptiveQuantizationMap { get; set; } + public JxlPlane? AdaptiveQuantizationMap { get; set; } - public JxlImageF? DiffBuffer { get; set; } + public JxlPlane? DiffBuffer { get; set; } public void InitializeBuffer(Configuration configuration) { @@ -1237,14 +1278,22 @@ public void InitializeBuffer(Configuration configuration) for (int i = this.PreErosion.Count; i < numThreads; i++) { this.PreErosion.Add( - new JxlImageF( + new JxlPlane( configuration, (JxlEncoderParameters.TileDimensionsInBlocks * 2) + 2, (JxlEncoderParameters.TileDimensionsInBlocks * 2) + 2)); } } - public bool TryComputeTile(float butteraugliTarget, float scale, JxlImage3F xyb, Rectangle rectIn, Rectangle rectOut, int parallelIndex, JxlImageF mask, JxlImageF mask1x1) + public bool TryComputeTile( + float butteraugliTarget, + float scale, + JxlImage3 xyb, + Rectangle rectIn, + Rectangle rectOut, + int parallelIndex, + JxlPlane mask, + JxlPlane mask1x1) { const float matchGammaOffset = 0.019f; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlEntropyCoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlEntropyCoder.cs index 242d2288fd..620030b5ee 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlEntropyCoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlEntropyCoder.cs @@ -4,7 +4,7 @@ using System.Numerics; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding; @@ -125,10 +125,10 @@ public static bool TokenizeCoefficients( ReadOnlyMemory[] acRows, JxlAcStrategyImage acStrategy, JxlYCbCrChromaSubsampling cs, - JxlImage3I tmpNumNzeroes, + JxlImage3 tmpNumNzeroes, List output, - JxlImageB qdc, - JxlImageI qf, + JxlPlane qdc, + JxlPlane qf, JxlBlockContextMap blockCtxMap) { int xsizeBlocks = rect.Width; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlExternalImageEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlExternalImageEncoder.cs index fdf7250e53..8fe7a110d1 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlExternalImageEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlExternalImageEncoder.cs @@ -4,7 +4,6 @@ using SixLabors.ImageSharp.Formats.Jxl.Cms; using SixLabors.ImageSharp.Formats.Jxl.IO; using SixLabors.ImageSharp.Formats.Jxl.Memory; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; @@ -21,7 +20,16 @@ internal class JxlExternalImageEncoder _ => 0 }; - private static void ConvertFromExternalPlaneNoSizeCheck(Configuration configuration, Memory data, int xSize, int ySize, int stride, int bitsPerSample, JxlPixelFormat format, int c, JxlImageF channel) + private static void ConvertFromExternalPlaneNoSizeCheck( + Configuration configuration, + Memory data, + int xSize, + int ySize, + int stride, + int bitsPerSample, + JxlPixelFormat format, + int c, + JxlPlane channel) { if (format.DataType == JxlDataType.Byte) { @@ -84,7 +92,15 @@ bool SaveValue(int index, float value) _ = Parallel.For(0, ySize, configuration.GetParallelOptions(), ConvertRow); } - private static bool ConvertFromExternal(Configuration configuration, Memory data, int xSize, int ySize, int bitsPerSample, JxlPixelFormat format, int c, JxlPlane channel) + private static bool ConvertFromExternal( + Configuration configuration, + Memory data, + int xSize, + int ySize, + int bitsPerSample, + JxlPixelFormat format, + int c, + JxlPlane channel) { int bytesPerChannel = JxlDataTypeBytes(format.DataType); @@ -131,7 +147,16 @@ private static bool ConvertFromExternal(Configuration configuration, Memory bytes, int xSize, int ySize, JxlColorEncoding currentColorEncoding, int bitsPerSample, JxlPixelFormat format, JxlImageBundle imageBundle, bool setAlpha) + public static bool ConvertFromExternal( + Configuration configuration, + Memory bytes, + int xSize, + int ySize, + JxlColorEncoding currentColorEncoding, + int bitsPerSample, + JxlPixelFormat format, + JxlImageBundle imageBundle, + bool setAlpha) { int colorChannels = currentColorEncoding.Channels; bool hasAlpha = format.Channels is 2 or 4; @@ -141,7 +166,7 @@ public static bool ConvertFromExternal(Configuration configuration, Memory throw new InvalidOperationException($"Expected {colorChannels} color channels, received only {format.Channels} channels."); } - JxlImage3F color = new(configuration, xSize, ySize); + JxlImage3 color = new(configuration, xSize, ySize); for (int c = 0; c < colorChannels; c++) { @@ -164,7 +189,7 @@ public static bool ConvertFromExternal(Configuration configuration, Memory if (setAlpha) { - JxlImageF alpha = new(configuration, xSize, ySize); + JxlPlane alpha = new(configuration, xSize, ySize); if (hasAlpha) { @@ -187,13 +212,27 @@ public static bool ConvertFromExternal(Configuration configuration, Memory return true; } - public static bool BufferToImageF(Configuration configuration, JxlPixelFormat pixelFormat, int xSize, int ySize, Memory buffer, JxlImageF channel) + public static bool BufferToImageF( + Configuration configuration, + JxlPixelFormat pixelFormat, + int xSize, + int ySize, + Memory buffer, + JxlPlane channel) { int bitDepth = JxlDataTypeBytes(pixelFormat.DataType) * 8; return ConvertFromExternal(configuration, buffer, xSize, ySize, bitDepth, pixelFormat, 0, channel); } - public static bool BufferToImageBundle(Configuration configuration, JxlPixelFormat pixelFormat, int xSize, int ySize, Memory buffer, JxlColorEncoding currentColorEncoding, JxlImageBundle imageBundle, bool setAlpha) + public static bool BufferToImageBundle( + Configuration configuration, + JxlPixelFormat pixelFormat, + int xSize, + int ySize, + Memory buffer, + JxlColorEncoding currentColorEncoding, + JxlImageBundle imageBundle, + bool setAlpha) { int bitDepth = JxlDataTypeBytes(pixelFormat.DataType) * 8; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFrameEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFrameEncoder.cs index 223b21d4f3..8a1c6440ee 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFrameEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFrameEncoder.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Memory; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; @@ -40,7 +40,7 @@ public static uint GetGroupSizeShift(int width, int height, JxlCompressParameter #pragma warning restore format } - public static unsafe void SimplifyInvisible(JxlImage3F image, JxlImageF alpha, bool lossless) + public static unsafe void SimplifyInvisible(JxlImage3 image, JxlPlane alpha, bool lossless) { for (int c = 0; c < 3; c++) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGaborish.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGaborish.cs index 59faeb18a4..45f9fb8ee5 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGaborish.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGaborish.cs @@ -2,7 +2,7 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; @@ -17,7 +17,7 @@ internal static class JxlGaborish -0.09495815671340026f, -0.041031725066768575f, 0.013710004822696948f, 0.006510206083837737f, -0.0014789063378272242f]; - public static void InverseGaborish(Configuration configuration, JxlImage3F inOut, Rectangle rect, InlineArray3 mul) + public static void InverseGaborish(Configuration configuration, JxlImage3 inOut, Rectangle rect, InlineArray3 mul) { InlineArray3 weights = default; @@ -40,7 +40,7 @@ public static void InverseGaborish(Configuration configuration, JxlImage3F inOut }; } - using JxlImageF temp = new(configuration, inOut.Plane(2).XSize, inOut.Plane(2).YSize); + using JxlPlane temp = new(configuration, inOut.Plane(2).XSize, inOut.Plane(2).YSize); JxlImageOperations.ForceCopyImage(inOut.Plane(2), temp); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs index 7f8adc1a17..66535185c0 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs @@ -6,7 +6,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; @@ -498,7 +498,13 @@ process at 64 lanes at a time. } } - public static void ComputeCoefficients(Configuration configuration, int groupIndex, JxlPassesEncoderState encoderState, JxlImage3F opsin, Rectangle rectangle, JxlImage3F dc) + public static void ComputeCoefficients( + Configuration configuration, + int groupIndex, + JxlPassesEncoderState encoderState, + JxlImage3 opsin, + Rectangle rectangle, + JxlImage3 dc) { Rectangle blockGroupRect = encoderState.Shared.FrameDimensions.BlockGroupRect(groupIndex); Rectangle cmapRect = new( @@ -514,7 +520,7 @@ public static void ComputeCoefficients(Configuration configuration, int groupInd int dcStride = dc.PixelsPerRow; int opsinStride = opsin.PixelsPerRow; - JxlImageI fullQuantField = encoderState.RawQuantField; + JxlPlane fullQuantField = encoderState.RawQuantField; JxlCompressParameters cparams = encoderState.CompressParameters; int dctScratchSize = 3 * (JxlSimdUtils.MaxVectorSize / sizeof(float)) * JxlAcStrategy.MaximumBlockDimension; @@ -698,7 +704,13 @@ private static void NegMulAdd(Span a, Span b, Span c, Span< TensorPrimitives.Subtract(c, d, d); } - public static bool EncodeGroupTokenizedCoefficients(int groupIndex, int passIndex, int histogramIndex, JxlPassesEncoderState encState, JxlBitWriter writer, JxlAuxiliaryOutput auxOut) + public static bool EncodeGroupTokenizedCoefficients( + int groupIndex, + int passIndex, + int histogramIndex, + JxlPassesEncoderState encState, + JxlBitWriter writer, + JxlAuxiliaryOutput auxOut) { int numHistograms = encState.Shared.HistogramCount; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlImageBundleEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlImageBundleEncoder.cs index 067ef0bee7..7dec72c4a9 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlImageBundleEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlImageBundleEncoder.cs @@ -3,7 +3,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Cms; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; @@ -14,12 +14,12 @@ public static bool TryApplyColorTransform( Configuration configuration, JxlColorEncoding currentEncoding, float intensityTarget, - JxlImage3F color, - JxlImageF? black, + JxlImage3 color, + JxlPlane? black, Rectangle rect, JxlColorEncoding desiredEncoding, JxlCmsInterface cms, - ref JxlImage3F output) + ref JxlImage3 output) { if (currentEncoding.IsGray != desiredEncoding.IsGray) { @@ -53,7 +53,7 @@ public static bool TryApplyColorTransform( // We can't use a ref variable inside of a lambda expression, // so we store its value in a variable. The output variable // will not be assigned. - JxlImage3F outValue = output; + JxlImage3 outValue = output; _ = Parallel.For(0, rect.Height, configuration.GetParallelOptions(), y => { @@ -134,7 +134,7 @@ public static bool CopyToT( Rectangle rectangle, JxlColorEncoding desiredEncoding, JxlCmsInterface cms, - JxlImage3F output) + JxlImage3 output) => TryApplyColorTransform( configuration, ib.CurrentColorEncoding!, @@ -160,7 +160,7 @@ public static bool TransformIfNeeded( return true; } - using JxlImage3F color = new(configuration, input.Color!.XSize, input.Color.YSize); + using JxlImage3 color = new(configuration, input.Color!.XSize, input.Color.YSize); if (!JxlImageOperations.CopyImage(input.Color!, color)) { @@ -174,11 +174,11 @@ public static bool TransformIfNeeded( if (input.ContainsExtraChannels()) { - List extraChannels = []; + List> extraChannels = []; - foreach (JxlImageF extraChannel in input.EnumerateExtraChannels()) + foreach (JxlPlane extraChannel in input.EnumerateExtraChannels()) { - JxlImageF ec = new(configuration, extraChannel.XSize, extraChannel.YSize); + JxlPlane ec = new(configuration, extraChannel.XSize, extraChannel.YSize); if (!JxlImageOperations.CopyImage(extraChannel, ec)) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlXybEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlXybEncoder.cs index 0005d9df2a..41f8f25c30 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlXybEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlXybEncoder.cs @@ -8,7 +8,7 @@ using System.Runtime.Intrinsics; using SixLabors.ImageSharp.Formats.Jxl.Cms; using SixLabors.ImageSharp.Formats.Jxl.Cms.TransferFunctions; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; @@ -225,7 +225,7 @@ private static Vector LinearFromSRgb(Vector encoded) private static void LinearSrgbToXyb( Configuration configuration, Memory premulAbsorb, - JxlImage3F image) + JxlImage3 image) { int xSize = image.XSize; @@ -263,7 +263,7 @@ void ProcessRow(int y, ReadOnlySpan premulAbsorb) private static void SRgbToXyb( Configuration configuration, Memory premulAbsorb, - JxlImage3F image) + JxlImage3 image) { int xSize = image.XSize; @@ -301,8 +301,8 @@ void ProcessRow(int y, ReadOnlySpan premulAbsorb) private static void SrgbToXybAndLinear( Configuration configuration, Memory premulAbsorb, - JxlImage3F image, - JxlImage3F linear) + JxlImage3 image, + JxlImage3 linear) { int xSize = image.XSize; @@ -373,10 +373,10 @@ public static bool ToXyb( Configuration configuration, JxlColorEncoding currentEncoding, float intensityTarget, - JxlImageF? black, - JxlImage3F image, + JxlPlane? black, + JxlImage3 image, JxlCmsInterface cms, - JxlImage3F? linear) + JxlImage3? linear) { if (black is not null) { @@ -449,12 +449,12 @@ public static bool ToXyb( public static void RgbToYcbcr( Configuration configuration, - JxlImageF rPlane, - JxlImageF gPlane, - JxlImageF bPlane, - JxlImageF yPlane, - JxlImageF cbPlane, - JxlImageF crPlane) + JxlPlane rPlane, + JxlPlane gPlane, + JxlPlane bPlane, + JxlPlane yPlane, + JxlPlane cbPlane, + JxlPlane crPlane) { int xSize = rPlane.XSize; int ySize = rPlane.YSize; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseEncoder.cs index aa1a7f975f..2b9367768d 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseEncoder.cs @@ -4,7 +4,7 @@ using System.Buffers; using System.Numerics.Tensors; using System.Runtime.InteropServices; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Optimize; using SixLabors.ImageSharp.Formats.Jxl.Processing.Noise; @@ -24,7 +24,7 @@ internal static class JxlNoiseEncoder [-0.25f, -1.0f, -0.25f], ]; - public static float GetScoreSumsOfAbsoluteDifferences(JxlImage3F opsin, int x, int y, int blockSize) + public static float GetScoreSumsOfAbsoluteDifferences(JxlImage3 opsin, int x, int y, int blockSize) { const int smallBlockSizeX = 3; const int smallBlockSizeY = 4; @@ -78,7 +78,7 @@ public static float GetScoreSumsOfAbsoluteDifferences(JxlImage3F opsin, int x, i public static IMemoryOwner GetSadScoresForPatches( Configuration configuration, - JxlImage3F opsin, + JxlImage3 opsin, int blockSize, int binCount, JxlNoiseHistogram sadHistogram) @@ -100,7 +100,11 @@ public static IMemoryOwner GetSadScoresForPatches( return sadScores; } - public static void OptimizeNoiseParameters(Configuration configuration, List noiseLevel, JxlNoiseParameters noiseParameters, float multiplier) + public static void OptimizeNoiseParameters( + Configuration configuration, + List noiseLevel, + JxlNoiseParameters noiseParameters, + float multiplier) { const double maxError = 1e-3; const double precision = 1e-8; @@ -152,7 +156,7 @@ public static void OptimizeNoiseParameters(Configuration configuration, List GetNoiseLevel( - JxlImage3F opsin, + JxlImage3 opsin, Span textureStrength, float threshold, int blockSize) @@ -278,7 +282,7 @@ public static void EncodeFloatParameter(float value, float precision, JxlBitWrit writer.Write(10, absValQuant); } - public static bool GetNoiseParameter(Configuration configuration, JxlImage3F opsin, JxlNoiseParameters noiseParams, float qualityCoefficient) + public static bool GetNoiseParameter(Configuration configuration, JxlImage3 opsin, JxlNoiseParameters noiseParams, float qualityCoefficient) { const int blockSize = 8; const int binCount = 256; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs index 487bce74cb..f3f1388fb8 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs @@ -5,7 +5,7 @@ using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Image; @@ -18,7 +18,7 @@ internal sealed class JxlImageBundle /// /// Image data for additional channels. /// - private List extraChannels = []; + private List> extraChannels = []; /// /// Initializes a new instance of the class. @@ -137,7 +137,7 @@ public JxlImageBundle() /// /// Gets the color data. /// - public JxlImage3F? Color { get; private set; } + public JxlImage3? Color { get; private set; } /// /// Gets a value indicating whether the color data is present and usable. @@ -189,7 +189,7 @@ public int YSize /// /// Gets the black extra channel. /// - public JxlImageF? Black + public JxlPlane? Black { get { @@ -206,7 +206,7 @@ public JxlImageF? Black /// /// Gets the alpha extra channel. /// - public JxlImageF? Alpha + public JxlPlane? Alpha { get { @@ -255,7 +255,7 @@ public bool VerifySizes() return false; } - foreach (JxlImageF ec in this.extraChannels) + foreach (JxlPlane ec in this.extraChannels) { if (ec.XSize != xs || ec.YSize != ys) { @@ -279,7 +279,7 @@ public bool VerifySizes() /// /// Output color data. /// True if color isn't null. - public bool TryGetColor(out JxlImage3F? color) + public bool TryGetColor(out JxlImage3? color) { color = null; if (this.Color is not null) @@ -293,7 +293,7 @@ public bool TryGetColor(out JxlImage3F? color) /// /// Removes the color data, replacing it with a new Image3F with 0 as width and height. /// - public void RemoveColor() => this.Color = new JxlImage3F(); + public void RemoveColor() => this.Color = new JxlImage3(); /// /// Removes all extra channels, if any. @@ -310,15 +310,15 @@ public bool TryGetColor(out JxlImage3F? color) /// Returns an enumerable for extra channels. /// /// Extra channels enumerable. - public IEnumerable EnumerateExtraChannels() => this.extraChannels; + public IEnumerable> EnumerateExtraChannels() => this.extraChannels; /// /// Adds a specified extra channel. /// /// The extra channel to be added to the image bundle. - public void AddExtraChannel(JxlImageF channel) => this.extraChannels.Add(channel); + public void AddExtraChannel(JxlPlane channel) => this.extraChannels.Add(channel); - public JxlImageF GetExtraChannel(int i) => this.extraChannels[i]; + public JxlPlane GetExtraChannel(int i) => this.extraChannels[i]; /// /// Sets the extra channels. @@ -328,9 +328,9 @@ public bool TryGetColor(out JxlImage3F? color) /// True if each plane had width and height greater than 0 and sizes are correct /// after changing the extra channels; false otherwise. /// - public bool TrySetExtraChannels(List extraChannels) + public bool TrySetExtraChannels(List> extraChannels) { - foreach (JxlImageF plane in extraChannels) + foreach (JxlPlane plane in extraChannels) { if (plane.XSize == 0 || plane.YSize == 0) { @@ -349,7 +349,7 @@ public bool TrySetExtraChannels(List extraChannels) /// The alpha channel to set. /// True if it was set successfully; false otherwise. /// Thrown if the corresponding extra channel has incorrect info. - public bool TrySetAlpha(JxlImageF alpha) + public bool TrySetAlpha(JxlPlane alpha) { if (this.Metadata is null) { @@ -399,7 +399,7 @@ public bool VerifyMetadata() if (this.Metadata?.HasAlpha == true) { - JxlImageF? img = this.Alpha; + JxlPlane? img = this.Alpha; if (img?.XSize == 0) { throw new InvalidOperationException("Alpha should not have width equal to 0"); @@ -422,7 +422,7 @@ public bool VerifyMetadata() /// Color data. /// Current color encoding. /// True if setting the image succeeded; otherwise false. - public bool SetFromImage(JxlImage3F color, JxlColorEncoding current) + public bool SetFromImage(JxlImage3 color, JxlColorEncoding current) { if (color.XSize == 0 || color.YSize == 0) { @@ -463,7 +463,7 @@ public bool ShrinkTo(int width, int height) } } - foreach (JxlImageF extraChannel in this.extraChannels) + foreach (JxlPlane extraChannel in this.extraChannels) { if (!extraChannel.ShrinkTo(width, height)) { @@ -492,7 +492,7 @@ public JxlImageBundle Copy(Configuration configuration) if (this.Color is not null) { - copy.Color = new JxlImage3F(configuration, this.Color.XSize, this.Color.YSize); + copy.Color = new JxlImage3(configuration, this.Color.XSize, this.Color.YSize); } copy.CurrentColorEncoding = this.CurrentColorEncoding; @@ -500,9 +500,9 @@ public JxlImageBundle Copy(Configuration configuration) copy.ColorTransform = this.ColorTransform; copy.ChromaSubsampling = this.ChromaSubsampling; - foreach (JxlImageF plane in this.extraChannels) + foreach (JxlPlane plane in this.extraChannels) { - JxlImageF ec = new(configuration, plane.XSize, plane.YSize); + JxlPlane ec = new(configuration, plane.XSize, plane.YSize); if (!JxlImageOperations.CopyImage(plane, ec)) { throw new InvalidOperationException("Cannot copy extra channel"); @@ -519,7 +519,7 @@ public bool CopyTo( Rectangle rectangle, JxlColorEncoding desiredEncoding, JxlCmsInterface cms, - JxlImage3F output) + JxlImage3 output) => JxlImageBundleEncoder.CopyToT( configuration, this.Metadata!, diff --git a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs index 60d9b56fba..8114bd6bde 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs @@ -4,7 +4,6 @@ using System.Numerics; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Memory; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Image; @@ -18,10 +17,14 @@ internal static class JxlImageOperations /// /// Returns true if first image has same width and height as the second image. /// + /// The sample type of the first plane. + /// The sample type of the second plane. /// First image /// Second image /// True if width and height is equal. - public static bool SameSize(JxlPlaneBase a, JxlPlaneBase b) => a.XSize == b.XSize && a.YSize == b.YSize; + public static bool SameSize(JxlPlane a, JxlPlane b) + where TFrom : unmanaged + where TTo : unmanaged => a.XSize == b.XSize && a.YSize == b.YSize; /// /// Returns true if first image has same width and height as the second image. @@ -58,7 +61,7 @@ public static bool CopyImage(JxlPlane from, JxlPlane to) { Span rowFrom = from.GetRow(y); Span rowTo = to.GetRow(y); - rowFrom.CopyTo(rowTo); + rowFrom[..from.XSize].CopyTo(rowTo); } return true; @@ -108,7 +111,7 @@ public static bool CopyImage(JxlImage3 from, JxlImage3 to) { Span rowFrom = from.PlaneRow(c, y); Span rowTo = to.PlaneRow(c, y); - rowFrom.CopyTo(rowTo); + rowFrom[..from.XSize].CopyTo(rowTo); } } @@ -148,7 +151,7 @@ public static void ForceCopyImage(JxlImage3 from, JxlImage3 to) public static bool CopyImageTo(Rectangle rectFrom, JxlPlane from, Rectangle rectTo, JxlPlane to) where T : unmanaged { - if (rectFrom != rectTo) + if (rectFrom.Size != rectTo.Size) { return false; } @@ -172,7 +175,7 @@ public static bool CopyImageTo(Rectangle rectFrom, JxlPlane from, Rectangl { Span rowFrom = from.GetRow(rectFrom, y); Span rowTo = to.GetRow(rectTo, y); - rowFrom.CopyTo(rowTo); + rowFrom[..rectFrom.Width].CopyTo(rowTo); } return true; @@ -196,7 +199,7 @@ public static bool CopyImageTo(Rectangle rectFrom, JxlPlane from, Rectangl public static bool CopyImageTo(Rectangle rectFrom, JxlImage3 from, Rectangle rectTo, JxlImage3 to) where T : unmanaged { - if (rectFrom != rectTo) + if (rectFrom.Size != rectTo.Size) { return false; } @@ -230,7 +233,7 @@ public static bool ConvertPlaneAndClamp(Rectangle rectFrom, JxlPlane where TFrom : unmanaged, INumber where TTo : unmanaged, INumber { - if (rectFrom != rectTo) + if (rectFrom.Size != rectTo.Size) { return false; } @@ -392,7 +395,7 @@ public static void FillImage(T value, JxlPlane image) for (int y = 0; y < image.YSize; y++) { Span row = image.GetRow(y); - row.Fill(value); + row[..image.XSize].Fill(value); } } @@ -425,7 +428,7 @@ public static void ZeroFillImage(JxlPlane image) for (int y = 0; y < image.YSize; y++) { - image.GetRow(y).Clear(); + image.GetRow(y)[..image.XSize].Clear(); } } @@ -495,7 +498,7 @@ public static void FillPlane(T value, JxlPlane image, Rectangle rect) for (int y = 0; y < rect.Height; y++) { Span row = image.GetRow(rect, y); - row.Fill(value); + row[..rect.Width].Fill(value); } } @@ -557,9 +560,9 @@ private static bool DownsampleImageCore(JxlPlane input, int factor, JxlPl /// The image to downsample. /// Downsampling factor. /// A new downsampled image. It can later be disposed. - public static JxlImageF? DownsampleImage(Configuration configuration, JxlImageF image, int factor) + public static JxlPlane? DownsampleImage(Configuration configuration, JxlPlane image, int factor) { - JxlImageF downsampled = new( + JxlPlane downsampled = new( configuration, JxlMath.DivCeil(image.XSize, factor) + JxlFrameDimensions.BlockDimensions, JxlMath.DivCeil(image.YSize, factor) + JxlFrameDimensions.BlockDimensions); @@ -580,14 +583,14 @@ private static bool DownsampleImageCore(JxlPlane input, int factor, JxlPl /// The image to downsample. /// Downsampling factor. /// A new downsampled image. It can later be disposed. - public static JxlImage3F? DownsampleImage(Configuration configuration, JxlImage3F opsin, int factor) + public static JxlImage3? DownsampleImage(Configuration configuration, JxlImage3 opsin, int factor) { if (factor == 1) { return null; } - JxlImage3F downsampled = new( + JxlImage3 downsampled = new( configuration, JxlMath.DivCeil(opsin.XSize, factor) + JxlFrameDimensions.BlockDimensions, JxlMath.DivCeil(opsin.YSize, factor) + JxlFrameDimensions.BlockDimensions); @@ -629,10 +632,10 @@ public static bool PadImageToBlockMultipleInPlace(JxlImage3 input, int bl for (int y = 0; y < ySizeOriginal; y++) { Span row = input.PlaneRow(plane, y); - row[xSizeOriginal..].Fill(row[xSizeOriginal - 1]); + row.Slice(xSizeOriginal, xSize - xSizeOriginal).Fill(row[xSizeOriginal - 1]); } - Span sourceRow = input.PlaneRow(plane, ySizeOriginal - 1); + Span sourceRow = input.PlaneRow(plane, ySizeOriginal - 1)[..xSize]; for (int y = ySizeOriginal; y < ySize; y++) { sourceRow.CopyTo(input.PlaneRow(plane, y)); diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelationMap.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelationMap.cs index 9ce8e2e86a..0fb3e1112b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelationMap.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelationMap.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; @@ -13,9 +13,9 @@ internal sealed class JxlColorCorrelationMap { public JxlColorCorrelation Base { get; set; } = new(); - public JxlImageSB? YToXMap { get; set; } + public JxlPlane? YToXMap { get; set; } - public JxlImageSB? YToBMap { get; set; } + public JxlPlane? YToBMap { get; set; } public bool DecodeDc(JxlBitReader reader) => this.Base.DecodeDc(reader); @@ -25,8 +25,8 @@ public static JxlColorCorrelationMap Create(Configuration configuration, int wid (int xBlocks, int yBlocks) = (JxlMath.DivCeil(width, JxlChromaFromLuma.ColorTileDimension), JxlMath.DivCeil(height, JxlChromaFromLuma.ColorTileDimension)); - map.YToXMap = new JxlImageSB(configuration, xBlocks, yBlocks); - map.YToBMap = new JxlImageSB(configuration, xBlocks, yBlocks); + map.YToXMap = new JxlPlane(configuration, xBlocks, yBlocks); + map.YToBMap = new JxlPlane(configuration, xBlocks, yBlocks); map.YToXMap.Clear(); map.YToBMap.Clear(); diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlCompressedDc.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlCompressedDc.cs index 3bede20ba8..117cb6ad33 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlCompressedDc.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlCompressedDc.cs @@ -5,7 +5,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; @@ -57,7 +57,15 @@ private static unsafe void ComputePixelChannelPacked(float dcFactor, ReadOnlySpa } } - private static void ComputePixelChannel(float dcFactor, ReadOnlySpan rowTop, ReadOnlySpan row, ReadOnlySpan rowBottom, out float mc, out float sm, ref float gap, int x) + private static void ComputePixelChannel( + float dcFactor, + ReadOnlySpan rowTop, + ReadOnlySpan row, + ReadOnlySpan rowBottom, + out float mc, + out float sm, + ref float gap, + int x) { float tl = rowTop[x - 1]; float tc = rowTop[x]; @@ -143,7 +151,7 @@ private static void ComputePixelScalar( outRows2[x] = ((smB - mcB) * factor) + mcB; } - public static bool AdaptiveDCSmoothing(Configuration configuration, InlineArray3 dcFactors, JxlImage3F dc) + public static bool AdaptiveDCSmoothing(Configuration configuration, InlineArray3 dcFactors, JxlImage3 dc) { int xSize = dc.XSize; int ySize = dc.YSize; @@ -154,7 +162,7 @@ public static bool AdaptiveDCSmoothing(Configuration configuration, InlineArray3 } // Don't dispose. - JxlImage3F smoothed = new(configuration, xSize, ySize); + JxlImage3 smoothed = new(configuration, xSize, ySize); // Fill borders that the loop below will not. // First and last rows are unused by the processing loop. @@ -217,7 +225,16 @@ public static bool AdaptiveDCSmoothing(Configuration configuration, InlineArray3 return true; } - public static void DequantDC(Rectangle r, JxlImage3F dc, JxlImageB quantDc, JxlModularImage input, InlineArray3 dcFactors, float mul, InlineArray3 cflFactors, JxlYCbCrChromaSubsampling chromaSubsampling, JxlBlockContextMap bctx) + public static void DequantDC( + Rectangle r, + JxlImage3 dc, + JxlPlane quantDc, + JxlModularImage input, + InlineArray3 dcFactors, + float mul, + InlineArray3 cflFactors, + JxlYCbCrChromaSubsampling chromaSubsampling, + JxlBlockContextMap bctx) { if (chromaSubsampling.Is444) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs index 8e60721a6f..99307cbc8c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs @@ -6,7 +6,6 @@ using System.Runtime.InteropServices; using System.Runtime.Intrinsics; using SixLabors.ImageSharp.Formats.Jxl.Memory; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; @@ -20,7 +19,7 @@ internal static class JxlConvolve /// Weighted sum of 1x5 pixels around ix, iy with [wx2, wx1, wx0, wx1, wx2]. /// public static float WeightedSumBorder( - JxlImageF input, + JxlPlane input, Func wrapY, long ix, long iy, @@ -46,7 +45,7 @@ public static float WeightedSumBorder( } public static Vector WeightedSum( - JxlImageF input, + JxlPlane input, Func wrapY, int ix, long iy, @@ -71,7 +70,7 @@ public static Vector WeightedSum( return sum2 + (sum1 + sum0); } - public static float Symmetric5Border(JxlImageF input, Func wrapY, long ix, long iy, ref JxlWeightsSymmetric5 weights) + public static float Symmetric5Border(JxlPlane input, Func wrapY, long ix, long iy, ref JxlWeightsSymmetric5 weights) { float w0 = weights.GetCVector()[0]; float w1 = weights.GetRVector()[0]; @@ -95,7 +94,7 @@ public static float Symmetric5Border(JxlImageF input, Func wrap } public static void Symmetric5Interior( - JxlImageF image, + JxlPlane image, int ix, Func wrapY, int rix, @@ -123,7 +122,7 @@ public static void Symmetric5Interior( } public static void Symmetric5Row( - JxlImageF image, + JxlPlane image, Func wrapY, in Rectangle rect, long iy, @@ -187,7 +186,7 @@ public static bool Symmetric5( } public static float SlowSymmetric3Pixel( - JxlImageF image, + JxlPlane image, int x, int y, int width, @@ -220,7 +219,7 @@ public static float SlowSymmetric3Pixel( } public static void SlowSymmetric3Row( - JxlImageF image, + JxlPlane image, int y, int width, int height, @@ -303,7 +302,7 @@ public static void SlowSymmetric3( } public static float SlowSeparablePixel( - JxlImageF image, + JxlPlane image, Rectangle rect, int x, int y, @@ -334,10 +333,10 @@ public static float SlowSeparablePixel( } public static void SlowSeparable( - JxlImageF input, + JxlPlane input, Rectangle inputRect, JxlWeightsSeparable5 weights, - JxlImageF output, + JxlPlane output, Rectangle outputRect, int radius) { @@ -363,10 +362,10 @@ public static void SlowSeparable( } public static void SlowSeparable5( - JxlImageF input, + JxlPlane input, Rectangle inputRect, JxlWeightsSeparable5 weights, - JxlImageF output, + JxlPlane output, Rectangle outputRect) => SlowSeparable(input, inputRect, weights, output, outputRect, 2); diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs index 7e6a585146..8c56b240c6 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs @@ -5,7 +5,7 @@ using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Patch; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; @@ -29,9 +29,9 @@ internal class JxlPassesSharedState public JxlQuantizer Quantizer { get; set; } - public JxlImageI RawQuantField { get; set; } + public JxlPlane RawQuantField { get; set; } - public JxlImageB EpfSharpness { get; set; } + public JxlPlane EpfSharpness { get; set; } public JxlColorCorrelationMap ColorMap { get; set; } @@ -41,13 +41,13 @@ internal class JxlPassesSharedState public List CoeffOrders { get; set; } = []; - public JxlImageB QuantDc { get; set; } + public JxlPlane QuantDc { get; set; } - public JxlImage3F DcStorage { get; set; } + public JxlImage3 DcStorage { get; set; } - public JxlImage3F Dc { get; set; } + public JxlImage3 Dc { get; set; } - public JxlImage3F[] DcFrames { get; set; } = new JxlImage3F[4]; + public JxlImage3[] DcFrames { get; set; } = new JxlImage3[4]; public JxlReferenceFrame[] ReferenceFrames { get; set; } = new JxlReferenceFrame[4]; @@ -67,8 +67,8 @@ public JxlPassesSharedState(Configuration configuration, JxlFrameHeader frameHea JxlFrameDimensions dimensions = frameHeader.FrameDimensions; this.AcStrategy = JxlAcStrategyImage.Create(configuration, dimensions.XSizeBlocks, dimensions.YSizeBlocks); - this.RawQuantField = new JxlImageI(configuration, dimensions.XSizeBlocks, dimensions.YSizeBlocks); - this.EpfSharpness = new JxlImageB(configuration, dimensions.XSizeBlocks, dimensions.YSizeBlocks); + this.RawQuantField = new JxlPlane(configuration, dimensions.XSizeBlocks, dimensions.YSizeBlocks); + this.EpfSharpness = new JxlPlane(configuration, dimensions.XSizeBlocks, dimensions.YSizeBlocks); this.ColorMap = JxlColorCorrelationMap.Create(configuration, dimensions.XSize, dimensions.YSize); this.CoeffOrderSize = JxlCoefficientOrder.CoefficientOrderMaxSize; @@ -88,7 +88,7 @@ public JxlPassesSharedState(Configuration configuration, JxlFrameHeader frameHea } } - this.QuantDc = new JxlImageB(configuration, dimensions.XSizeBlocks, dimensions.YSizeBlocks); + this.QuantDc = new JxlPlane(configuration, dimensions.XSizeBlocks, dimensions.YSizeBlocks); bool useDcFrame = (frameHeader.Flags & (ulong)JxlFrameHeaderFlags.Dc) != 0; if (!encoder && useDcFrame) @@ -98,7 +98,7 @@ public JxlPassesSharedState(Configuration configuration, JxlFrameHeader frameHea throw new InvalidOperationException("DC level for DC frames cannot be equal to 4"); } - this.DcStorage = new JxlImage3F(); + this.DcStorage = new JxlImage3(); this.Dc = this.DcFrames[(int)frameHeader.DcLevel]; if (this.Dc.XSize == 0) @@ -110,7 +110,7 @@ public JxlPassesSharedState(Configuration configuration, JxlFrameHeader frameHea } else { - this.DcStorage = new JxlImage3F(configuration, dimensions.XSizeBlocks, dimensions.YSizeBlocks); + this.DcStorage = new JxlImage3(configuration, dimensions.XSizeBlocks, dimensions.YSizeBlocks); this.Dc = this.DcStorage; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularChannel.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularChannel.cs index 4d227853bb..6dccb4e9f6 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularChannel.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularChannel.cs @@ -1,12 +1,12 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Memory; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular; /// -/// A wrapper over for modular operations. +/// A wrapper over for modular operations. /// internal sealed class JxlModularChannel : IDisposable { @@ -16,7 +16,7 @@ public JxlModularChannel(Configuration configuration, int width, int height, int this.VerticalShift = vertShift; this.Width = width; this.Height = height; - this.Plane = new JxlImageI(configuration, width, height); + this.Plane = new JxlPlane(configuration, width, height); } /// @@ -53,7 +53,7 @@ public JxlModularChannel(Configuration configuration, int width, int height, int /// /// Gets or sets the backing plane buffer. /// - public JxlImageI Plane { get; set; } + public JxlPlane Plane { get; set; } public void Shrink(Configuration configuration) { @@ -63,7 +63,7 @@ public void Shrink(Configuration configuration) } this.Plane.Dispose(); - this.Plane = new JxlImageI(configuration, this.Width, this.Height); + this.Plane = new JxlPlane(configuration, this.Width, this.Height); } public void Shrink(Configuration configuration, int newWidth, int newHeight) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs index 4e306c1f40..8b0ed40cb9 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs @@ -3,7 +3,7 @@ using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Common.Helpers; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; @@ -79,7 +79,7 @@ internal static class JxlPalette private static readonly DenseMatrix DefaultOffsets = new(DefaultOffsetsData); /// - /// Static delta palette used by . + /// Static delta palette used by . /// private static readonly int[][] DeltaPalette = [ @@ -222,7 +222,14 @@ public static void MetaPalette(Configuration configuration, JxlModularImage inpu /// For weighted prediction. /// Thrown when the input for prediction is invalid. /// Thrown when there are too many channels. - public static void InversePalette(Configuration configuration, JxlModularImage input, int beginC, int nbColors, int nbDeltas, JxlPredictor predictor, JxlModularHeader weightedHeader) + public static void InversePalette( + Configuration configuration, + JxlModularImage input, + int beginC, + int nbColors, + int nbDeltas, + JxlPredictor predictor, + JxlModularHeader weightedHeader) { if (input.MetaChannels < 1) { @@ -376,8 +383,8 @@ public static void InversePalette(Configuration configuration, JxlModularImage i } else { - JxlImageI plane = input.Channels[c0].Plane; - JxlImageI indices = new(configuration, plane.XSize, plane.YSize); + JxlPlane plane = input.Channels[c0].Plane; + JxlPlane indices = new(configuration, plane.XSize, plane.YSize); input.Channels[c0].Plane = indices; if (predictor == JxlPredictor.Weighted) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs index 68ff16318f..e5edc6fdf8 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs @@ -5,7 +5,6 @@ using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.Memory; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; @@ -293,7 +292,7 @@ public void ComputeGlobalScaleAndQuant(float quantDc, float quantMedian, float q /// /// Quantizes the specified rectangular selection. /// - public void SetQuantFieldRect(JxlImageF qf, in Rectangle rect, JxlImageI rawQuantField) + public void SetQuantFieldRect(JxlPlane qf, in Rectangle rect, JxlPlane rawQuantField) { for (int y = 0; y < rect.Height; y++) { @@ -312,7 +311,7 @@ public void SetQuantFieldRect(JxlImageF qf, in Rectangle rect, JxlImageI rawQuan /// /// Set the quant field. /// - public bool SetQuantField(Configuration configuration, float quantDc, JxlImageF qf, JxlImageI? rawQuantField) + public bool SetQuantField(Configuration configuration, float quantDc, JxlPlane qf, JxlPlane? rawQuantField) { IMemoryOwner data = configuration.MemoryAllocator.Allocate(qf.XSize * qf.YSize); Span dataSpan = data.Memory.Span; @@ -363,7 +362,7 @@ public bool SetQuantField(Configuration configuration, float quantDc, JxlImageF return true; } - public void SetQuant(float quantDc, float quantAc, JxlImageI rawQuantField) + public void SetQuant(float quantDc, float quantAc, JxlPlane rawQuantField) { this.ComputeGlobalScaleAndQuant(quantDc, quantAc, 0); diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf0Stage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf0Stage.cs index a7bcafcc3d..49ae18ffd6 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf0Stage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf0Stage.cs @@ -3,7 +3,7 @@ using System.Runtime.CompilerServices; using System.Runtime.Intrinsics; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using SixLabors.ImageSharp.Memory; @@ -27,9 +27,9 @@ internal sealed class Epf0Stage : RenderPipelineStageBase ]; private readonly JxlLoopFilter loopFilter; - private readonly JxlImageF sigma; + private readonly JxlPlane sigma; - public Epf0Stage(JxlLoopFilter loopFilter, JxlImageF sigma, Configuration configuration) : base(configuration) + public Epf0Stage(JxlLoopFilter loopFilter, JxlPlane sigma, Configuration configuration) : base(configuration) { this.loopFilter = loopFilter; this.sigma = sigma; diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf1Stage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf1Stage.cs index 5dc19b6626..b6fbac0408 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf1Stage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf1Stage.cs @@ -3,7 +3,7 @@ using System.Runtime.CompilerServices; using System.Runtime.Intrinsics; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using SixLabors.ImageSharp.Memory; @@ -16,9 +16,9 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; internal sealed class Epf1Stage : RenderPipelineStageBase { private readonly JxlLoopFilter loopFilter; - private readonly JxlImageF sigma; + private readonly JxlPlane sigma; - public Epf1Stage(Configuration configuration, JxlLoopFilter loopFilter, JxlImageF sigma) + public Epf1Stage(Configuration configuration, JxlLoopFilter loopFilter, JxlPlane sigma) : base(configuration) { this.loopFilter = loopFilter; diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf2Stage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf2Stage.cs index 7100df484c..d64047d06b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf2Stage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf2Stage.cs @@ -3,7 +3,7 @@ using System.Runtime.CompilerServices; using System.Runtime.Intrinsics; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using SixLabors.ImageSharp.Memory; @@ -16,9 +16,9 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; internal sealed class Epf2Stage : RenderPipelineStageBase { private readonly JxlLoopFilter loopFilter; - private readonly JxlImageF sigma; + private readonly JxlPlane sigma; - public Epf2Stage(JxlLoopFilter loopFilter, JxlImageF sigma, Configuration configuration) + public Epf2Stage(JxlLoopFilter loopFilter, JxlPlane sigma, Configuration configuration) : base(configuration) { this.loopFilter = loopFilter; diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToImage3FStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToImage3FStage.cs index a7ededa877..6d50f8531f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToImage3FStage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToImage3FStage.cs @@ -1,12 +1,12 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; -internal class WriteToImage3FStage(Configuration configuration, JxlImage3F image) +internal class WriteToImage3FStage(Configuration configuration, JxlImage3 image) : RenderPipelineStageBase(configuration) { public override string Name => "WriteI3F"; diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToImageBundleStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToImageBundleStage.cs index 7d4d163ad8..6b9c9337f4 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToImageBundleStage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToImageBundleStage.cs @@ -2,7 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Jxl.Cms; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Memory; @@ -29,14 +29,14 @@ public override void SetInputSizes(Span inputSizes) } } - JxlImage3F tmp = new(this.GetConfiguration(), inputSizes[0].Width, inputSizes[0].Height); + JxlImage3 tmp = new(this.GetConfiguration(), inputSizes[0].Width, inputSizes[0].Height); if (!bundle.SetFromImage(tmp, colorEncoding)) { throw new InvalidOperationException("Could not set the image of an image bundle"); } - foreach (JxlImageF ec in bundle.EnumerateExtraChannels()) + foreach (JxlPlane ec in bundle.EnumerateExtraChannels()) { // We don't want a memory leak here, as we're // removing old extra channels and inserting @@ -48,7 +48,7 @@ public override void SetInputSizes(Span inputSizes) for (int c = 3; c < inputSizes.Length; c++) { - JxlImageF ch = new(this.GetConfiguration(), inputSizes[c].Width, inputSizes[c].Height); + JxlPlane ch = new(this.GetConfiguration(), inputSizes[c].Width, inputSizes[c].Height); bundle.AddExtraChannel(ch); } } @@ -67,7 +67,7 @@ public override void ProcessRow(Buffer2D> inputRows, Buffer2D ec in bundle.EnumerateExtraChannels()) { if (ec.XSize < xPos + width) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplines.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplines.cs index c40462b8c5..52abd7c410 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplines.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplines.cs @@ -3,7 +3,7 @@ using System.Buffers; using System.Runtime.InteropServices; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; @@ -226,12 +226,12 @@ private static int AdjacentFind(Span span) return span.Length; } - public void AddTo(JxlImage3F opsin, Rectangle opsinRect) => this.Apply(add: true, opsin, opsinRect); + public void AddTo(JxlImage3 opsin, Rectangle opsinRect) => this.Apply(add: true, opsin, opsinRect); public void AddToRow(Memory rowX, Memory rowY, Memory rowB, int y, int x0, int x1) => this.ApplyToRow(add: true, rowX, rowY, rowB, y, x0, x1); - public void SubtractFrom(JxlImage3F opsin) => this.Apply(add: false, opsin, opsin.GetRectangle()); + public void SubtractFrom(JxlImage3 opsin) => this.Apply(add: false, opsin, opsin.GetRectangle()); private void ApplyToRow(bool add, Memory rowX, Memory rowY, Memory rowB, int y, int x0, int x1) { @@ -253,7 +253,7 @@ private void ApplyToRow(bool add, Memory rowX, Memory rowY, Memory MemoryMarshal.Cast(this.segmentYStart.Memory.Span)); } - private void Apply(bool add, JxlImage3F opsin, Rectangle opsinRect) + private void Apply(bool add, JxlImage3 opsin, Rectangle opsinRect) { if (this.segments.Count == 0) { diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs index 8adabd83e7..bdd85769ee 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs @@ -2,7 +2,7 @@ // Licensed under the Six Labors Split License. using Microsoft.Diagnostics.Runtime.Interop; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; @@ -20,16 +20,16 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; /// public class ButteraugliTests { - private static JxlImage3F SinglePixelImage(float r, float g, float b) + private static JxlImage3 SinglePixelImage(float r, float g, float b) { - JxlImage3F img = new(TestEnvironment.Configuration, 1, 1); + JxlImage3 img = new(TestEnvironment.Configuration, 1, 1); img.PlaneRow(0, 0)[0] = r; img.PlaneRow(1, 0)[0] = r; img.PlaneRow(2, 0)[0] = r; return img; } - private static void AddUniformNoise(JxlImage3F img, float d, ulong seed) + private static void AddUniformNoise(JxlImage3 img, float d, ulong seed) { Rng generator = new(seed); @@ -47,7 +47,7 @@ private static void AddUniformNoise(JxlImage3F img, float d, ulong seed) } } - private static void AddEdge(JxlImage3F image, float d, int x0, int y0) + private static void AddEdge(JxlImage3 image, float d, int x0, int y0) { int h = Math.Min(image.YSize - y0, 100); int w = Math.Min(image.XSize - x0, 5); @@ -66,11 +66,11 @@ private static void AddEdge(JxlImage3F image, float d, int x0, int y0) [Fact] public void TestSinglePixel() { - JxlImage3F rgb0 = SinglePixelImage(0.5f, 0.5f, 0.5f); - JxlImage3F rgb1 = SinglePixelImage(0.5f, 0.49f, 0.5f); + JxlImage3 rgb0 = SinglePixelImage(0.5f, 0.5f, 0.5f); + JxlImage3 rgb1 = SinglePixelImage(0.5f, 0.49f, 0.5f); ButteraugliParameters butteraugliParameters = new(); - JxlImageF diffmap = new(); + JxlPlane diffmap = new(); Assert.True( Butteraugli.ButteraugliInterface(TestEnvironment.Configuration, rgb0, rgb1, butteraugliParameters, diffmap, out double diffval), @@ -78,7 +78,7 @@ public void TestSinglePixel() Assert.True(new TolerantMath(0.5).AreEqual(diffval, 2.5), $"Diff value isn't even close to 2.5 (it's {diffval})"); - JxlImageF diffmap2 = new(); + JxlPlane diffmap2 = new(); Assert.True(Butteraugli.ButteraugliInterfaceInPlace( TestEnvironment.Configuration, rgb0, @@ -103,15 +103,15 @@ public void TestLargeImage() JxlTestFrame frame = img.AddFrame(); frame.RandomFill(777); - JxlImage3F rgb0 = GetColorImage(img.Ppf); - JxlImage3F rgb1 = new(TestEnvironment.Configuration, xSize, ySize); + JxlImage3 rgb0 = GetColorImage(img.Ppf); + JxlImage3 rgb1 = new(TestEnvironment.Configuration, xSize, ySize); JxlImageOperations.CopyImage(rgb0, rgb1); AddUniformNoise(rgb1, 0.02f, 7777uL); AddEdge(rgb1, 0.1f, xSize / 2, xSize / 2); ButteraugliParameters butteraugliParameters = new(); - JxlImageF diffmap = new(); + JxlPlane diffmap = new(); Assert.True( Butteraugli.ButteraugliInterface(TestEnvironment.Configuration,, rgb0, rgb1, butteraugliParameters, diffmap, out double diffval), "Couldn't initialize Butteraugli"); @@ -120,7 +120,7 @@ public void TestLargeImage() Assert.True(new TolerantMath(0.5).AreEqual(diffval, 4.0), $"Diff isn't even close to 4.0 (diffval={diffval})"); Assert.True(new TolerantMath(0.5).AreEqual(distp, 1.5), $"Distance isn't even close to 4.0 (distp={distp})"); - JxlImageF diffmap2 = new(); + JxlPlane diffmap2 = new(); Assert.True( Butteraugli.ButteraugliInterfaceInPlace( TestEnvironment.Configuration, diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/Decoder/JxlDecoderCoreTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Decoder/JxlDecoderCoreTests.cs index b603a42de0..d8e17ce826 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/Decoder/JxlDecoderCoreTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Decoder/JxlDecoderCoreTests.cs @@ -17,7 +17,6 @@ using SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Memory; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; @@ -301,18 +300,18 @@ static void Upsample7(JxlPlane input, JxlPlane output) } } - JxlImage3F preview = new(configuration, ib.XSize * 7, ib.YSize * 7); + JxlImage3 preview = new(configuration, ib.XSize * 7, ib.YSize * 7); for (int c = 0; c < 3; c++) { Upsample7(ib.Color!.Plane(c), preview.Plane(c)); } - List extraChannels = []; + List> extraChannels = []; - foreach (JxlImageF extraChannel in ib.EnumerateExtraChannels()) + foreach (JxlPlane extraChannel in ib.EnumerateExtraChannels()) { - JxlImageF ec = new(configuration, ib.XSize * 7, ib.YSize * 7); + JxlPlane ec = new(configuration, ib.XSize * 7, ib.YSize * 7); Upsample7(extraChannel, ec); extraChannels.Add(ec); @@ -341,7 +340,13 @@ private static List CreateTestJpegXLCodeStream(List pixels, int widt // the return value _if_ add_container indicates a box format. // // (comment from libjxl source) - private static List CreateTestJpegXLCodeStream(Configuration configuration, Span pixels, int width, int height, int numChannels, TestCodestreamParameters parameters) + private static List CreateTestJpegXLCodeStream( + Configuration configuration, + Span pixels, + int width, + int height, + int numChannels, + TestCodestreamParameters parameters) { bool grayscale = numChannels <= 2; bool haveAlpha = (numChannels & 1) == 0; @@ -740,8 +745,8 @@ public void TestSpotColors() io.CodecMetadata.ImageMetadata!.ColorEncoding = JxlColorEncoding.LinearSRgb(); - using JxlImage3F main = new(TestEnvironment.Configuration, width, height); - using JxlImageF spot = new(TestEnvironment.Configuration, width, height); + using JxlImage3 main = new(TestEnvironment.Configuration, width, height); + using JxlPlane spot = new(TestEnvironment.Configuration, width, height); JxlImageOperations.ZeroFillImage(main); JxlImageOperations.ZeroFillImage(spot); @@ -776,7 +781,7 @@ public void TestSpotColors() io.CodecMetadata.ImageMetadata!.ExtraChannels.Add(info); - List ec = [spot]; + List> ec = [spot]; Assert.True(io.Frames[0].TrySetExtraChannels(ec)); JxlCompressParameters compressParameters = new() diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/GaborishTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/GaborishTests.cs index 67461ddc89..62f93eafb3 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/GaborishTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/GaborishTests.cs @@ -4,7 +4,6 @@ using System.Runtime.CompilerServices; using System.Runtime.Intrinsics; using SixLabors.ImageSharp.Formats.Jxl.Memory; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; using SixLabors.ImageSharp.Formats.Jxl.Processing; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; @@ -49,10 +48,10 @@ private static void ConvolveGaborish( } private static void TestRoundTrip( - JxlImage3F input, + JxlImage3 input, float maxL1) { - JxlImage3F fwd = new(TestEnvironment.Configuration, input.XSize, input.YSize); + JxlImage3 fwd = new(TestEnvironment.Configuration, input.XSize, input.YSize); ConvolveGaborish( input.Plane(0), @@ -98,7 +97,7 @@ private static void TestRoundTrip( [Fact] public void TestZero() { - JxlImage3F input = new(TestEnvironment.Configuration, 20, 20); + JxlImage3 input = new(TestEnvironment.Configuration, 20, 20); JxlImageOperations.ZeroFillImage(input); TestRoundTrip(input, 0.0f); } @@ -106,7 +105,7 @@ public void TestZero() [Fact] public void TestFlat() { - JxlImage3F input = new(TestEnvironment.Configuration, 20, 20); + JxlImage3 input = new(TestEnvironment.Configuration, 20, 20); JxlImageOperations.FillImage(1.0f, input); TestRoundTrip(input, 1e-5f); } diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/ImageOperationsTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/ImageOperationsTests.cs index a7751b2c49..df8741c548 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/ImageOperationsTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/ImageOperationsTests.cs @@ -2,13 +2,203 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Jxl.Memory; +using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Tests.Memory; namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; public class ImageOperationsTests { + /// + /// Checks typed allocation, distinct rows, sample offsets, and initialized vector padding. + /// + /// The logical sample width. + [Theory] + [InlineData(1)] + [InlineData(15)] + [InlineData(16)] + [InlineData(17)] + [InlineData(33)] + public void PlaneRows_PreserveLayoutAndPadding(int width) + { + TestMemoryAllocator allocator = new(byte.MaxValue); + allocator.EnableNonThreadSafeLogging(); + Configuration configuration = TestEnvironment.Configuration.Clone(); + configuration.MemoryAllocator = allocator; + + using JxlPlane plane = new(configuration, width, 3, 2); + Assert.Equal(typeof(ushort), Assert.Single(allocator.AllocationLog).ElementType); + + for (int y = 0; y < 3; y++) + { + plane.GetRow(y)[..width].Fill((ushort)(y + 1)); + } + + // Prefix and padding do not contain samples. Dirty allocator storage must not leak into + // unused vector lanes, and writing one row must not overwrite the next row. + Assert.Equal((ushort)0, plane.GetRowMinus(0, 2)[0]); + Assert.Equal((ushort)0, plane.GetRowMinus(0, 2)[1]); + + for (int y = 0; y < 3; y++) + { + Span row = plane.GetRow(y); + for (int x = 0; x < width; x++) + { + Assert.Equal((ushort)(y + 1), row[x]); + } + + for (int x = width; x < plane.PixelsPerRow; x++) + { + Assert.Equal((ushort)0, row[x]); + } + } + + plane.GetRowMemory(1).Span[0] = 42; + Assert.Equal((ushort)42, plane.GetRow(1)[0]); + Assert.Equal((ushort)1, plane.GetRowPlus(1, -plane.PixelsPerRow)[0]); + } + + /// + /// Checks that logical resizing preserves allocation layout and rejects invalid dimensions. + /// + [Fact] + public void PlaneShrink_PreservesStorage() + { + using JxlPlane plane = new(TestEnvironment.Configuration, 33, 3); + plane.GetRow(2)[32] = 123; + int stride = plane.PixelsPerRow; + + Assert.True(plane.ShrinkTo(1, 1)); + Assert.True(plane.ShrinkTo(33, 3)); + Assert.Equal(stride, plane.PixelsPerRow); + Assert.Equal(123, plane.GetRow(2)[32]); + + Assert.False(plane.ShrinkTo(-1, 1)); + Assert.False(plane.ShrinkTo(34, 1)); + Assert.False(plane.ShrinkTo(1, 4)); + Assert.Equal(new Rectangle(0, 0, 33, 3), plane.GetRectangle()); + } + + /// + /// Checks that rectangle operations do not modify other samples or later rows. + /// + [Fact] + public void PlaneRectangleOperations_RespectLogicalBounds() + { + using JxlPlane source = new(TestEnvironment.Configuration, 5, 4); + using JxlPlane destination = new(TestEnvironment.Configuration, 7, 5); + + for (int y = 0; y < source.YSize; y++) + { + for (int x = 0; x < source.XSize; x++) + { + source.GetRow(y)[x] = (y * 10) + x; + } + } + + JxlImageOperations.FillImage(-1, destination); + Rectangle sourceRectangle = new(1, 1, 2, 2); + Rectangle destinationRectangle = new(3, 2, 2, 2); + Assert.True(JxlImageOperations.CopyImageTo(sourceRectangle, source, destinationRectangle, destination)); + + for (int y = 0; y < destination.YSize; y++) + { + Span row = destination.GetRow(y); + for (int x = 0; x < destination.XSize; x++) + { + int expected = destinationRectangle.Contains(x, y) ? ((y - 1) * 10) + x - 2 : -1; + Assert.Equal(expected, row[x]); + } + } + + JxlImageOperations.FillPlane(99, destination, new Rectangle(1, 0, 1, 1)); + Assert.Equal(-1, destination.GetRow(0)[0]); + Assert.Equal(99, destination.GetRow(0)[1]); + Assert.Equal(-1, destination.GetRow(0)[2]); + Assert.Equal(-1, destination.GetRow(1)[1]); + } + + /// + /// Checks that exchanging planes transfers both their owners and their row layouts. + /// + [Fact] + public void PlaneSwap_TransfersOwnership() + { + TestMemoryAllocator allocator = new(); + allocator.EnableNonThreadSafeLogging(); + Configuration configuration = TestEnvironment.Configuration.Clone(); + configuration.MemoryAllocator = allocator; + + using (JxlPlane first = new(configuration, 3, 2, 1)) + using (JxlPlane second = new(configuration, 17, 3, 5)) + { + first.GetRow(1)[2] = 12; + second.GetRow(2)[16] = 99; + + first.Swap(second); + Assert.Equal((ushort)99, first.GetRow(2)[16]); + Assert.Equal((ushort)12, second.GetRow(1)[2]); + Assert.Equal(new Rectangle(0, 0, 17, 3), first.GetRectangle()); + Assert.Equal(new Rectangle(0, 0, 3, 2), second.GetRectangle()); + } + + Assert.Equal(2, allocator.ReturnLog.Count); + Assert.Equal(0, allocator.LiveBufferBytes); + } + + /// + /// Checks that a failed third-plane allocation releases the first two planes. + /// + [Fact] + public void Image3Construction_ReleasesStorageOnFailure() + { + Configuration configuration = TestEnvironment.Configuration.Clone(); + configuration.MemoryAllocator = MemoryAllocator.Create(new MemoryAllocatorOptions { AccumulativeAllocationLimitMegabytes = 1 }); + using MemoryAllocatorValidator.TestMemoryDiagnostics allocations = MemoryAllocatorValidator.MonitorAllocations(); + + // Each float plane uses half a MiB. The third plane exceeds the allocator's one-MiB + // budget. Both earlier allocations must be released when construction fails. + Assert.Throws(() => new JxlImage3(configuration, 512, 256)); + Assert.Equal(2, allocations.TotalAllocated); + Assert.Equal(0, allocations.TotalRemainingAllocated); + + using JxlImage3 smaller = new(configuration, 16, 16); + Assert.Equal(3, allocations.TotalRemainingAllocated); + } + + /// + /// Checks the stored transform type and first-block flag for rectangular transforms. + /// + /// The transform type. + /// The covered block width. + /// The covered block height. + [Theory] + [InlineData(JxlAcStrategyType.DCT16X8, 1, 2)] + [InlineData(JxlAcStrategyType.DCT8X16, 2, 1)] + public void AcStrategyRows_PreserveRectangularExtent(JxlAcStrategyType type, int width, int height) + { + using JxlAcStrategyImage image = JxlAcStrategyImage.Create(TestEnvironment.Configuration, width, height); + image.FillInvalid(); + Assert.True(image.Set(0, 0, type)); + + // Each eight-pixel block stores the same transform type. Only the first block has + // the low bit set. The expected extents come from libjxl's transform-selection table. + for (int y = 0; y < height; y++) + { + JxlAcStrategyRow row = image.GetRow(y); + for (int x = 0; x < width; x++) + { + Assert.Equal(type, row[x].Strategy); + Assert.Equal(x == 0 && y == 0, row[x].IsFirstBlock); + } + } + + Assert.Equal(1, image.CountBlocks(type)); + } + private static void TestFillImpl(JxlImage3 image, string layout) where T : unmanaged { diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/OpsinImageTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/OpsinImageTests.cs index a9603c8cc2..935f3b825f 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/OpsinImageTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/OpsinImageTests.cs @@ -2,7 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Jxl.Cms; -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; @@ -26,7 +26,7 @@ public class OpsinImageTests /// Output B channel private static void LinearSRgbToOpsin(float rgbR, float rgbG, float rgbB, out float xybX, out float xybY, out float xybB) { - using JxlImage3F linear = new(TestEnvironment.Configuration, 1, 1); + using JxlImage3 linear = new(TestEnvironment.Configuration, 1, 1); linear.PlaneRow(0, 0)[0] = rgbR; linear.PlaneRow(1, 0)[0] = rgbG; linear.PlaneRow(2, 0)[0] = rgbB; @@ -35,7 +35,7 @@ private static void LinearSRgbToOpsin(float rgbR, float rgbG, float rgbB, out fl JxlColorEncoding.LinearSRgb, JxlShared.DefaultIntensityTarget, null, - new JxlImage3F(), + new JxlImage3(), JxlCmsInterface.CreateDefaultCms(), null)); xybX = linear.PlaneRow(0, 0)[0]; @@ -55,12 +55,12 @@ private static void LinearSRgbToOpsin(float rgbR, float rgbG, float rgbB, out fl /// Output Blue channel private static void OpsinToLinearSRgb(float xybX, float xybY, float xybB, out float rgbR, out float rgbG, out float rgbB) { - using JxlImage3F opsin = new(TestEnvironment.Configuration, 1, 1); + using JxlImage3 opsin = new(TestEnvironment.Configuration, 1, 1); opsin.PlaneRow(0, 0)[0] = xybX; opsin.PlaneRow(1, 0)[0] = xybY; opsin.PlaneRow(2, 0)[0] = xybB; - using JxlImage3F linear = new(TestEnvironment.Configuration, 1, 1); + using JxlImage3 linear = new(TestEnvironment.Configuration, 1, 1); JxlOpsinParameters opsinParameters = new(); opsinParameters.Initialize(intensityTarget: 255.0f); diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/OpsinInverseTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/OpsinInverseTests.cs index 5083e0d2f4..ef2c71b78a 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/OpsinInverseTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/OpsinInverseTests.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Memory; namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; @@ -15,7 +15,7 @@ public class OpsinInverseTests /// Input Y'Cb'Cr source. /// Output RGB. /// Region in which conversion will happen. - private static void SlowYCbCrToRgb(JxlImage3F yCbCr, JxlImage3F rgb, Rectangle rect) + private static void SlowYCbCrToRgb(JxlImage3 yCbCr, JxlImage3 rgb, Rectangle rect) { int width = rect.Width; int height = rect.Height; From 09cbf895242d93dbb864c5295bd70b32d1300fe3 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 28 Oct 2026 23:08:38 +1000 Subject: [PATCH 229/328] Run JPEG XL traversals sequentially with reusable scratch storage Replace all 23 Parallel.For calls with direct loops. Reuse Squeeze stack buffers, one adaptive-quantization scratch set, and one roundtrip group cache. Release replaced coefficient buffers and scratch owners, preserve masking ownership through swaps, and correct the DC smoothing border range against libjxl. Release compilation remains blocked by 48 declaration errors in unfinished codec components. Runtime tests have not run. --- .../Decoder/Group/JxlGroupDecoderCache.cs | 68 +++++- .../Decoder/JxlExternalImageDecoder.cs | 8 +- .../DotDetection/JxlDotDetectionUtils.cs | 6 +- .../Encoder/JxlAdaptiveQuantization.cs | 194 +++++++++++------- .../Encoder/JxlExternalImageEncoder.cs | 5 +- .../Encoder/JxlImageBundleEncoder.cs | 15 +- .../Jxl/Processing/Encoder/JxlXybEncoder.cs | 82 ++++---- .../Formats/Jxl/Processing/JxlCompressedDc.cs | 4 +- .../Modular/Transforms/JxlPalette.cs | 66 +++--- .../Processing/Modular/Transforms/JxlRct.cs | 12 +- .../Modular/Transforms/JxlSqueeze.cs | 61 +++--- .../Jxl/Processing/ImageOperationsTests.cs | 33 +++ 12 files changed, 359 insertions(+), 195 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoderCache.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoderCache.cs index e64d3ab2b0..3c55cbe1fd 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoderCache.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoderCache.cs @@ -12,11 +12,26 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Group; /// /// Cache for the JPEG XL Group Decoder. /// -internal sealed class JxlGroupDecoderCache(Configuration configuration) +internal sealed class JxlGroupDecoderCache(Configuration configuration) : IDisposable { + /// + /// Owns the floating-point coefficient and transform workspace. + /// private IMemoryOwner? floatMemory; + + /// + /// Owns the full-width quantized coefficients. + /// private IMemoryOwner? int32Memory; + + /// + /// Owns the narrow quantized coefficients. + /// private IMemoryOwner? int16Memory; + + /// + /// The largest transform area supported by the current workspace. + /// private int maxBlockArea; public Memory DecodeGroupBlockScratchSpace { get; set; } @@ -52,6 +67,11 @@ public void InitializeDcBufferOnce() } } + /// + /// Initializes pass storage and grows coefficient scratch only when a larger transform is required. + /// + /// The number of coefficient passes. + /// The bit mask of transform types used by the group. public void InitializeOnce(int numPasses, int usedACStrategy) { for (int i = 0; i < numPasses; i++) @@ -82,11 +102,32 @@ public void InitializeOnce(int numPasses, int usedACStrategy) if (maxBlockArea > this.maxBlockArea) { - this.maxBlockArea = maxBlockArea; + IMemoryOwner floatMemory = configuration.MemoryAllocator.Allocate(maxBlockArea * 7); + IMemoryOwner? int32Memory = null; + IMemoryOwner? int16Memory = null; - this.floatMemory = configuration.MemoryAllocator.Allocate(this.maxBlockArea * 7); - this.int32Memory = configuration.MemoryAllocator.Allocate(this.maxBlockArea * 3); - this.int16Memory = configuration.MemoryAllocator.Allocate(this.maxBlockArea * 3); + try + { + int32Memory = configuration.MemoryAllocator.Allocate(maxBlockArea * 3); + int16Memory = configuration.MemoryAllocator.Allocate(maxBlockArea * 3); + } + catch + { + int16Memory?.Dispose(); + int32Memory?.Dispose(); + floatMemory.Dispose(); + throw; + } + + // Publish the replacement only after every allocation succeeds. Later groups reuse + // these buffers until a larger transform is needed; growth releases the old owners. + this.floatMemory?.Dispose(); + this.int32Memory?.Dispose(); + this.int16Memory?.Dispose(); + this.floatMemory = floatMemory; + this.int32Memory = int32Memory; + this.int16Memory = int16Memory; + this.maxBlockArea = maxBlockArea; } this.DecodeGroupBlockScratchSpace = this.floatMemory?.Memory ?? Memory.Empty; @@ -102,4 +143,21 @@ public void InitializeOnce(int numPasses, int usedACStrategy) this.ScratchSpaceMemory = Memory.Empty; } } + + /// + /// Releases coefficient scratch, nonzero-count planes, and DC storage owned by this cache. + /// + public void Dispose() + { + this.floatMemory?.Dispose(); + this.int32Memory?.Dispose(); + this.int16Memory?.Dispose(); + + foreach (JxlImage3 nonZeroes in this.NumberOfNonZeroes) + { + nonZeroes.Dispose(); + } + + this.DcBuffer.Dispose(); + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlExternalImageDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlExternalImageDecoder.cs index 20b6def5fd..fddfce1a6b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlExternalImageDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlExternalImageDecoder.cs @@ -44,7 +44,7 @@ public static JxlPlane UndoOrientation(Configuration configuration, JxlExi { output = JxlPlane.Create(configuration, xSize, ySize); - _ = Parallel.For(0, ySize, configuration.GetParallelOptions(), (y) => + for (int y = 0; y < ySize; y++) { Span rowIn = image.GetRow(y); Span rowOut = output.GetRow(y); @@ -53,13 +53,13 @@ public static JxlPlane UndoOrientation(Configuration configuration, JxlExi { rowOut[xSize - x - 1] = rowIn[x]; } - }); + } } else if (undoOrientation == JxlExifOrientation.Rotate180) { output = JxlPlane.Create(configuration, xSize, ySize); - _ = Parallel.For(0, ySize, configuration.GetParallelOptions(), (y) => + for (int y = 0; y < ySize; y++) { Span rowIn = image.GetRow(y); Span rowOut = output.GetRow(ySize - y - 1); @@ -68,7 +68,7 @@ public static JxlPlane UndoOrientation(Configuration configuration, JxlExi { rowOut[xSize - x - 1] = rowIn[x]; } - }); + } } // TODO } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlDotDetectionUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlDotDetectionUtils.cs index 5f72b69b22..43783c7e91 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlDotDetectionUtils.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlDotDetectionUtils.cs @@ -27,7 +27,7 @@ internal static class JxlDotDetectionUtils /// /// Sum of Square Differences /// - /// Contains the memory allocator and parallel options. + /// The configuration that owns the output allocator. /// Original image /// Smooth image /// Sum of Square Differences coefficients @@ -39,7 +39,7 @@ public static JxlPlane Ssd(Configuration configuration, JxlImage3 JxlPlane sumOfSquares = new(configuration, original.XSize, original.YSize); - _ = Parallel.For(0, original.YSize, configuration.GetParallelOptions(), y => + for (int y = 0; y < original.YSize; y++) { Span originalRow0 = original.Plane(0).GetRow(y); Span originalRow1 = original.Plane(1).GetRow(y); @@ -64,7 +64,7 @@ public static JxlPlane Ssd(Configuration configuration, JxlImage3 Vector weightedSumOfSquareDiffs = v0 + v1 + v2; weightedSumOfSquareDiffs.CopyTo(sumOfSquaresRow[x..]); } - }); + } return sumOfSquares; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs index 7fa3460f8d..9b849d7f64 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs @@ -501,7 +501,7 @@ public static bool FuzzyErosion(float butteraugliTarget, Rectangle fromRect, Jxl return true; } - private static void Blur1x1Masking(Configuration configuration, ref JxlPlane mask1x1, Rectangle rect) + private static void Blur1x1Masking(Configuration configuration, JxlPlane mask1x1, Rectangle rect) { const float filterMask1x1_0 = 0.364911248f; const float filterMask1x1_1 = 0.05f; @@ -529,20 +529,31 @@ private static void Blur1x1Masking(Configuration configuration, ref JxlPlane temp = new(configuration, rect.Width, rect.Height); + using JxlPlane temp = new(configuration, rect.Width, rect.Height); if (!JxlConvolve.Symmetric5(mask1x1, in rect, ref weights, temp, temp.GetRectangle())) { throw new InvalidOperationException("Symmetric5 convolution failed"); } - mask1x1 = temp; + mask1x1.Swap(temp); } + /// + /// Computes the block quantization map and masking planes with reusable tile scratch storage. + /// + /// The configuration that owns the allocator. + /// The requested perceptual error target. + /// The source XYB planes. + /// The source region with dimensions divisible by eight. + /// The quantization scale. + /// The returned block masking plane. + /// The returned full-resolution masking plane. + /// The owned block quantization map. The caller owns all three output planes. public static JxlPlane AdaptiveQuantizationMap( Configuration configuration, float butteraugliTarget, - JxlPlane xyb, + JxlImage3 xyb, Rectangle rect, float scale, out JxlPlane mask, @@ -558,49 +569,55 @@ public static JxlPlane AdaptiveQuantizationMap( throw new ArgumentException("Rectangle height must be divisible by 8", nameof(rect)); } - AdaptiveQuantizationInternal impl = new(); - int xSizeBlocks = rect.Width / 8; int ySizeBlocks = rect.Height / 8; + JxlPlane quantizationMap = new(configuration, xSizeBlocks, ySizeBlocks); + JxlPlane? allocatedMask = null; + JxlPlane? allocatedMask1x1 = null; - impl.AdaptiveQuantizationMap = new JxlPlane(configuration, xSizeBlocks, ySizeBlocks); - mask = new JxlPlane(configuration, xSizeBlocks, ySizeBlocks); - mask1x1 = new JxlPlane(configuration, xyb.XSize, xyb.YSize); - - // out variables cannot be captured in a local function - JxlPlane maskRef = mask; - JxlPlane mask1x1Ref = mask1x1; - - impl.InitializeBuffer(configuration); - - void ProcessTile(int idx) + try { - int numTilesX = JxlMath.DivCeil(xSizeBlocks, JxlEncoderParameters.TileDimensionsInBlocks); + allocatedMask = new(configuration, xSizeBlocks, ySizeBlocks); + allocatedMask1x1 = new(configuration, xyb.XSize, xyb.YSize); + mask = allocatedMask; + mask1x1 = allocatedMask1x1; - int tx = (int)(idx % (uint)numTilesX); - int ty = (int)(idx / (uint)numTilesX); - - int by0 = ty * JxlEncoderParameters.TileDimensionsInBlocks; - int by1 = Math.Min((ty + 1) * JxlEncoderParameters.TileDimensionsInBlocks, ySizeBlocks); - - int bx0 = tx * JxlEncoderParameters.TileDimensionsInBlocks; - int bx1 = Math.Min((tx + 1) * JxlEncoderParameters.TileDimensionsInBlocks, xSizeBlocks); - - Rectangle rectOut = new(bx0, by0, bx1 - bx0, by1 - by0); + using AdaptiveQuantizationInternal impl = new(configuration, quantizationMap); + int numTilesX = JxlMath.DivCeil(xSizeBlocks, JxlEncoderParameters.TileDimensionsInBlocks); + int numTilesY = JxlMath.DivCeil(ySizeBlocks, JxlEncoderParameters.TileDimensionsInBlocks); - if (!impl.TryComputeTile(butteraugliTarget, scale, xyb, rect, rectOut, idx, maskRef, mask1x1Ref)) + // Tiles run sequentially and reuse one difference row and one erosion plane. + // Tile coordinates select the output region; they never select a scratch allocation. + for (int ty = 0; ty < numTilesY; ty++) { - throw new InvalidOperationException("Couldn't process tile"); - } - } + int by0 = ty * JxlEncoderParameters.TileDimensionsInBlocks; + int by1 = Math.Min(by0 + JxlEncoderParameters.TileDimensionsInBlocks, ySizeBlocks); - int numTiles = JxlMath.DivCeil(xSizeBlocks, JxlEncoderParameters.TileDimensionsInBlocks) * - JxlMath.DivCeil(ySizeBlocks, JxlEncoderParameters.TileDimensionsInBlocks); + for (int tx = 0; tx < numTilesX; tx++) + { + int bx0 = tx * JxlEncoderParameters.TileDimensionsInBlocks; + int bx1 = Math.Min(bx0 + JxlEncoderParameters.TileDimensionsInBlocks, xSizeBlocks); + Rectangle rectOut = new(bx0, by0, bx1 - bx0, by1 - by0); - _ = Parallel.For(0, numTiles, configuration.GetParallelOptions(), ProcessTile); - Blur1x1Masking(configuration, ref mask1x1, rect); + if (!impl.TryComputeTile(butteraugliTarget, scale, xyb, rect, rectOut, mask, mask1x1)) + { + throw new InvalidOperationException("Could not process the quantization tile."); + } + } + } - return impl.AdaptiveQuantizationMap!; + Blur1x1Masking(configuration, mask1x1, rect); + return quantizationMap; + } + catch + { + // Output ownership transfers only on success. Nullable locals record which allocations + // completed, so an allocation or transform failure releases every acquired output. + allocatedMask1x1?.Dispose(); + allocatedMask?.Dispose(); + quantizationMap.Dispose(); + throw; + } } private static JxlPlane TileDistMap(Configuration configuration, JxlPlane distMap, int tileSize, int margin, JxlAcStrategyImage acStrategy) @@ -756,9 +773,9 @@ private static JxlImageBundle RoundtripImage( decState.PreparePipeline(frameHeader, encState.Shared.Metadata.ImageMetadata!, decoded, options); - JxlGroupDecoderCache[] groupDecCaches = new JxlGroupDecoderCache[numThreads]; + using JxlGroupDecoderCache groupDecCache = new(configuration); - decState.RenderPipeline.PrepareForThreads(numThreads, useGroupIds: false); + decState.RenderPipeline.PrepareForThreads(1, useGroupIds: false); void ProcessGroup(int groupIndex) { @@ -769,15 +786,15 @@ void ProcessGroup(int groupIndex) decState); } - RenderPipelineInput input = decState.RenderPipeline.GetInputBuffers(groupIndex, groupIndex); + RenderPipelineInput input = decState.RenderPipeline.GetInputBuffers(groupIndex, 0); JxlGroupDecoder.DecodeGroupForRoundtrip( frameHeader, encState.Coeffs, groupIndex, decState, - groupDecCaches[groupIndex], - groupIndex, + groupDecCache, + 0, input, null, null); @@ -791,7 +808,10 @@ void ProcessGroup(int groupIndex) input.Done(); } - _ = Parallel.For(0, numGroups, configuration.GetParallelOptions(), ProcsesGr); + for (int i = 0; i < numGroups; i++) + { + ProcessGroup(i); + } encState.SpecialFrames.RemoveRange(numSpecialFrames, encState.SpecialFrames.Count - numSpecialFrames); return decoded; @@ -1261,47 +1281,79 @@ public static bool FindBestQuantizer( return true; } - private sealed class AdaptiveQuantizationInternal + /// + /// Owns the scratch storage reused by sequential quantization tiles. + /// + private sealed class AdaptiveQuantizationInternal : IDisposable { - public List> PreErosion { get; set; } = []; + /// + /// Initializes the tile scratch storage and borrows the caller's output map. + /// + /// The configuration that owns the allocator. + /// The output map, which remains owned by the caller. + public AdaptiveQuantizationInternal(Configuration configuration, JxlPlane quantizationMap) + { + this.AdaptiveQuantizationMap = quantizationMap; + this.PreErosion = new( + configuration, + (JxlEncoderParameters.TileDimensionsInBlocks * 2) + 2, + (JxlEncoderParameters.TileDimensionsInBlocks * 2) + 2); - public JxlPlane? AdaptiveQuantizationMap { get; set; } + try + { + this.DiffBuffer = new(configuration, JxlEncoderParameters.TileDimensions + 8, 1); + } + catch + { + this.PreErosion.Dispose(); + throw; + } + } - public JxlPlane? DiffBuffer { get; set; } + /// + /// Gets the erosion plane reused for each tile. + /// + public JxlPlane PreErosion { get; } - public void InitializeBuffer(Configuration configuration) - { - int numThreads = configuration.MaxDegreeOfParallelism == -1 ? Environment.ProcessorCount : configuration.MaxDegreeOfParallelism; + /// + /// Gets the borrowed block quantization output. + /// + public JxlPlane AdaptiveQuantizationMap { get; } - this.DiffBuffer = new(configuration, JxlEncoderParameters.TileDimensions + 8, numThreads); + /// + /// Gets the difference row reused while accumulating four input rows. + /// + public JxlPlane DiffBuffer { get; } - for (int i = this.PreErosion.Count; i < numThreads; i++) - { - this.PreErosion.Add( - new JxlPlane( - configuration, - (JxlEncoderParameters.TileDimensionsInBlocks * 2) + 2, - (JxlEncoderParameters.TileDimensionsInBlocks * 2) + 2)); - } + /// + public void Dispose() + { + this.DiffBuffer.Dispose(); + this.PreErosion.Dispose(); } + /// + /// Computes one tile using the shared difference row and erosion plane. + /// + /// The requested perceptual error target. + /// The quantization scale. + /// The source XYB planes. + /// The source region. + /// The tile region in the output map. + /// The caller-owned block masking plane. + /// The caller-owned full-resolution masking plane. + /// Whether the tile could be computed from the supplied regions. public bool TryComputeTile( float butteraugliTarget, float scale, JxlImage3 xyb, Rectangle rectIn, Rectangle rectOut, - int parallelIndex, JxlPlane mask, JxlPlane mask1x1) { const float matchGammaOffset = 0.019f; - if (this.DiffBuffer is null) - { - return false; - } - if (rectIn.X0() % JxlFrameDimensions.BlockDimensions != 0 || rectIn.Y0() % JxlFrameDimensions.BlockDimensions != 0) { return false; @@ -1390,7 +1442,7 @@ public bool TryComputeTile( yEnd += 4; } - if (!this.PreErosion[parallelIndex].ShrinkTo((xEnd - xStart) / 4, (yEnd - yStart) / 4)) + if (!this.PreErosion.ShrinkTo((xEnd - xStart) / 4, (yEnd - yStart) / 4)) { return false; } @@ -1405,7 +1457,7 @@ public bool TryComputeTile( Span rowIn = xyb.PlaneRow(1, y); Span rowIn1 = xyb.PlaneRow(1, y1); Span rowIn2 = xyb.PlaneRow(1, y2); - Span rowOut = this.DiffBuffer!.GetRow(parallelIndex); + Span rowOut = this.DiffBuffer.GetRow(0); // Pass Span variables instead of capturing them as a ref struct // variable cannot be captured. @@ -1481,7 +1533,7 @@ void ScalarPixel(int x, Span rowIn, Span rowIn1, Span rowIn if (y % 4 == 3) { - Span rowDOut = this.PreErosion[parallelIndex].GetRow((y - yStart) / 4); + Span rowDOut = this.PreErosion.GetRow((y - yStart) / 4); for (int qx = 0; qx < (xEnd - xStart) / 4; qx++) { @@ -1501,14 +1553,14 @@ void ScalarPixel(int x, Span rowIn, Span rowIn1, Span rowIn width: rectOut.Width * 2, height: rectOut.Height * 2); - if (!FuzzyErosion(butteraugliTarget, fromRect, this.PreErosion[parallelIndex], rectOut, this.AdaptiveQuantizationMap!)) + if (!FuzzyErosion(butteraugliTarget, fromRect, this.PreErosion, rectOut, this.AdaptiveQuantizationMap)) { return false; } for (int y = 0; y < rectOut.Height; y++) { - Span aqMapRow = this.AdaptiveQuantizationMap!.GetRow(rectOut, y); + Span aqMapRow = this.AdaptiveQuantizationMap.GetRow(rectOut, y); Span maskRow = mask.GetRow(rectOut, y); for (int x = 0; x < rectOut.Width; x++) @@ -1517,7 +1569,7 @@ void ScalarPixel(int x, Span rowIn, Span rowIn1, Span rowIn } } - PerBlockModulations(butteraugliTarget, xyb.Plane(0), xyb.Plane(1), xyb.Plane(2), rectIn, scale, rectOut, this.AdaptiveQuantizationMap!); + PerBlockModulations(butteraugliTarget, xyb.Plane(0), xyb.Plane(1), xyb.Plane(2), rectIn, scale, rectOut, this.AdaptiveQuantizationMap); return true; } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlExternalImageEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlExternalImageEncoder.cs index 8fe7a110d1..caed1a447b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlExternalImageEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlExternalImageEncoder.cs @@ -89,7 +89,10 @@ bool SaveValue(int index, float value) LoadFloatRow(data[offset..], xSize, bytesPerPixel, format.DataType, littleEndian, scale, SaveValue); } - _ = Parallel.For(0, ySize, configuration.GetParallelOptions(), ConvertRow); + for (int i = 0; i < ySize; i++) + { + ConvertRow(i); + } } private static bool ConvertFromExternal( diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlImageBundleEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlImageBundleEncoder.cs index 7dec72c4a9..7fab45a569 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlImageBundleEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlImageBundleEncoder.cs @@ -50,12 +50,7 @@ public static bool TryApplyColorTransform( transform.Initialize(configuration, currentEncoding, desiredEncoding, intensityTarget, rect.Width); - // We can't use a ref variable inside of a lambda expression, - // so we store its value in a variable. The output variable - // will not be assigned. - JxlImage3 outValue = output; - - _ = Parallel.For(0, rect.Height, configuration.GetParallelOptions(), y => + for (int y = 0; y < rect.Height; y++) { Span sourceBuffer = transform.GetSourceBuffer(); @@ -101,9 +96,9 @@ public static bool TryApplyColorTransform( throw new InvalidOperationException("Could not run color space transform"); } - Span rowOut0 = outValue.PlaneRow(0, y); - Span rowOut1 = outValue.PlaneRow(1, y); - Span rowOut2 = outValue.PlaneRow(2, y); + Span rowOut0 = output.PlaneRow(0, y); + Span rowOut1 = output.PlaneRow(1, y); + Span rowOut2 = output.PlaneRow(2, y); if (isGray) { @@ -121,7 +116,7 @@ public static bool TryApplyColorTransform( rowOut2[x] = destBuffer[x3 + 2]; } } - }); + } return false; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlXybEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlXybEncoder.cs index 41f8f25c30..aa97f9d682 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlXybEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlXybEncoder.cs @@ -222,10 +222,12 @@ private static Vector ZeroIfNegative(Vector v) => private static Vector LinearFromSRgb(Vector encoded) => JxlSRgbTransferFunction.DisplayFromEncoded(encoded); - private static void LinearSrgbToXyb( - Configuration configuration, - Memory premulAbsorb, - JxlImage3 image) + /// + /// Converts linear sRGB planes to XYB in place. + /// + /// The broadcast absorbance matrix and cube-root bias coefficients. + /// The source planes, which receive XYB samples. + private static void LinearSrgbToXyb(ReadOnlySpan premulAbsorb, JxlImage3 image) { int xSize = image.XSize; @@ -254,16 +256,18 @@ void ProcessRow(int y, ReadOnlySpan premulAbsorb) } } - _ = Parallel.For(0, image.YSize, configuration.GetParallelOptions(), x => + for (int y = 0; y < image.YSize; y++) { - ProcessRow(x, premulAbsorb.Span); - }); + ProcessRow(y, premulAbsorb); + } } - private static void SRgbToXyb( - Configuration configuration, - Memory premulAbsorb, - JxlImage3 image) + /// + /// Converts encoded sRGB planes to XYB in place. + /// + /// The broadcast absorbance matrix and cube-root bias coefficients. + /// The source planes, which receive XYB samples. + private static void SRgbToXyb(ReadOnlySpan premulAbsorb, JxlImage3 image) { int xSize = image.XSize; @@ -292,17 +296,19 @@ void ProcessRow(int y, ReadOnlySpan premulAbsorb) } } - _ = Parallel.For(0, image.YSize, configuration.GetParallelOptions(), x => + for (int y = 0; y < image.YSize; y++) { - ProcessRow(x, premulAbsorb.Span); - }); + ProcessRow(y, premulAbsorb); + } } - private static void SrgbToXybAndLinear( - Configuration configuration, - Memory premulAbsorb, - JxlImage3 image, - JxlImage3 linear) + /// + /// Converts encoded sRGB planes to XYB and retains the linear sRGB samples. + /// + /// The broadcast absorbance matrix and cube-root bias coefficients. + /// The source planes, which receive XYB samples. + /// The caller-owned planes that receive linear sRGB samples. + private static void SrgbToXybAndLinear(ReadOnlySpan premulAbsorb, JxlImage3 image, JxlImage3 linear) { int xSize = image.XSize; @@ -339,7 +345,10 @@ void ProcessRow(int y, ReadOnlySpan premulAbsorb) } } - _ = Parallel.For(0, image.YSize, configuration.GetParallelOptions(), x => ProcessRow(x, premulAbsorb.Span)); + for (int y = 0; y < image.YSize; y++) + { + ProcessRow(y, premulAbsorb); + } } private static void ComputePremulAbsorb(float intensityTarget, Span premulAbsorb) @@ -396,33 +405,33 @@ public static bool ToXyb( int lanes = Vector.Count; - Memory premulAbsorb = new float[lanes * 12]; - ComputePremulAbsorb(intensityTarget, premulAbsorb.Span); - - bool wantLinear = linear is not null; + // Twelve coefficients are broadcast across the active vector lanes. The bounded + // workspace stays on the calling thread and is reused for every row of this transform. + Span premulAbsorb = stackalloc float[lanes * 12]; + ComputePremulAbsorb(intensityTarget, premulAbsorb); JxlColorEncoding linearSrgb = JxlColorEncoding.LinearSRgb(currentEncoding.IsGray); if (linearSrgb.SameColorEncoding(currentEncoding)) { - if (wantLinear) + if (linear is not null) { - JxlImageOperations.ForceCopyImage(image, linear!); + JxlImageOperations.ForceCopyImage(image, linear); } - LinearSrgbToXyb(configuration, premulAbsorb, image); + LinearSrgbToXyb(premulAbsorb, image); return true; } if (currentEncoding.IsSRgb) { - if (wantLinear) + if (linear is not null) { - SrgbToXybAndLinear(configuration, premulAbsorb, image, linear!); + SrgbToXybAndLinear(premulAbsorb, image, linear); return true; } - SRgbToXyb(configuration, premulAbsorb, image); + SRgbToXyb(premulAbsorb, image); return true; } @@ -436,14 +445,14 @@ public static bool ToXyb( image.GetRectangle(), linearSrgb, cms, - wantLinear ? linear! : image); + linear ?? image); - if (wantLinear) + if (linear is not null) { - JxlImageOperations.ForceCopyImage(linear!, image); + JxlImageOperations.ForceCopyImage(linear, image); } - LinearSrgbToXyb(configuration, premulAbsorb, image); + LinearSrgbToXyb(premulAbsorb, image); return true; } @@ -522,6 +531,9 @@ void Transform(int index) } } - _ = Parallel.For(0, numStripes, configuration.GetParallelOptions(), Transform); + for (int i = 0; i < numStripes; i++) + { + Transform(i); + } } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlCompressedDc.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlCompressedDc.cs index 117cb6ad33..818ac01905 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlCompressedDc.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlCompressedDc.cs @@ -172,7 +172,7 @@ public static bool AdaptiveDCSmoothing(Configuration configuration, InlineArray3 dc.PlaneRow(c, ySize - 1).CopyTo(smoothed.PlaneRow(c, ySize - 1)); } - _ = Parallel.For(1, ySize, configuration.GetParallelOptions(), y => + for (int y = 1; y < ySize - 1; y++) { ReadOnlySpan rowTopX = dc.PlaneRow(0, y - 1); ReadOnlySpan rowTopY = dc.PlaneRow(1, y - 1); @@ -219,7 +219,7 @@ public static bool AdaptiveDCSmoothing(Configuration configuration, InlineArray3 { ComputePixelScalar(dcFactors, rowTopX, rowTopY, rowTopB, rowX, rowY, rowB, rowBottomX, rowBottomY, rowBottomB, rowOutX, rowOutY, rowOutB, x); } - }); + } dc.Swap(smoothed); return true; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs index 8b0ed40cb9..92a09bbdcb 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs @@ -273,7 +273,7 @@ public static void InversePalette( { if (nb == 1) { - _ = Parallel.For(0, h, configuration.GetParallelOptions(), y => + for (int y = 0; y < h; y++) { Span p = channel.GetRow(y); Span paletteData = palette.GetRow(0); @@ -284,11 +284,11 @@ public static void InversePalette( p[x] = GetPaletteValue(paletteData, index, 0, oneRow, bitDepth); } - }); + } } else if (nb == 2) { - _ = Parallel.For(0, h, configuration.GetParallelOptions(), y => + for (int y = 0; y < h; y++) { Span paletteData = palette.GetRow(0); Span p0 = channel.GetRow(y); @@ -302,11 +302,11 @@ public static void InversePalette( p0[x] = GetPaletteValue(paletteData, index0, 0, oneRow, bitDepth); p1[x] = GetPaletteValue(paletteData, index1, 0, oneRow, bitDepth); } - }); + } } else if (nb == 3) { - _ = Parallel.For(0, h, configuration.GetParallelOptions(), y => + for (int y = 0; y < h; y++) { Span paletteData = palette.GetRow(0); Span p0 = channel.GetRow(y); @@ -323,11 +323,11 @@ public static void InversePalette( p1[x] = GetPaletteValue(paletteData, index1, 0, oneRow, bitDepth); p2[x] = GetPaletteValue(paletteData, index2, 0, oneRow, bitDepth); } - }); + } } else if (nb == 4) { - _ = Parallel.For(0, h, configuration.GetParallelOptions(), y => + for (int y = 0; y < h; y++) { Span paletteData = palette.GetRow(0); Span p0 = channel.GetRow(y); @@ -347,11 +347,11 @@ public static void InversePalette( p2[x] = GetPaletteValue(paletteData, index2, 0, oneRow, bitDepth); p3[x] = GetPaletteValue(paletteData, index3, 0, oneRow, bitDepth); } - }); + } } else if (nb == 5) { - _ = Parallel.For(0, h, configuration.GetParallelOptions(), y => + for (int y = 0; y < h; y++) { Span paletteData = palette.GetRow(0); Span p0 = channel.GetRow(y); @@ -374,7 +374,7 @@ public static void InversePalette( p3[x] = GetPaletteValue(paletteData, index3, 0, oneRow, bitDepth); p4[x] = GetPaletteValue(paletteData, index4, 0, oneRow, bitDepth); } - }); + } } else { @@ -389,23 +389,30 @@ public static void InversePalette( if (predictor == JxlPredictor.Weighted) { - _ = Parallel.For(0, nb, configuration.GetParallelOptions(), c => + for (int c = 0; c < nb; c++) { - JxlModularChannel channel = input.Channels[c0 + c]; - JxlModularState wpState = new(weightedHeader, channel.Width); + JxlModularChannel outputChannel = input.Channels[c0 + c]; + JxlModularState wpState = new(weightedHeader, outputChannel.Width); Span paletteData = palette.GetRow(0); - for (int y = 0; y < channel.Height; y++) + for (int y = 0; y < outputChannel.Height; y++) { - Span p = channel.GetRow(y); + Span p = outputChannel.GetRow(y); Span idx = indices.GetRow(y); - for (int x = 0; x < channel.Width; x++) + for (int x = 0; x < outputChannel.Width; x++) { int index = idx[x]; int value = 0; int paletteEntry = GetPaletteValue(paletteData, index, c, oneRow, bitDepth); - JxlPredictionResult pred = JxlContextPrediction.PredictTreeNoWeightedPrediction(channel.Width, p[x..], oneRowImage, x, y, predictor, wpState); + JxlPredictionResult pred = JxlContextPrediction.PredictTreeNoWeightedPrediction( + outputChannel.Width, + p[x..], + oneRowImage, + x, + y, + predictor, + wpState); if (index < nbDeltas) { @@ -417,24 +424,24 @@ public static void InversePalette( } p[x] = value; - wpState.UpdatePredictionErrors(p[x], x, y, channel.Width); + wpState.UpdatePredictionErrors(p[x], x, y, outputChannel.Width); } } - }); + } } else { - _ = Parallel.For(0, nb, configuration.GetParallelOptions(), c => + for (int c = 0; c < nb; c++) { - JxlModularChannel channel = input.Channels[c0 + c]; + JxlModularChannel outputChannel = input.Channels[c0 + c]; Span paletteData = palette.GetRow(0); - for (int y = 0; y < channel.Height; y++) + for (int y = 0; y < outputChannel.Height; y++) { - Span p = channel.GetRow(y); + Span p = outputChannel.GetRow(y); Span idx = indices.GetRow(y); - for (int x = 0; x < channel.Width; x++) + for (int x = 0; x < outputChannel.Width; x++) { int index = idx[x]; int value = 0; @@ -442,7 +449,14 @@ public static void InversePalette( if (index < nbDeltas) { - JxlPredictionResult pred = JxlContextPrediction.PredictNoTreeNoWeightedPrediction(channel.Width, p[x..], oneRowImage, x, y, predictor); + JxlPredictionResult pred = JxlContextPrediction.PredictNoTreeNoWeightedPrediction( + outputChannel.Width, + p[x..], + oneRowImage, + x, + y, + predictor); + value = pred.Guess + paletteEntry; } else @@ -453,7 +467,7 @@ public static void InversePalette( p[x] = value; } } - }); + } } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlRct.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlRct.cs index 66f0774865..77c1bc88c8 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlRct.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlRct.cs @@ -184,7 +184,7 @@ public static void InverseRct(Configuration configuration, JxlModularImage img, return; } - _ = Parallel.For(0, h, configuration.GetParallelOptions(), y => + for (int y = 0; y < h; y++) { // Get input channels Span in0 = img.Channels[m].GetRow(y); @@ -197,7 +197,7 @@ public static void InverseRct(Configuration configuration, JxlModularImage img, Span out2 = img.Channels[m + ((permutation + 2 - (permutation / 3)) % 3)].GetRow(y); InverseRctRow(custom, in0, in1, in2, out0, out1, out2); - }); + } } /// @@ -334,7 +334,7 @@ private static void RctPermute(InlineArray3 input, int permut /// Performs Forward Reversible Color Transform. (Internal method) /// /// - /// Configuration for parallelism. + /// The codec configuration. /// /// /// Input channels. @@ -356,7 +356,7 @@ private static void ForwardRctCore(Configuration configuration, InlineArray3 + for (int y = 0; y < height; y++) { // Get input channels Span i0 = inp[0].GetRow(y); @@ -369,14 +369,14 @@ private static void ForwardRctCore(Configuration configuration, InlineArray3 o2 = output[2].GetRow(y); ForwardRctRow(transform, i0, i1, i2, o0, o1, o2); - }); + } } /// /// Performs forward Reversible Color Transform (RCT). /// /// - /// Configuration for parallelism. + /// The codec configuration. /// /// /// Image to perform Reversible Color Transform. diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs index 8030b1c0d7..43506b07b8 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs @@ -225,20 +225,23 @@ public static void InverseHorizontalSqueeze(Configuration configuration, JxlModu return; } - // The number of rows a single parallel iteration computes - // is stored here. - const int rowsPerThread = 8; - - // rowsPerThread * 9, aligned to the power of 2. - const int rowsPerThreadMul9Alignment = 128; - - // rowsPerThread * 8, aligned to the power of 2. - const int rowsPerThreadMul8Alignment = 64; - - _ = Parallel.For(0, JxlMath.DivCeil(inputChannel.Height, rowsPerThread), configuration.GetParallelOptions(), idx => + // Transposition works on eight rows at a time. Reuse its stack storage for all blocks. + const int rowsPerBlock = 8; + + // The average block has one extra column for the next-sample predictor. Each of the + // other planes contains an eight-by-eight block. Allocate them once, outside traversal. + Span bpAvg = stackalloc int[rowsPerBlock * 9]; + Span bpResidual = stackalloc int[rowsPerBlock * 8]; + Span bpOutEven = stackalloc int[rowsPerBlock * 8]; + Span bpOutOdd = stackalloc int[rowsPerBlock * 8]; + Span bpOutEvenT = stackalloc int[rowsPerBlock * 8]; + Span bpOutOddT = stackalloc int[rowsPerBlock * 8]; + int rowBlockCount = JxlMath.DivCeil(inputChannel.Height, rowsPerBlock); + + for (int idx = 0; idx < rowBlockCount; idx++) { - int y0 = idx * rowsPerThread; - int rows = Math.Min(rowsPerThread, inputChannel.Height - y0); + int y0 = idx * rowsPerBlock; + int rows = Math.Min(rowsPerBlock, inputChannel.Height - y0); int x = 0; int onerow_in = inputChannel.Plane.PixelsPerRow; @@ -249,26 +252,19 @@ public static void InverseHorizontalSqueeze(Configuration configuration, JxlModu Span pOut = outputChannel.GetRow(y0); ref int pOutRef = ref MemoryMarshal.GetReference(pOut); - Span bpAvg = stackalloc int[rowsPerThreadMul9Alignment].Slice(0, rowsPerThread * 9); - Span bpResidual = stackalloc int[rowsPerThreadMul8Alignment].Slice(0, rowsPerThread * 8); - Span bpOutEven = stackalloc int[rowsPerThreadMul8Alignment].Slice(0, rowsPerThread * 8); - Span bpOutOdd = stackalloc int[rowsPerThreadMul8Alignment].Slice(0, rowsPerThread * 8); - Span bpOutEvenT = stackalloc int[rowsPerThreadMul8Alignment].Slice(0, rowsPerThread * 8); - Span bpOutOddT = stackalloc int[rowsPerThreadMul8Alignment].Slice(0, rowsPerThread * 8); - ref int bpOutEvenTRef = ref MemoryMarshal.GetReference(bpOutEvenT); ref int bpOutOddTRef = ref MemoryMarshal.GetReference(bpOutOddT); int n = Vector256.Count; - if (inputResidualChannel.Width > 16 && rows == rowsPerThread) + if (inputResidualChannel.Width > 16 && rows == rowsPerBlock) { for (; x < inputResidualChannel.Width - 9; x += 8) { JxlSimdUtils.Transpose8x8Block(pResidual[x..], bpResidual, onerow_inr); JxlSimdUtils.Transpose8x8Block(pAverage[x..], bpAvg, onerow_in); - for (int y = 0; y < rowsPerThread; y++) + for (int y = 0; y < rowsPerBlock; y++) { bpAvg[64 + y] = pAverage[x + 8 + (onerow_in * y)]; } @@ -290,12 +286,12 @@ public static void InverseHorizontalSqueeze(Configuration configuration, JxlModu JxlSimdUtils.Transpose8x8Block(bpOutEven, bpOutEvenT, 8); JxlSimdUtils.Transpose8x8Block(bpOutOdd, bpOutOddT, 8); - for (int y = 0; y < rowsPerThread; y++) + for (int y = 0; y < rowsPerBlock; y++) { // y * 8 int y8 = y << 3; - for (int i = 0; i < rowsPerThread; i += n) + for (int i = 0; i < rowsPerBlock; i += n) { int offset = y8 + i; @@ -315,7 +311,7 @@ public static void InverseHorizontalSqueeze(Configuration configuration, JxlModu { UnsqueezeRow(y0 + y, x); } - }); + } input.Channels[c] = outputChannel; @@ -392,14 +388,15 @@ public static void InverseVerticalSqueeze(Configuration configuration, JxlModula return; } - // The number of columns a single parallel iteration computes - // is stored here. - const int colsPerThread = 8; + // Process one vector-width column block at a time. The previous output row + // remains available for prediction while traversal stays on the calling thread. + const int columnsPerBlock = 8; + int columnBlockCount = JxlMath.DivCeil(inputChannel.Width, columnsPerBlock); - _ = Parallel.For(0, JxlMath.DivCeil(inputChannel.Width, colsPerThread), configuration.GetParallelOptions(), idx => + for (int idx = 0; idx < columnBlockCount; idx++) { - int x0 = idx * colsPerThread; - int x1 = Math.Min((idx + 1) * colsPerThread, inputChannel.Width); + int x0 = idx * columnsPerBlock; + int x1 = Math.Min((idx + 1) * columnsPerBlock, inputChannel.Width); int w = x1 - x0; for (int y = 0; y < inputResidualChannel.Height; y++) @@ -439,7 +436,7 @@ public static void InverseVerticalSqueeze(Configuration configuration, JxlModula pNOut[x] = output - diff; } } - }); + } if ((outputChannel.Height & 1) > 0) { diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/ImageOperationsTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/ImageOperationsTests.cs index df8741c548..a47ebc8c63 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/ImageOperationsTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/ImageOperationsTests.cs @@ -3,6 +3,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Group; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using SixLabors.ImageSharp.Memory; @@ -12,6 +13,38 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; public class ImageOperationsTests { + /// + /// Checks that groups reuse coefficient scratch and release replaced buffers when it grows. + /// + [Fact] + public void GroupScratch_ReusesAndReleasesAllocations() + { + TestMemoryAllocator allocator = new(); + allocator.EnableNonThreadSafeLogging(); + Configuration configuration = TestEnvironment.Configuration.Clone(); + configuration.MemoryAllocator = allocator; + + using (JxlGroupDecoderCache cache = new(configuration)) + { + int smallTransform = JxlAcStrategy.GetTypeBit(JxlAcStrategyType.DCT); + cache.InitializeOnce(0, smallTransform); + Assert.Equal(3, allocator.AllocationLog.Count); + + cache.InitializeOnce(0, smallTransform); + Assert.Equal(3, allocator.AllocationLog.Count); + Assert.Empty(allocator.ReturnLog); + + // A larger transform replaces the three coefficient buffers. The previous owners + // must be returned immediately rather than retained until the cache is disposed. + cache.InitializeOnce(0, JxlAcStrategy.GetTypeBit(JxlAcStrategyType.DCT32X32)); + Assert.Equal(6, allocator.AllocationLog.Count); + Assert.Equal(3, allocator.ReturnLog.Count); + } + + Assert.Equal(6, allocator.ReturnLog.Count); + Assert.Equal(0, allocator.LiveBufferBytes); + } + /// /// Checks typed allocation, distinct rows, sample offsets, and initialized vector padding. /// From 8d9a8bda3d326758a928c81cf8b03b04e633109e Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 28 Oct 2026 23:24:41 +1000 Subject: [PATCH 230/328] Correct JPEG XL bit reads and extension length validation Preserve cross-block peeks and exact consumed-bit positions, handle short reads and full-width fields, and skip ranges beyond one reservoir block. Validate extension sums and signed end positions at the parser boundary and remove the unused general SafeAdd overloads. Propagate failed header visits and incomplete input from CanRead. Add fixed field and reservoir regression cases. Fixed extension vectors were independently checked against the U64 encoding. Release compilation remains blocked by 48 declaration errors; C# runtime tests have not run. --- .../Formats/Jxl/Fields/JxlBundle.cs | 16 +- .../Formats/Jxl/Fields/JxlReadVisitor.cs | 65 +++-- .../Jxl/Processing/Decoder/JxlBitReader.cs | 261 +++++++++++------- .../Formats/Jxl/Processing/JxlMath.cs | 66 ----- .../Formats/Jxl/IO/FieldsTests.cs | 65 +++++ .../Formats/Jxl/Processing/BitsTests.cs | 116 ++++++++ 6 files changed, 389 insertions(+), 200 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs index f7b8c921b1..e91af81bf0 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs @@ -98,16 +98,24 @@ public static bool CanEncode(IJxlFields fields, ref int extensionBits, ref long } /// - /// Tries to read the fields from a bit-reader. + /// Reads fields and reports whether they are complete and valid. /// /// The bit-reader. /// The fields. - /// Status of the read operation. + /// Whether all fields are complete and valid. + /// The operation advances the reader and updates the supplied fields. It does not restore either on failure. public static bool CanRead(JxlBitReader reader, IJxlFields fields) { JxlReadVisitor visitor = new(reader); - _ = visitor.Visit(fields); - return visitor.OK; + + try + { + return visitor.Visit(fields) && visitor.OK; + } + catch (EndOfStreamException) + { + return false; + } } public static bool Write(IJxlFields fields, JxlBitWriter writer, JxlLayerType layer, JxlAuxiliaryOutput auxOutput) diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs index d54dba6706..e8ef0488fa 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs @@ -1,48 +1,66 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.Processing; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; namespace SixLabors.ImageSharp.Formats.Jxl.Fields; +/// +/// Reads serialized JPEG XL fields and validates extension bit budgets. +/// +/// The bit reader for the current bounded header. internal sealed class JxlReadVisitor(JxlBitReader reader) : JxlVisitorBase { + /// + /// The total declared extension payload length in bits. + /// private ulong totalExtensionBits; - private bool notEnoughBytes; + + /// + /// The bit position after the extension lengths, or zero when no extensions are present. + /// private long posAfterExtSize; - private readonly ulong[] extensionBits = new ulong[JxlBundle.MaxExtensions]; - public bool OK { get; private set; } + /// + /// Gets whether decoded field values satisfy their format constraints. + /// + public bool OK { get; private set; } = true; + /// public override bool IsReading => true; + /// public override bool Bits(int bits, uint defaultValue, ref uint value) { value = JxlBitsCoder.Read(bits, reader); - return this.ThrowIfEndOfStreamOrReturnTrue(); + return this.OK; } + /// public override bool U32(JxlU32Enc enc, uint defaultValue, ref uint value) { value = JxlU32Coder.Read(enc, reader); - return this.ThrowIfEndOfStreamOrReturnTrue(); + return this.OK; } + /// public override bool U64(ulong defaultValue, ref ulong value) { value = JxlU64Coder.Read(reader); - return this.ThrowIfEndOfStreamOrReturnTrue(); + return this.OK; } + /// public override bool F16(float defaultValue, ref float value) { this.OK &= JxlF16Coder.Read(reader, ref value); - return this.ThrowIfEndOfStreamOrReturnTrue(); + return this.OK; } + /// public override void SetDefault(IJxlFields fields) => JxlBundle.SetDefault(fields); + /// public override bool BeginExtensions(ref ulong extensions) { if (!base.BeginExtensions(ref extensions)) @@ -57,24 +75,28 @@ public override bool BeginExtensions(ref ulong extensions) for (ulong remainingExtensions = extensions; remainingExtensions != 0; remainingExtensions &= remainingExtensions - 1) { - ulong idxExtension = JxlMath.Num0BitsBelowLS1Bit_Nonzero(remainingExtensions); - if (!this.U64(0, ref this.extensionBits[idxExtension])) + ulong extensionBits = 0; + + if (!this.U64(0, ref extensionBits)) { return false; } - if (!JxlMath.SafeAdd(this.totalExtensionBits, this.extensionBits[idxExtension], ref this.totalExtensionBits)) + // Each set flag carries one length. Check remaining UInt64 capacity before adding + // the untrusted length; wrapped totals would skip less data than the header declares. + if (extensionBits > ulong.MaxValue - this.totalExtensionBits) { - DebugGuard.IsTrue(false, "Extension bits overflow; the codestream is not valid"); - return false; } + + this.totalExtensionBits += extensionBits; } this.posAfterExtSize = reader.TotalBitsConsumed; return this.posAfterExtSize != 0; } + /// public override bool EndExtensions() { if (!base.EndExtensions()) @@ -87,25 +109,18 @@ public override bool EndExtensions() return true; } - if (this.notEnoughBytes) - { - return true; - } - long bitsRead = reader.TotalBitsConsumed; - long end = 0; - if (!JxlMath.SafeAdd(this.posAfterExtSize, this.totalExtensionBits, ref end)) + // Bit positions use Int64. Reject lengths that cannot form a representable end position + // before converting or adding them. Invalid header lengths are not debug-only failures. + if (this.totalExtensionBits > (ulong)(long.MaxValue - this.posAfterExtSize)) { - DebugGuard.IsTrue(false, "Invalid extension size."); - return false; } + long end = this.posAfterExtSize + (long)this.totalExtensionBits; if (bitsRead > end) { - DebugGuard.IsTrue(false, "Read more extension bits than budgeted"); - return false; } @@ -113,7 +128,7 @@ public override bool EndExtensions() if (remainingBits != 0) { - reader.SkipBits64((uint)remainingBits); + reader.SkipBits64((ulong)remainingBits); } return true; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs index 0f199f27cc..2a28e772ff 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs @@ -6,165 +6,216 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; /// -/// Represents a bitstream reader. +/// Reads least-significant-bit-first JPEG XL fields from a caller-owned stream. /// +/// The stream bounded to the current codestream or section. +/// +/// Prefetch can advance the stream, but only reads and skips advance the logical bit position. +/// The caller owns the stream and must keep it alive while using this reader. +/// internal class JxlBitReader(Stream stream) { + /// + /// The next unread bits, starting at bit zero. + /// private ulong buffer; - private uint bufferRemainingBits; - private int pointer; /// - /// Gets the total number of bits consumed. + /// The number of unread bits in the current buffer. + /// + private int bufferRemainingBits; + + /// + /// Prefetched bits following the current buffer. + /// + private ulong lookahead; + + /// + /// The number of valid bits in the lookahead buffer. + /// + private int lookaheadBits; + + /// + /// The number of bytes fetched from the stream. /// - public long TotalBitsConsumed => ((long)this.pointer * 8) + (64 - this.bufferRemainingBits); + private long bytesFetched; /// - /// Fetches a new buffer. + /// Gets the number of bits consumed, excluding prefetched bits. /// - private void RefillCore() + public long TotalBitsConsumed => (this.bytesFetched * 8) - this.bufferRemainingBits - this.lookaheadBits; + + /// + /// Reads up to one complete reservoir block, including streams that return short reads. + /// + /// The little-endian block, with unused high bits cleared. + /// The number of valid bits in the block. + private void RefillCore(out ulong value, out int bits) { - Span temp = stackalloc byte[8]; - int bytesRead = stream.Read(temp); - if (bytesRead == 8) - { - this.buffer = BinaryPrimitives.ReadUInt64LittleEndian(temp); - this.bufferRemainingBits = 64u; - this.pointer += 8; - } - else + Span bytes = stackalloc byte[sizeof(ulong)]; + int count = 0; + + while (count < bytes.Length) { - if (bytesRead == 0) - { - throw new EndOfStreamException(); - } + int read = stream.Read(bytes[count..]); - ulong value = 0; - for (int i = 0; i < bytesRead; i++) + if (read == 0) { - value |= (ulong)temp[i] << (8 * i); + break; } - this.buffer = value; - this.bufferRemainingBits = (uint)(bytesRead * 8); - this.pointer += bytesRead; + count += read; } + + // Stream.Read may return fewer bytes than requested without reaching EOF. Only a zero + // result ends the refill. Clear unused high bytes so no uninitialized stack data is read. + bytes[count..].Clear(); + value = BinaryPrimitives.ReadUInt64LittleEndian(bytes); + bits = count * 8; + this.bytesFetched += count; } - public void JumpToByteBoundary() + /// + /// Ensures that the requested field is buffered without consuming any bits. + /// + /// The field width, from one to 64 bits. + private void EnsureBits(int bits) { - uint remainder = (uint)(this.TotalBitsConsumed % 8); - - if (remainder == 0) + if (this.bufferRemainingBits == 0) { - return; + this.RefillCore(out this.buffer, out this.bufferRemainingBits); } - if (this.ReadBits32(8u - remainder) != 0) + if (bits > this.bufferRemainingBits && this.lookaheadBits == 0) { - throw new InvalidDataException("Non-zero padding bits"); + this.RefillCore(out this.lookahead, out this.lookaheadBits); } - } - private void MaybeRefill() - { - if (this.bufferRemainingBits <= 0) + // A short final block cannot satisfy a larger field. Preserve the buffered bits on + // failure; neither their count nor the logical position may wrap or advance. + if (bits > this.bufferRemainingBits + this.lookaheadBits) { - this.RefillCore(); + throw new EndOfStreamException(); } } - private ulong ReadBits64Core(uint n, bool peek = false) + /// + /// Reads or peeks one field using the same reservoir and bit ordering. + /// + /// The field width, from zero to 64 bits. + /// Whether to leave the logical position unchanged. + /// The unsigned field value. + private ulong ReadBitsCore(int bits, bool peek) { - DebugGuard.MustBeLessThanOrEqualTo(n, 64u, nameof(n)); - this.MaybeRefill(); - - if (n <= this.bufferRemainingBits) + if (bits == 0) { - ulong result = this.buffer & ((1UL << (int)n) - 1); - - if (!peek) - { - this.buffer >>= (int)n; - this.bufferRemainingBits -= n; - } - - return result; + return 0; } - else - { - uint bitsFromCurrent = this.bufferRemainingBits; - ulong part = this.buffer & ((1UL << (int)bitsFromCurrent) - 1); - - this.buffer >>= (int)bitsFromCurrent; - this.bufferRemainingBits = 0; - this.RefillCore(); + this.EnsureBits(bits); - uint bitsFromNext = n - bitsFromCurrent; - ulong nextPart = this.buffer & ((1UL << (int)bitsFromNext) - 1); - - if (!peek) - { - this.buffer >>= (int)bitsFromNext; - this.bufferRemainingBits -= bitsFromNext; - } + // Current bits occupy the low end. A field crossing the block boundary continues + // into lookahead. Keep both blocks until consumption so repeated peeks are identical. + ulong value = this.buffer; - return part | (nextPart << (int)bitsFromCurrent); + if (bits > this.bufferRemainingBits) + { + value |= this.lookahead << this.bufferRemainingBits; } - } - private uint ReadBits32Core(uint n, bool peek = false) - { - DebugGuard.MustBeLessThanOrEqualTo(n, 32u, nameof(n)); - this.MaybeRefill(); + // This mask also handles a full 64-bit field. Shifting 1UL by 64 would wrap to bit zero. + value &= ulong.MaxValue >> (64 - bits); - if (n <= this.bufferRemainingBits) + if (peek) { - uint result = (uint)(this.buffer & ((1UL << (int)n) - 1)); - - if (!peek) - { - this.buffer >>= (int)n; - this.bufferRemainingBits -= n; - } + return value; + } - return result; + if (bits < this.bufferRemainingBits) + { + this.buffer >>= bits; + this.bufferRemainingBits -= bits; } else { - uint bitsFromCurrent = this.bufferRemainingBits; - uint part = (uint)(this.buffer & ((1UL << (int)bitsFromCurrent) - 1)); - - this.buffer >>= (int)bitsFromCurrent; - this.bufferRemainingBits = 0; - - this.RefillCore(); + // Promote remaining lookahead bits after consuming the current block. The shift + // is at most 63: a crossing field always consumes at least one current-buffer bit. + int consumedLookahead = bits - this.bufferRemainingBits; + this.buffer = this.lookahead >> consumedLookahead; + this.bufferRemainingBits = this.lookaheadBits - consumedLookahead; + this.lookahead = 0; + this.lookaheadBits = 0; + } - uint bitsFromNext = n - bitsFromCurrent; - uint nextPart = (uint)(this.buffer & ((1UL << (int)bitsFromNext) - 1)); + return value; + } - if (!peek) - { - this.buffer >>= (int)bitsFromNext; - this.bufferRemainingBits -= bitsFromNext; - } + /// + /// Consumes zero padding up to the next byte boundary. + /// + /// The alignment bits are not zero. + public void JumpToByteBoundary() + { + int padding = (int)((8 - (this.TotalBitsConsumed & 7)) & 7); - return part | (nextPart << (int)bitsFromCurrent); + if (this.ReadBits32((uint)padding) != 0) + { + throw new InvalidImageContentException("JPEG XL byte-alignment padding is not zero."); } } - public virtual uint ReadBits32(uint bits) => this.ReadBits32Core(bits, peek: false); + /// + /// Reads an unsigned field of up to 32 bits. + /// + /// The field width, from zero to 32 bits. + /// The field value. + public virtual uint ReadBits32(uint bits) => (uint)this.ReadBitsCore((int)bits, false); - public virtual uint PeekBits32(uint bits) => this.ReadBits32Core(bits, peek: true); + /// + /// Peeks an unsigned field of up to 32 bits without consuming it. + /// + /// The field width, from zero to 32 bits. + /// The field value. + public virtual uint PeekBits32(uint bits) => (uint)this.ReadBitsCore((int)bits, true); - public virtual void SkipBits32(uint bits) => _ = this.ReadBits32(bits); + /// + /// Skips the requested number of bits. + /// + /// The number of bits to skip. + public virtual void SkipBits32(uint bits) => this.SkipBits64(bits); - public virtual ulong ReadBits64(ulong bits) => this.ReadBits64Core((uint)bits, peek: false); + /// + /// Reads an unsigned field of up to 64 bits. + /// + /// The field width, from zero to 64 bits. + /// The field value. + public virtual ulong ReadBits64(ulong bits) => this.ReadBitsCore((int)bits, false); - public virtual ulong PeekBits64(ulong bits) => this.ReadBits64Core((uint)bits, peek: true); + /// + /// Peeks an unsigned field of up to 64 bits without consuming it. + /// + /// The field width, from zero to 64 bits. + /// The field value. + public virtual ulong PeekBits64(ulong bits) => this.ReadBitsCore((int)bits, true); - public virtual void SkipBits64(ulong bits) => _ = this.ReadBits64(bits); + /// + /// Skips a bit range, including ranges longer than one reservoir block. + /// + /// The number of bits to skip. + public virtual void SkipBits64(ulong bits) + { + while (bits >= 64) + { + this.ReadBitsCore(64, false); + bits -= 64; + } - public virtual bool ReadBoolean() => this.ReadBits32Core(1, peek: false) == 1; + this.ReadBitsCore((int)bits, false); + } + + /// + /// Reads the next Boolean field. + /// + /// Whether the next bit is one. + public virtual bool ReadBoolean() => this.ReadBits32(1) != 0; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs index e8582d8cc0..a9d592f19e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs @@ -213,72 +213,6 @@ public static bool SafeAdd(int a, int b, out int sum) } } - /// - /// Performs addition and returns a boolean indicating whether the addition led to - /// an overflow. - /// - /// The augend - /// The addend - /// The resulting sum of addition. - /// A boolean indicating whether the addition led to an overflow. - /// - /// This method is meant for unsigned addition only. Negative values - /// won't work properly. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool SafeAdd(long a, long b, out long sum) - { - unchecked - { - sum = a + b; - return sum >= a; - } - } - - /// - /// Performs addition and returns a boolean indicating whether the addition led to - /// an overflow. - /// - /// The augend - /// The addend - /// The resulting sum of addition. - /// A boolean indicating whether the addition led to an overflow. - /// - /// This method is meant for unsigned addition only. Negative values - /// won't work properly. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool SafeAdd(uint a, uint b, out uint sum) - { - unchecked - { - sum = a + b; - return sum >= a; - } - } - - /// - /// Performs addition and returns a boolean indicating whether the addition led to - /// an overflow. - /// - /// The augend - /// The addend - /// The resulting sum of addition. - /// A boolean indicating whether the addition led to an overflow. - /// - /// This method is meant for unsigned addition only. Negative values - /// won't work properly. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool SafeAdd(ulong a, ulong b, out ulong sum) - { - unchecked - { - sum = a + b; - return sum >= a; - } - } - /// /// Rounds the dimensions up to block dimensions. /// diff --git a/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs index b715b48a60..b09453f914 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs @@ -13,6 +13,71 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jxl.IO; public class FieldsTests { + /// + /// Checks that probing an incomplete header reports failure without accepting default field status. + /// + [Fact] + public void CanRead_RejectsIncompleteHeader() + { + using MemoryStream input = new(); + JxlBitReader reader = new(input); + + Assert.False(JxlBundle.CanRead(reader, new OldBundle())); + } + + /// + /// Checks that unknown extension data is skipped using its full declared bit length. + /// + [Fact] + public void ReadExtensions_SkipsRemainingPayload() + { + // U64 flag value 1 uses six bits. U64 payload length 5 uses six more bits. + // Five payload bits, 10101, follow. These bytes are fixed LSB-first field data. + using MemoryStream input = new([0x41, 0x54, 0x01]); + JxlBitReader reader = new(input); + JxlReadVisitor visitor = new(reader); + ulong extensions = 0; + + Assert.True(visitor.BeginExtensions(ref extensions)); + Assert.Equal(1UL, extensions); + Assert.Equal(12, reader.TotalBitsConsumed); + Assert.Equal(1U, reader.ReadBits32(2)); + Assert.True(visitor.EndExtensions()); + Assert.Equal(17, reader.TotalBitsConsumed); + } + + /// + /// Checks that extension lengths cannot wrap their accumulated UInt64 bit budget. + /// + [Fact] + public void ReadExtensions_RejectsAccumulatedLengthOverflow() + { + // Flag value 3 selects two extensions. Their lengths are UInt64.MaxValue and one. + // A wrapped sum of zero would incorrectly accept the header without skipping its payload. + using MemoryStream input = new([0xC9, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00]); + JxlReadVisitor visitor = new(new JxlBitReader(input)); + ulong extensions = 0; + + Assert.False(visitor.BeginExtensions(ref extensions)); + Assert.Equal(3UL, extensions); + } + + /// + /// Checks that extension ends fit the reader's signed bit-position range. + /// + [Fact] + public void ReadExtensions_RejectsUnrepresentableEnd() + { + // One extension declares UInt64.MaxValue payload bits. Its length fits UInt64, + // but adding the already-consumed header cannot produce an Int64 end position. + using MemoryStream input = new([0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F]); + JxlReadVisitor visitor = new(new JxlBitReader(input)); + ulong extensions = 0; + + Assert.True(visitor.BeginExtensions(ref extensions)); + Assert.False(visitor.EndExtensions()); + } + private sealed class OldBundle : IJxlFields { private uint oldSmall; diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/BitsTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/BitsTests.cs index 22f04cbe81..84fffea3ae 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/BitsTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/BitsTests.cs @@ -2,11 +2,127 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Jxl.Processing; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; public class BitsTests { + /// + /// Checks zero-width, partial-width, and full-width unsigned fields. + /// + /// The field width. + /// The expected low-bit value. + [Theory] + [InlineData(0UL, 0UL)] + [InlineData(1UL, 1UL)] + [InlineData(31UL, 0x7FFFFFFFUL)] + [InlineData(32UL, 0xFFFFFFFFUL)] + [InlineData(63UL, 0x7FFFFFFFFFFFFFFFUL)] + [InlineData(64UL, 0xFFFFFFFFFFFFFFFFUL)] + public void ReadBits64_PreservesFieldWidth(ulong bits, ulong expected) + { + using MemoryStream input = new([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]); + JxlBitReader reader = new(input); + + Assert.Equal(0, reader.TotalBitsConsumed); + Assert.Equal(expected, reader.PeekBits64(bits)); + Assert.Equal(0, reader.TotalBitsConsumed); + Assert.Equal(expected, reader.ReadBits64(bits)); + Assert.Equal((long)bits, reader.TotalBitsConsumed); + } + + /// + /// Checks that cross-block peeks preserve both bits and logical position on short-read streams. + /// + /// Whether each stream read returns at most one byte. + [Theory] + [InlineData(false)] + [InlineData(true)] + public void PeekBits64_PreservesCrossBlockInput(bool shortReads) + { + byte[] bytes = [0xEF, 0xCD, 0xAB, 0x89, 0x67, 0x45, 0x23, 0x01, 0xF1, 0x32, 0x54, 0x76, 0x98, 0xBA, 0xDC, 0xFE]; + using MemoryStream input = shortReads ? new SingleByteReadStream(bytes) : new MemoryStream(bytes); + JxlBitReader reader = new(input); + + Assert.Equal(0UL, reader.ReadBits64(0)); + Assert.Equal(0, input.Position); + Assert.Equal(0xFU, reader.ReadBits32(4)); + + // The next 64 bits contain 60 bits from the first block and the low nibble of 0xF1. + Assert.Equal(0x10123456789ABCDEUL, reader.PeekBits64(64)); + Assert.Equal(0x10123456789ABCDEUL, reader.PeekBits64(64)); + Assert.Equal(4, reader.TotalBitsConsumed); + Assert.Equal(0x10123456789ABCDEUL, reader.ReadBits64(64)); + Assert.Equal(0xFU, reader.ReadBits32(4)); + Assert.Equal(0xFEDCBA98765432UL, reader.ReadBits64(56)); + Assert.Equal(128, reader.TotalBitsConsumed); + Assert.Throws(() => reader.ReadBoolean()); + Assert.Equal(128, reader.TotalBitsConsumed); + } + + /// + /// Checks that an incomplete field does not consume or wrap the available bit count. + /// + [Fact] + public void ReadBits_TruncatedFieldRetainsAvailableBits() + { + using MemoryStream input = new([0x12, 0x34]); + JxlBitReader reader = new(input); + + Assert.Throws(() => reader.ReadBits32(17)); + Assert.Equal(0, reader.TotalBitsConsumed); + Assert.Equal(0x3412U, reader.ReadBits32(16)); + Assert.Equal(16, reader.TotalBitsConsumed); + } + + /// + /// Checks skipping beyond one reservoir block and validating byte-alignment padding. + /// + [Fact] + public void SkipBits64_PreservesFollowingBytes() + { + using MemoryStream input = new([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]); + JxlBitReader reader = new(input); + + reader.SkipBits64(72); + Assert.Equal(72, reader.TotalBitsConsumed); + Assert.Equal(9U, reader.ReadBits32(8)); + Assert.Equal(80, reader.TotalBitsConsumed); + } + + /// + /// Checks the format rule that byte-alignment padding contains only zero bits. + /// + [Fact] + public void JumpToByteBoundary_RequiresZeroPadding() + { + using MemoryStream validInput = new([0x01]); + JxlBitReader valid = new(validInput); + Assert.True(valid.ReadBoolean()); + valid.JumpToByteBoundary(); + Assert.Equal(8, valid.TotalBitsConsumed); + + using MemoryStream invalidInput = new([0x03]); + JxlBitReader invalid = new(invalidInput); + Assert.True(invalid.ReadBoolean()); + Assert.Throws(() => invalid.JumpToByteBoundary()); + } + + /// + /// Returns at most one byte per read to exercise the Stream short-read contract. + /// + /// The fixed input bytes. + private sealed class SingleByteReadStream(byte[] bytes) : MemoryStream(bytes) + { + /// + /// Reads at most one byte into the supplied buffer. + /// + /// The destination buffer. + /// The number of bytes read. + public override int Read(Span buffer) => base.Read(buffer[..Math.Min(buffer.Length, 1)]); + } + [Fact] public void TestNumZeroBits() { From 520873123658f6b7cf977526ec44f6903b88e765 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 09:53:41 +1000 Subject: [PATCH 231/328] Reuse shared RGB primary coordinates in JPEG XL --- .../Formats/Jxl/Cms/JxlCieXyPrimaries.cs | 27 ---------- .../Formats/Jxl/Cms/JxlColorEncoding.cs | 51 ++++++++----------- 2 files changed, 20 insertions(+), 58 deletions(-) delete mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlCieXyPrimaries.cs diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlCieXyPrimaries.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlCieXyPrimaries.cs deleted file mode 100644 index 7e5a4367e1..0000000000 --- a/src/ImageSharp/Formats/Jxl/Cms/JxlCieXyPrimaries.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.ColorProfiles; - -namespace SixLabors.ImageSharp.Formats.Jxl.Cms; - -/// -/// RGB primaries for CIEXY -/// -internal struct JxlCieXyPrimaries -{ - /// - /// Gets or sets the R component - /// - public CieXyChromaticityCoordinates R { get; set; } - - /// - /// Gets or sets the G component - /// - public CieXyChromaticityCoordinates G { get; set; } - - /// - /// Gets or sets the B component - /// - public CieXyChromaticityCoordinates B { get; set; } -} diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs index 9a87dccc2d..2139794e46 100644 --- a/src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.ColorProfiles; + namespace SixLabors.ImageSharp.Formats.Jxl.Cms; internal sealed class JxlColorEncoding @@ -33,44 +35,31 @@ internal sealed class JxlColorEncoding public int Channels => (this.ColorSpace == JxlColorSpace.Gray) ? 1 : 3; - public bool TryGetPrimaries(out JxlCieXyPrimaries xy) + /// + /// Gets the red, green, and blue primary chromaticities from the decoded color-encoding fields. + /// + /// The primary chromaticities, or the default value when no primary fields are available. + /// Whether decoded color fields are available and the color space supplies RGB primaries. + public bool TryGetPrimaries(out RgbPrimariesChromaticityCoordinates primaries) { - xy = default; + primaries = default; - if (!this.HasPrimaries || !this.HasPrimaries) + // An ICC-only encoding has no decoded primary fields. Gray and XYB do not supply RGB primary fields either. + if (!this.HaveFields || !this.HasPrimaries) { return false; } - switch (this.Primaries) + // Preserve the coordinates used by libjxl color_encoding_cms.h. Its sRGB coordinates differ slightly + // from the ideal standard coordinates, so replacing them with a working-space preset would change this conversion. + primaries = this.Primaries switch { - case JxlPrimaries.Custom: - xy.R = this.Red.GetValue(); - xy.G = this.Green.GetValue(); - xy.B = this.Blue.GetValue(); - break; - - case JxlPrimaries.SRgb: - xy.R = new(0.639998686f, 0.330010138f); - xy.G = new(0.300003784f, 0.600003357f); - xy.B = new(0.150002046f, 0.059997204f); - break; - - case JxlPrimaries.Bt2020: - xy.R = new(0.708f, 0.292f); - xy.G = new(0.170f, 0.797f); - xy.B = new(0.131f, 0.046f); - break; - - case JxlPrimaries.P3: - xy.R = new(0.680f, 0.320f); - xy.G = new(0.265f, 0.690f); - xy.B = new(0.150f, 0.060f); - break; - - default: - throw new InvalidOperationException("Invalid primaries: " + this.Primaries); - } + JxlPrimaries.Custom => new(this.Red.GetValue(), this.Green.GetValue(), this.Blue.GetValue()), + JxlPrimaries.SRgb => new(new(0.639998686F, 0.330010138F), new(0.300003784F, 0.600003357F), new(0.150002046F, 0.059997204F)), + JxlPrimaries.Bt2020 => new(new(0.708F, 0.292F), new(0.170F, 0.797F), new(0.131F, 0.046F)), + JxlPrimaries.P3 => new(new(0.680F, 0.320F), new(0.265F, 0.690F), new(0.150F, 0.060F)), + _ => throw new InvalidOperationException("Invalid primaries: " + this.Primaries) + }; return true; } From a0c99a3a0aaacf015fba256ea8b76bf6b84bda5a Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 09:57:46 +1000 Subject: [PATCH 232/328] Reuse shared JPEG XL color contracts and remove unused CMS shell --- .../Jxl/Cms/JxlChromaticAdaptationMatrices.cs | 26 ------------ src/ImageSharp/Formats/Jxl/Cms/JxlCmsImpl.cs | 41 ------------------- .../Formats/Jxl/Cms/JxlColorEncoding.cs | 6 ++- .../Jxl/Cms/JxlExtraTransferFunction.cs | 32 --------------- .../Formats/Jxl/Cms/JxlRenderingIntent.cs | 13 ------ src/ImageSharp/Formats/Jxl/Cms/README.md | 4 -- .../Jxl/TestUtils/ColorEncodingDescriptor.cs | 6 ++- 7 files changed, 10 insertions(+), 118 deletions(-) delete mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlChromaticAdaptationMatrices.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlCmsImpl.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlExtraTransferFunction.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlRenderingIntent.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Cms/README.md diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlChromaticAdaptationMatrices.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlChromaticAdaptationMatrices.cs deleted file mode 100644 index 9c75d633b7..0000000000 --- a/src/ImageSharp/Formats/Jxl/Cms/JxlChromaticAdaptationMatrices.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; - -namespace SixLabors.ImageSharp.Formats.Jxl.Cms; - -/// -/// Chromatic adaption matrix lookups. -/// -internal static class JxlChromaticAdaptationMatrices -{ - public static readonly JxlMatrix3x3F Bradford = new( - [ - [0.8951f, 0.2664f, -0.1614f], - [-0.7502f, 1.7135f, 0.0367f], - [0.0389f, -0.0685f, 1.0296f] - ]); - - public static readonly JxlMatrix3x3F InverseBradford = new( - [ - [0.9869929f, -0.1470543f, 0.1599627f], - [0.9869929f, -0.1470543f, 0.1599627f], - [-0.0085287f, 0.0400428f, 0.9684867f] - ]); -} diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlCmsImpl.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlCmsImpl.cs deleted file mode 100644 index 87986aa742..0000000000 --- a/src/ImageSharp/Formats/Jxl/Cms/JxlCmsImpl.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Jxl.Cms; - -/// -/// Implementation of the Color Management System. -/// -// TODO -internal class JxlCmsImpl -{ - public Memory IccSource { get; set; } - - public Memory IccDestination { get; set; } - - public bool ApplyHlgOotf { get; set; } - - public int HlgOotfNumChannels { get; set; } - - public float[] HlgOotfLuminances { get; set; } = new float[3]; - - public int ChannelsSource { get; set; } - - public int ChannelsDestination { get; set; } - - public List SourceStorage { get; set; } = []; - - public List> BufferSource { get; set; } = []; - - public List DestinationStorage { get; set; } = []; - - public List> BufferDestination { get; set; } = []; - - public float IntensityTarget { get; set; } - - public bool SkipLCms { get; set; } - - public JxlExtraTransferFunction PreProcess { get; set; } = JxlExtraTransferFunction.None; - - public JxlExtraTransferFunction PostProcess { get; set; } = JxlExtraTransferFunction.None; -} diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs index 2139794e46..e8cff64d07 100644 --- a/src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; namespace SixLabors.ImageSharp.Formats.Jxl.Cms; @@ -11,7 +12,10 @@ internal sealed class JxlColorEncoding public JxlPrimaries Primaries { get; set; } = JxlPrimaries.SRgb; - public JxlRenderingIntent RenderingIntent { get; set; } = JxlRenderingIntent.Relative; + /// + /// Gets or sets the rendering intent requested by the color encoding. + /// + public IccRenderingIntent RenderingIntent { get; set; } = IccRenderingIntent.MediaRelativeColorimetric; public bool HaveFields { get; set; } = true; diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlExtraTransferFunction.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlExtraTransferFunction.cs deleted file mode 100644 index c3e1705400..0000000000 --- a/src/ImageSharp/Formats/Jxl/Cms/JxlExtraTransferFunction.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Formats.Jxl.Cms.TransferFunctions; - -namespace SixLabors.ImageSharp.Formats.Jxl.Cms; - -/// -/// Specifies the kind of extra transfer function. -/// -internal enum JxlExtraTransferFunction : byte -{ - /// - /// No transfer functions. - /// - None, - - /// - /// Perceptual Quantization. - /// - Pq, - - /// - /// Hybrid Log Gamma. . - /// - Hlg, - - /// - /// sRGB. . - /// - SRgb, -} diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlRenderingIntent.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlRenderingIntent.cs deleted file mode 100644 index 739dddcccd..0000000000 --- a/src/ImageSharp/Formats/Jxl/Cms/JxlRenderingIntent.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Jxl.Cms; - -internal enum JxlRenderingIntent : byte -{ - // Values match ICC sRGB encodings - Perceptual, // Good for photos, requires a profile with LUT - Relative, // Good for logos - Saturation, // Perhaps useful for CG with fully saturated colors - Absolute, // Leaves white point unchanged; good for proofing -} diff --git a/src/ImageSharp/Formats/Jxl/Cms/README.md b/src/ImageSharp/Formats/Jxl/Cms/README.md deleted file mode 100644 index 557a4c8b91..0000000000 --- a/src/ImageSharp/Formats/Jxl/Cms/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# CMS -This is the JPEG XL Color Management System component. - -Not to be confused with Content Management System. diff --git a/tests/ImageSharp.Tests/Formats/Jxl/TestUtils/ColorEncodingDescriptor.cs b/tests/ImageSharp.Tests/Formats/Jxl/TestUtils/ColorEncodingDescriptor.cs index 581faf1a2b..4d8a492bb7 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/TestUtils/ColorEncodingDescriptor.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/TestUtils/ColorEncodingDescriptor.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Jxl.Cms; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; namespace SixLabors.ImageSharp.Tests.Formats.Jxl.TestUtils; @@ -15,5 +16,8 @@ internal struct ColorEncodingDescriptor public JxlTransferFunction TransferFunction { get; set; } - public JxlRenderingIntent RenderingIntent { get; set; } + /// + /// Gets or sets the rendering intent for the test color encoding. + /// + public IccRenderingIntent RenderingIntent { get; set; } } From f143f3a9b8da6005fe9cb90836d73ed3e552e926 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 10:05:52 +1000 Subject: [PATCH 233/328] Remove unused JPEG XL math overloads --- .../Formats/Jxl/Processing/JxlMath.cs | 100 ------------------ 1 file changed, 100 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs index a9d592f19e..147df053cd 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs @@ -73,25 +73,6 @@ public static int DivCeil(int a, int b) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int RoundUpTo(int value, int align) => DivCeil(value, align) * align; - /// - /// Ensures that rounded to the multiple of . - /// - /// The value to round. - /// The allowed step size. - /// - /// , transformed to ensure that it stays within the step size of . - /// - /// - /// - /// If value=4, align=6, the result is 6. If value=7 and align=3 the result is 9. - /// - /// - /// This will not work correctly for negative values. - /// - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static uint RoundUpTo(uint value, uint align) => Numerics.DivideCeil(value, align) * align; - /// /// Performs subtraction & returns a boolean indicating whether or not did the subtraction /// result in an overflow. @@ -158,39 +139,6 @@ public static bool SafeMultiply(int a, int b, out int product) return true; } - /// - /// A slow, but safe multiplication method which returns false on an overflow. - /// - /// The multiplier - /// The multiplicand - /// The resulting value (or 0 on error) - /// - /// If the multiplication leads to an overflow returns false, - /// otherwise returns true and places the output in . - /// - /// - /// This method is meant for unsigned multiplication only. Negative values - /// won't work properly. - /// - public static bool SafeMultiply(uint a, uint b, out uint product) - { - product = 0; - - if (a == 0 || b == 0) - { - return true; - } - - if (b > (uint.MaxValue / a)) - { - return false; - } - - product = a * b; - - return true; - } - /// /// Performs addition and returns a boolean indicating whether the addition led to /// an overflow. @@ -213,34 +161,6 @@ public static bool SafeAdd(int a, int b, out int sum) } } - /// - /// Rounds the dimensions up to block dimensions. - /// - /// The dimensions to round up to block dimensions. - /// The rounded dimensions. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int RoundUpToBlockDimensions(int dim) - { - unchecked - { - return (dim + 7) & ~7; - } - } - - /// - /// Rounds the dimensions up to block dimensions. - /// - /// The dimensions to round up to block dimensions. - /// The rounded dimensions. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static uint RoundUpToBlockDimensions(uint dim) - { - unchecked - { - return (dim + 7u) & ~7u; - } - } - /// /// Rounds the specified number of bits to a multiple of bytes. /// @@ -269,26 +189,6 @@ public static uint RoundUpBitsToByteMultiple(uint bits) } } - /// - /// Multiplies by π. - /// - /// The value to multiply. - /// - /// multiplier * π - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float Pi(float multiplier) => multiplier * MathF.PI; - - /// - /// Multiplies by π. - /// - /// The value to multiply. - /// - /// multiplier * π - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static double Pi(double multiplier) => multiplier * Math.PI; - /// /// Returns the amount of zero bits before the highest 1 bit. /// From 32daad40fe350adb4afb1fc2ceccacfa007e7086 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 10:08:25 +1000 Subject: [PATCH 234/328] Use shared ICC profiles for JPEG XL color metadata --- src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs | 6 +++++- src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegData.cs | 7 ++++++- .../Formats/Jxl/Processing/Decoder/JxlToJpegDecoder.cs | 5 ++++- .../Formats/Jxl/Processing/Image/JxlImageBundle.cs | 2 +- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs index e8cff64d07..e91631258b 100644 --- a/src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs @@ -19,7 +19,11 @@ internal sealed class JxlColorEncoding public bool HaveFields { get; set; } = true; - public JxlIccBytes? Icc { get; set; } + /// + /// Gets or sets the ICC profile reconstructed from the compressed profile payload. + /// + /// The profile contains decoded ICC data. Compressed JPEG XL profile bytes do not belong here. + public IccProfile? IccProfile { get; set; } public JxlColorSpace ColorSpace { get; set; } = JxlColorSpace.Rgb; diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegData.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegData.cs index 4555442bcb..53d8857e4b 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegData.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegData.cs @@ -96,7 +96,12 @@ public void CalculateMcuSize(JpegScanInfo scan, out int mcusPerRow, out int mcuR mcuRows = JxlMath.DivCeil(this.Height * verticalGroup, 8 * maxVSampFactor); } - public static void SetJpegDataFromIcc(Span icc, JpegData jpegData) + /// + /// Restores retained ICC profile bytes into the original JPEG APP marker payloads. + /// + /// The original, decompressed ICC profile bytes. These bytes remain unchanged. + /// The JPEG marker storage to update. + public static void SetJpegDataFromIcc(ReadOnlySpan icc, JpegData jpegData) { int iccPos = 0; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlToJpegDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlToJpegDecoder.cs index cd7a56da76..a8fad18bcd 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlToJpegDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlToJpegDecoder.cs @@ -72,7 +72,10 @@ public void SetImageBundleJpegData(JxlImageBundle bundle) { if (this.IsOutputSet && this.jpegData is not null) { - JpegData.SetJpegDataFromIcc(bundle.Metadata!.ColorEncoding!.Icc, this.jpegData); + // JPEG reconstruction must retain the original profile bytes. The shared profile exposes them + // read-only without copying or rewriting tag order, padding, or the original header. + ReadOnlyMemory icc = bundle.Metadata!.ColorEncoding!.IccProfile?.GetData() ?? ReadOnlyMemory.Empty; + JpegData.SetJpegDataFromIcc(icc.Span, this.jpegData); bundle.JpegData = this.jpegData; } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs index f3f1388fb8..f3e88e2e05 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs @@ -387,7 +387,7 @@ public bool TrySetAlpha(JxlPlane alpha) /// Rare. public bool VerifyMetadata() { - if (this.CurrentColorEncoding?.Icc?.IsEmpty == true) + if (this.CurrentColorEncoding?.IccProfile?.GetData().IsEmpty == true) { return false; } From c34cc6006d0ccb7daac5d3467709ad9e352c90bc Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 10:21:56 +1000 Subject: [PATCH 235/328] Use known RGB working spaces for JPEG XL primaries --- .../ColorProfiles/KnownRgbWorkingSpaces.cs | 12 +++++++++++- src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs | 10 +++++----- .../ColorProfiles/RgbWorkingSpaceTests.cs | 1 + 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/ImageSharp/ColorProfiles/KnownRgbWorkingSpaces.cs b/src/ImageSharp/ColorProfiles/KnownRgbWorkingSpaces.cs index 04058783fb..edff75a47e 100644 --- a/src/ImageSharp/ColorProfiles/KnownRgbWorkingSpaces.cs +++ b/src/ImageSharp/ColorProfiles/KnownRgbWorkingSpaces.cs @@ -7,7 +7,7 @@ namespace SixLabors.ImageSharp.ColorProfiles; /// -/// Chromaticity coordinates based on: +/// Provides predefined RGB working spaces. /// public static class KnownRgbWorkingSpaces { @@ -57,6 +57,16 @@ public static class KnownRgbWorkingSpaces new CieXyChromaticityCoordinates(0.170F, 0.797F), new CieXyChromaticityCoordinates(0.131F, 0.046F))); + /// + /// The Display P3 working space, with P3 primaries, the D65 white point, and the sRGB transfer function. + /// + public static readonly RgbWorkingSpace DisplayP3 = new SRgbWorkingSpace( + KnownIlluminants.D65, + new RgbPrimariesChromaticityCoordinates( + new CieXyChromaticityCoordinates(0.680F, 0.320F), + new CieXyChromaticityCoordinates(0.265F, 0.690F), + new CieXyChromaticityCoordinates(0.150F, 0.060F))); + /// /// ECI Rgb v2 working space. /// diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs index e91631258b..c1077b62ca 100644 --- a/src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs @@ -58,14 +58,14 @@ public bool TryGetPrimaries(out RgbPrimariesChromaticityCoordinates primaries) return false; } - // Preserve the coordinates used by libjxl color_encoding_cms.h. Its sRGB coordinates differ slightly - // from the ideal standard coordinates, so replacing them with a working-space preset would change this conversion. + // JPEG XL signals primaries, white point, and transfer function separately. Read only the primary coordinates + // from the known working spaces. Their white points and transfer functions do not replace the encoded fields. primaries = this.Primaries switch { JxlPrimaries.Custom => new(this.Red.GetValue(), this.Green.GetValue(), this.Blue.GetValue()), - JxlPrimaries.SRgb => new(new(0.639998686F, 0.330010138F), new(0.300003784F, 0.600003357F), new(0.150002046F, 0.059997204F)), - JxlPrimaries.Bt2020 => new(new(0.708F, 0.292F), new(0.170F, 0.797F), new(0.131F, 0.046F)), - JxlPrimaries.P3 => new(new(0.680F, 0.320F), new(0.265F, 0.690F), new(0.150F, 0.060F)), + JxlPrimaries.SRgb => KnownRgbWorkingSpaces.SRgb.ChromaticityCoordinates, + JxlPrimaries.Bt2020 => KnownRgbWorkingSpaces.Rec2020.ChromaticityCoordinates, + JxlPrimaries.P3 => KnownRgbWorkingSpaces.DisplayP3.ChromaticityCoordinates, _ => throw new InvalidOperationException("Invalid primaries: " + this.Primaries) }; diff --git a/tests/ImageSharp.Tests/ColorProfiles/RgbWorkingSpaceTests.cs b/tests/ImageSharp.Tests/ColorProfiles/RgbWorkingSpaceTests.cs index 315b347b62..c00408a390 100644 --- a/tests/ImageSharp.Tests/ColorProfiles/RgbWorkingSpaceTests.cs +++ b/tests/ImageSharp.Tests/ColorProfiles/RgbWorkingSpaceTests.cs @@ -24,6 +24,7 @@ public class RgbWorkingSpaceTests KnownRgbWorkingSpaces.SRgbSimplified, KnownRgbWorkingSpaces.Rec709, KnownRgbWorkingSpaces.Rec2020, + KnownRgbWorkingSpaces.DisplayP3, KnownRgbWorkingSpaces.ECIRgbv2, KnownRgbWorkingSpaces.AdobeRgb1998, KnownRgbWorkingSpaces.ApplesRgb, From ed536d9a1fac42f19f7dbfd21d7fa9cf25de2746 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 10:47:59 +1000 Subject: [PATCH 236/328] Initialize JPEG XL metadata at its owning boundaries --- .../Formats/Jxl/Cms/JxlColorEncoding.cs | 13 +- .../Formats/Jxl/Fields/JxlBundle.cs | 8 +- src/ImageSharp/Formats/Jxl/IO/JxlCodecIO.cs | 6 +- .../Jxl/IO/Metadata/JxlCodecMetadata.cs | 57 ++++++-- .../Jxl/IO/Metadata/JxlImageMetadata.cs | 126 +++++++++--------- .../Formats/Jxl/IO/Metadata/JxlToneMapping.cs | 8 +- .../Processing/Decoder/JxlDecoderInternal.cs | 84 +++++------- .../Jxl/Processing/Decoder/JxlNoiseDecoder.cs | 2 +- .../Decoder/JxlOutputEncodingInfo.cs | 8 +- .../Processing/Decoder/JxlToJpegDecoder.cs | 27 +++- .../Decoder/Modular/JxlModularFrameDecoder.cs | 4 +- .../Comparator/ButteraugliJxlComparator.cs | 10 +- .../Encoder/JxlAdaptiveQuantization.cs | 6 +- .../Encoder/JxlImageBundleEncoder.cs | 6 +- .../Jxl/Processing/Image/JxlImageBundle.cs | 91 +++++++------ .../RenderPipeline/ToneMappingStage.cs | 2 +- .../Formats/Jxl/IO/ImageBundleTests.cs | 70 ++++++++++ 17 files changed, 330 insertions(+), 198 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs index c1077b62ca..0c64760105 100644 --- a/src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs @@ -6,6 +6,9 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Cms; +/// +/// Describes JPEG XL color fields and an optional decoded ICC profile. +/// internal sealed class JxlColorEncoding { public JxlWhitePoint WhitePoint { get; set; } = JxlWhitePoint.D65; @@ -22,14 +25,20 @@ internal sealed class JxlColorEncoding /// /// Gets or sets the ICC profile reconstructed from the compressed profile payload. /// - /// The profile contains decoded ICC data. Compressed JPEG XL profile bytes do not belong here. + /// + /// A null value means that no ICC profile is attached. Named color fields do not require a generated profile. + /// The decoder attaches the decoded profile to the original metadata encoding before it publishes the headers. + /// public IccProfile? IccProfile { get; set; } public JxlColorSpace ColorSpace { get; set; } = JxlColorSpace.Rgb; public bool Cmyk { get; set; } - public JxlCustomTransferFunction TransferFunction { get; set; } + /// + /// Gets or sets the transfer function, which defaults to sRGB when the header uses default color fields. + /// + public JxlCustomTransferFunction TransferFunction { get; set; } = new(); public JxlCustomXy White { get; set; } diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs index e91af81bf0..7b730e933f 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs @@ -153,19 +153,19 @@ public static bool WriteCodestreamHeaders(JxlCodecMetadata metadata, JxlBitWrite return false; } - if (!WriteSizeHeader(metadata.Size!, writer, JxlLayerType.Header, auxOut)) + if (!WriteSizeHeader(metadata.Size, writer, JxlLayerType.Header, auxOut)) { return false; } - if (!WriteImageMetadata(metadata.ImageMetadata!, writer, JxlLayerType.Header, auxOut)) + if (!WriteImageMetadata(metadata.ImageMetadata, writer, JxlLayerType.Header, auxOut)) { return false; } - metadata.CustomTransformData!.NonserializedXybEncoded = metadata.ImageMetadata!.XybEncoded; + metadata.CustomTransformData.NonserializedXybEncoded = metadata.ImageMetadata.XybEncoded; - return Write(metadata.CustomTransformData!, writer, JxlLayerType.Header, auxOut); + return Write(metadata.CustomTransformData, writer, JxlLayerType.Header, auxOut); } public static bool WriteFrameHeader(JxlFrameHeader frame, JxlBitWriter writer, JxlAuxiliaryOutput auxOut) diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlCodecIO.cs b/src/ImageSharp/Formats/Jxl/IO/JxlCodecIO.cs index 215c8808a2..f9d212a6f4 100644 --- a/src/ImageSharp/Formats/Jxl/IO/JxlCodecIO.cs +++ b/src/ImageSharp/Formats/Jxl/IO/JxlCodecIO.cs @@ -42,11 +42,11 @@ public int LastStillFrame public JxlImageBundle Main => this.Frames[this.LastStillFrame]; - public int Width => this.CodecMetadata.Size!.XSize; + public int Width => this.CodecMetadata.Size.XSize; - public int Height => this.CodecMetadata.Size!.YSize; + public int Height => this.CodecMetadata.Size.YSize; - public void SetSize(int width, int height) => this.CodecMetadata.Size!.Set(width, height); + public void SetSize(int width, int height) => this.CodecMetadata.Size.Set(width, height); public void ShrinkTo(int width, int height) { diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCodecMetadata.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCodecMetadata.cs index 5a3aaeecb7..a13dd7d314 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCodecMetadata.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCodecMetadata.cs @@ -3,21 +3,45 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +/// +/// Owns the headers shared by all frames in one codestream. +/// internal sealed class JxlCodecMetadata { - public JxlImageMetadata? ImageMetadata { get; set; } + /// + /// Gets the image metadata shared by the codestream's frames. + /// + /// The parser updates this instance. Frame bundles retain the same metadata throughout decoding. + public JxlImageMetadata ImageMetadata { get; } = new(); - public JxlSizeHeader? Size { get; set; } + /// + /// Gets the size header that the parser fills before decoding frames. + /// + public JxlSizeHeader Size { get; } = new(); - public JxlCustomTransformData? CustomTransformData { get; set; } + /// + /// Gets the transform data that the parser fills before decoding frames. + /// + public JxlCustomTransformData CustomTransformData { get; } = new(); - public int XSize => this.Size?.XSize ?? 0; + /// + /// Gets the encoded image width in pixels. + /// + public int XSize => this.Size.XSize; - public int YSize => this.Size?.YSize ?? 0; + /// + /// Gets the encoded image height in pixels. + /// + public int YSize => this.Size.YSize; + /// + /// Gets the preview width after applying the requested orientation policy. + /// + /// Whether to retain the encoded orientation instead of rotating the samples. + /// The preview width in pixels. public int GetOrientedPreviewXSize(bool keepOrientation) { - if (this.ImageMetadata!.Orientation > 4 && !keepOrientation) + if (this.ImageMetadata.Orientation > 4 && !keepOrientation) { return this.ImageMetadata.PreviewSize.YSize; } @@ -25,9 +49,14 @@ public int GetOrientedPreviewXSize(bool keepOrientation) return this.ImageMetadata.PreviewSize.XSize; } + /// + /// Gets the preview height after applying the requested orientation policy. + /// + /// Whether to retain the encoded orientation instead of rotating the samples. + /// The preview height in pixels. public int GetOrientedPreviewYSize(bool keepOrientation) { - if (this.ImageMetadata!.Orientation > 4 && !keepOrientation) + if (this.ImageMetadata.Orientation > 4 && !keepOrientation) { return this.ImageMetadata.PreviewSize.XSize; } @@ -35,9 +64,14 @@ public int GetOrientedPreviewYSize(bool keepOrientation) return this.ImageMetadata.PreviewSize.YSize; } + /// + /// Gets the image width after applying the requested orientation policy. + /// + /// Whether to retain the encoded orientation instead of rotating the samples. + /// The image width in pixels. public int GetOrientedXSize(bool keepOrientation) { - if (this.ImageMetadata!.Orientation > 4 && !keepOrientation) + if (this.ImageMetadata.Orientation > 4 && !keepOrientation) { return this.YSize; } @@ -45,9 +79,14 @@ public int GetOrientedXSize(bool keepOrientation) return this.XSize; } + /// + /// Gets the image height after applying the requested orientation policy. + /// + /// Whether to retain the encoded orientation instead of rotating the samples. + /// The image height in pixels. public int GetOrientedYSize(bool keepOrientation) { - if (this.ImageMetadata!.Orientation > 4 && !keepOrientation) + if (this.ImageMetadata.Orientation > 4 && !keepOrientation) { return this.XSize; } diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs index 500fefb1c8..800f9d5362 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs @@ -4,28 +4,35 @@ using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Cms; using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.Processing; namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +/// +/// Stores the original image description and color encoding shared by the codestream's frames. +/// internal sealed class JxlImageMetadata : IJxlFields { private bool allDefault; - private JxlBitDepthMetadata? bitDepth; + private JxlBitDepthMetadata bitDepth = new(); private bool modular16BitBufferSufficient; private bool xybEncoded; - private JxlColorEncoding? colorEncoding; + private JxlColorEncoding colorEncoding = new(); private int orientation = 1; private bool havePreview; private bool haveAnimation; private bool haveIntrinsicSize; private JxlSizeHeader intrinsicSize = new(); - private JxlToneMapping? toneMapping; + private JxlToneMapping toneMapping = new(); private int extraChannelCount; private List extraChannels = []; private JxlPreviewHeader previewSize = new(); private JxlAnimationHeader animation = new(); private long extensions; + /// + /// Initializes a new instance of the class with the codestream's default fields. + /// public JxlImageMetadata() => JxlBundle.Init(this); public bool AllDefault @@ -34,7 +41,10 @@ public bool AllDefault set => this.allDefault = value; } - public JxlBitDepthMetadata? BitDepth + /// + /// Gets or sets the original sample representation. + /// + public JxlBitDepthMetadata BitDepth { get => this.bitDepth; set => this.bitDepth = value; @@ -52,7 +62,11 @@ public bool XybEncoded set => this.xybEncoded = value; } - public JxlColorEncoding? ColorEncoding + /// + /// Gets or sets the original color encoding and its optional decoded ICC profile. + /// + /// Converting frame samples changes the bundle's current encoding, not this original encoding. + public JxlColorEncoding ColorEncoding { get => this.colorEncoding; set => this.colorEncoding = value; @@ -88,7 +102,10 @@ public JxlSizeHeader IntrinsicSize set => this.intrinsicSize = value; } - public JxlToneMapping? ToneMapping + /// + /// Gets or sets the intensity and tone-mapping fields. + /// + public JxlToneMapping ToneMapping { get => this.toneMapping; set => this.toneMapping = value; @@ -126,27 +143,13 @@ public long Extensions public bool NonserializedOnlyParseBasicInfos { get; set; } + /// + /// Gets or sets the intensity target in nits. + /// public float IntensityTarget { - get - { - float intensityTarget = this.ToneMapping?.IntensityTarget ?? 0f; - - if (intensityTarget == 0f) - { - throw new InvalidOperationException("Intensity target should be present"); - } - - return intensityTarget; - } - - set - { - if (this.ToneMapping != null) - { - this.ToneMapping.IntensityTarget = value; - } - } + get => this.ToneMapping.IntensityTarget; + set => this.ToneMapping.IntensityTarget = value; } public int AlphaBits @@ -175,60 +178,61 @@ public int AlphaBits public JxlExifOrientation GetExifOrientation() => (JxlExifOrientation)this.Orientation; + /// + /// Specifies half-precision floating-point input samples. + /// public void SetFloat16Samples() { - if (this.BitDepth != null) - { - this.BitDepth.BitsPerSample = 16; - this.BitDepth.ExponentBitsPerSample = 5; - this.BitDepth.FloatingPointSample = true; - } + this.BitDepth.BitsPerSample = 16; + this.BitDepth.ExponentBitsPerSample = 5; + this.BitDepth.FloatingPointSample = true; this.Modular16BitBufferSufficient = false; } + /// + /// Specifies single-precision floating-point input samples. + /// public void SetFloat32Samples() { - if (this.BitDepth != null) - { - this.BitDepth.BitsPerSample = 32; - this.BitDepth.ExponentBitsPerSample = 8; - this.BitDepth.FloatingPointSample = true; - } + this.BitDepth.BitsPerSample = 32; + this.BitDepth.ExponentBitsPerSample = 8; + this.BitDepth.FloatingPointSample = true; this.Modular16BitBufferSufficient = false; } + /// + /// Specifies unsigned integer input samples at the supplied precision. + /// + /// The number of bits per input sample. public void SetUIntSamples(int bits) { - if (this.BitDepth != null) - { - this.BitDepth.BitsPerSample = (uint)bits; - this.BitDepth.ExponentBitsPerSample = 0; - this.BitDepth.FloatingPointSample = false; - } + this.BitDepth.BitsPerSample = (uint)bits; + this.BitDepth.ExponentBitsPerSample = 0; + this.BitDepth.FloatingPointSample = false; this.Modular16BitBufferSufficient = bits <= 12; } + /// + /// Sets the nominal peak intensity for PQ, HLG, or the default transfer function. + /// public void SetIntensityTarget() { - JxlCustomTransferFunction? tf = this.ColorEncoding?.TransferFunction; + JxlCustomTransferFunction tf = this.ColorEncoding.TransferFunction; - if (tf is not null) + if (tf.IsPq) { - if (tf.Value.IsPq) - { - this.SetIntensityTarget(10000); - } - else if (tf.Value.IsHlg) - { - this.SetIntensityTarget(1000); - } - else - { - this.SetIntensityTarget(DefaultIntensityTarget); - } + this.IntensityTarget = 10000; + } + else if (tf.IsHlg) + { + this.IntensityTarget = 1000; + } + else + { + this.IntensityTarget = JxlMath.DefaultIntensityTarget; } } @@ -244,7 +248,7 @@ public bool Visit(JxlVisitor visitor) // Bundle.AllDefault does not allow usage when reading because it may abort // when a codestream has invalid values. When reading, extraFields is // overwritten below, so AllDefault is not needed. - bool toneMappingDefault = !visitor.IsReading && JxlBundle.AllDefault(this.toneMapping!); + bool toneMappingDefault = !visitor.IsReading && JxlBundle.AllDefault(this.toneMapping); bool extraFields = this.orientation != 1 || this.havePreview || @@ -316,7 +320,7 @@ public bool Visit(JxlVisitor visitor) this.haveAnimation = false; } - if (!visitor.VisitNested(this.bitDepth!)) + if (!visitor.VisitNested(this.bitDepth)) { return false; } @@ -366,14 +370,14 @@ ref Unsafe.As(ref this.extraChannelCount))) return false; } - if (!visitor.VisitNested(this.colorEncoding!)) + if (!visitor.VisitNested(this.colorEncoding)) { return false; } if (visitor.Conditional(extraFields)) { - if (!visitor.VisitNested(this.toneMapping!)) + if (!visitor.VisitNested(this.toneMapping)) { return false; } diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlToneMapping.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlToneMapping.cs index 3c3b5a498a..346926a9a8 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlToneMapping.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlToneMapping.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.Processing; namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; @@ -14,6 +15,11 @@ internal sealed class JxlToneMapping : IJxlFields private float linearBelow; private float minNits; + /// + /// Initializes a new instance of the class with the codestream's default intensities. + /// + public JxlToneMapping() => JxlBundle.Init(this); + public bool AllDefault { get => this.allDefault; @@ -59,7 +65,7 @@ public bool Visit(JxlVisitor visitor) return true; } - if (!visitor.F16(JxlFieldExpressions.Value(JxlConstants.DefaultIntensityTarget), ref this.intensityTarget)) + if (!visitor.F16(JxlMath.DefaultIntensityTarget, ref this.intensityTarget)) { return false; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderInternal.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderInternal.cs index 779730875e..542aa710fb 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderInternal.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderInternal.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Buffers; +using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Jxl.Fields; @@ -68,11 +69,6 @@ internal sealed class JxlDecoderInternal : ImageDecoderCore, IDisposable /// private bool postHeaders; - /// - /// ICC profile for JPEG XL metadata, if present. - /// - private IccProfile? iccProfile; - /// /// The frame index box, if present. /// @@ -233,14 +229,9 @@ internal sealed class JxlDecoderInternal : ImageDecoderCore, IDisposable private readonly List extraChannelOutputs = []; /// - /// Codec metadata if present. + /// Metadata owned by the current decode operation and shared with its frame bundles. /// - private JxlCodecMetadata? metadata; - - /// - /// Image metadata if present. - /// - private JxlImageMetadata? imageMetadata; + private JxlCodecMetadata metadata; /// /// The image bundle. @@ -519,13 +510,13 @@ public Size CurrentDimensions if (this.frameHeader?.IsPreviewFrame == true) { - width = this.metadata!.GetOrientedPreviewXSize(this.keepOrientation); - height = this.metadata!.GetOrientedPreviewYSize(this.keepOrientation); + width = this.metadata.GetOrientedPreviewXSize(this.keepOrientation); + height = this.metadata.GetOrientedPreviewYSize(this.keepOrientation); } else { - width = this.metadata!.GetOrientedXSize(this.keepOrientation); - height = this.metadata!.GetOrientedYSize(this.keepOrientation); + width = this.metadata.GetOrientedXSize(this.keepOrientation); + height = this.metadata.GetOrientedYSize(this.keepOrientation); if (!this.coalescing) { @@ -534,7 +525,7 @@ public Size CurrentDimensions width = dim.XSizeUpsampled; height = dim.YSizeUpsampled; - if (!this.keepOrientation && this.metadata.ImageMetadata!.Orientation > 4) + if (!this.keepOrientation && this.metadata.ImageMetadata.Orientation > 4) { RuntimeUtility.Swap(ref width, ref height); } @@ -851,7 +842,7 @@ private static uint GetBitDepth(JxlBitDepth bitDepth, JxlImageMetadata metadata, } else if (bitDepth.Type == JxlBitDepthType.FromCodeStream) { - return metadata.BitDepth!.BitsPerSample; + return metadata.BitDepth.BitsPerSample; } else if (bitDepth.Type == JxlBitDepthType.Custom) { @@ -1239,6 +1230,7 @@ public static bool CheckSizeLimit(int width, int height) /// additionally, releases memory used by buffers and replaces /// them with new fresh copies. /// + [MemberNotNull(nameof(metadata))] public void RewindDecodingState() { this.decoderStage = JxlDecoderStage.Initialized; @@ -1251,8 +1243,6 @@ public void RewindDecodingState() this.gotAllHeaders = false; this.postHeaders = false; - this.iccProfile = null; - this.gotPreviewImage = false; this.previewFrame = false; this.filePosition = 0; @@ -1329,9 +1319,11 @@ public void RewindDecodingState() this.nextSection = 0; this.sectionProcessed.Clear(); - this.imageBundle.Reset(); + this.imageBundle = null; + + // A rewind starts a new codestream. Its frames share this metadata, including the original profile. + // Constructing metadata here prevents a later frame from retaining the previous codestream's profile. this.metadata = new JxlCodecMetadata(); - this.imageMetadata = this.metadata.ImageMetadata; this.frameHeader = new() { @@ -1357,6 +1349,7 @@ public void RewindDecodingState() /// /// Resets the decoder to its default values. /// + [MemberNotNull(nameof(metadata))] public void Reset() { this.RewindDecodingState(); @@ -1520,12 +1513,12 @@ public void ReadBasicInfo(Stream stream) JxlBitReader bitReader = new(stream); - if (!this.ReadBundle(stream, bitReader, this.metadata!.Size!)) + if (!this.ReadBundle(stream, bitReader, this.metadata.Size)) { throw new InvalidDataException("Could not parse the size header"); } - if (!this.ReadBundle(stream, bitReader, this.metadata!.ImageMetadata!)) + if (!this.ReadBundle(stream, bitReader, this.metadata.ImageMetadata)) { throw new InvalidDataException("Could not parse the image metadata"); } @@ -1538,9 +1531,7 @@ public void ReadBasicInfo(Stream stream) this.codestreamBitsAhead = rem; this.gotBasicInfo = true; this.basicInfoSizeHint = 0; - this.imageMetadata = this.metadata.ImageMetadata; - - if (!CheckSizeLimit(this.metadata.Size!.XSize, this.metadata.Size.YSize)) + if (!CheckSizeLimit(this.metadata.Size.XSize, this.metadata.Size.YSize)) { throw new InvalidOperationException("The image is too large"); } @@ -1559,7 +1550,7 @@ public void ReadAllHeaders() JxlBitReader reader = new(this.stream); reader.SkipBits64((ulong)this.codestreamBitsAhead); - this.metadata!.CustomTransformData!.NonserializedXybEncoded = this.metadata.ImageMetadata!.XybEncoded; + this.metadata.CustomTransformData.NonserializedXybEncoded = this.metadata.ImageMetadata.XybEncoded; if (!this.ReadBundle(span, reader, this.metadata.CustomTransformData)) { @@ -1579,20 +1570,16 @@ public void ReadAllHeaders() JxlBitReader bitReader = new(sp); bitReader.SkipBits64((ulong)this.codestreamBitsAhead); - if (this.metadata!.ImageMetadata!.ColorEncoding!.NeedsIcc) + if (this.metadata.ImageMetadata.ColorEncoding.NeedsIcc) { - // TODO: optimize this? ImageSharp ICC doesn't support spans - // so we need to allocate an array. + // TODO: Decode the JPEG XL compressed ICC payload before constructing the profile. IccDataReader reader = new(sp.ToArray()); IccProfileHeader header = IccReader.ReadHeader(reader); IccTagDataEntry[] tagData = IccReader.ReadTagData(reader); - IccProfile icc = new(header, tagData); - this.iccProfile = icc; + // Keep the profile on its original encoding. Frame bundles and JPEG reconstruction share this owner. + this.metadata.ImageMetadata.ColorEncoding.IccProfile = new IccProfile(header, tagData); sp = sp[reader.Index..]; - - byte[] iccRawData = icc.ToByteArray(); - this.metadata.ImageMetadata.ColorEncoding.SetIccRaw(iccRawData); } this.gotAllHeaders = true; @@ -1608,8 +1595,6 @@ public void ReadAllHeaders() { this.passesState.OutputEncodingInfo.DesiredIntensityTarget = this.desiredIntensityTarget; } - - this.imageMetadata = this.metadata.ImageMetadata; } /// @@ -1720,7 +1705,7 @@ public int ProcessCodestream() this.postHeaders = true; - if (!this.gotPreviewImage && this.metadata!.ImageMetadata!.HavePreview) + if (!this.gotPreviewImage && this.metadata.ImageMetadata.HavePreview) { this.previewFrame = true; } @@ -1740,14 +1725,11 @@ public int ProcessCodestream() throw new InvalidOperationException("Cannot decode frames following a JPEG reconstruction frame"); } - this.imageBundle ??= new(this.imageMetadata!); - - if (!this.jpegDecoder.SetImageBundleJpegData(this.imageBundle!)) - { - throw new InvalidOperationException("Cannot set JXL->JPEG decoder image bundle"); - } + // Create a frame bundle only when a frame is needed, after its codestream headers have been read. + this.imageBundle ??= new(this.metadata.ImageMetadata); + this.jpegDecoder.SetImageBundleJpegData(this.imageBundle); - this.frameDecoder = new(this.passesState!, this.metadata!, useSlowRenderingPipeline: false); + this.frameDecoder = new(this.passesState!, this.metadata, useSlowRenderingPipeline: false); this.frameHeader = new() { Metadata = this.metadata @@ -1756,7 +1738,7 @@ public int ProcessCodestream() Span span = this.GetCodeStreamSpan(); JxlBitReader reader = new(span); - this.frameDecoder.InitializeFrame(reader, this.imageBundle!, this.previewFrame); + this.frameDecoder.InitializeFrame(reader, this.imageBundle, this.previewFrame); this.AdvanceCodeStream(reader.TotalBitsConsumed / JxlMath.BitsPerByte); this.frameHeader = this.frameDecoder.GetFrameHeader(); @@ -1921,7 +1903,7 @@ public int ProcessCodestream() if (this.imageOutBufferSet) { Size dimensions = this.CurrentDimensions; - int bitsPerSample = GetBitDepth(this.imageOutputBitDepth, this.metadata!.ImageMetadata!, this.imageOutputFormat); + int bitsPerSample = GetBitDepth(this.imageOutputBitDepth, this.metadata.ImageMetadata, this.imageOutputFormat); this.frameDecoder.SetImageOutput( new PixelCallback( @@ -1940,7 +1922,7 @@ public int ProcessCodestream() for (int i = 0; i < this.extraChannelOutputs.Count; i++) { JxlExtraChannelOutput extra = this.extraChannelOutputs[i]; - int ecBitsPerSample = GetBitDepth(this.imageOutputBitDepth, this.metadata.ImageMetadata!.ExtraChannels[i], extra.Format); + int ecBitsPerSample = GetBitDepth(this.imageOutputBitDepth, this.metadata.ImageMetadata.ExtraChannels[i], extra.Format); this.frameDecoder.AddExtraChannelOutput( extra.Buffer, @@ -2011,7 +1993,7 @@ public int ProcessCodestream() } this.frameStage = JxlFrameStage.Header; - this.imageBundle.Reset(); + this.imageBundle = null; if (this.previewFrame) { @@ -2261,7 +2243,7 @@ public int ProcessBoxes(Stream stream) this.jpegDecoder!.WriteOutput(this.imageBundle!.JpegData); this.reconstructionOutputJpeg = JpegReconstructionStage.None; - this.imageBundle.Reset(); + this.imageBundle = null; if ((this.eventsWanted & JxlDecoderStatus.FullImage) != 0) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs index c02c156a40..dc20fb3002 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs @@ -93,7 +93,7 @@ private static void PrepareNoiseInput( int gy = groupIndex / frameDim.XSizeGroups; JxlRenderPipelineInput input = decState.RenderPipeline.GetInputBuffers(groupIndex, thread); - int noiseCStart = 3 + frameHeader.Metadata!.ImageMetadata!.ExtraChannelCount; + int noiseCStart = 3 + frameHeader.Metadata!.ImageMetadata.ExtraChannelCount; InlineArray3<(JxlPlane Image, Rectangle Rect)> rects = default; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOutputEncodingInfo.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOutputEncodingInfo.cs index 616d7ceeb8..9571c84f9f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOutputEncodingInfo.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOutputEncodingInfo.cs @@ -48,13 +48,15 @@ internal sealed class JxlOutputEncodingInfo public void SetFromMetadata(JxlCodecMetadata metadata) { - JxlImageMetadata imageMetadata = metadata.ImageMetadata ?? throw new InvalidOperationException("Missing image metadata"); + JxlImageMetadata imageMetadata = metadata.ImageMetadata; this.OriginalColorEncoding = imageMetadata.ColorEncoding; this.OriginalIntensityTarget = imageMetadata.IntensityTarget; this.DesiredIntensityTarget = this.OriginalIntensityTarget; - JxlOpsinInverseMatrix inverseMatrix = metadata.CustomTransformData?.OpsinInverseMatrix ?? throw new InvalidOperationException("Missing Opsin inverse matrix or transform data"); + JxlOpsinInverseMatrix inverseMatrix = metadata.CustomTransformData.OpsinInverseMatrix + ?? throw new InvalidOperationException("Missing opsin inverse matrix"); + this.OriginalInverseMatrix = inverseMatrix.InverseMatrix; this.DefaultTransform = inverseMatrix.AllDefault; this.XybEncoded = imageMetadata.XybEncoded; @@ -71,7 +73,7 @@ public void SetFromMetadata(JxlCodecMetadata metadata) inverseMatrix.QuantBiases.AsSpan().CopyTo(parameters.QuantBiases); - bool origOK = JxlXybDecoder.CanOutputToColorEncoding(this.OriginalColorEncoding ?? throw new InvalidCastException("Missing color encoding")); + bool origOK = JxlXybDecoder.CanOutputToColorEncoding(imageMetadata.ColorEncoding); bool origGrey = this.OriginalColorEncoding.IsGray; return this.SetColorEncoding(!this.XybEncoded || origOK ? this.OriginalColorEncoding : JxlColorEncoding.LinearSrgb(origGrey)); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlToJpegDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlToJpegDecoder.cs index a8fad18bcd..df9c414a37 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlToJpegDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlToJpegDecoder.cs @@ -5,6 +5,7 @@ using SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg; using SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; @@ -68,14 +69,30 @@ public void StartBox(bool boxUntilEof, long contentsSize) } } + /// + /// Attaches JPEG reconstruction data and restores ICC markers from the original image metadata. + /// + /// The frame bundle that shares the original codestream metadata. + /// ICC markers require a profile that the codestream does not supply. public void SetImageBundleJpegData(JxlImageBundle bundle) { - if (this.IsOutputSet && this.jpegData is not null) + if (this.IsOutputSet) { - // JPEG reconstruction must retain the original profile bytes. The shared profile exposes them - // read-only without copying or rewriting tag order, padding, or the original header. - ReadOnlyMemory icc = bundle.Metadata!.ColorEncoding!.IccProfile?.GetData() ?? ReadOnlyMemory.Empty; - JpegData.SetJpegDataFromIcc(icc.Span, this.jpegData); + // A converted frame can have a different current encoding. JPEG reconstruction uses the original + // metadata profile. GetData borrows the retained bytes when the profile was created from decoded bytes. + IccProfile? profile = bundle.Metadata.ColorEncoding.IccProfile; + + if (profile is not null) + { + JpegData.SetJpegDataFromIcc(profile.GetData().Span, this.jpegData); + } + else if (this.jpegData.AppMarkerTypes.Contains(JpegAppMarkerType.Icc)) + { + // The reconstruction box and codestream must agree. An absent profile is valid only when + // no ICC marker needs its payload. Do not replace a missing payload with an empty byte sequence. + throw new InvalidImageContentException("JPEG reconstruction requires an ICC profile that the codestream does not supply."); + } + bundle.JpegData = this.jpegData; } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Modular/JxlModularFrameDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Modular/JxlModularFrameDecoder.cs index 26976060cd..16af64c308 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Modular/JxlModularFrameDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Modular/JxlModularFrameDecoder.cs @@ -268,8 +268,8 @@ public void DecodeQuantizerTable(int requiredSizeX, int requiredSizeY, JxlBitRea public void DecodeGlobalInfo(JxlBitReader reader, JxlFrameHeader frameHeader, bool allowTruncatedGroup) { bool decodeColor = frameHeader.Encoding == JxlFrameEncoding.Modular; - JxlImageMetadata metadata = frameHeader.Metadata!.ImageMetadata!; - bool isGray = metadata.ColorEncoding!.IsGray; + JxlImageMetadata metadata = frameHeader.Metadata!.ImageMetadata; + bool isGray = metadata.ColorEncoding.IsGray; // We initially assume we have three color channels. // It could be XYB, RGB, etc. See the if statement diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/ButteraugliJxlComparator.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/ButteraugliJxlComparator.cs index 0a34bef6b0..f63d569395 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/ButteraugliJxlComparator.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/ButteraugliJxlComparator.cs @@ -34,7 +34,7 @@ public override void CompareWith(Configuration configuration, JxlImageBundle act throw new InvalidOperationException("Image must have same size"); } - JxlImageMetadata metadata = actual.Metadata!; + JxlImageMetadata metadata = actual.Metadata; JxlImageBundle store = new(metadata); JxlTransformsEncoder.TransformIfNeeded(configuration, actual, JxlColorEncoding.LinearSRgb(actual.IsGray), cms, store, out JxlImageBundle actualLinearSRgb); @@ -42,12 +42,12 @@ public override void CompareWith(Configuration configuration, JxlImageBundle act JxlPlane tempDiffmap = new(configuration, this.width, this.height); JxlImage3 scaledActualLinearSRgb = actualLinearSRgb.Color!; - if (this.intensityTarget != 0 && actual.Metadata!.IntensityTarget != this.intensityTarget) + if (this.intensityTarget != 0 && actual.Metadata.IntensityTarget != this.intensityTarget) { JxlImage3 scaledActualLinearSRgbStore = new(configuration, this.width, this.height); scaledActualLinearSRgb = scaledActualLinearSRgbStore; - float scale = actual.Metadata!.IntensityTarget / this.intensityTarget; + float scale = actual.Metadata.IntensityTarget / this.intensityTarget; for (int c = 0; c < 3; ++c) { for (int y = 0; y < this.height; ++y) @@ -70,7 +70,7 @@ public override void CompareWith(Configuration configuration, JxlImageBundle act public override void SetReferenceImage(Configuration configuration, JxlImageBundle reference) { - JxlImageMetadata metadata = reference.Metadata!; + JxlImageMetadata metadata = reference.Metadata; JxlImageBundle store = new(metadata); JxlTransformsEncoder.TransformIfNeeded(configuration, reference, JxlColorEncoding.LinearSRgb(reference.IsGray), cms, store, out JxlImageBundle refLinearSRgb); @@ -78,6 +78,6 @@ public override void SetReferenceImage(Configuration configuration, JxlImageBund this.width = reference.XSize; this.height = reference.YSize; - this.intensityTarget = reference.Metadata!.IntensityTarget; + this.intensityTarget = reference.Metadata.IntensityTarget; } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs index 9b849d7f64..6ff47945e3 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs @@ -769,9 +769,9 @@ private static JxlImageBundle RoundtripImage( RenderNoise = false }; - JxlImageMetadata metadata = decoded.Metadata!; + JxlImageMetadata metadata = decoded.Metadata; - decState.PreparePipeline(frameHeader, encState.Shared.Metadata.ImageMetadata!, decoded, options); + decState.PreparePipeline(frameHeader, encState.Shared.Metadata.ImageMetadata, decoded, options); using JxlGroupDecoderCache groupDecCache = new(configuration); @@ -925,7 +925,7 @@ public static bool FindBestQuantization( float originalButteraugli = cparams.OriginalButteraugliDistance; ButteraugliParameters parameters = new(); - JxlCustomTransferFunction tf = frameHeader.Metadata!.ImageMetadata!.ColorEncoding!.TransferFunction; + JxlCustomTransferFunction tf = frameHeader.Metadata!.ImageMetadata.ColorEncoding.TransferFunction; parameters.IntensityTarget = tf.IsPq || tf.IsHlg ? frameHeader.Metadata.ImageMetadata.IntensityTarget : 0; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlImageBundleEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlImageBundleEncoder.cs index 7fab45a569..42e9c26f89 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlImageBundleEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlImageBundleEncoder.cs @@ -132,7 +132,7 @@ public static bool CopyToT( JxlImage3 output) => TryApplyColorTransform( configuration, - ib.CurrentColorEncoding!, + ib.CurrentColorEncoding, metadata.IntensityTarget, ib.Color!, ib.Black!, @@ -149,7 +149,7 @@ public static bool TransformIfNeeded( JxlImageBundle store, ref JxlImageBundle output) { - if (input.CurrentColorEncoding!.IsSameColorEncoding(desiredEncoding) && !input.ContainsBlack) + if (input.CurrentColorEncoding.IsSameColorEncoding(desiredEncoding) && !input.ContainsBlack) { output = input; return true; @@ -162,7 +162,7 @@ public static bool TransformIfNeeded( return false; } - if (!store.SetFromImage(color, input.CurrentColorEncoding!)) + if (!store.SetFromImage(color, input.CurrentColorEncoding)) { return false; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs index f3e88e2e05..2fc5555f0b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs @@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Image; /// -/// An image bundle. +/// Stores frame sample planes, the original image metadata, and the current sample encoding. /// internal sealed class JxlImageBundle { @@ -21,18 +21,15 @@ internal sealed class JxlImageBundle private List> extraChannels = []; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class with the specified image metadata. /// - public JxlImageBundle() + /// The original metadata shared by frames from this codestream. + public JxlImageBundle(JxlImageMetadata metadata) { + this.Metadata = metadata; + this.CurrentColorEncoding = metadata.ColorEncoding; } - /// - /// Initializes a new instance of the class with the specified image metadata. - /// - /// Initial image metadata. - public JxlImageBundle(JxlImageMetadata? metadata) => this.Metadata = metadata; - /// /// Gets or sets the optional name of the bundle. /// @@ -97,42 +94,43 @@ public JxlImageBundle() /// /// Gets a value indicating whether the black extra channel is present. /// - public bool ContainsBlack => this.Metadata?.FindExtraChannel(JxlExtraChannel.Black) is not null; + public bool ContainsBlack => this.Metadata.FindExtraChannel(JxlExtraChannel.Black) is not null; /// /// Gets a value indicating whether the alpha extra channel is present. /// - public bool ContainsAlpha => this.Metadata?.FindExtraChannel(JxlExtraChannel.Alpha) is not null; + public bool ContainsAlpha => this.Metadata.FindExtraChannel(JxlExtraChannel.Alpha) is not null; /// /// Gets a value indicating whether the alpha channel is premultiplied. /// - public bool IsAlphaPremultiplied => this.Metadata?.FindExtraChannel(JxlExtraChannel.Alpha)?.AlphaAssociated == true; + public bool IsAlphaPremultiplied => this.Metadata.FindExtraChannel(JxlExtraChannel.Alpha)?.AlphaAssociated == true; /// /// Gets a value indicating whether the color encoding specifies Gray. /// - public bool IsGray => this.CurrentColorEncoding?.IsGray == true; + public bool IsGray => this.CurrentColorEncoding.IsGray; /// /// Gets a value indicating whether the color encoding specifies sRGB. /// - public bool IsSrgb => this.CurrentColorEncoding?.IsSrgb == true; + public bool IsSrgb => this.CurrentColorEncoding.IsSrgb; /// /// Gets a value indicating whether the color encoding specifies linear sRGB. /// - public bool IsLinearSrgb => this.CurrentColorEncoding?.IsLinearSrgb == true; + public bool IsLinearSrgb => this.CurrentColorEncoding.IsLinearSrgb; /// - /// Gets the current color encoding for this image. + /// Gets the encoding of the current sample planes. /// - public JxlColorEncoding? CurrentColorEncoding { get; private set; } + /// Changing this encoding does not replace the original encoding or ICC profile in . + public JxlColorEncoding CurrentColorEncoding { get; private set; } /// - /// Gets the image metadata for this image bundle. + /// Gets the original image metadata shared with the codestream. /// - public JxlImageMetadata? Metadata { get; } + public JxlImageMetadata Metadata { get; } /// /// Gets the color data. @@ -193,13 +191,15 @@ public JxlPlane? Black { get { - if (!this.ContainsBlack || this.Metadata is null) + JxlExtraChannelInfo? channel = this.Metadata.FindExtraChannel(JxlExtraChannel.Black); + + if (channel is null) { return null; } - int ec = this.Metadata!.FindExtraChannel(JxlExtraChannel.Black) - this.Metadata.ExtraChannelInfo.Data; - return this.extraChannels[ec]; + // Extra-channel metadata and planes use the same order in the codestream. + return this.extraChannels[this.Metadata.ExtraChannels.IndexOf(channel)]; } } @@ -210,30 +210,31 @@ public JxlPlane? Alpha { get { - if (!this.ContainsAlpha || this.Metadata is null) + JxlExtraChannelInfo? channel = this.Metadata.FindExtraChannel(JxlExtraChannel.Alpha); + + if (channel is null) { return null; } - int ec = this.Metadata!.FindExtraChannel(JxlExtraChannel.Alpha) - this.Metadata.ExtraChannelInfo.Data; - return this.extraChannels[ec]; + return this.extraChannels[this.Metadata.ExtraChannels.IndexOf(channel)]; } } /// /// Gets the oriented X size. /// - public int OrientedXSize => this.Metadata?.Orientation > 4 ? this.YSize : this.XSize; + public int OrientedXSize => this.Metadata.Orientation > 4 ? this.YSize : this.XSize; /// /// Gets the oriented Y size. /// - public int OrientedYSize => this.Metadata?.Orientation > 4 ? this.XSize : this.YSize; + public int OrientedYSize => this.Metadata.Orientation > 4 ? this.XSize : this.YSize; /// /// Gets the real bit depth. /// - public uint RealBitDepth => this.Metadata!.BitDepth!.BitsPerSample; + public uint RealBitDepth => this.Metadata.BitDepth.BitsPerSample; /// /// Returns false if the width or height is 0 and any extra channel @@ -351,12 +352,7 @@ public bool TrySetExtraChannels(List> extraChannels) /// Thrown if the corresponding extra channel has incorrect info. public bool TrySetAlpha(JxlPlane alpha) { - if (this.Metadata is null) - { - return false; - } - - JxlExtraChannelInfo? eci = this.Metadata!.FindExtraChannel(JxlExtraChannel.Alpha); + JxlExtraChannelInfo? eci = this.Metadata.FindExtraChannel(JxlExtraChannel.Alpha); if (eci is null) { @@ -368,11 +364,11 @@ public bool TrySetAlpha(JxlPlane alpha) return false; } - int eciIndex = this.Metadata.ExtraChannelInfo.Data; + int eciIndex = this.Metadata.ExtraChannels.IndexOf(eci); if (eciIndex != this.extraChannels.Count) { - throw new InvalidOperationException("The SetAlpha parameter is incorrect"); + throw new InvalidOperationException("Alpha must be appended in the metadata's extra-channel order."); } this.extraChannels.Add(alpha); @@ -384,20 +380,26 @@ public bool TrySetAlpha(JxlPlane alpha) /// Ensures that the metadata of this image is valid. /// /// True if metadata is correct. False if it isn't. - /// Rare. + /// The alpha plane has zero width. public bool VerifyMetadata() { - if (this.CurrentColorEncoding?.IccProfile?.GetData().IsEmpty == true) + // Named color fields can define a conversion without an ICC profile. ICC-only encodings cannot. + if (!this.CurrentColorEncoding.HaveFields && this.CurrentColorEncoding.IccProfile is null) + { + return false; + } + + if (this.CurrentColorEncoding.IccProfile is not null && this.CurrentColorEncoding.IccProfile.GetData().IsEmpty) { return false; } - if (this.Metadata?.ColorEncoding?.IsGray != this.IsGray) + if (this.Metadata.ColorEncoding.IsGray != this.IsGray) { return false; } - if (this.Metadata?.HasAlpha == true) + if (this.Metadata.HasAlpha) { JxlPlane? img = this.Alpha; if (img?.XSize == 0) @@ -406,7 +408,7 @@ public bool VerifyMetadata() } } - int alphaBits = this.Metadata?.AlphaBits ?? 0; + int alphaBits = this.Metadata.AlphaBits; if (alphaBits > 32) { @@ -417,7 +419,7 @@ public bool VerifyMetadata() } /// - /// Updates the bundle from the sepcified image. + /// Updates the bundle from the specified image. /// /// Color data. /// Current color encoding. @@ -429,7 +431,8 @@ public bool SetFromImage(JxlImage3 color, JxlColorEncoding current) return false; } - if (this.Metadata?.ColorEncoding?.IsGray == this.IsGray) + // All frame encodings must keep the original gray/RGB channel model. + if (this.Metadata.ColorEncoding.IsGray != current.IsGray) { return false; } @@ -522,7 +525,7 @@ public bool CopyTo( JxlImage3 output) => JxlImageBundleEncoder.CopyToT( configuration, - this.Metadata!, + this.Metadata, this, rectangle, desiredEncoding, diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/ToneMappingStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/ToneMappingStage.cs index cd12640c73..27461f6d3c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/ToneMappingStage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/ToneMappingStage.cs @@ -32,7 +32,7 @@ public ToneMappingStage(Configuration configuration, JxlOutputEncodingInfo outpu } JxlCustomTransferFunction originalTf = outputEncodingInfo.OriginalColorEncoding!.TransferFunction; - JxlCustomTransferFunction destinationTf = outputEncodingInfo.ColorEncoding!.TransferFunction; + JxlCustomTransferFunction destinationTf = outputEncodingInfo.ColorEncoding.TransferFunction; if (originalTf.IsPq && outputEncodingInfo.DesiredIntensityTarget < outputEncodingInfo.OriginalIntensityTarget) { diff --git a/tests/ImageSharp.Tests/Formats/Jxl/IO/ImageBundleTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/IO/ImageBundleTests.cs index ea5272edd2..d62a339ac7 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/IO/ImageBundleTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/IO/ImageBundleTests.cs @@ -1,16 +1,86 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers.Binary; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.Cms; using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; +using SixLabors.ImageSharp.Tests.ColorProfiles.Icc; namespace SixLabors.ImageSharp.Tests.Formats.Jxl.IO; public class ImageBundleTests { + /// + /// Checks that a sample conversion cannot replace the original ICC bytes used for JPEG reconstruction. + /// + [Fact] + public void JpegReconstruction_UsesOriginalProfileAfterEncodingChange() + { + JxlCodecIO io = new(); + IccProfile original = TestIccProfiles.GetProfile(TestIccProfiles.StandardRgbV2); + io.CodecMetadata.ImageMetadata.ColorEncoding.IccProfile = original; + ReadOnlyMemory expected = original.GetData(); + + // A JPEG APP2 marker stores a two-byte length, the ICC tag, a sequence number, and a marker count. + // Reconstruction has already reserved this payload before it asks the bundle for the original profile. + byte[] marker = new byte[17 + expected.Length]; + marker[0] = 0xE2; + BinaryPrimitives.WriteUInt16BigEndian(marker.AsSpan(1), checked((ushort)(marker.Length - 1))); + JpegDataConstants.IccProfileTag.CopyTo(marker.AsSpan(3)); + marker[15] = 1; + marker[16] = 1; + + JxlToJpegDecoder decoder = new() { Output = Stream.Null }; + JpegData jpeg = decoder.GetJpegData(); + jpeg.AppMarkerTypes.Add(JpegAppMarkerType.Icc); + jpeg.AppData.Add([.. marker]); + + // The converted samples have another profile. Reconstruction must still use the codestream's original profile. + io.Main.OverrideProfile(new JxlColorEncoding { IccProfile = TestIccProfiles.GetProfile(TestIccProfiles.RommRgb) }); + decoder.SetImageBundleJpegData(io.Main); + + Assert.Same(jpeg, io.Main.JpegData); + Assert.True(expected.Span.SequenceEqual(CollectionsMarshal.AsSpan(jpeg.AppData[0])[17..])); + } + + /// + /// Checks that an absent original profile is accepted only when no ICC marker requires a payload. + /// + [Theory] + [InlineData(false)] + [InlineData(true)] + public void JpegReconstruction_RequiresProfileForIccMarkers(bool hasIccMarker) + { + JxlCodecIO io = new(); + JxlToJpegDecoder decoder = new() { Output = Stream.Null }; + JpegData jpeg = decoder.GetJpegData(); + + if (hasIccMarker) + { + // APP2 declares length 17, the ICC_PROFILE tag, sequence/count 1, and one payload byte. + // The codestream has no profile to supply that byte. + jpeg.AppMarkerTypes.Add(JpegAppMarkerType.Icc); + jpeg.AppData.Add([0xE2, 0, 17, 73, 67, 67, 95, 80, 82, 79, 70, 73, 76, 69, 0, 1, 1, 0]); + + Assert.Throws(() => decoder.SetImageBundleJpegData(io.Main)); + Assert.Null(io.Main.JpegData); + } + else + { + decoder.SetImageBundleJpegData(io.Main); + + Assert.Same(jpeg, io.Main.JpegData); + } + } + [Fact] public void ExtraChannel() { From 89c9b915f325cd97d04f43f27d9efc2f7f0d35d8 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 11:12:58 +1000 Subject: [PATCH 237/328] Fix and simplify JPEG XL field visitors --- .../Jxl/Fields/JxlAllDefaultVisitor.cs | 14 +- .../Formats/Jxl/Fields/JxlBundle.cs | 5 + .../Formats/Jxl/Fields/JxlCanEncodeVisitor.cs | 2 +- .../Formats/Jxl/Fields/JxlExtensionStates.cs | 28 ++- .../Formats/Jxl/Fields/JxlInitVisitor.cs | 8 +- .../Formats/Jxl/Fields/JxlReadVisitor.cs | 2 +- .../Jxl/Fields/JxlSetDefaultVisitor.cs | 8 +- .../Formats/Jxl/Fields/JxlVisitor.cs | 219 +++++++++++++++--- .../Formats/Jxl/Fields/JxlVisitorBase.cs | 76 ------ .../Formats/Jxl/IO/FieldsTests.cs | 59 +++++ 10 files changed, 290 insertions(+), 131 deletions(-) delete mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlVisitorBase.cs diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlAllDefaultVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlAllDefaultVisitor.cs index cfe1430193..fcfd63cdbd 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlAllDefaultVisitor.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlAllDefaultVisitor.cs @@ -3,31 +3,35 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Fields; -internal sealed class JxlAllDefaultVisitor : JxlVisitorBase +/// +/// Checks whether all applicable serialized fields have their default values. +/// +internal sealed class JxlAllDefaultVisitor : JxlVisitor { public bool IsAllDefault { get; private set; } = true; public override bool Bits(int bits, uint defaultValue, ref uint value) { - this.IsAllDefault = value == defaultValue; + // Retain earlier mismatches. A later default-valued field cannot make the complete bundle default. + this.IsAllDefault &= value == defaultValue; return true; } public override bool U32(JxlU32Enc enc, uint defaultValue, ref uint value) { - this.IsAllDefault = value == defaultValue; + this.IsAllDefault &= value == defaultValue; return true; } public override bool U64(ulong defaultValue, ref ulong value) { - this.IsAllDefault = value == defaultValue; + this.IsAllDefault &= value == defaultValue; return true; } public override bool F16(float defaultValue, ref float value) { - this.IsAllDefault = MathF.Abs(value - defaultValue) < 1E-6f; + this.IsAllDefault &= MathF.Abs(value - defaultValue) < 1E-6f; return true; } diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs index 7b730e933f..a642da2364 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs @@ -16,6 +16,11 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Fields; /// internal static class JxlBundle { + /// + /// Maximum extension count and nested extension-state depth. Both use one bit per entry in a UInt64. + /// + public const int MaxExtensions = 64; + /// /// Initializes the specified JXL fields. /// diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlCanEncodeVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlCanEncodeVisitor.cs index 87192bfefc..75f2355e21 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlCanEncodeVisitor.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlCanEncodeVisitor.cs @@ -5,7 +5,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Fields; -internal sealed class JxlCanEncodeVisitor : JxlVisitorBase +internal sealed class JxlCanEncodeVisitor : JxlVisitor { private long encodedBits; private ulong extensions; diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlExtensionStates.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlExtensionStates.cs index 2cc3c82581..cb91d5329e 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlExtensionStates.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlExtensionStates.cs @@ -3,27 +3,46 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Fields; -internal sealed class JxlExtensionStates +/// +/// Stores the begun and ended extension flags for nested bundles in two UInt64 masks. +/// +internal struct JxlExtensionStates { private ulong begun; private ulong ended; - public bool IsBegun => (this.begun & 1) != 0; + /// + /// Gets whether the current bundle began its extension fields. + /// + public readonly bool IsBegun => (this.begun & 1) != 0; - public bool IsEnded => (this.ended & 1) != 0; + /// + /// Gets whether the current bundle ended its extension fields. + /// + public readonly bool IsEnded => (this.ended & 1) != 0; + /// + /// Enters a nested bundle with no extension flags set. + /// public void Push() { + // The least significant bit represents the current bundle. Preserve the parent bits above it. this.begun <<= 1; this.ended <<= 1; } + /// + /// Restores the parent bundle's extension flags. + /// public void Pop() { this.begun >>= 1; this.ended >>= 1; } + /// + /// Marks the current bundle's extension fields as begun. + /// public void Begin() { DebugGuard.IsFalse(this.IsBegun, nameof(this.IsBegun), "This must be false."); @@ -32,6 +51,9 @@ public void Begin() this.begun++; } + /// + /// Marks the current bundle's extension fields as ended. + /// public void End() { DebugGuard.IsTrue(this.IsBegun, nameof(this.IsBegun), "This must be true."); diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlInitVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlInitVisitor.cs index 28c16c4e46..347c56dc07 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlInitVisitor.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlInitVisitor.cs @@ -4,10 +4,9 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Fields; /// -/// This variant of sets values -/// to be the default value. +/// Initializes a bundle's own fields. Nested bundles initialize their fields in their constructors. /// -internal sealed class JxlInitVisitor : JxlVisitorBase +internal sealed class JxlInitVisitor : JxlVisitor { public override bool Bits(int bits, uint defaultValue, ref uint value) { @@ -15,7 +14,8 @@ public override bool Bits(int bits, uint defaultValue, ref uint value) return true; } - public override bool U32(JxlU32Distribution d0, JxlU32Distribution d1, JxlU32Distribution d2, JxlU32Distribution d3, uint defaultValue, ref uint value) + /// + public override bool U32(JxlU32Enc enc, uint defaultValue, ref uint value) { value = defaultValue; return true; diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs index e8ef0488fa..dcd56f9229 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs @@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Fields; /// Reads serialized JPEG XL fields and validates extension bit budgets. /// /// The bit reader for the current bounded header. -internal sealed class JxlReadVisitor(JxlBitReader reader) : JxlVisitorBase +internal sealed class JxlReadVisitor(JxlBitReader reader) : JxlVisitor { /// /// The total declared extension payload length in bits. diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlSetDefaultVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlSetDefaultVisitor.cs index aab1bc90ba..b329d09896 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlSetDefaultVisitor.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlSetDefaultVisitor.cs @@ -4,10 +4,9 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Fields; /// -/// This is similar to InitVisitor but also initializes -/// nested fields. +/// Restores serialized defaults in a bundle and its nested bundles. /// -internal sealed class JxlSetDefaultVisitor : JxlVisitorBase +internal sealed class JxlSetDefaultVisitor : JxlVisitor { public override bool Bits(int bits, uint defaultValue, ref uint value) { @@ -15,7 +14,8 @@ public override bool Bits(int bits, uint defaultValue, ref uint value) return true; } - public override bool U32(JxlU32Distribution d0, JxlU32Distribution d1, JxlU32Distribution d2, JxlU32Distribution d3, uint defaultValue, ref uint value) + /// + public override bool U32(JxlU32Enc enc, uint defaultValue, ref uint value) { value = defaultValue; return true; diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlVisitor.cs index 7fa79f75d2..9d15ad7854 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlVisitor.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlVisitor.cs @@ -6,71 +6,185 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Fields; /// -/// Base JPEG XL visitor that can visit all fields of a class. -/// This is highly similar to the following Reflection code, but with -/// lower overhead: -/// -/// // Pseudocode -/// void Visit(Type type) -/// { -/// foreach (PropertyInfo property in type.GetProperties()) -/// { -/// /* visitor implementation */(property); -/// } -/// } -/// +/// Provides the primitive operations used to read, write, and initialize JPEG XL header fields. /// -internal class JxlVisitor +internal abstract class JxlVisitor { + /// + /// Extension flags for active nested bundles. Each level occupies one bit in the state masks. + /// + private JxlExtensionStates extensionStates; + + /// + /// Number of active bundles in the extension-state masks. + /// + private int depth; + + /// + /// Gets whether this visitor reads fields from the input bitstream. + /// public virtual bool IsReading => false; - public virtual bool Visit(IJxlFields fields) => false; + /// + /// Visits the fields in one bundle. + /// + /// The fields to process. + /// Whether the field operation succeeded. + public bool Visit(IJxlFields fields) + { + if (this.depth >= JxlBundle.MaxExtensions) + { + return false; + } + + this.depth++; + this.extensionStates.Push(); - public virtual bool Boolean(bool defaultValue, ref bool value) => false; + try + { + bool visited = fields.Visit(this); + + if (visited && this.extensionStates.IsBegun && !this.extensionStates.IsEnded) + { + throw new InvalidOperationException("A completed bundle did not end its extension fields."); + } + + return visited; + } + finally + { + // A truncated primitive can throw. Restore the parent scope even when this bundle cannot finish. + this.extensionStates.Pop(); + this.depth--; + } + } + + /// + /// Visits a one-bit Boolean field. + /// + /// The default field value. + /// The field to process. + /// Whether the field operation succeeded. + public virtual bool Boolean(bool defaultValue, ref bool value) + { + uint bits = value ? 1U : 0U; + + if (!this.Bits(1, defaultValue ? 1U : 0U, ref bits)) + { + return false; + } - public virtual bool U32(JxlU32Enc enc, uint defaultValue, ref uint value) => false; + value = bits != 0; + return true; + } - public virtual bool U32( + /// + /// Visits an unsigned integer using the supplied four-alternative encoding. + /// + /// The wire alternatives. + /// The default field value. + /// The field to process. + /// Whether the field operation succeeded. + public abstract bool U32(JxlU32Enc enc, uint defaultValue, ref uint value); + + /// + /// Visits an unsigned integer using four explicit wire alternatives. + /// + /// The first wire alternative. + /// The second wire alternative. + /// The third wire alternative. + /// The fourth wire alternative. + /// The default field value. + /// The field to process. + /// Whether the field operation succeeded. + public bool U32( JxlU32Distribution d0, JxlU32Distribution d1, JxlU32Distribution d2, JxlU32Distribution d3, uint defaultValue, ref uint value) - => this.U32(new JxlU32Enc(d0, d1, d2, d3), value, ref defaultValue); - - public virtual unsafe bool Enum(T defaultValue, ref T value) - where T : unmanaged + => this.U32(new JxlU32Enc(d0, d1, d2, d3), defaultValue, ref value); + + /// + /// Visits an enumeration using the JPEG XL enumeration distribution. + /// + /// A byte-sized or 32-bit enumeration used by the codec's headers. + /// The enumeration's default field value. + /// The field to read, write, or compare. + /// Whether the field operation succeeded and its value is defined. + public virtual bool Enum(T defaultValue, ref T value) + where T : unmanaged, System.Enum { - DebugGuard.IsTrue(sizeof(T) == 4, "We use unsafe bit casting so anything beside 4 bytes will break memory layout"); + // Header enums use byte storage. The shared ICC intent uses UInt32 storage. Visit a UInt32 local + // so the reader never writes four bytes through a reference to a one-byte enum field. + uint encoded = Unsafe.SizeOf() == sizeof(byte) ? Unsafe.BitCast(value) : Unsafe.BitCast(value); + uint encodedDefault = Unsafe.SizeOf() == sizeof(byte) ? Unsafe.BitCast(defaultValue) : Unsafe.BitCast(defaultValue); - ref uint u32 = ref Unsafe.As(ref value); if (!this.U32( JxlFieldExpressions.Value(0), JxlFieldExpressions.Value(1), JxlFieldExpressions.BitsOffset(4, 2), JxlFieldExpressions.BitsOffset(6, 18), - Unsafe.BitCast(defaultValue), - ref u32)) + encodedDefault, + ref encoded)) { return false; } - return System.Enum.IsDefined(typeof(T), value); - } - - public virtual bool Bits(int bits, uint defaultValue, ref uint value) => false; + // The four wire alternatives cover 0 through 81, so every decoded value fits in byte storage. + // Validate before publishing it. An invalid wire value must not overwrite the previous field value. + T decoded = Unsafe.SizeOf() == sizeof(byte) ? Unsafe.BitCast((byte)encoded) : Unsafe.BitCast(encoded); - public virtual bool U64(ulong defaultValue, ref ulong value) => false; + if (!System.Enum.IsDefined(decoded)) + { + return false; + } - public virtual bool F16(float defaultValue, ref float value) => false; + value = decoded; + return true; + } + /// + /// Visits a fixed-width unsigned integer field. + /// + /// The number of encoded bits. + /// The default field value. + /// The field to process. + /// Whether the field operation succeeded. + public abstract bool Bits(int bits, uint defaultValue, ref uint value); + + /// + /// Visits a variable-length unsigned 64-bit integer field. + /// + /// The default field value. + /// The field to process. + /// Whether the field operation succeeded. + public abstract bool U64(ulong defaultValue, ref ulong value); + + /// + /// Visits a half-precision field represented as a float in memory. + /// + /// The default field value. + /// The field to process. + /// Whether the field operation succeeded. + public abstract bool F16(float defaultValue, ref float value); + + /// + /// Selects whether a conditional group of fields is visited. + /// + /// Whether the group is present for the current field values. + /// Whether to visit the group. public virtual bool Conditional(bool condition) => condition; + /// + /// Visits the flag that replaces all serialized fields with their defaults. + /// + /// The bundle whose default values the visitor can compare or restore. + /// The encoded shortcut flag. + /// Whether the shortcut flag is set. public virtual bool AllDefault(IJxlFields fields, ref bool allDefault) { - // Do not remove the fields parameter, derived classes - // use it. if (!this.Boolean(true, ref allDefault)) { return false; @@ -79,14 +193,45 @@ public virtual bool AllDefault(IJxlFields fields, ref bool allDefault) return allDefault; } + /// + /// Restores a bundle's serialized defaults when required by the visitor's operation. + /// + /// The bundle to reset. public virtual void SetDefault(IJxlFields fields) { - // Used by derived methods. + // Size and default comparisons must not reset the fields. The read visitor implements the encoded shortcut. } + /// + /// Visits a nested bundle in its own extension scope. + /// + /// The nested fields. + /// Whether the nested operation succeeded. public virtual bool VisitNested(IJxlFields fields) => this.Visit(fields); - public virtual bool BeginExtensions(ref ulong extensions) => false; + /// + /// Starts the extension fields selected by a 64-bit flag set. + /// + /// The extension flags to process. + /// Whether the field operation succeeded. + public virtual bool BeginExtensions(ref ulong extensions) + { + if (!this.U64(0, ref extensions)) + { + return false; + } - public virtual bool EndExtensions() => false; + this.extensionStates.Begin(); + return true; + } + + /// + /// Completes the current bundle's extension fields. + /// + /// Whether the extension operation succeeded. + public virtual bool EndExtensions() + { + this.extensionStates.End(); + return true; + } } diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlVisitorBase.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlVisitorBase.cs deleted file mode 100644 index ff3ee5a3d5..0000000000 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlVisitorBase.cs +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Jxl.Fields; - -internal class JxlVisitorBase : JxlVisitor -{ - private readonly JxlExtensionStates extensionStates = new(); - private int depth; - - public override bool Visit(IJxlFields fields) - { - if (this.depth >= JxlBundle.MaxExtensions) - { - return false; - } - - this.depth++; - this.extensionStates.Push(); - - bool visited = fields.Visit(this); - - if (visited) - { - if (!(!this.extensionStates.IsBegun || this.extensionStates.IsEnded)) - { - throw new InvalidOperationException("Invalid extension state"); - } - } - - this.extensionStates.Pop(); - - if (this.depth == 0) - { - throw new InvalidOperationException("Depth must not be 0"); - } - this.depth--; - - return visited; - } - - public override bool Boolean(bool defaultValue, ref bool value) - { - uint bits = value ? 1u : 0u; - if (!this.Bits(1, defaultValue ? 1u : 0u, ref bits)) - { - return false; - } - - if (bits > 1u) - { - throw new InvalidOperationException("Invalid bits"); - } - - value = bits == 1u; - - return true; - } - - public override bool BeginExtensions(ref ulong extensions) - { - if (!this.U64(0uL, ref extensions)) - { - return false; - } - - this.extensionStates.Begin(); - return true; - } - - public override bool EndExtensions() - { - this.extensionStates.End(); - return true; - } -} diff --git a/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs index b09453f914..4a87406f38 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.Cms; using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; @@ -8,11 +9,69 @@ using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; namespace SixLabors.ImageSharp.Tests.Formats.Jxl.IO; public class FieldsTests { + /// + /// Checks byte and UInt32 enum fields against fixed LSB-first wire data. + /// + [Fact] + public void ReadEnums_PreservesTheirStorageWidths() + { + // Six bits encode primaries 9, followed by six bits for intent 3. Both use selector 2. + using MemoryStream input = new([0x9E, 0x01]); + JxlBitReader reader = new(input); + JxlReadVisitor visitor = new(reader); + JxlPrimaries primaries = JxlPrimaries.SRgb; + IccRenderingIntent intent = IccRenderingIntent.Perceptual; + + Assert.True(visitor.Enum(JxlPrimaries.SRgb, ref primaries)); + Assert.True(visitor.Enum(IccRenderingIntent.MediaRelativeColorimetric, ref intent)); + Assert.Equal(JxlPrimaries.Bt2020, primaries); + Assert.Equal(IccRenderingIntent.AbsoluteColorimetric, intent); + Assert.Equal(12, reader.TotalBitsConsumed); + } + + /// + /// Checks that an undefined enum value cannot replace the previous field value. + /// + [Fact] + public void ReadEnum_RejectsUndefinedValue() + { + // Selector 3 followed by six zero bits encodes value 18, which is not a primary code point. + using MemoryStream input = new([0x03]); + JxlReadVisitor visitor = new(new JxlBitReader(input)); + JxlPrimaries primaries = JxlPrimaries.SRgb; + + Assert.False(visitor.Enum(JxlPrimaries.SRgb, ref primaries)); + Assert.Equal(JxlPrimaries.SRgb, primaries); + } + + /// + /// Checks initialization, accumulated default comparisons, and explicit default restoration. + /// + [Fact] + public void BundleDefaults_RetainEarlierNonDefaultFields() + { + OldBundle bundle = new(); + + Assert.Equal(1U, bundle.OldSmall); + Assert.True(JxlBundle.AllDefault(bundle)); + + bundle.OldSmall = 2; + + Assert.False(JxlBundle.AllDefault(bundle)); + Assert.Equal(2U, bundle.OldSmall); + + JxlBundle.SetDefault(bundle); + + Assert.Equal(1U, bundle.OldSmall); + Assert.True(JxlBundle.AllDefault(bundle)); + } + /// /// Checks that probing an incomplete header reports failure without accepting default field status. /// From 1cb69e3e86ccfe0727db49324cf3c878ccffdc9b Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 11:18:05 +1000 Subject: [PATCH 238/328] Correct JPEG XL histogram and alias-table decoding --- .../Formats/Jxl/IO/Entropy/JxlAnsHelper.cs | 100 +++--- .../Processing/Decoder/Ans/JxlAnsReader.cs | 337 ++++++++++-------- .../Formats/Jxl/Processing/AnsCommonTests.cs | 53 ++- 3 files changed, 296 insertions(+), 194 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHelper.cs b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHelper.cs index b4315ebb12..f6e814462f 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHelper.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHelper.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; @@ -16,22 +17,24 @@ internal static class JxlAnsHelper public static int GetPopulationCountPrecision(int logCount, int shift) => Math.Max(0, Math.Min(logCount, shift - ((JxlAnsConstants.AnsLogTableSize - logCount) >> 1))); - // NOTE: The result may potentially be large, so prefer using a memory allocator + /// + /// Distributes a total frequency as evenly as possible across an alphabet. + /// + /// The configuration whose allocator owns the returned counts. + /// The alphabet size, from one through the total frequency. + /// The total frequency required by the entropy table. + /// The count owner, which the caller must dispose. public static IMemoryOwner CreateFlatHistogram(Configuration configuration, int length, int totalCount) { - DebugGuard.MustBeLessThanOrEqualTo(length, 0, nameof(length)); - DebugGuard.MustBeGreaterThan(length, totalCount, nameof(length)); + DebugGuard.MustBeGreaterThan(length, 0, nameof(length)); + DebugGuard.MustBeLessThanOrEqualTo(length, totalCount, nameof(length)); int count = totalCount / length; IMemoryOwner result = configuration.MemoryAllocator.Allocate(length); - Span resultSpan = result.Memory.Span; - uint unsignedCount = (uint)count; - - for (int i = 0; i < length; i++) - { - resultSpan[i] = unsignedCount; - } + Span resultSpan = result.GetSpan(); + resultSpan.Fill((uint)count); + // Add the division remainder to the first symbols so the table retains the exact required total. int remCounts = totalCount % length; for (int i = 0; i < remCounts; i++) { @@ -67,9 +70,17 @@ public static JxlAnsSymbol Lookup(ReadOnlySpan table, int value, in return symbol; } - public static bool InitAliasTable(Span preDistribution, uint logRange, int logAlphaSize, Span entries) + /// + /// Builds alias buckets whose frequencies sum to the supplied range. + /// + /// The symbol frequencies. Trailing zeros are ignored; an empty alphabet represents symbol zero. + /// The exponent of the total frequency. + /// The exponent of the bucket count, no greater than the range exponent. + /// Storage for every alias bucket. + /// Whether the frequencies form a valid normalized distribution. + public static bool InitAliasTable(ReadOnlySpan preDistribution, uint logRange, int logAlphaSize, Span entries) { - DebugGuard.MustBeLessThan(logAlphaSize, (int)logRange, nameof(logAlphaSize)); + DebugGuard.MustBeLessThanOrEqualTo(logAlphaSize, (int)logRange, nameof(logAlphaSize)); int range = 1 << (int)logRange; int tableSize = 1 << logAlphaSize; @@ -81,13 +92,7 @@ public static bool InitAliasTable(Span preDistribution, uint logRange, int distributionPointer--; } - if (distributionPointer < 0) - { - preDistribution[0] = (uint)range; - distributionPointer = 0; - } - - Span distribution = preDistribution[..(distributionPointer + 1)]; + ReadOnlySpan distribution = preDistribution[..(distributionPointer + 1)]; if (distribution.Length > tableSize) { @@ -95,15 +100,18 @@ public static bool InitAliasTable(Span preDistribution, uint logRange, int } int entrySize = range >> logAlphaSize; - int singleSymbol = -1; - uint sum = 0; + + // An empty alphabet is the degenerate symbol zero, with the full range as its frequency. + // Keep the input unchanged; this case writes its complete table below without a temporary distribution. + int singleSymbol = distribution.IsEmpty ? 0 : -1; + uint sum = distribution.IsEmpty ? (uint)range : 0; for (int sym = 0; sym < distribution.Length; sym++) { uint value = distribution[sym]; sum += value; - if (value == JxlAnsConstants.AnsTableSize) + if (value == range) { if (singleSymbol != -1) { @@ -135,18 +143,20 @@ public static bool InitAliasTable(Span preDistribution, uint logRange, int jxlEntry.Cutoff = 0; jxlEntry.Offsets1 = (ushort)(entrySize * i); jxlEntry.Frequency0 = 0; - jxlEntry.Frequency1XorFrequency0 = JxlAnsConstants.AnsTableSize; + jxlEntry.Frequency1XorFrequency0 = (ushort)range; } return true; } - Span underfullPosn = stackalloc uint[distribution.Length]; - Span overfullPosn = stackalloc uint[distribution.Length]; - Span cutoffs = stackalloc uint[1 << logAlphaSize]; + // Missing symbols still occupy empty buckets. Each work list must therefore cover the full table, + // rather than only the encoded alphabet. Counts below delimit initialized stack entries. + Span underfullPosn = stackalloc uint[tableSize]; + Span overfullPosn = stackalloc uint[tableSize]; + Span cutoffs = stackalloc uint[tableSize]; - int underfullPointer = 0; - int overfullPointer = 0; + int underfullCount = 0; + int overfullCount = 0; for (int i = 0; i < distribution.Length; i++) { @@ -156,37 +166,37 @@ public static bool InitAliasTable(Span preDistribution, uint logRange, int if (currentCutoff > entrySize) { - overfullPosn[overfullPointer] = (uint)i; - overfullPointer++; + overfullPosn[overfullCount] = (uint)i; + overfullCount++; } else if (currentCutoff < entrySize) { - underfullPosn[underfullPointer] = (uint)i; - underfullPointer++; + underfullPosn[underfullCount] = (uint)i; + underfullCount++; } } for (int i = distribution.Length; i < tableSize; i++) { cutoffs[i] = 0; - underfullPosn[underfullPointer] = (uint)i; - underfullPointer++; + underfullPosn[underfullCount] = (uint)i; + underfullCount++; } uint unsignedEntrySize = (uint)entrySize; - while (overfullPointer >= 0) + // Fill one underfull bucket from the tail of one overfull symbol. Pop before reading because + // the counters hold element counts. Only the donor can need another work-list entry afterward. + while (overfullCount > 0) { - uint overfullIndex = overfullPosn[overfullPointer]; - overfullPointer--; + uint overfullIndex = overfullPosn[--overfullCount]; - if (underfullPointer <= -1) + if (underfullCount == 0) { return false; } - uint underfullIndex = underfullPosn[underfullPointer]; - underfullPointer--; + uint underfullIndex = underfullPosn[--underfullCount]; int signedOverfullIndex = (int)overfullIndex; int signedUnderfullIndex = (int)underfullIndex; @@ -203,13 +213,13 @@ public static bool InitAliasTable(Span preDistribution, uint logRange, int if (currentCutoff < entrySize) { - underfullPosn[underfullPointer] = overfullIndex; - underfullPointer++; + underfullPosn[underfullCount] = overfullIndex; + underfullCount++; } else if (currentCutoff > entrySize) { - overfullPosn[overfullPointer] = overfullIndex; - overfullPointer++; + overfullPosn[overfullCount] = overfullIndex; + overfullCount++; } } @@ -226,6 +236,8 @@ public static bool InitAliasTable(Span preDistribution, uint logRange, int } else { + // The borrowed range starts after this bucket's own symbols. Remove that prefix from + // the donor offset so Lookup can add the bucket-local position for either symbol. entry.Offsets1 -= (ushort)currentCutoff; entry.Cutoff = (byte)currentCutoff; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsReader.cs index 418d710eac..75b73dad29 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsReader.cs @@ -95,218 +95,244 @@ public static uint DecodeVariableLengthUint16(JxlBitReader reader) return 0u; } - // NOTE: this method returns null on failure. - // If the return value is a valid IMemoryOwner object, - // then it succeeded. - public static IMemoryOwner? ReadHistogram(Configuration configuration, int precisionBits, JxlBitReader reader) + /// + /// Reads one normalized histogram and rejects malformed counts. + /// + /// The configuration whose allocator owns the histogram storage. + /// The exponent of the required total frequency. + /// The encoded histogram fields. + /// The histogram owner, which the caller must dispose. + public static IMemoryOwner ReadHistogram(Configuration configuration, int precisionBits, JxlBitReader reader) { int range = 1 << precisionBits; bool isSimpleCode = reader.ReadBoolean(); - IMemoryOwner counts; + IMemoryOwner? counts = null; - if (isSimpleCode) + try { - Span symbols = [0, 0]; - - uint maxSymbol = 0u; - uint symCount = reader.ReadBits32(1u) + 1u; - for (uint i = 0; i < symCount; i++) + if (isSimpleCode) { - uint symbol = DecodeVariableLengthUint8(reader); - if (symbol > maxSymbol) + Span symbols = [0, 0]; + + uint maxSymbol = 0u; + uint symCount = reader.ReadBits32(1u) + 1u; + for (uint i = 0; i < symCount; i++) { - maxSymbol = symbol; + uint symbol = DecodeVariableLengthUint8(reader); + if (symbol > maxSymbol) + { + maxSymbol = symbol; + } + + symbols[(int)i] = symbol; } - symbols[(int)i] = symbol; - } + // Only the encoded symbols receive frequencies. All gaps must remain zero when the allocator reuses storage. + counts = configuration.MemoryAllocator.Allocate((int)maxSymbol + 1, AllocationOptions.Clean); + Span countsSpan = counts.GetSpan(); - // Up to 256 items - counts = configuration.MemoryAllocator.Allocate((int)maxSymbol + 1); - Span countsSpan = counts.Memory.Span; + if (symCount == 1) + { + countsSpan[(int)symbols[0]] = (uint)range; + } + else + { + if (symbols[0] == symbols[1]) + { + throw new InvalidOperationException("The data is corrupt"); + } - if (symCount == 1) - { - countsSpan[(int)symbols[0]] = (uint)range; + countsSpan[(int)symbols[0]] = reader.ReadBits32((uint)precisionBits); + countsSpan[(int)symbols[1]] = (uint)range - countsSpan[(int)symbols[0]]; + } } else { - if (symbols[0] == symbols[1]) + bool isFlat = reader.ReadBoolean(); + + if (isFlat) { - counts.Dispose(); - throw new InvalidOperationException("The data is corrupt"); + uint alphabetSize = DecodeVariableLengthUint8(reader) + 1u; + if (alphabetSize > range) + { + throw new InvalidOperationException("The histogram alphabet exceeds its frequency range."); + } + + counts = JxlAnsHelper.CreateFlatHistogram(configuration, (int)alphabetSize, range); + return counts; } - countsSpan[(int)symbols[0]] = reader.ReadBits32((uint)precisionBits); - countsSpan[(int)symbols[1]] = (uint)range - countsSpan[(int)symbols[0]]; - } - } - else - { - bool isFlat = reader.ReadBoolean(); + int upperBoundLog = JxlMath.FloorLog2Nonzero(JxlAnsConstants.AnsLogTableSize + 1); + int log = 0; - if (isFlat) - { - uint alphabetSize = DecodeVariableLengthUint8(reader) + 1u; - if (alphabetSize <= range) + for (; log < upperBoundLog; log++) { - return null; - } - - counts = JxlAnsHelper.CreateFlatHistogram(configuration, (int)alphabetSize, range); - return counts; - } + bool logIncrementBit = reader.ReadBoolean(); - int upperBoundLog = JxlMath.FloorLog2Nonzero(JxlAnsConstants.AnsLogTableSize + 1); - int log = 0; + if (!logIncrementBit) + { + break; + } + } - for (; log < upperBoundLog; log++) - { - bool logIncrementBit = reader.ReadBoolean(); + uint shift = (reader.ReadBits32((uint)log) | (1u << log)) - 1u; - if (!logIncrementBit) + if (shift > JxlAnsConstants.AnsLogTableSize + 1) { - break; + throw new InvalidOperationException("Invalid shift"); } - } - - uint shift = (reader.ReadBits32((uint)log) | (1u << log)) - 1u; - - if (shift > JxlAnsConstants.AnsLogTableSize + 1) - { - throw new InvalidOperationException("Invalid shift"); - } - uint length = DecodeVariableLengthUint8(reader) + 3u; + uint length = DecodeVariableLengthUint8(reader) + 3u; - counts = configuration.MemoryAllocator.Allocate((int)length); - Span countsSpan = counts.Memory.Span; + // Zero-frequency symbols do not write a count. Clean storage also supplies the omitted count until it is reconstructed. + counts = configuration.MemoryAllocator.Allocate((int)length, AllocationOptions.Clean); + Span countsSpan = counts.GetSpan(); - uint totalCount = 0; + uint totalCount = 0; - // The length variable can represent up to 258 elements, - // so it'd be more beneficial to allocate on the stack - // than pool an array. - Span logCounts = stackalloc int[(int)length]; + // The encoded length is at most 258 symbols. Stack storage bounds this temporary table; + // clear skipped RLE positions so their log-frequency flags have the same zero state as the native vector. + Span logCounts = stackalloc int[(int)length]; + logCounts.Clear(); - // stackalloc doesn't zero-init, so clear just in case. - logCounts.Clear(); + int omitLog = -1; + int omitPos = -1; - int omitLog = -1; - int omitPos = -1; + // Only RLE starts write a run length. All other entries must remain zero. + Span same = stackalloc int[(int)length]; + same.Clear(); - // See comments for logCounts definition - Span same = stackalloc int[(int)length]; - same.Clear(); + for (int i = 0; i < length; i++) + { + uint index = reader.PeekBits32(7); + reader.SkipBits32(HuffmanLookup[index][0]); + logCounts[i] = HuffmanLookup[index][1] - 1; - for (int i = 0; i < length; i++) - { - uint index = reader.PeekBits32(7); - reader.SkipBits32(HuffmanLookup[index][0]); - logCounts[i] = HuffmanLookup[index][1] - 1; + if (logCounts[i] == JxlAnsConstants.AnsLogTableSize) + { + uint rleLength = DecodeVariableLengthUint8(reader); + same[i] = (int)rleLength + 5; + i += (int)rleLength + 3; + continue; + } - if (logCounts[i] == JxlAnsConstants.AnsLogTableSize) - { - uint rleLength = DecodeVariableLengthUint8(reader); - same[i] = (int)rleLength + 5; - i += (int)rleLength + 3; - continue; + if (logCounts[i] > omitLog) + { + omitLog = logCounts[i]; + omitPos = i; + } } - if (logCounts[i] > omitLog) + if (omitPos < 0) { - omitLog = logCounts[i]; - omitPos = i; + throw new InvalidOperationException("The histogram is corrupt or invalid."); } - } - - if (omitPos < 0) - { - counts.Dispose(); - throw new InvalidOperationException("The histogram is corrupt or invalid."); - } - - if (omitPos + 1 < length && logCounts[omitPos + 1] == JxlAnsConstants.AnsLogTableSize) - { - counts.Dispose(); - throw new InvalidOperationException("The histogram is corrupt or invalid."); - } - - int previous = 0; - int sameCount = 0; - for (int i = 0; i < length; i++) - { - if (same[i] > 0) + if (omitPos + 1 < length && logCounts[omitPos + 1] == JxlAnsConstants.AnsLogTableSize) { - sameCount = same[i] - 1; - previous = i > 0 ? (int)countsSpan[i - 1] : 0; + throw new InvalidOperationException("The histogram is corrupt or invalid."); } - if (sameCount > 0) - { - countsSpan[i] = (uint)previous; - sameCount--; - } - else - { - int code = logCounts[i]; + int previous = 0; + int sameCount = 0; - if (i == omitPos || code < 0) + for (int i = 0; i < length; i++) + { + if (same[i] > 0) { - continue; + sameCount = same[i] - 1; + previous = i > 0 ? (int)countsSpan[i - 1] : 0; } - else if (shift == 0 || code == 0) + + if (sameCount > 0) { - countsSpan[i] = 1u << code; + countsSpan[i] = (uint)previous; + sameCount--; } else { - int bitCount = JxlAnsHelper.GetPopulationCountPrecision(code, (int)shift); - countsSpan[i] = (1u << code) + (reader.ReadBits32((uint)bitCount) << (code - bitCount)); + int code = logCounts[i]; + + if (i == omitPos || code < 0) + { + continue; + } + else if (shift == 0 || code == 0) + { + countsSpan[i] = 1u << code; + } + else + { + int bitCount = JxlAnsHelper.GetPopulationCountPrecision(code, (int)shift); + countsSpan[i] = (1u << code) + (reader.ReadBits32((uint)bitCount) << (code - bitCount)); + } } - } - totalCount += countsSpan[i]; - } + totalCount += countsSpan[i]; + } - countsSpan[omitPos] = (uint)range - totalCount; + // The omitted symbol must retain at least one unit of frequency. Check before unsigned subtraction can wrap. + if (totalCount >= (uint)range) + { + throw new InvalidOperationException("The histogram count is incorrect."); + } - if (countsSpan[omitPos] <= 0) - { - counts.Dispose(); - throw new InvalidOperationException("The histogram count is incorrect."); + countsSpan[omitPos] = (uint)range - totalCount; } - } - return counts; + return counts; + } + catch + { + // Ownership reaches the caller only after every field is valid. Truncation and malformed fields must release pooled counts. + counts?.Dispose(); + throw; + } } + /// + /// Reads the prefix decoders or ANS alias tables for all mapped histograms. + /// + /// The configuration used for temporary histogram storage. + /// The histogram count established by the context map. + /// The largest symbol count allowed by the hybrid-integer configuration. + /// The encoded histogram fields. + /// The code that retains the decoded tables and symbol metadata. public static void DecodeAnsCodes(Configuration configuration, int numHistograms, int maxAlphabetSize, JxlBitReader input, JxlAnsCode result) { - result.DegenerateSymbols = new(numHistograms); + // Set the logical length before taking a span. List capacity alone does not create writable elements. + CollectionsMarshal.SetCount(result.DegenerateSymbols, numHistograms); CollectionsMarshal.AsSpan(result.DegenerateSymbols).Fill(-1); if (result.UsePrefixCode) { - result.HuffmanData.Resize(numHistograms); + result.HuffmanData.Clear(); + result.HuffmanData.EnsureCapacity(numHistograms); Span alphabetSizes = stackalloc ushort[numHistograms]; for (int c = 0; c < numHistograms; c++) { - alphabetSizes[c] = (ushort)(DecodeVariableLengthUint16(input) + 1); - if (alphabetSizes[c] > maxAlphabetSize) + // The encoded maximum plus one can equal 65536. Validate before narrowing to the table's ushort size. + uint alphabetSize = DecodeVariableLengthUint16(input) + 1; + + if (alphabetSize > maxAlphabetSize) { - throw new InvalidOperationException($"Alphabet size is too long ({alphabetSizes[c]})"); + throw new InvalidOperationException($"Alphabet size is too long ({alphabetSize})"); } + + alphabetSizes[c] = (ushort)alphabetSize; } for (int c = 0; c < numHistograms; c++) { + // Each histogram needs a decoder instance; extending a list of references would only add null entries. + JxlHuffmanDecoder decoder = new(); + result.HuffmanData.Add(decoder); + if (alphabetSizes[c] > 1) { - if (!result.HuffmanData[c].ReadFromBitStream(alphabetSizes[c], input)) + if (!decoder.ReadFromBitStream(alphabetSizes[c], input)) { throw new InvalidOperationException($"Invalid huffman tree number: {c}, alphabet size = {alphabetSizes[c]}"); } @@ -314,10 +340,10 @@ public static void DecodeAnsCodes(Configuration configuration, int numHistograms else { // 0-bit codes don't require extension tables - result.HuffmanData[c].Table = new JxlHuffmanCode[1u << JxlHuffmanDecoder.HuffmanTableBits]; + decoder.Table = new JxlHuffmanCode[1u << JxlHuffmanDecoder.HuffmanTableBits]; } - foreach (JxlHuffmanCode h in result.HuffmanData[c].Table) + foreach (JxlHuffmanCode h in decoder.Table) { if (h.Bits <= JxlHuffmanDecoder.HuffmanTableBits) { @@ -340,13 +366,10 @@ public static void DecodeAnsCodes(Configuration configuration, int numHistograms for (int c = 0; c < numHistograms; c++) { - using IMemoryOwner? countsMemory = ReadHistogram(configuration, JxlAnsConstants.AnsLogTableSize, input); - if (countsMemory is null) - { - continue; - } + using IMemoryOwner countsMemory = ReadHistogram(configuration, JxlAnsConstants.AnsLogTableSize, input); Span counts = countsMemory.GetSpan(); + if (counts.Length > maxAlphabetSize) { throw new InvalidOperationException("Alphabet size is too long"); @@ -428,7 +451,22 @@ public static void DecodeUIntConfigurations(int logAlphaSize, Span contextMap, bool disallowLz77) + /// + /// Reads context mapping, hybrid-integer configurations, and their entropy tables. + /// + /// The configuration used for histogram storage. + /// The encoded entropy fields. + /// The data-context count, before any LZ77 distance context. + /// The decoded entropy code. + /// The map from data contexts to histograms. + /// Whether the owning stream forbids LZ77 references. + public static void DecodeHistograms( + Configuration configuration, + JxlBitReader br, + int numContexts, + JxlAnsCode code, + ref List contextMap, + bool disallowLz77) { if (!JxlBundle.Read(br, code.Lz77)) { @@ -448,12 +486,17 @@ public static void DecodeHistograms(Configuration configuration, JxlBitReader br } int numHistograms = 1; - contextMap.Resize(numContexts); + CollectionsMarshal.SetCount(contextMap, numContexts); if (numContexts > 1) { JxlContextMapDecoder.DecodeContextMap(configuration, contextMap, ref numHistograms, br); } + else + { + // A single context has no encoded map. It always selects histogram zero, including when a code object is reused. + contextMap[0] = 0; + } code.Lz77.NonserializedDistanceContext = contextMap[^1]; code.UsePrefixCode = br.ReadBoolean(); @@ -462,7 +505,7 @@ public static void DecodeHistograms(Configuration configuration, JxlBitReader br ? (byte)JxlAnsConstants.PrefixMaxBits : (byte)(br.ReadBits32(2) + 5); - code.UIntConfig.Resize(numHistograms); + CollectionsMarshal.SetCount(code.UIntConfig, numHistograms); DecodeUIntConfigurations(code.LogAlphaSize, CollectionsMarshal.AsSpan(code.UIntConfig), br); diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/AnsCommonTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/AnsCommonTests.cs index 87b2fc6153..2a00267b47 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/AnsCommonTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/AnsCommonTests.cs @@ -1,13 +1,23 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Tests.Memory; namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; public class AnsCommonTests { - private static void VerifyAliasDistribution(Span distribution, uint logRange) + /// + /// Checks that alias lookup visits every symbol offset exactly once at its declared frequency. + /// + /// The independently specified frequencies. + /// The exponent of their sum. + private static void VerifyAliasDistribution(ReadOnlySpan distribution, uint logRange) { const int logAlphaSize = 8; @@ -18,17 +28,22 @@ private static void VerifyAliasDistribution(Span distribution, uint logRang uint range = 1u << (int)logRange; List[] offsets = new List[distribution.Length]; + for (int i = 0; i < offsets.Length; i++) + { + offsets[i] = new List((int)distribution[i]); + } + for (int i = 0; i < range; i++) { JxlAnsSymbol s = JxlAnsHelper.Lookup(table, i, JxlAnsConstants.AnsLogTableSize - 8, (1 << (JxlAnsConstants.AnsLogTableSize - 8)) - 1); - offsets[s.Value] ??= []; + Assert.Equal((int)distribution[s.Value], s.Frequency); offsets[s.Value].Add(s.Offset); } for (int i = 0; i < distribution.Length; i++) { - Assert.Equal(distribution[i], offsets[i].Count); + Assert.Equal((int)distribution[i], offsets[i].Count); offsets[i].Sort(); for (int j = 0; j < offsets[i].Count; j++) @@ -44,5 +59,37 @@ public void AliasDistributionSmoke() VerifyAliasDistribution([JxlAnsConstants.AnsTableSize / 2, JxlAnsConstants.AnsTableSize / 2], JxlAnsConstants.AnsLogTableSize); VerifyAliasDistribution([JxlAnsConstants.AnsTableSize], JxlAnsConstants.AnsLogTableSize); VerifyAliasDistribution([0, 0, 0, JxlAnsConstants.AnsTableSize, 0], JxlAnsConstants.AnsLogTableSize); + VerifyAliasDistribution([2048, 1024, 1024], JxlAnsConstants.AnsLogTableSize); + VerifyAliasDistribution([0, 2048, 0, 1024, 1024], JxlAnsConstants.AnsLogTableSize); + } + + /// + /// Checks that a sparse single-symbol histogram cannot retain dirty pooled counts in its unused entries. + /// + [Fact] + public void ReadHistogram_ClearsUnspecifiedCounts() + { + Configuration configuration = new() { MemoryAllocator = new TestMemoryAllocator() }; + + // Simple histogram, one symbol, symbol index 3. Each unused count must be zero, not allocator contents. + using MemoryStream input = new([0x4D]); + using IMemoryOwner counts = JxlAnsReader.ReadHistogram(configuration, JxlAnsConstants.AnsLogTableSize, new JxlBitReader(input)); + ReadOnlySpan expected = [0, 0, 0, JxlAnsConstants.AnsTableSize]; + + Assert.True(counts.GetSpan().SequenceEqual(expected)); + } + + /// + /// Checks the valid one-symbol flat histogram against fixed wire data. + /// + [Fact] + public void ReadHistogram_AcceptsFlatAlphabet() + { + // Not simple, flat, alphabet size one. The alphabet needs no explicit frequency bits. + using MemoryStream input = new([0x02]); + using IMemoryOwner counts = JxlAnsReader.ReadHistogram(Configuration.Default, JxlAnsConstants.AnsLogTableSize, new JxlBitReader(input)); + + Assert.Equal(1, counts.GetSpan().Length); + Assert.Equal((uint)JxlAnsConstants.AnsTableSize, counts.GetSpan()[0]); } } From 0974466c77507c62ffb60b8c856fa25fe00af7e3 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 11:24:09 +1000 Subject: [PATCH 239/328] Correct JPEG XL ANS symbol and hybrid integer decoding --- .../Decoder/Ans/JxlAnsSymbolReader.cs | 20 ++++++--- .../Formats/Jxl/Processing/AnsCommonTests.cs | 44 +++++++++++++++++++ 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsSymbolReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsSymbolReader.cs index 9ae7cb4d0b..e34f645795 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsSymbolReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsSymbolReader.cs @@ -110,13 +110,19 @@ public static JxlAnsSymbolReader Create(JxlAnsCode code, JxlBitReader br, int di return new JxlAnsSymbolReader(code, br, distanceMultiplier, lz77); } - public bool CheckAnsFinalState() => this.state == JxlAnsConstants.AnsTableSize << 16; + /// + /// Checks that decoding consumed the entropy stream's terminal ANS state. + /// + /// Whether the stream ended at the format's signature state. + public bool CheckAnsFinalState() => this.state == JxlAnsConstants.AnsSignature << 16; public int ReadSymbolAnsWithoutRefill(int histogramIndex, JxlBitReader br) { uint res = this.state & (JxlAnsConstants.AnsTableSize - 1); - Span table = this.aliasTables.AsSpan(0, histogramIndex << this.logAlphaSize); + // Histograms occupy consecutive, equal-sized tables. The histogram selects the starting offset, + // not the prefix length. Histogram zero therefore still has one complete table. + Span table = this.aliasTables.AsSpan(histogramIndex << this.logAlphaSize, 1 << this.logAlphaSize); JxlAnsSymbol symbol = JxlAnsHelper.Lookup(table, (int)res, this.logEntrySize, this.entrySizeMinus1); this.state = (uint)((symbol.Frequency * (this.state >> JxlAnsConstants.AnsLogTableSize)) + symbol.Offset); @@ -161,7 +167,7 @@ public static int ReadHybridUIntConfiguration(JxlAnsHybridUIntConfiguration conf uint splitToken = config.SplitToken; uint msbInToken = config.MsbInToken; uint lsbInToken = config.LsbInToken; - uint splitExponent = config.SplitToken; + uint splitExponent = config.SplitExponent; if (token < splitToken) { @@ -170,6 +176,7 @@ public static int ReadHybridUIntConfiguration(JxlAnsHybridUIntConfiguration conf uint numBits = splitExponent - (msbInToken + lsbInToken) + (uint)((token - splitToken) >> (int)(msbInToken + lsbInToken)); + // Keep malformed shift counts within the UInt32 primitive's width, as in the reference decoder. numBits &= 31u; int low = token & ((1 << (int)lsbInToken) - 1); @@ -196,6 +203,9 @@ public void ReadHybridUIntClusteredHuffRleOnly(int context, JxlBitReader br, ref run = ReadHybridUIntConfiguration(this.lz77LengthUInt, token - this.lz77Threshold, br) + this.lz77MinLength - 1; + + // A run token repeats the previous value. It does not also encode a new literal. + return; } value = ReadHybridUIntConfiguration(this.configs[context], token, br); @@ -291,10 +301,10 @@ public int ReadHybridUIntInlined(bool usesLz77, int context, JxlBitReader br, Li => this.ReadHybridUIntClustered(usesLz77, contextMap[context], br); public int ReadHybridUInt(int context, JxlBitReader br, Span contextMap) - => this.ReadHybridUIntClustered(true, contextMap[context], br); + => this.ReadHybridUIntClustered(this.UsesLz77, contextMap[context], br); public int ReadHybridUInt(int context, JxlBitReader br, List contextMap) - => this.ReadHybridUIntClustered(true, contextMap[context], br); + => this.ReadHybridUIntClustered(this.UsesLz77, contextMap[context], br); public bool IsSingleValueAndAdvance(int context, ref int value, int count) { diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/AnsCommonTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/AnsCommonTests.cs index 2a00267b47..87f277e095 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/AnsCommonTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/AnsCommonTests.cs @@ -12,6 +12,50 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; public class AnsCommonTests { + /// + /// Checks hybrid-integer expansion against fixed tokens and bit payloads. + /// + /// The number of high value bits carried by the token. + /// The number of low value bits carried by the token. + /// The decoded UInt32 value. + [Theory] + [InlineData(2, 0, 37)] + [InlineData(1, 1, 42)] + public void ReadHybridUInt_UsesSplitExponent(int mostSignificantBits, int leastSignificantBits, int expected) + { + JxlAnsHybridUIntConfiguration configuration = new(4, (uint)mostSignificantBits, (uint)leastSignificantBits); + + // Token 20 selects three payload bits in both configurations. The fixed payload is binary 101. + using MemoryStream input = new([0x05]); + JxlBitReader reader = new(input); + + Assert.Equal(expected, JxlAnsSymbolReader.ReadHybridUIntConfiguration(configuration, 20, reader)); + Assert.Equal(3, reader.TotalBitsConsumed); + } + + /// + /// Checks the terminal state against the ANS signature rather than the frequency-table size. + /// + /// The high word of the fixed little-endian ANS state. + /// Whether the state is terminal. + [Theory] + [InlineData(0x13, true)] + [InlineData(0x10, false)] + public void CheckAnsFinalState_UsesFormatSignature(byte signature, bool expected) + { + // Ten header bits select no LZ77, ANS, a 32-entry table, split exponent zero, and symbol zero. + // The following 32 LSB-first bits hold the terminal state. Its high word begins at bit 26. + using MemoryStream input = new([0x80, 0, 0, (byte)(signature << 2), 0, 0]); + JxlBitReader bitReader = new(input); + JxlAnsCode code = new(); + List contextMap = []; + JxlAnsReader.DecodeHistograms(Configuration.Default, bitReader, 1, code, ref contextMap, false); + + using JxlAnsSymbolReader reader = JxlAnsSymbolReader.Create(code, bitReader, 0); + + Assert.Equal(expected, reader.CheckAnsFinalState()); + } + /// /// Checks that alias lookup visits every symbol offset exactly once at its declared frequency. /// From e581cc89fe243826a6e9c21f64a6b8fab78c8cbf Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 11:40:29 +1000 Subject: [PATCH 240/328] Implement JPEG XL color-header fields and ICC state rules --- .../Formats/Jxl/Cms/JxlColorEncoding.cs | 191 ++++++++++++++++- .../Jxl/Cms/JxlCustomTransferFunction.cs | 93 ++++++++- src/ImageSharp/Formats/Jxl/Cms/JxlCustomXy.cs | 39 ++++ .../Formats/Jxl/IO/FieldsTests.cs | 195 ++++++++++++++++++ 4 files changed, 513 insertions(+), 5 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs index 0c64760105..b32b089aaa 100644 --- a/src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Metadata.Profiles.Icc; namespace SixLabors.ImageSharp.Formats.Jxl.Cms; @@ -9,8 +10,23 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Cms; /// /// Describes JPEG XL color fields and an optional decoded ICC profile. /// -internal sealed class JxlColorEncoding +internal sealed class JxlColorEncoding : IJxlFields { + /// + /// The encoded shortcut that replaces the serialized color fields with their defaults. + /// + private bool allDefault; + + /// + /// Profile attached after the compressed payload is decoded. It is not part of the serialized color fields. + /// + private IccProfile? iccProfile; + + /// + /// Initializes a new instance of the class. + /// + public JxlColorEncoding() => JxlBundle.Init(this); + public JxlWhitePoint WhitePoint { get; set; } = JxlWhitePoint.D65; public JxlPrimaries Primaries { get; set; } = JxlPrimaries.SRgb; @@ -20,19 +36,51 @@ internal sealed class JxlColorEncoding /// public IccRenderingIntent RenderingIntent { get; set; } = IccRenderingIntent.MediaRelativeColorimetric; + /// + /// Gets or sets whether white point, primaries and transfer fields describe the current encoding. + /// + /// ColorSpace remains valid for an ICC-only header even when this value is false. public bool HaveFields { get; set; } = true; + /// + /// Gets or sets whether the codestream contains a compressed ICC payload instead of direct color fields. + /// + /// This serialized flag remains set after the decoder attaches the reconstructed profile. + public bool NeedsIcc { get; set; } + /// /// Gets or sets the ICC profile reconstructed from the compressed profile payload. /// /// /// A null value means that no ICC profile is attached. Named color fields do not require a generated profile. /// The decoder attaches the decoded profile to the original metadata encoding before it publishes the headers. + /// Attaching a profile invalidates the direct color fields until they are derived from that profile. + /// This property does not change the serialized flag. /// - public IccProfile? IccProfile { get; set; } + public IccProfile? IccProfile + { + get => this.iccProfile; + set + { + this.iccProfile = value; + + if (value is not null) + { + this.HaveFields = false; + this.Cmyk = value.Header.DataColorSpace == IccColorSpaceType.Cmyk; + } + else + { + this.Cmyk = false; + } + } + } public JxlColorSpace ColorSpace { get; set; } = JxlColorSpace.Rgb; + /// + /// Gets or sets whether the attached profile describes four CMYK device components. + /// public bool Cmyk { get; set; } /// @@ -52,6 +100,145 @@ internal sealed class JxlColorEncoding public int Channels => (this.ColorSpace == JxlColorSpace.Gray) ? 1 : 3; + /// + /// Visits the serialized color fields without reading or generating an ICC profile. + /// + /// The header field operation. + /// Whether all present fields use valid code points and gamma values. + public bool Visit(JxlVisitor visitor) + { + if (visitor.IsReading) + { + // A new header describes a new encoding, even when this object is reused. Its old profile cannot + // describe the incoming fields. Generic default/size visitors preserve this nonserialized state. + this.iccProfile = null; + this.Cmyk = false; + } + + if (visitor.AllDefault(this, ref this.allDefault)) + { + // Reset every serialized field, including dormant custom coordinates, when the shortcut is read. + // The visitor preserves nonserialized state, including an attached profile. + visitor.SetDefault(this); + } + else + { + bool needsIcc = this.NeedsIcc; + + if (!visitor.Boolean(false, ref needsIcc)) + { + return false; + } + + this.NeedsIcc = needsIcc; + JxlColorSpace colorSpace = this.ColorSpace; + + // Color space is always present. It determines channel decoding even when an ICC payload follows. + if (!visitor.Enum(JxlColorSpace.Rgb, ref colorSpace)) + { + return false; + } + + this.ColorSpace = colorSpace; + + if (visitor.Conditional(!needsIcc)) + { + bool implicitWhitePoint = colorSpace == JxlColorSpace.Xyb; + + if (implicitWhitePoint) + { + this.WhitePoint = JxlWhitePoint.D65; + } + + if (visitor.Conditional(!implicitWhitePoint)) + { + JxlWhitePoint whitePoint = this.WhitePoint; + + if (!visitor.Enum(JxlWhitePoint.D65, ref whitePoint)) + { + return false; + } + + this.WhitePoint = whitePoint; + + if (visitor.Conditional(whitePoint == JxlWhitePoint.Custom)) + { + // These nested fields are structs. Visit their primitive fields directly and assign the changed + // value back; passing them through IJxlFields would box a copy and lose the decoded coordinates. + JxlCustomXy white = this.White; + + if (!white.Visit(visitor)) + { + return false; + } + + this.White = white; + } + } + + if (visitor.Conditional(this.HasPrimaries)) + { + JxlPrimaries primaries = this.Primaries; + + if (!visitor.Enum(JxlPrimaries.SRgb, ref primaries)) + { + return false; + } + + this.Primaries = primaries; + + if (visitor.Conditional(primaries == JxlPrimaries.Custom)) + { + JxlCustomXy red = this.Red; + JxlCustomXy green = this.Green; + JxlCustomXy blue = this.Blue; + + if (!red.Visit(visitor) || !green.Visit(visitor) || !blue.Visit(visitor)) + { + return false; + } + + this.Red = red; + this.Green = green; + this.Blue = blue; + } + } + + JxlCustomTransferFunction transferFunction = this.TransferFunction; + + if (!transferFunction.Visit(visitor, colorSpace)) + { + return false; + } + + this.TransferFunction = transferFunction; + IccRenderingIntent intent = this.RenderingIntent; + + if (!visitor.Enum(IccRenderingIntent.MediaRelativeColorimetric, ref intent)) + { + return false; + } + + this.RenderingIntent = intent; + + // Unknown values are defined enum members, but only an ICC payload can describe their color meaning. + if (colorSpace == JxlColorSpace.Unknown || transferFunction.IsUnknown) + { + return false; + } + } + } + + if (visitor.IsReading) + { + // ICC-only headers retain ColorSpace for reconstruction. Their other direct fields are not valid, + // even while IccProfile is still null and the compressed payload has not yet been decoded. + this.HaveFields = !this.NeedsIcc; + } + + return true; + } + /// /// Gets the red, green, and blue primary chromaticities from the decoded color-encoding fields. /// diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlCustomTransferFunction.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlCustomTransferFunction.cs index acf049d4a7..1215e908cb 100644 --- a/src/ImageSharp/Formats/Jxl/Cms/JxlCustomTransferFunction.cs +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlCustomTransferFunction.cs @@ -1,21 +1,39 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.Fields; + namespace SixLabors.ImageSharp.Formats.Jxl.Cms; +/// +/// Stores a named transfer function or the exact integer gamma field from a JPEG XL header. +/// internal struct JxlCustomTransferFunction { private const uint MaxGamma = 8192; private const uint GammaMultiplier = 10000000; + private const uint XybGamma = GammaMultiplier / 3; + /// + /// Initializes a new instance of the struct with sRGB transfer defaults. + /// public JxlCustomTransferFunction() { } + /// + /// Gets or sets whether the encoding uses a custom gamma instead of a named transfer function. + /// public bool HaveGamma { get; set; } - public uint Gamma { get; set; } + /// + /// Gets or sets the OETF exponent multiplied by ten million. Direct wire decoding preserves this integer. + /// + public uint Gamma { get; set; } = GammaMultiplier; + /// + /// Gets or sets the named transfer function used when is false. + /// public JxlTransferFunction TransferFunction { get; set; } = JxlTransferFunction.SRgb; public readonly bool IsUnknown => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.Unknown; @@ -32,6 +50,71 @@ public JxlCustomTransferFunction() public readonly bool IsDci => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.Dci; + /// + /// Visits the named transfer function or exact integer gamma for the current color space. + /// + /// The header field operation. + /// The color space that determines whether transfer fields are implicit. + /// Whether the transfer fields satisfy the wire encoding and gamma bounds. + public bool Visit(JxlVisitor visitor, JxlColorSpace colorSpace) + { + bool implicitTransfer = colorSpace == JxlColorSpace.Xyb; + + if (implicitTransfer) + { + // XYB has an implicit one-third exponent. It consumes no transfer bits. + // Keep the encoded integer approximation, rather than converting through a floating-point setter. + this.HaveGamma = true; + this.Gamma = XybGamma; + this.TransferFunction = JxlTransferFunction.Unknown; + } + + if (visitor.Conditional(!implicitTransfer)) + { + bool haveGamma = this.HaveGamma; + + if (!visitor.Boolean(false, ref haveGamma)) + { + return false; + } + + this.HaveGamma = haveGamma; + + if (visitor.Conditional(haveGamma)) + { + uint gamma = this.Gamma; + + if (!visitor.Bits(24, GammaMultiplier, ref gamma)) + { + return false; + } + + // The OETF exponent is gamma / 10000000 and must be within [1/8192,1]. + // Integer validation preserves the exact decoded value and cannot round an invalid exponent into range. + if (gamma > GammaMultiplier || ((ulong)gamma * MaxGamma) < GammaMultiplier) + { + return false; + } + + this.Gamma = gamma; + } + + if (visitor.Conditional(!haveGamma)) + { + JxlTransferFunction transferFunction = this.TransferFunction; + + if (!visitor.Enum(JxlTransferFunction.SRgb, ref transferFunction)) + { + return false; + } + + this.TransferFunction = transferFunction; + } + } + + return true; + } + public readonly JxlTransferFunction GetTransferFunction() { if (this.HaveGamma) @@ -42,10 +125,14 @@ public readonly JxlTransferFunction GetTransferFunction() return this.TransferFunction; } - public void SetTransferFunction(JxlTransferFunction tf) + /// + /// Selects a named transfer function instead of a custom gamma. + /// + /// The transfer function to select. + public void SetTransferFunction(JxlTransferFunction transferFunction) { this.HaveGamma = false; - this.TransferFunction = tf; + this.TransferFunction = transferFunction; } public readonly float GetGamma() diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlCustomXy.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlCustomXy.cs index 95652cdaf4..5e74dbd12c 100644 --- a/src/ImageSharp/Formats/Jxl/Cms/JxlCustomXy.cs +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlCustomXy.cs @@ -2,6 +2,8 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Cms; @@ -15,10 +17,47 @@ internal struct JxlCustomXy private const int Min = -0x200000; private const int Max = 0x1FFFFF; + /// + /// Wire alternatives for both sign-packed coordinates. Offsets begin at the corresponding power of two. + /// + private static readonly JxlU32Enc CoordinateEncoding = new( + JxlFieldExpressions.Bits(19), + JxlFieldExpressions.BitsOffset(19, 1U << 19), + JxlFieldExpressions.BitsOffset(20, 1U << 20), + JxlFieldExpressions.BitsOffset(21, 1U << 21)); + public int X { get; set; } public int Y { get; set; } + /// + /// Visits the signed integer coordinates without boxing this value type. + /// + /// The header field operation. + /// Whether both coordinates could be processed. + public bool Visit(JxlVisitor visitor) + { + // The U32 alternatives encode a signed coordinate after sign packing. Their maximum packed value + // is 4194303, which restores the complete coordinate range from -2097152 through 2097151. + uint x = JxlPackSigned.PackUnsigned(this.X); + + if (!visitor.U32(CoordinateEncoding, 0, ref x)) + { + return false; + } + + this.X = JxlPackSigned.UnpackSigned(x); + uint y = JxlPackSigned.PackUnsigned(this.Y); + + if (!visitor.U32(CoordinateEncoding, 0, ref y)) + { + return false; + } + + this.Y = JxlPackSigned.UnpackSigned(y); + return true; + } + public readonly CieXyChromaticityCoordinates GetValue() => new( x: this.X * (1.0f / Multiplier), y: this.Y * (1.0f / Multiplier)); diff --git a/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs index 4a87406f38..b2e413aefb 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs @@ -10,11 +10,206 @@ using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; using SixLabors.ImageSharp.Metadata.Profiles.Icc; +using SixLabors.ImageSharp.Tests.ColorProfiles.Icc; namespace SixLabors.ImageSharp.Tests.Formats.Jxl.IO; public class FieldsTests { + /// + /// Checks that the one-bit default shortcut replaces earlier serialized color fields. + /// + [Fact] + public void ReadColorHeader_DefaultShortcutRestoresFields() + { + JxlColorEncoding encoding = new() + { + NeedsIcc = true, + HaveFields = false, + ColorSpace = JxlColorSpace.Unknown, + WhitePoint = JxlWhitePoint.Custom, + White = new() { X = 123, Y = -456 }, + Primaries = JxlPrimaries.Custom, + Red = new() { X = 654, Y = -321 }, + RenderingIntent = IccRenderingIntent.AbsoluteColorimetric, + TransferFunction = new() { HaveGamma = true, Gamma = 3333334 }, + IccProfile = TestIccProfiles.GetProfile(TestIccProfiles.Fogra39) + }; + + Assert.True(encoding.Cmyk); + + // A set first bit replaces the complete serialized color header. No ICC flag or color-space bits follow. + using MemoryStream input = new([0x01]); + JxlBitReader reader = new(input); + + Assert.True(JxlBundle.Read(reader, encoding)); + Assert.Equal(1, reader.TotalBitsConsumed); + Assert.False(encoding.NeedsIcc); + Assert.True(encoding.HaveFields); + Assert.Null(encoding.IccProfile); + Assert.False(encoding.Cmyk); + Assert.Equal(JxlColorSpace.Rgb, encoding.ColorSpace); + Assert.Equal(JxlWhitePoint.D65, encoding.WhitePoint); + Assert.Equal(JxlPrimaries.SRgb, encoding.Primaries); + Assert.Equal(IccRenderingIntent.MediaRelativeColorimetric, encoding.RenderingIntent); + Assert.True(encoding.TransferFunction.IsSrgb); + Assert.Equal(0, encoding.White.X); + Assert.Equal(0, encoding.White.Y); + Assert.Equal(0, encoding.Red.X); + Assert.Equal(0, encoding.Red.Y); + Assert.True(JxlBundle.AllDefault(encoding)); + } + + /// + /// Checks that ICC-only headers retain their color space before the profile payload is available. + /// + [Theory] + [InlineData(new byte[] { 0x06 }, 1, 4)] + [InlineData(new byte[] { 0x1A }, 3, 8)] + public void ReadColorHeader_IccFlagKeepsColorSpace(byte[] wire, int expectedColorSpace, int expectedBits) + { + // After all_default=0 and want_icc=1, only the color-space enum is present in this header. + using MemoryStream input = new(wire); + JxlBitReader reader = new(input); + JxlColorEncoding encoding = new(); + + Assert.True(JxlBundle.Read(reader, encoding)); + Assert.Equal(expectedBits, reader.TotalBitsConsumed); + Assert.Equal((JxlColorSpace)expectedColorSpace, encoding.ColorSpace); + Assert.True(encoding.NeedsIcc); + Assert.False(encoding.HaveFields); + Assert.Null(encoding.IccProfile); + Assert.False(encoding.TryGetPrimaries(out _)); + } + + /// + /// Checks custom white and primary coordinate order against fixed signed-integer wire data. + /// + [Fact] + public void ReadColorHeader_CustomCoordinatesRetainWireOrder() + { + // RGB, custom white, custom primaries, sRGB transfer and absolute intent. Coordinates use millionths: + // white=(312700,329000), red=(640000,330000), green=(300000,600000), blue=(150000,60000). + using MemoryStream input = new( + [0x20, 0x84, 0xAF, 0x98, 0xA0, 0x14, 0x24, 0x08, 0x80, 0x38, 0x81, 0x48, 0x28, + 0xE0, 0x93, 0x08, 0xF8, 0x24, 0x80, 0x4F, 0x12, 0x60, 0xEA, 0x70, 0x0D]); + + JxlBitReader reader = new(input); + JxlColorEncoding encoding = new(); + + Assert.True(JxlBundle.Read(reader, encoding)); + Assert.Equal(199, reader.TotalBitsConsumed); + Assert.Equal(JxlWhitePoint.Custom, encoding.WhitePoint); + Assert.Equal(JxlPrimaries.Custom, encoding.Primaries); + Assert.Equal((312700, 329000), (encoding.White.X, encoding.White.Y)); + Assert.Equal((640000, 330000), (encoding.Red.X, encoding.Red.Y)); + Assert.Equal((300000, 600000), (encoding.Green.X, encoding.Green.Y)); + Assert.Equal((150000, 60000), (encoding.Blue.X, encoding.Blue.Y)); + Assert.True(encoding.TransferFunction.IsSrgb); + Assert.Equal(IccRenderingIntent.AbsoluteColorimetric, encoding.RenderingIntent); + Assert.False(encoding.NeedsIcc); + Assert.True(encoding.HaveFields); + } + + /// + /// Checks exact gamma integers and both limits of the encoded OETF exponent. + /// + [Theory] + [InlineData(new byte[] { 0x50, 0x89, 0x09, 0x00, 0x02 }, 1220U, false)] + [InlineData(new byte[] { 0x50, 0x8B, 0x09, 0x00, 0x02 }, 1221U, true)] + [InlineData(new byte[] { 0x50, 0xAD, 0xB9, 0x65, 0x02 }, 3333334U, true)] + [InlineData(new byte[] { 0x50, 0x01, 0x2D, 0x31, 0x03 }, 10000000U, true)] + [InlineData(new byte[] { 0x50, 0x03, 0x2D, 0x31, 0x03 }, 10000001U, false)] + public void ReadColorHeader_CustomGammaRespectsIntegerBounds(byte[] wire, uint gamma, bool valid) + { + // Default RGB primaries and white, followed by have_gamma=1, the 24-bit integer and relative intent. + // 3333334 must remain distinct from the implicit XYB exponent 3333333. + using MemoryStream input = new(wire); + JxlBitReader reader = new(input); + JxlColorEncoding encoding = new(); + + Assert.Equal(valid, JxlBundle.Read(reader, encoding)); + Assert.Equal(valid ? 35 : 33, reader.TotalBitsConsumed); + + if (valid) + { + Assert.True(encoding.TransferFunction.HaveGamma); + Assert.Equal(gamma, encoding.TransferFunction.Gamma); + } + } + + /// + /// Checks that XYB supplies its white point and transfer function without reading their fields. + /// + [Fact] + public void ReadColorHeader_XybUsesImplicitFields() + { + // all_default=0, want_icc=0, XYB enum, relative intent. No white, primary or transfer bits are present. + using MemoryStream input = new([0x08, 0x01]); + JxlBitReader reader = new(input); + JxlColorEncoding encoding = new(); + + Assert.True(JxlBundle.Read(reader, encoding)); + Assert.Equal(10, reader.TotalBitsConsumed); + Assert.Equal(JxlColorSpace.Xyb, encoding.ColorSpace); + Assert.Equal(JxlWhitePoint.D65, encoding.WhitePoint); + Assert.True(encoding.TransferFunction.HaveGamma); + Assert.Equal(3333333U, encoding.TransferFunction.Gamma); + Assert.False(encoding.HasPrimaries); + Assert.True(encoding.HaveFields); + } + + /// + /// Checks that grayscale headers read white point and transfer fields without primary fields. + /// + [Fact] + public void ReadColorHeader_GrayOmitsPrimaries() + { + // all_default=0, want_icc=0, Gray, D65, have_gamma=0, BT.709 transfer, relative intent. + using MemoryStream input = new([0x94, 0x02]); + JxlBitReader reader = new(input); + JxlColorEncoding encoding = new(); + + Assert.True(JxlBundle.Read(reader, encoding)); + Assert.Equal(11, reader.TotalBitsConsumed); + Assert.Equal(JxlColorSpace.Gray, encoding.ColorSpace); + Assert.Equal(JxlWhitePoint.D65, encoding.WhitePoint); + Assert.False(encoding.HasPrimaries); + Assert.True(encoding.TransferFunction.Is709); + Assert.True(encoding.HaveFields); + } + + /// + /// Checks that a defined Unknown transfer code cannot describe colors without an ICC payload. + /// + [Fact] + public void ReadColorHeader_UnknownTransferRequiresIcc() + { + // Explicit RGB/D65/sRGB primaries, Unknown transfer, relative intent, with want_icc=0. + using MemoryStream input = new([0x50, 0x84, 0x00]); + JxlBitReader reader = new(input); + + Assert.False(JxlBundle.Read(reader, new JxlColorEncoding())); + Assert.Equal(17, reader.TotalBitsConsumed); + } + + /// + /// Checks both ends of the signed custom-coordinate encoding without floating-point rounding. + /// + [Fact] + public void ReadCustomCoordinates_SupportSignedWireExtremes() + { + // Each selector is 3. The 21-bit payloads restore packed values 4194303 and 4194302. + using MemoryStream input = new([0xFF, 0xFF, 0xFF, 0xFD, 0xFF, 0x3F]); + JxlBitReader reader = new(input); + JxlCustomXy coordinates = new(); + + Assert.True(coordinates.Visit(new JxlReadVisitor(reader))); + Assert.Equal(46, reader.TotalBitsConsumed); + Assert.Equal(-2097152, coordinates.X); + Assert.Equal(2097151, coordinates.Y); + } + /// /// Checks byte and UInt32 enum fields against fixed LSB-first wire data. /// From 9891ad738ea1231eb3621e6569d3e85fb292a2f4 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 12:00:39 +1000 Subject: [PATCH 241/328] Share JPEG XL and WebP LZ77 distance codes and prefix scans --- .../Compression/Lz77DistanceCodes.cs | 52 +++++++++++++++++++ .../Processing/Decoder/Ans/JxlAnsReader.cs | 29 ----------- .../Decoder/Ans/JxlAnsSymbolReader.cs | 7 +-- .../Encoder/Lz77/JxlLz77HashChain.cs | 16 +++--- .../Processing/Encoder/Lz77/JxlLz77HashMap.cs | 25 +++++---- .../Formats/Webp/Lossless/LosslessUtils.cs | 20 +++---- .../Webp/Lossless/WebpLosslessDecoder.cs | 16 ++---- .../Formats/Webp/WebpLookupTables.cs | 16 ------ .../Formats/WebP/LosslessUtilsTests.cs | 49 +++++++++++++++++ 9 files changed, 141 insertions(+), 89 deletions(-) create mode 100644 src/ImageSharp/Compression/Lz77DistanceCodes.cs diff --git a/src/ImageSharp/Compression/Lz77DistanceCodes.cs b/src/ImageSharp/Compression/Lz77DistanceCodes.cs new file mode 100644 index 0000000000..4b0803460d --- /dev/null +++ b/src/ImageSharp/Compression/Lz77DistanceCodes.cs @@ -0,0 +1,52 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Compression; + +/// +/// Decodes the short two-dimensional LZ77 distances shared by lossless WebP and JPEG XL. +/// +internal static class Lz77DistanceCodes +{ + /// + /// The number of short distances. Each codec handles its remaining distance symbols separately. + /// + public const int Count = 120; + + /// + /// Packed row and column offsets in wire order. Static byte data requires no managed table allocation. + /// + private static ReadOnlySpan Offsets => + [ + 0x18, 0x07, 0x17, 0x19, 0x28, 0x06, 0x27, 0x29, 0x16, 0x1a, + 0x26, 0x2a, 0x38, 0x05, 0x37, 0x39, 0x15, 0x1b, 0x36, 0x3a, + 0x25, 0x2b, 0x48, 0x04, 0x47, 0x49, 0x14, 0x1c, 0x35, 0x3b, + 0x46, 0x4a, 0x24, 0x2c, 0x58, 0x45, 0x4b, 0x34, 0x3c, 0x03, + 0x57, 0x59, 0x13, 0x1d, 0x56, 0x5a, 0x23, 0x2d, 0x44, 0x4c, + 0x55, 0x5b, 0x33, 0x3d, 0x68, 0x02, 0x67, 0x69, 0x12, 0x1e, + 0x66, 0x6a, 0x22, 0x2e, 0x54, 0x5c, 0x43, 0x4d, 0x65, 0x6b, + 0x32, 0x3e, 0x78, 0x01, 0x77, 0x79, 0x53, 0x5d, 0x11, 0x1f, + 0x64, 0x6c, 0x42, 0x4e, 0x76, 0x7a, 0x21, 0x2f, 0x75, 0x7b, + 0x31, 0x3f, 0x63, 0x6d, 0x52, 0x5e, 0x00, 0x74, 0x7c, 0x41, + 0x4f, 0x10, 0x20, 0x62, 0x6e, 0x30, 0x73, 0x7d, 0x51, 0x5f, + 0x40, 0x72, 0x7e, 0x61, 0x6f, 0x50, 0x71, 0x7f, 0x60, 0x70 + ]; + + /// + /// Decodes a zero-based short-distance index for the supplied row width or distance multiplier. + /// + /// The short-distance index, from zero through minus one. + /// The number of values represented by one preceding row. + /// The backward distance in values, with a minimum of one. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetDistance(int index, int multiplier) + { + // The upper nibble stores the preceding-row count. The lower nibble stores 8 minus the column offset. + // Narrow rows can produce zero or negative distances; both formats define those entries as distance one. + int offset = Offsets[index]; + int distance = (8 - (offset & 15)) + ((offset >> 4) * multiplier); + return distance > 1 ? distance : 1; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsReader.cs index 75b73dad29..1cdde7ff67 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsReader.cs @@ -15,8 +15,6 @@ internal static class JxlAnsReader { public const int WindowSize = 1 << 20; - public const int NumSpecialDistances = 120; - // Prefer jagged arrays over multidimensional arrays // for performance. Collection expressions help represent // jagged arrays easily. @@ -40,33 +38,6 @@ internal static class JxlAnsReader [3, 10], [4, 4], [3, 7], [4, 1], [3, 6], [3, 8], [3, 9], [4, 2], ]; - private static readonly sbyte[][] SpecialDistances = - [ - [0, 1], [1, 0], [1, 1], [-1, 1], [0, 2], [2, 0], [1, 2], [-1, 2], - [2, 1], [-2, 1], [2, 2], [-2, 2], [0, 3], [3, 0], [1, 3], [-1, 3], - [3, 1], [-3, 1], [2, 3], [-2, 3], [3, 2], [-3, 2], [0, 4], [4, 0], - [1, 4], [-1, 4], [4, 1], [-4, 1], [3, 3], [-3, 3], [2, 4], [-2, 4], - [4, 2], [-4, 2], [0, 5], [3, 4], [-3, 4], [4, 3], [-4, 3], [5, 0], - [1, 5], [-1, 5], [5, 1], [-5, 1], [2, 5], [-2, 5], [5, 2], [-5, 2], - [4, 4], [-4, 4], [3, 5], [-3, 5], [5, 3], [-5, 3], [0, 6], [6, 0], - [1, 6], [-1, 6], [6, 1], [-6, 1], [2, 6], [-2, 6], [6, 2], [-6, 2], - [4, 5], [-4, 5], [5, 4], [-5, 4], [3, 6], [-3, 6], [6, 3], [-6, 3], - [0, 7], [7, 0], [1, 7], [-1, 7], [5, 5], [-5, 5], [7, 1], [-7, 1], - [4, 6], [-4, 6], [6, 4], [-6, 4], [2, 7], [-2, 7], [7, 2], [-7, 2], - [3, 7], [-3, 7], [7, 3], [-7, 3], [5, 6], [-5, 6], [6, 5], [-6, 5], - [8, 0], [4, 7], [-4, 7], [7, 4], [-7, 4], [8, 1], [8, 2], [6, 6], - [-6, 6], [8, 3], [5, 7], [-5, 7], [7, 5], [-7, 5], [8, 4], [6, 7], - [-6, 7], [7, 6], [-7, 6], [8, 5], [7, 7], [-7, 7], [8, 6], [8, 7] - ]; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int SpecialDistance(int index, int multiplier) - { - Span indexDistance = SpecialDistances[index]; - int dist = indexDistance[0] + (multiplier * indexDistance[1]); - return dist > 1 ? dist : 1; - } - public static uint DecodeVariableLengthUint8(JxlBitReader reader) { if (reader.ReadBoolean()) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsSymbolReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsSymbolReader.cs index e34f645795..2e81cb67a5 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsSymbolReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsSymbolReader.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Buffers; +using SixLabors.ImageSharp.Compression; using SixLabors.ImageSharp.Formats.Jxl.IO; using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; @@ -30,7 +31,7 @@ internal sealed class JxlAnsSymbolReader : IDisposable private readonly int numSpecialDistances; private readonly JxlAnsHybridUIntConfiguration lz77LengthUInt = new(); private readonly int[]? lz77Window; - private readonly int[] specialDistances = new int[JxlAnsReader.NumSpecialDistances]; + private readonly int[] specialDistances = new int[Lz77DistanceCodes.Count]; private JxlAnsSymbolReader(JxlAnsCode code, JxlBitReader br, int distanceMultiplier, int[]? lz77WindowStorage) { @@ -61,11 +62,11 @@ private JxlAnsSymbolReader(JxlAnsCode code, JxlBitReader br, int distanceMultipl this.lz77LengthUInt = code.Lz77.LengthUintConfig; this.lz77Threshold = (int)code.Lz77.MinimumSymbol; this.lz77MinLength = (int)code.Lz77.MinimumLength; - this.numSpecialDistances = distanceMultiplier == 0 ? 0 : JxlAnsReader.NumSpecialDistances; + this.numSpecialDistances = distanceMultiplier == 0 ? 0 : Lz77DistanceCodes.Count; for (int i = 0; i < this.numSpecialDistances; i++) { - this.specialDistances[i] = JxlAnsReader.SpecialDistance(i, distanceMultiplier); + this.specialDistances[i] = Lz77DistanceCodes.GetDistance(i, distanceMultiplier); } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77HashChain.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77HashChain.cs index 0e70d3863c..36e142de6f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77HashChain.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77HashChain.cs @@ -4,7 +4,7 @@ using System.Buffers; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; +using SixLabors.ImageSharp.Compression; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; using SixLabors.ImageSharp.Memory; @@ -97,12 +97,12 @@ public JxlLz77HashChain( { // Count down, so if due to small distance multiplier multiple distances // map to the same code, the smallest code will be used in the end. - for (int i = JxlAnsReader.NumSpecialDistances - 1; i >= 0; --i) + for (int i = Lz77DistanceCodes.Count - 1; i >= 0; --i) { - this.specialDistTable[JxlAnsReader.SpecialDistance(i, distanceMultiplier)] = i; + this.specialDistTable[Lz77DistanceCodes.GetDistance(i, distanceMultiplier)] = i; } - this.numSpecialDistances = JxlAnsReader.NumSpecialDistances; + this.numSpecialDistances = Lz77DistanceCodes.Count; } } @@ -257,11 +257,9 @@ public void FindMatches(int pos, int maxDist, JxlLz77MatchFoundCallback foundMat j += r; } - while (i < end && data[i] == data[j]) - { - i++; - j++; - } + // Continue after the zero-run shortcut. Both sequences stop at the current candidate's limit; + // the earlier history position has at least the same available values. + i += MemoryExtensions.CommonPrefixLength(data.Slice(i, end - i), data.Slice(j, end - i)); len = (uint)(i - pos); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77HashMap.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77HashMap.cs index 2765750bf4..1cef91ce39 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77HashMap.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Lz77/JxlLz77HashMap.cs @@ -1,7 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Compression; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Lz77; @@ -32,7 +33,7 @@ public JxlLz77HashMap(List data, int distanceMultiplier, int bucketSize, i if (distanceMultiplier != 0) { - this.numSpecialDistances = JxlAnsReader.NumSpecialDistances; + this.numSpecialDistances = Lz77DistanceCodes.Count; this.specialDistTable = new List((distanceMultiplier * 8) + 8); for (int i = 0; i < (distanceMultiplier * 8) + 8; i++) @@ -40,9 +41,9 @@ public JxlLz77HashMap(List data, int distanceMultiplier, int bucketSize, i this.specialDistTable.Add(-1); } - for (int i = 0; i < JxlAnsReader.NumSpecialDistances; i++) + for (int i = 0; i < Lz77DistanceCodes.Count; i++) { - this.specialDistTable[JxlAnsReader.SpecialDistance(i, distanceMultiplier)] = (sbyte)i; + this.specialDistTable[Lz77DistanceCodes.GetDistance(i, distanceMultiplier)] = (sbyte)i; } } } @@ -105,6 +106,12 @@ private void Insert(int pos, int hash) bucket.Index = (bucket.Index + 1) % this.bucketSize; } + /// + /// Counts matching values from an earlier candidate to the end of the current sequence. + /// + /// The earlier candidate position. + /// The current position, strictly after a. + /// The number of equal leading values. private int MatchLength(int a, int b) { if (a >= b) @@ -112,14 +119,10 @@ private int MatchLength(int a, int b) throw new InvalidOperationException("a must be less than b"); } - int len = 0; + ReadOnlySpan values = CollectionsMarshal.AsSpan(this.data); - while (b + len < this.data.Count && this.data[a + len] == this.data[b + len]) - { - len++; - } - - return len; + // The later position bounds the match. Since a is earlier, both views cover that same number of values. + return MemoryExtensions.CommonPrefixLength(values.Slice(a, values.Length - b), values[b..]); } private struct HashBucket diff --git a/src/ImageSharp/Formats/Webp/Lossless/LosslessUtils.cs b/src/ImageSharp/Formats/Webp/Lossless/LosslessUtils.cs index 293ef94db7..1e69a0cc14 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/LosslessUtils.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/LosslessUtils.cs @@ -45,19 +45,19 @@ public static int FindMatchLength(ReadOnlySpan array1, ReadOnlySpan return VectorMismatch(array1, array2, maxLimit); } + /// + /// Counts equal leading values up to the caller's match-length limit. + /// + /// The first candidate sequence, covering at least length values. + /// The second candidate sequence, covering at least length values. + /// The maximum number of values to compare. + /// The first unequal value's index, or length when every compared value matches. [MethodImpl(InliningOptions.ShortMethod)] public static int VectorMismatch(ReadOnlySpan array1, ReadOnlySpan array2, int length) { - int matchLen = 0; - ref uint array1Ref = ref MemoryMarshal.GetReference(array1); - ref uint array2Ref = ref MemoryMarshal.GetReference(array2); - - while (matchLen < length && Unsafe.Add(ref array1Ref, (uint)matchLen) == Unsafe.Add(ref array2Ref, (uint)matchLen)) - { - matchLen++; - } - - return matchLen; + // The runtime compares uint sequences with its SIMD prefix scan and returns an element count. + // Limit both views so an equal prefix cannot extend beyond the current match candidate. + return MemoryExtensions.CommonPrefixLength(array1[..length], array2[..length]); } [MethodImpl(InliningOptions.ShortMethod)] diff --git a/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs b/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs index ebfdafa135..6b5e74bc26 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs @@ -7,6 +7,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.Compression; using SixLabors.ImageSharp.Formats.Webp.BitReader; using SixLabors.ImageSharp.Formats.Webp.ColorConverters; using SixLabors.ImageSharp.Memory; @@ -42,8 +43,6 @@ internal sealed class WebpLosslessDecoder private const uint PackedNonLiteralCode = 0; - private static readonly int CodeToPlaneCodes = WebpLookupTables.CodeToPlane.Length; - // Memory needed for lookup tables of one Huffman tree group. Red, blue, alpha and distance alphabets are constant (256 for red, blue and alpha, 40 for // distance) and lookup table sizes for them in worst case are 630 and 410 respectively. Size of green alphabet depends on color cache size and is equal // to 256 (green component values) + 24 (length prefix values) + color_cache_size (between 0 and 2048). @@ -947,18 +946,13 @@ private static uint GetMetaIndex(IMemoryOwner huffmanImage, int xSize, int private static int PlaneCodeToDistance(int xSize, int planeCode) { - if (planeCode > CodeToPlaneCodes) + if (planeCode > Lz77DistanceCodes.Count) { - return planeCode - CodeToPlaneCodes; + return planeCode - Lz77DistanceCodes.Count; } - int distCode = WebpLookupTables.CodeToPlane[planeCode - 1]; - int yOffset = distCode >> 4; - int xOffset = 8 - (distCode & 0xf); - int dist = (yOffset * xSize) + xOffset; - - // dist < 1 can happen if xSize is very small. - return dist >= 1 ? dist : 1; + // WebP numbers short-distance codes from one; the shared codebook uses zero-based indices. + return Lz77DistanceCodes.GetDistance(planeCode - 1, xSize); } /// diff --git a/src/ImageSharp/Formats/Webp/WebpLookupTables.cs b/src/ImageSharp/Formats/Webp/WebpLookupTables.cs index 64c97f7d8a..4c45329bc8 100644 --- a/src/ImageSharp/Formats/Webp/WebpLookupTables.cs +++ b/src/ImageSharp/Formats/Webp/WebpLookupTables.cs @@ -526,22 +526,6 @@ internal static class WebpLookupTables 2010.27454072f, 2019.69737440f, 2029.12591044f, 2038.56012640f ]; - public static readonly int[] CodeToPlane = - [ - 0x18, 0x07, 0x17, 0x19, 0x28, 0x06, 0x27, 0x29, 0x16, 0x1a, - 0x26, 0x2a, 0x38, 0x05, 0x37, 0x39, 0x15, 0x1b, 0x36, 0x3a, - 0x25, 0x2b, 0x48, 0x04, 0x47, 0x49, 0x14, 0x1c, 0x35, 0x3b, - 0x46, 0x4a, 0x24, 0x2c, 0x58, 0x45, 0x4b, 0x34, 0x3c, 0x03, - 0x57, 0x59, 0x13, 0x1d, 0x56, 0x5a, 0x23, 0x2d, 0x44, 0x4c, - 0x55, 0x5b, 0x33, 0x3d, 0x68, 0x02, 0x67, 0x69, 0x12, 0x1e, - 0x66, 0x6a, 0x22, 0x2e, 0x54, 0x5c, 0x43, 0x4d, 0x65, 0x6b, - 0x32, 0x3e, 0x78, 0x01, 0x77, 0x79, 0x53, 0x5d, 0x11, 0x1f, - 0x64, 0x6c, 0x42, 0x4e, 0x76, 0x7a, 0x21, 0x2f, 0x75, 0x7b, - 0x31, 0x3f, 0x63, 0x6d, 0x52, 0x5e, 0x00, 0x74, 0x7c, 0x41, - 0x4f, 0x10, 0x20, 0x62, 0x6e, 0x30, 0x73, 0x7d, 0x51, 0x5f, - 0x40, 0x72, 0x7e, 0x61, 0x6f, 0x50, 0x71, 0x7f, 0x60, 0x70 - ]; - public static readonly uint[] PlaneToCodeLut = [ 96, 73, 55, 39, 23, 13, 5, 1, 255, 255, 255, 255, 255, 255, 255, 255, diff --git a/tests/ImageSharp.Tests/Formats/WebP/LosslessUtilsTests.cs b/tests/ImageSharp.Tests/Formats/WebP/LosslessUtilsTests.cs index 27a1cc6aa2..0d915aa368 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/LosslessUtilsTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/LosslessUtilsTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Compression; using SixLabors.ImageSharp.Formats.Webp.Lossless; using SixLabors.ImageSharp.Tests.TestUtilities; @@ -9,6 +10,54 @@ namespace SixLabors.ImageSharp.Tests.Formats.Webp; [Trait("Format", "Webp")] public class LosslessUtilsTests { + /// + /// Checks row, column, endpoint, and narrow-row distances in the shared wire codebook. + /// + /// The zero-based short-distance index. + /// The number of values in each row. + /// The backward distance defined by the row and column offsets. + [Theory] + [InlineData(0, 10, 10)] + [InlineData(1, 10, 1)] + [InlineData(3, 10, 9)] + [InlineData(96, 10, 8)] + [InlineData(119, 10, 78)] + [InlineData(3, 1, 1)] + [InlineData(79, 1, 1)] + public void ShortDistance_DecodesRowAndColumnOffsets(int index, int width, int expected) + => Assert.Equal(expected, Lz77DistanceCodes.GetDistance(index, width)); + + /// + /// Checks bounded match scanning with runtime SIMD enabled. + /// + [Fact] + public void FindMatchLength_WithHardwareIntrinsics_Works() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunFindMatchLengthTest, HwIntrinsics.AllowAll); + + /// + /// Checks the same bounded match scanning without hardware intrinsics. + /// + [Fact] + public void FindMatchLength_WithoutHardwareIntrinsics_Works() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunFindMatchLengthTest, HwIntrinsics.DisableHWIntrinsic); + + /// + /// Checks the match limit, a partial-word mismatch, and the encoder's best-match rejection. + /// + private static void RunFindMatchLengthTest() + { + ReadOnlySpan left = [0x01020304, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; + ReadOnlySpan right = [0x01020304, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0x01000010]; + + // The final words differ in one byte. A byte-based SIMD scan must report complete uint values. + Assert.Equal(16, LosslessUtils.VectorMismatch(left, right, left.Length)); + Assert.Equal(9, LosslessUtils.VectorMismatch(left, right, 9)); + + // A candidate can extend the previous best only when the values at that position match. + Assert.Equal(16, LosslessUtils.FindMatchLength(left, right, 15, left.Length)); + Assert.Equal(0, LosslessUtils.FindMatchLength(left, right, 16, left.Length)); + } + private static void RunCombinedShannonEntropyTest() { int[] x = From 58bc2cc3dc638d4ba44685fb0b79ab42a09abb0a Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 12:12:12 +1000 Subject: [PATCH 242/328] Use allocator-owned JPEG XL entropy histories and borrowed spans --- .../Compression/Lz77DistanceCodes.cs | 2 +- .../Jxl/Processing/Decoder/Ans/JxlAnsCode.cs | 5 +- .../Decoder/Ans/JxlAnsSymbolReader.cs | 294 +++++++++--------- .../BlockLoader/JxlBitStreamBlockLoader.cs | 226 ++++++-------- .../Decoder/Group/JxlGroupDecoder.cs | 64 ++-- .../Decoder/JxlContextMapDecoder.cs | 43 ++- .../Decoder/Patch/JxlPatchDictionary.cs | 52 ++-- .../Jxl/Processing/JxlCoefficientOrder.cs | 18 +- .../Jxl/Processing/JxlPassesSharedState.cs | 12 +- .../Modular/Encoding/JxlMaDecoder.cs | 51 ++- .../Modular/Encoding/JxlModularEncoding.cs | 177 +++++++++-- .../Processing/Splines/JxlQuantizedSpline.cs | 60 +++- .../Jxl/Processing/Splines/JxlSplines.cs | 24 +- .../Formats/Jxl/Processing/AnsCommonTests.cs | 2 +- .../Jxl/Processing/EntropyCoderTests.cs | 13 +- 15 files changed, 618 insertions(+), 425 deletions(-) diff --git a/src/ImageSharp/Compression/Lz77DistanceCodes.cs b/src/ImageSharp/Compression/Lz77DistanceCodes.cs index 4b0803460d..ab50b2f0be 100644 --- a/src/ImageSharp/Compression/Lz77DistanceCodes.cs +++ b/src/ImageSharp/Compression/Lz77DistanceCodes.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Compression; /// -/// Decodes the short two-dimensional LZ77 distances shared by lossless WebP and JPEG XL. +/// Decodes short LZ77 distances expressed as row and column offsets. /// internal static class Lz77DistanceCodes { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsCode.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsCode.cs index d0674bf409..f4aecd865e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsCode.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsCode.cs @@ -22,7 +22,10 @@ internal sealed class JxlAnsCode public int MaxNumBits { get; set; } - public JxlAnsEntry[]? AliasTables { get; set; } + /// + /// Gets or sets the ANS lookup tables. Prefix-coded streams do not use these tables. + /// + public JxlAnsEntry[] AliasTables { get; set; } = []; public void UpdateMaxNumBits(int ctx, int symbol) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsSymbolReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsSymbolReader.cs index 2e81cb67a5..89b4afb18b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsSymbolReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsSymbolReader.cs @@ -5,40 +5,60 @@ using SixLabors.ImageSharp.Compression; using SixLabors.ImageSharp.Formats.Jxl.IO; using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; +/// +/// Decodes entropy symbols and hybrid integers, and owns the optional LZ77 history for one stream. +/// internal sealed class JxlAnsSymbolReader : IDisposable { - private const int MaxCheckpointInterval = 512; + private const uint WindowMask = JxlAnsReader.WindowSize - 1; - private const int WindowMask = JxlAnsReader.WindowSize - 1; - - private readonly JxlAnsEntry[] aliasTables = []; - private readonly List huffmanDecoders = []; + private readonly JxlAnsEntry[] aliasTables; + private readonly List huffmanDecoders; private readonly bool usePrefixCode; - private uint state = JxlAnsConstants.AnsTableSize << 16; - private readonly List configs = []; + private uint state; + private readonly List configs; private readonly int logAlphaSize; private readonly int logEntrySize; private readonly int entrySizeMinus1; - private int numDecoded; - private int numToCopy; - private int copyPos; + // The wire decoder counts UInt32 symbols. Keep these counters unsigned so high-bit values + // cannot become negative lengths or positions before the ring index is masked. + private uint numDecoded; + private uint numToCopy; + private uint copyPos; private readonly int lz77Context; private readonly int lz77MinLength; private readonly int lz77Threshold = 1 << 20; private readonly int numSpecialDistances; - private readonly JxlAnsHybridUIntConfiguration lz77LengthUInt = new(); - private readonly int[]? lz77Window; - private readonly int[] specialDistances = new int[Lz77DistanceCodes.Count]; + private readonly JxlAnsHybridUIntConfiguration lz77LengthUInt; + + /// + /// Allocator-owned history. Absent when the entropy header disables LZ77. + /// + private readonly IMemoryOwner? lz77Window; - private JxlAnsSymbolReader(JxlAnsCode code, JxlBitReader br, int distanceMultiplier, int[]? lz77WindowStorage) + /// + /// Cached distances for the 120 short-distance symbols. Existing inline storage avoids a separate array allocation. + /// + private InlineArray128 specialDistances; + + /// + /// Initializes a new instance of the class from decoded entropy tables. + /// + /// The configuration whose allocator owns the history buffer. + /// The decoded entropy tables and hybrid-integer configurations. + /// The reader positioned at the entropy stream's initial state. + /// The row width used by short-distance symbols, or zero for linear distances. + private JxlAnsSymbolReader(Configuration configuration, JxlAnsCode code, JxlBitReader br, int distanceMultiplier) { - this.aliasTables = code.AliasTables!; + this.aliasTables = code.AliasTables; this.huffmanDecoders = code.HuffmanData; this.usePrefixCode = code.UsePrefixCode; this.configs = code.UIntConfig; + this.lz77LengthUInt = code.Lz77.LengthUintConfig; if (!this.usePrefixCode) { @@ -57,9 +77,7 @@ private JxlAnsSymbolReader(JxlAnsCode code, JxlBitReader br, int distanceMultipl return; } - this.lz77Window = lz77WindowStorage; this.lz77Context = code.Lz77.NonserializedDistanceContext; - this.lz77LengthUInt = code.Lz77.LengthUintConfig; this.lz77Threshold = (int)code.Lz77.MinimumSymbol; this.lz77MinLength = (int)code.Lz77.MinimumLength; this.numSpecialDistances = distanceMultiplier == 0 ? 0 : Lz77DistanceCodes.Count; @@ -68,8 +86,15 @@ private JxlAnsSymbolReader(JxlAnsCode code, JxlBitReader br, int distanceMultipl { this.specialDistances[i] = Lz77DistanceCodes.GetDistance(i, distanceMultiplier); } + + // Allocate only after the initial state and distance setup succeed. A truncated header cannot strand an owner. + // The ring is initialized by literals or the format's zero-distance rule before any history is read. + this.lz77Window = configuration.MemoryAllocator.Allocate(JxlAnsReader.WindowSize); } + /// + /// Gets whether the prefix stream can encode only literals and repetitions of the previous value. + /// public bool IsHuffRleOnly { get @@ -100,16 +125,27 @@ public bool IsHuffRleOnly } } + /// + /// Gets whether this stream uses LZ77 references. + /// public bool UsesLz77 => this.lz77Window is not null; - public static JxlAnsSymbolReader Create(JxlAnsCode code, JxlBitReader br, int distanceMultiplier) - { - int[]? lz77 = code.Lz77.Enabled - ? ArrayPool.Shared.Rent(JxlAnsReader.WindowSize) - : null; + /// + /// Gets the borrowed LZ77 history, or an empty span when LZ77 is disabled. + /// + /// Borrow this span before a decode loop and pass it through that loop. It remains valid until disposal. + public Span Lz77Window => this.lz77Window is null ? Span.Empty : this.lz77Window.GetSpan(); - return new JxlAnsSymbolReader(code, br, distanceMultiplier, lz77); - } + /// + /// Creates a symbol reader and allocates history only when its decoded entropy header requires it. + /// + /// The configuration whose allocator owns the history buffer. + /// The decoded entropy tables. + /// The reader positioned at the stream's initial state. + /// The row width used by short-distance symbols, or zero for linear distances. + /// The reader, which the caller must dispose. + public static JxlAnsSymbolReader Create(Configuration configuration, JxlAnsCode code, JxlBitReader br, int distanceMultiplier) + => new(configuration, code, br, distanceMultiplier); /// /// Checks that decoding consumed the entropy stream's terminal ANS state. @@ -163,6 +199,13 @@ public int ReadSymbol(int histogramIndex, JxlBitReader br) return this.ReadSymbolWithoutRefill(histogramIndex, br); } + /// + /// Expands a hybrid-integer token and its payload without changing entropy or history state. + /// + /// The split exponent and token bit allocation from the entropy header. + /// The entropy-decoded token. + /// The reader positioned at the token's remaining low-order bits. + /// The UInt32 value, represented by the same bit pattern in an Int32. public static int ReadHybridUIntConfiguration(JxlAnsHybridUIntConfiguration config, int token, JxlBitReader br) { uint splitToken = config.SplitToken; @@ -177,6 +220,7 @@ public static int ReadHybridUIntConfiguration(JxlAnsHybridUIntConfiguration conf uint numBits = splitExponent - (msbInToken + lsbInToken) + (uint)((token - splitToken) >> (int)(msbInToken + lsbInToken)); + // Keep malformed shift counts within the UInt32 primitive's width, as in the reference decoder. numBits &= 31u; @@ -193,7 +237,14 @@ public static int ReadHybridUIntConfiguration(JxlAnsHybridUIntConfiguration conf return (int)result; } - public void ReadHybridUIntClusteredHuffRleOnly(int context, JxlBitReader br, ref int value, ref int run) + /// + /// Reads a literal or repeated-value run from a prefix stream restricted to the RLE-only distance code. + /// + /// The histogram index. + /// The encoded symbols. + /// The previous value, replaced only by a literal. + /// The number of additional repetitions after the current output value. + public void ReadHybridUIntClusteredHuffRleOnly(int context, JxlBitReader br, ref uint value, ref uint run) { DebugGuard.IsTrue(this.IsHuffRleOnly, "This method is meant for huffman RLE only"); br.Refill(); @@ -201,18 +252,26 @@ public void ReadHybridUIntClusteredHuffRleOnly(int context, JxlBitReader br, ref int token = this.ReadSymbolHuffmanWithoutRefill(context, br); if (token >= this.lz77Threshold) { - run = + run = (uint)( ReadHybridUIntConfiguration(this.lz77LengthUInt, token - this.lz77Threshold, br) + - this.lz77MinLength - 1; + this.lz77MinLength - 1); // A run token repeats the previous value. It does not also encode a new literal. return; } - value = ReadHybridUIntConfiguration(this.configs[context], token, br); + value = (uint)ReadHybridUIntConfiguration(this.configs[context], token, br); } - public int ReadHybridUIntClustered(bool usesLz77, int context, JxlBitReader br) + /// + /// Reads one integer from a selected histogram and updates the borrowed history when LZ77 is enabled. + /// + /// The LZ77 mode established by the stream header. + /// The histogram index. + /// The encoded symbols. + /// The history span borrowed from this reader before the decode loop. + /// The decoded UInt32 bit pattern, stored in an Int32. + public int ReadHybridUIntClustered(bool usesLz77, int context, JxlBitReader br, Span window) { int ret; @@ -220,9 +279,9 @@ public int ReadHybridUIntClustered(bool usesLz77, int context, JxlBitReader br) { if (this.numToCopy > 0) { - ret = this.lz77Window![(this.copyPos++) & WindowMask]; + ret = window[(int)((this.copyPos++) & WindowMask)]; this.numToCopy--; - this.lz77Window[(this.numDecoded++) & WindowMask] = ret; + window[(int)((this.numDecoded++) & WindowMask)] = ret; return ret; } } @@ -234,19 +293,21 @@ public int ReadHybridUIntClustered(bool usesLz77, int context, JxlBitReader br) { if (token >= this.lz77Threshold) { - this.numToCopy = ReadHybridUIntConfiguration(this.lz77LengthUInt, token - this.lz77Threshold, br) + this.lz77MinLength; + this.numToCopy = (uint)ReadHybridUIntConfiguration(this.lz77LengthUInt, token - this.lz77Threshold, br) + (uint)this.lz77MinLength; br.Refill(); - int d_token = this.ReadSymbolWithoutRefill(this.lz77Context, br); - int distance = ReadHybridUIntConfiguration(this.configs[this.lz77Context], d_token, br); + int distanceToken = this.ReadSymbolWithoutRefill(this.lz77Context, br); - if (distance < this.numSpecialDistances) + // Hybrid integers retain all 32 bits. Widen before adding one so the largest distance + // cannot wrap to zero. The format then bounds it by the available history and ring size. + ulong distance = (uint)ReadHybridUIntConfiguration(this.configs[this.lz77Context], distanceToken, br); + if (distance < (uint)this.numSpecialDistances) { - distance = this.specialDistances[distance]; + distance = (uint)this.specialDistances[(int)distance]; } else { - distance = distance + 1 - this.numSpecialDistances; + distance = distance + 1 - (uint)this.numSpecialDistances; } if (distance > this.numDecoded) @@ -259,56 +320,74 @@ public int ReadHybridUIntClustered(bool usesLz77, int context, JxlBitReader br) distance = JxlAnsReader.WindowSize; } - this.copyPos = this.numDecoded - distance; + this.copyPos = this.numDecoded - (uint)distance; if (distance == 0) { - if (this.lz77Window is null) - { - throw new InvalidOperationException("Missing LZ77 window"); - } - - int toFill = Math.Min(this.numToCopy, JxlAnsReader.WindowSize); - this.lz77Window.AsSpan(0, toFill).Clear(); + int toFill = (int)Math.Min(this.numToCopy, JxlAnsReader.WindowSize); + window[..toFill].Clear(); } + // Adding the minimum length can wrap a malformed UInt32 length. Reject that run + // instead of continuing to emit history values, as required by the native decoder. if (this.numToCopy < this.lz77MinLength) { this.numToCopy = 0; throw new InvalidOperationException("ANS decoder error"); } - ret = this.lz77Window![(this.copyPos++) & WindowMask]; + ret = window[(int)((this.copyPos++) & WindowMask)]; this.numToCopy--; - this.lz77Window[(this.numDecoded++) & WindowMask] = ret; + window[(int)((this.numDecoded++) & WindowMask)] = ret; return ret; } } ret = ReadHybridUIntConfiguration(this.configs[context], token, br); - if (usesLz77 && this.lz77Window is not null) + if (usesLz77) { - this.lz77Window[(this.numDecoded++) & WindowMask] = ret; + window[(int)((this.numDecoded++) & WindowMask)] = ret; } return ret; } - public int ReadHybridUIntInlined(bool usesLz77, int context, JxlBitReader br, Span contextMap) - => this.ReadHybridUIntClustered(usesLz77, contextMap[context], br); - - public int ReadHybridUIntInlined(bool usesLz77, int context, JxlBitReader br, List contextMap) - => this.ReadHybridUIntClustered(usesLz77, contextMap[context], br); - - public int ReadHybridUInt(int context, JxlBitReader br, Span contextMap) - => this.ReadHybridUIntClustered(this.UsesLz77, contextMap[context], br); + /// + /// Reads one mapped integer using the caller's invariant LZ77 mode. + /// + /// The mode established by the stream header. + /// The unclustered entropy context. + /// The encoded symbols. + /// The map from entropy contexts to histograms. + /// The history borrowed before the decode loop. + /// The decoded UInt32 bit pattern, stored in an Int32. + public int ReadHybridUInt(bool usesLz77, int context, JxlBitReader br, ReadOnlySpan contextMap, Span window) + => this.ReadHybridUIntClustered(usesLz77, contextMap[context], br, window); - public int ReadHybridUInt(int context, JxlBitReader br, List contextMap) - => this.ReadHybridUIntClustered(this.UsesLz77, contextMap[context], br); + /// + /// Reads one mapped integer using this reader's LZ77 mode. + /// + /// The unclustered entropy context. + /// The encoded symbols. + /// The map from entropy contexts to histograms. + /// The history borrowed before the decode loop. + /// The decoded UInt32 bit pattern, stored in an Int32. + public int ReadHybridUInt(int context, JxlBitReader br, ReadOnlySpan contextMap, Span window) + => this.ReadHybridUIntClustered(this.UsesLz77, contextMap[context], br, window); - public bool IsSingleValueAndAdvance(int context, ref int value, int count) + /// + /// Advances a constant-symbol stream without reading a symbol for each output value. + /// + /// The histogram index. + /// The number of output values. + /// The constant value, or zero when this optimization does not apply. + /// The history borrowed before the decode loop. + /// Whether the histogram encodes a single literal value. + public bool IsSingleValueAndAdvance(int context, int count, out uint value, Span window) { + value = 0; + if (this.usePrefixCode || this.numToCopy != 0) { return false; @@ -325,102 +404,21 @@ public bool IsSingleValueAndAdvance(int context, ref int value, int count) return false; } - value = symbol.Value; + value = (uint)symbol.Value; if (this.lz77Window is not null) { for (int i = 0; i < count; i++) { - this.lz77Window[(this.numDecoded++) & WindowMask] = symbol.Value; + window[(int)((this.numDecoded++) & WindowMask)] = symbol.Value; } } return true; } - public void Save(Checkpoint checkpoint) - { - checkpoint.State = this.state; - checkpoint.NumDecoded = this.numDecoded; - checkpoint.NumToCopy = this.numToCopy; - checkpoint.CopyPos = this.copyPos; - - if (this.lz77Window is not null) - { - int windowStart = this.numDecoded & WindowMask; - int windowEnd = (this.numDecoded + MaxCheckpointInterval) & WindowMask; - - if (windowEnd > windowStart) - { - this.lz77Window.AsSpan(windowStart, windowEnd - windowStart) - .CopyTo(this.lz77Window); - } - else - { - this.lz77Window.AsSpan(windowStart, JxlAnsReader.WindowSize - windowStart) - .CopyTo(this.lz77Window); - - this.lz77Window.AsSpan(0, windowEnd) - .CopyTo(this.lz77Window.AsSpan(JxlAnsReader.WindowSize - windowStart, windowEnd)); - } - } - } - - private void Restore(Checkpoint checkpoint) - { - this.state = checkpoint.State; - - if (this.numDecoded > checkpoint.NumDecoded + MaxCheckpointInterval) - { - throw new InvalidOperationException("numDecoded is too large"); - } - - this.numDecoded = checkpoint.NumDecoded; - this.numToCopy = checkpoint.NumToCopy; - this.copyPos = checkpoint.CopyPos; - - if (this.lz77Window is not null) - { - int windowStart = this.numDecoded & WindowMask; - int windowEnd = (this.numDecoded + MaxCheckpointInterval) & WindowMask; - - if (windowEnd > windowStart) - { - checkpoint.Lz77Window.AsSpan(0, windowEnd - windowStart) - .CopyTo(this.lz77Window.AsSpan(windowStart)); - } - else - { - int firstLength = JxlAnsReader.WindowSize - windowStart; - - checkpoint.Lz77Window.AsSpan(0, firstLength) - .CopyTo(this.lz77Window.AsSpan(windowStart)); - - checkpoint.Lz77Window.AsSpan(firstLength, windowEnd) - .CopyTo(this.lz77Window); - } - } - } - - public void Dispose() - { - if (this.lz77Window is not null) - { - ArrayPool.Shared.Return(this.lz77Window); - } - } - - // Use class because the Lz77Window property uses 2KB memory - public sealed class Checkpoint - { - public uint State { get; set; } - - public int NumToCopy { get; set; } - - public int CopyPos { get; set; } - - public int NumDecoded { get; set; } - - public int[] Lz77Window { get; set; } = new int[MaxCheckpointInterval]; - } -} + /// + /// Releases the allocator-owned LZ77 history. + /// + public void Dispose() => this.lz77Window?.Dispose(); +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/BlockLoader/JxlBitStreamBlockLoader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/BlockLoader/JxlBitStreamBlockLoader.cs index ae2e4b2b9a..9ab228322c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/BlockLoader/JxlBitStreamBlockLoader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/BlockLoader/JxlBitStreamBlockLoader.cs @@ -1,22 +1,19 @@ // 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.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; -using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Group.BlockLoader; /// -/// Loads coefficient blocks from the JPEG XL bitstream. +/// Decodes coefficient blocks and owns the entropy readers for one group. /// -internal sealed class JxlBitStreamBlockLoader : IJxlGetBlock +internal sealed class JxlBitStreamBlockLoader : IJxlGetBlock, IDisposable { private readonly JxlGroupDecoderCache groupDecCache; private readonly JxlBlockContextMap blockCtxMap; @@ -24,78 +21,77 @@ internal sealed class JxlBitStreamBlockLoader : IJxlGetBlock private readonly JxlPlane quantDc; private readonly JxlAnsSymbolReader[] decoders; private readonly JxlBitReader[] readers; - - private IMemoryOwner? shiftForPass; - private byte[]? coeffOrders; - private List[]? contextMaps; - - private int coeffOrderSize; - private int numPasses; - private int[] ctxOffset; - private int nzerosStride; - + private readonly List codes; + private readonly List coeffOrders; + private readonly List> contextMaps; + private readonly uint[] shiftForPass; + private readonly int coeffOrderSize; + private readonly int firstPass; + private readonly int[] ctxOffset; + private readonly int nzerosStride; + private readonly Rectangle rect; private InlineArray3 hshift; private InlineArray3 vshift; - - private int currentBy; private Memory qfRow; private Memory quantDcRow; - - private Rectangle rect; - - // 11 because JxlShared.MaximumNumberOfPasses = 11 - private InlineArray3>> rowNZeroes; - private InlineArray3>> rowNZeroesTop; - + private int initializedDecoders; + + /// + /// Initializes a new instance of the class. + /// + /// The parsed frame settings and pass shifts. + /// The owner of coefficient orders, quantization planes, and entropy tables. + /// The owner of the group's coefficient workspace. + /// The borrowed bit reader for each requested pass. + /// The first requested frame pass. + /// The number of requested passes. + /// The group's rectangle in the frame's block grid. public JxlBitStreamBlockLoader( + JxlFrameHeader frameHeader, + JxlPassesDecoderState decoderState, JxlGroupDecoderCache groupDecCache, - JxlBlockContextMap blockCtxMap, - JxlPlane rawQuantField, - JxlPlane quantDc, - JxlAnsSymbolReader[] decoders, - JxlBitReader[] readers) + JxlBitReader[] readers, + int firstPass, + int numPasses, + Rectangle rect) { this.groupDecCache = groupDecCache; - this.blockCtxMap = blockCtxMap; - this.rawQuantField = rawQuantField; - this.quantDc = quantDc; - this.decoders = decoders; + this.blockCtxMap = decoderState.Shared.BlockContextMap; + this.rawQuantField = decoderState.Shared.RawQuantField; + this.quantDc = decoderState.Shared.QuantDc; + this.codes = decoderState.Code; + this.coeffOrders = decoderState.Shared.CoeffOrders; + this.contextMaps = decoderState.ContextMap; + this.shiftForPass = frameHeader.Passes.Shift; + this.coeffOrderSize = decoderState.Shared.CoeffOrderSize; + this.firstPass = firstPass; + this.rect = rect; this.readers = readers; - this.shiftForPass = null; - this.coeffOrders = null; - this.coeffOrderSize = 0; - this.numPasses = 0; - this.ctxOffset = []; - this.nzerosStride = 0; - this.hshift = default; - this.vshift = default; - this.currentBy = 0; - this.qfRow = Memory.Empty; - this.quantDcRow = Memory.Empty; + this.decoders = new JxlAnsSymbolReader[numPasses]; + this.ctxOffset = new int[numPasses]; + this.nzerosStride = groupDecCache.NumberOfNonZeroes[0].Plane(0).PixelsPerRow; + + // Frame tables and planes outlive this group. Borrow them instead of copying the coefficient orders + // or allocating pass-shift storage. Only the entropy readers and their optional histories belong here. + JxlYCbCrChromaSubsampling subsampling = frameHeader.ChromaSubsampling!; + for (int c = 0; c < 3; c++) + { + this.hshift[c] = subsampling.HShift(c); + this.vshift[c] = subsampling.VShift(c); + } } - public JxlAnsSymbolReader[] Decoders => this.decoders; + /// + /// Gets the entropy readers after successful initialization. + /// + public ReadOnlySpan Decoders => this.decoders; /// public void StartRow(int by) { - this.currentBy = by; - this.qfRow = this.rawQuantField.GetRowMemory(by); - - for (int c = 0; c < 3; c++) - { - int sby = by >> this.vshift[c]; - this.quantDcRow = this.quantDc.GetRowMemory(this.rect.Y0() + by)[this.rect.X0()..]; - - for (int i = 0; i < this.numPasses; i++) - { - this.rowNZeroes[i][c] = this.groupDecCache.NumberOfNonZeroes[i].PlaneRowMemory(c, sby); - this.rowNZeroesTop[i][c] = - sby == 0 - ? default - : this.groupDecCache.NumberOfNonZeroes[i].PlaneRowMemory(c, sby - 1); - } - } + // Quantization planes use frame coordinates; coefficient positions inside the group remain local. + this.qfRow = this.rawQuantField.GetRowMemory(this.rect.Y + by)[this.rect.X..]; + this.quantDcRow = this.quantDc.GetRowMemory(this.rect.Y + by)[this.rect.X..]; } /// @@ -110,7 +106,14 @@ public bool TryLoadBlock( JxlDctAcPointer block2, JxlDctAcType acType) { - for (int c = 0; c < 3; c++) + // Quantization rows and frame coefficient orders stay fixed while this block's passes are decoded. + ReadOnlySpan quantDcRow = this.quantDcRow.Span; + ReadOnlySpan qfRow = this.qfRow.Span; + ReadOnlySpan orders = CollectionsMarshal.AsSpan(this.coeffOrders); + + // Y is channel 1. Its nonzero counts must be decoded before X and B, as in the native block traversal. + ReadOnlySpan channelOrder = [1, 0, 2]; + foreach (int c in channelOrder) { int sbx = bx >> this.hshift[c]; int sby = by >> this.vshift[c]; @@ -120,13 +123,11 @@ public bool TryLoadBlock( continue; } - for (int pass = 0; pass < this.numPasses; pass++) + for (int pass = 0; pass < this.decoders.Length; pass++) { + int framePass = this.firstPass + pass; Span rowNzeros = this.groupDecCache.NumberOfNonZeroes[pass].PlaneRow(c, sby); - ReadOnlySpan rowNzerosTop = sby == 0 - ? default - : this.groupDecCache.NumberOfNonZeroes[pass].PlaneRow(c, sby - 1); - + ReadOnlySpan rowNzerosTop = sby == 0 ? default : this.groupDecCache.NumberOfNonZeroes[pass].PlaneRow(c, sby - 1); JxlDctAcPointer block = c switch { 0 => block0, @@ -146,15 +147,15 @@ public bool TryLoadBlock( sbx, bx, acs, - this.coeffOrders!.AsSpan()[(pass * this.coeffOrderSize)..], + orders[(framePass * this.coeffOrderSize)..], this.readers[pass], this.decoders[pass], - CollectionsMarshal.AsSpan(this.contextMaps![pass]), - this.quantDcRow, - this.qfRow, + CollectionsMarshal.AsSpan(this.contextMaps[framePass]), + quantDcRow, + qfRow, this.blockCtxMap, block, - (int)this.shiftForPass!.Memory.Span[pass])) + (int)this.shiftForPass[framePass])) { return false; } @@ -164,69 +165,44 @@ public bool TryLoadBlock( return true; } - public bool Init( - JxlFrameHeader frameHeader, - JxlBitReader[] readers, - int numPasses, - int groupIndex, - int histogramSelectorBits, - Rectangle rect, - JxlPassesDecoderState decState, - int firstPass) + /// + /// Reads each pass's histogram selector and creates its allocator-owned entropy history. + /// + /// The configuration whose allocator owns the histories. + /// The bit count used to select a histogram set. + /// The number of histogram sets declared by the frame. + /// Whether every encoded histogram selector is valid. + public bool Init(Configuration configuration, int histogramSelectorBits, int numHistograms) { - for (int i = 0; i < 3; i++) + for (int pass = 0; pass < this.decoders.Length; pass++) { - this.hshift[i] = frameHeader.ChromaSubsampling!.HShift(i); - this.vshift[i] = frameHeader.ChromaSubsampling.VShift(i); - } - - this.coeffOrderSize = decState.Shared.CoeffOrderSize; - this.coeffOrders = decState.Shared.CoeffOrders.ToArray()[(firstPass * this.coeffOrderSize)..]; - - this.contextMaps = new List[numPasses]; - for (int i = 0; i < numPasses; i++) - { - this.contextMaps[i] = decState.ContextMap[firstPass + i]; - } - - this.numPasses = numPasses; - this.shiftForPass = decState.Shared.FrameHeader.Passes.Shift[firstPass..]; - this.nzerosStride = this.groupDecCache!.NumberOfNonZeroes[0].PixelsPerRow; - - this.ctxOffset = new int[numPasses]; - - for (int pass = 0; pass < numPasses; pass++) - { - int currentHistogram = 0; - - if (histogramSelectorBits != 0) - { - currentHistogram = (int)readers[pass].ReadBits32((uint)histogramSelectorBits); - } - - if (currentHistogram >= decState.Shared.NumHistograms) + int histogram = histogramSelectorBits == 0 ? 0 : (int)this.readers[pass].ReadBits32((uint)histogramSelectorBits); + if (histogram >= numHistograms) { return false; } - this.ctxOffset[pass] = currentHistogram * this.blockCtxMap.AcContextCount; + this.ctxOffset[pass] = histogram * this.blockCtxMap.AcContextCount; - this.decoders[pass] = - JxlAnsSymbolReader.Create( - decState.Code[pass + firstPass], - readers[pass]); + // VarDCT uses direct distances, not the modular image-row distance codebook. + // Count each completed reader so disposal also releases histories after a later pass fails. + this.decoders[pass] = JxlAnsSymbolReader.Create(configuration, this.codes[this.firstPass + pass], this.readers[pass], 0); + this.initializedDecoders++; } - for (int i = 1; i < numPasses; i++) + return true; + } + + /// + /// Releases the initialized entropy histories. Borrowed frame tables, planes, and bit readers remain owned by their callers. + /// + public void Dispose() + { + for (int pass = 0; pass < this.initializedDecoders; pass++) { - if (this.nzerosStride != this.groupDecCache.NumberOfNonZeroes[i].PixelsPerRow) - { - return false; - } + this.decoders[pass].Dispose(); } - this.rect = rect; - - return true; + this.initializedDecoders = 0; } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs index 24cf2fa7cb..017642826c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs @@ -396,7 +396,7 @@ private static bool DecodeGroupCore( } } - if (!getBlock.LoadBlock(bx, by, acs, size, log2CoveredBlocks, qblock0, qblock1, qblock2, acType)) + if (!getBlock.TryLoadBlock(bx, by, acs, size, log2CoveredBlocks, qblock0, qblock1, qblock2, acType)) { return false; } @@ -540,7 +540,7 @@ public static bool DecodeAcVarBlock( ReadOnlySpan qfRow, JxlBlockContextMap blockCtxMap, JxlDctAcPointer block, - int shift = 0) + int shift) { // Equal to number of LLF coefficients. int coveredBlocks = 1 << log2CoveredBlocks; @@ -552,9 +552,12 @@ public static bool DecodeAcVarBlock( int blockCtx = blockCtxMap.Context(qdcRow[lbx], (uint)qfRow[bx], ord, c); int nzeroCtx = blockCtxMap.NonZeroContext(predictedNzeros, blockCtx) + ctxOffset; - int nzeros = decoder.ReadHybridUintInlined(usesLz77, nzeroCtx, br, contextMap); - if (nzeros > size - coveredBlocks) + // Borrow history once for this block. Every coefficient advances the same pass reader. + Span window = decoder.Lz77Window; + int nzeros = decoder.ReadHybridUInt(usesLz77, nzeroCtx, br, contextMap, window); + + if ((uint)nzeros > (uint)(size - coveredBlocks)) { return false; } @@ -582,14 +585,10 @@ public static bool DecodeAcVarBlock( log2CoveredBlocks, prev); - int uCoeff = decoder.ReadHybridUintInlined(usesLz77, ctx, br, contextMap); - - // Hand-rolled version of UnpackSigned, shifting before conversion - // to signed integer to avoid undefined behavior. - int magnitude = uCoeff >> 1; - int negSign = (~uCoeff) & 1; + int uCoeff = decoder.ReadHybridUInt(usesLz77, ctx, br, contextMap, window); - int coeff = (magnitude ^ (negSign - 1)) << shift; + // Interpret the complete unsigned code before applying the progressive pass's coefficient shift. + int coeff = JxlPackSigned.UnpackSigned((uint)uCoeff) << shift; if (acType == JxlDctAcType.Ac16) { @@ -612,7 +611,26 @@ public static bool DecodeAcVarBlock( return true; } + /// + /// Decodes the requested coefficient passes and draws the group when the frame settings require it. + /// + /// The configuration whose allocator owns entropy histories. + /// The parsed frame and pass settings. + /// One bit reader per requested pass. + /// The number of passes to decode. + /// The frame's group index. + /// The owner of frame tables and reconstructed coefficients. + /// The reusable coefficient and transform workspace. + /// The existing render-workspace index. + /// The destination group buffers. + /// The JPEG reconstruction destination, when requested. + /// The first frame pass to decode. + /// Whether to draw before the last pass. + /// Whether this request contains only DC data. + /// Whether the decoded group requires rendering. + /// Whether coefficient decoding and entropy-state checks succeeded. public static bool DecodeGroup( + Configuration configuration, JxlFrameHeader frameHeader, JxlBitReader[] readers, int numPasses, @@ -627,7 +645,7 @@ public static bool DecodeGroup( bool dcOnly, ref bool shouldRunPipeline) { - bool draw = numPasses + firstPass == frameHeader.Passes.NumPasses || forceDraw; + bool draw = numPasses + firstPass == frameHeader.Passes.NumberOfPasses || forceDraw; shouldRunPipeline = draw; @@ -768,31 +786,23 @@ public static bool DecodeGroup( Rectangle blockGroupRect = decState.Shared.FrameDimensions.BlockGroupRect(groupIndex); - JxlBitStreamBlockLoader getBlock = new(); - - if (!getBlock.Init( - frameHeader, - readers, - numPasses, - groupIndex, - histogramSelectorBits, - blockGroupRect, - decState, - firstPass)) + // The group owns its entropy readers. Dispose them on all exits, including partial initialization. + using JxlBitStreamBlockLoader getBlock = new(frameHeader, decState, groupDecCache, readers, firstPass, numPasses, blockGroupRect); + if (!getBlock.Init(configuration, histogramSelectorBits, decState.Shared.NumHistograms)) { return false; } if (!DecodeGroupCore( frameHeader, - ref getBlock, + getBlock, groupDecCache, decState, thread, groupIndex, renderPipelineInput, jpegData, - draw)) + draw ? JxlDrawMode.Draw : JxlDrawMode.DoNotDraw)) { return false; } @@ -831,13 +841,13 @@ public static bool DecodeGroupForRoundtrip( return DecodeGroupCore( frameHeader, - ref getBlock, + getBlock, groupDecCache, decState, thread, groupIndex, renderPipelineInput, jpegData, - true); + JxlDrawMode.Draw); } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlContextMapDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlContextMapDecoder.cs index 9695384b48..10f6c97569 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlContextMapDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlContextMapDecoder.cs @@ -8,13 +8,22 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +/// +/// Reads the mapping from entropy contexts to their shared histograms. +/// internal static class JxlContextMapDecoder { private const int MaxClusters = 256; + /// + /// Checks that a decoded map uses every histogram and contains no index outside the declared count. + /// + /// The decoded histogram indices. + /// The histogram count derived from the map. public static void VerifyContextMap(List contextMap, int numHTrees) { Span haveHTree = stackalloc bool[numHTrees]; + haveHTree.Clear(); int numFound = 0; @@ -38,6 +47,13 @@ public static void VerifyContextMap(List contextMap, int numHTrees) } } + /// + /// Reads a nonempty context map, including its optional move-to-front representation. + /// + /// The configuration used to decode the map's entropy tables. + /// Storage whose logical length equals the number of entropy contexts. + /// The resulting histogram count. + /// The encoded context map. public static void DecodeContextMap(Configuration configuration, List contextMap, ref int numHTrees, JxlBitReader input) { // True - simple context map, without any ANS entropy coding, @@ -69,29 +85,30 @@ public static void DecodeContextMap(Configuration configuration, List cont JxlAnsCode ansCode = new(); List sinkContextMap = []; - if (!JxlAnsReader.DecodeHistograms(configuration, input, 1, ansCode, sinkContextMap, contextMap.Count <= 2)) - { - throw new InvalidOperationException("Context map histogram couldn't be decoded"); - } + JxlAnsReader.DecodeHistograms(configuration, input, 1, ansCode, ref sinkContextMap, contextMap.Count <= 2); - JxlAnsSymbolReader ans = JxlAnsSymbolReader.Create(ansCode, input); + // Context-map symbols use direct distances. A zero multiplier disables image-row distance shortcuts. + using JxlAnsSymbolReader ans = JxlAnsSymbolReader.Create(configuration, ansCode, input, 0); + // The nested stream owns its LZ77 window. Borrow it and the stable map once for the complete symbol loop. + Span window = ans.Lz77Window; + ReadOnlySpan contexts = CollectionsMarshal.AsSpan(sinkContextMap); int i = 0; - int maxSymbols = 0; while (i < cmSpan.Length) { - uint sym = ans.ReadHybridUInt(true, 0, input, sinkContextMap); - maxSymbols = sym > maxSymbols ? (int)sym : maxSymbols; + int sym = ans.ReadHybridUInt(0, input, contexts, window); + + // A context-map entry is one byte. Validate the full decoded UInt32 bit pattern before narrowing it. + if ((uint)sym >= MaxClusters) + { + throw new InvalidOperationException("Invalid cluster ID"); + } + cmSpan[i] = (byte)sym; i++; } - if (maxSymbols >= MaxClusters) - { - throw new InvalidOperationException("Invalid cluster ID"); - } - if (!ans.CheckAnsFinalState()) { throw new InvalidOperationException("Invalid context map"); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchDictionary.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchDictionary.cs index 30b259ff97..a18236609b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchDictionary.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchDictionary.cs @@ -48,6 +48,15 @@ public int GetReferences() return result; } + /// + /// Reads patch references, positions, and blending settings from their entropy stream. + /// + /// The configuration that owns entropy and patch-tree storage. + /// The encoded patch fields. + /// The destination width. + /// The destination height. + /// The available extra-channel count. + /// Whether decoded blending settings use extra channels. public void Decode( Configuration configuration, JxlBitReader br, @@ -62,19 +71,16 @@ public void Decode( List contextMap = []; JxlAnsCode code = new(); - var status = DecodeHistograms( - configuration, - br, - PatchDictionaryContexts, - code, - contextMap); + JxlAnsReader.DecodeHistograms(configuration, br, (int)JxlPatchContext.NumPatchDictionaryContexts, code, ref contextMap, false); - JxlAnsSymbolReader decoder = JxlAnsSymbolReader.Create(code, br); + // Patch fields use direct LZ77 distances; multiplier zero selects that native stream rule. + using JxlAnsSymbolReader decoder = JxlAnsSymbolReader.Create(configuration, code, br, 0); - ulong ReadNum(JxlPatchContext context) - => decoder.ReadHybridUnsignedInteger(context, br, contextMap); + // References and positions share one entropy stream. Borrow its map and LZ77 window before either loop. + ReadOnlySpan contexts = CollectionsMarshal.AsSpan(contextMap); + Span window = decoder.Lz77Window; - ulong numRefPatch = ReadNum(JxlPatchContext.NumRefPatch); + ulong numRefPatch = (uint)decoder.ReadHybridUInt((int)JxlPatchContext.NumRefPatch, br, contexts, window); ulong numPixels = xsize * ysize; ulong maxRefPatches = 1024 + (numPixels / 4); @@ -93,7 +99,7 @@ ulong ReadNum(JxlPatchContext context) { JxlPatchReferencePosition refPos = new() { - Ref = (int)ReadNum(JxlPatchContext.ReferenceFrame) + Ref = decoder.ReadHybridUInt((int)JxlPatchContext.ReferenceFrame, br, contexts, window) }; if (refPos.Ref >= kMaxNumReferenceFrames || this.referenceFrames[(int)refPos.Ref].Frame.XSize == 0) @@ -108,10 +114,10 @@ ulong ReadNum(JxlPatchContext context) JxlImageBundle ib = this.referenceFrames[refPos.Ref].Frame; - refPos.X0 = (int)ReadNum(JxlPatchContext.PatchReferencePosition); - refPos.Y0 = (int)ReadNum(JxlPatchContext.PatchReferencePosition); - refPos.XSize = (int)ReadNum(JxlPatchContext.PatchSize) + 1; - refPos.YSize = (int)ReadNum(JxlPatchContext.PatchSize) + 1; + refPos.X0 = decoder.ReadHybridUInt((int)JxlPatchContext.PatchReferencePosition, br, contexts, window); + refPos.Y0 = decoder.ReadHybridUInt((int)JxlPatchContext.PatchReferencePosition, br, contexts, window); + refPos.XSize = decoder.ReadHybridUInt((int)JxlPatchContext.PatchSize, br, contexts, window) + 1; + refPos.YSize = decoder.ReadHybridUInt((int)JxlPatchContext.PatchSize, br, contexts, window) + 1; if (refPos.X0 + refPos.XSize > ib.XSize) { @@ -123,7 +129,7 @@ ulong ReadNum(JxlPatchContext context) throw new InvalidOperationException("Invalid position specified in reference frame"); } - ulong idCount = ReadNum(JxlPatchContext.PatchCount); + ulong idCount = (uint)decoder.ReadHybridUInt((int)JxlPatchContext.PatchCount, br, contexts, window); if (idCount > maxPatches) { @@ -164,12 +170,12 @@ ulong ReadNum(JxlPatchContext context) if (i == 0) { - pos.X = (int)ReadNum(JxlPatchContext.PatchPosition); - pos.Y = (int)ReadNum(JxlPatchContext.PatchPosition); + pos.X = decoder.ReadHybridUInt((int)JxlPatchContext.PatchPosition, br, contexts, window); + pos.Y = decoder.ReadHybridUInt((int)JxlPatchContext.PatchPosition, br, contexts, window); } else { - long deltaX = JxlPackSigned.UnpackSigned((uint)ReadNum(JxlPatchContext.PatchOffset)); + long deltaX = JxlPackSigned.UnpackSigned((uint)decoder.ReadHybridUInt((int)JxlPatchContext.PatchOffset, br, contexts, window)); if (deltaX < 0 && (int)(-deltaX) > this.positions[^1].X) { @@ -178,7 +184,7 @@ ulong ReadNum(JxlPatchContext context) pos.X = (int)(this.positions[^1].X + deltaX); - long deltaY = JxlPackSigned.UnpackSigned((uint)ReadNum(JxlPatchContext.PatchOffset)); + long deltaY = JxlPackSigned.UnpackSigned((uint)decoder.ReadHybridUInt((int)JxlPatchContext.PatchOffset, br, contexts, window)); if (deltaY < 0 && (int)(-deltaY) > this.positions[^1].Y) { @@ -200,7 +206,7 @@ ulong ReadNum(JxlPatchContext context) for (int j = 0; j < this.blendingsStride; j++) { - uint blendMode = (uint)ReadNum(JxlPatchContext.PatchBlendMode); + uint blendMode = (uint)decoder.ReadHybridUInt((int)JxlPatchContext.PatchBlendMode, br, contexts, window); if ((int)blendMode >= (int)JxlPatchContext.PatchBlendMode) { @@ -224,7 +230,7 @@ ulong ReadNum(JxlPatchContext context) if (UsesAlpha(info.Mode) && chooseAlpha) { - info.AlphaChannel = (int)ReadNum(JxlPatchContext.PatchAlphaChannel); + info.AlphaChannel = decoder.ReadHybridUInt((int)JxlPatchContext.PatchAlphaChannel, br, contexts, window); if (info.AlphaChannel >= (int)numExtraChannels) { @@ -238,7 +244,7 @@ ulong ReadNum(JxlPatchContext context) if (UsesClamp(info.Mode)) { - info.Clamp = ReadNum(JxlPatchContext.PatchClamp) != 0; + info.Clamp = (uint)decoder.ReadHybridUInt((int)JxlPatchContext.PatchClamp, br, contexts, window) != 0; } else { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs index ac3f377741..2ac90e6738 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs @@ -51,7 +51,17 @@ public static uint CoeffOrderContext(uint value) return Math.Min(token, PermutationContexts - 1u); } - public static bool ReadPermutation(int skip, int size, Span order, JxlBitReader bitReader, JxlAnsSymbolReader reader, Span contextMap) + /// + /// Reads a coefficient permutation from an initialized entropy stream. + /// + /// The leading coefficients that keep their original order. + /// The coefficient count. + /// The destination permutation, or an empty span when only validation is required. + /// The encoded permutation fields. + /// The caller-owned entropy reader. + /// The stable mapping from permutation contexts to histograms. + /// Whether the Lehmer code describes a valid permutation. + public static bool ReadPermutation(int skip, int size, Span order, JxlBitReader bitReader, JxlAnsSymbolReader reader, ReadOnlySpan contextMap) { DebugGuard.MustBeLessThanOrEqualTo(size, 65536, nameof(size)); @@ -61,7 +71,9 @@ public static bool ReadPermutation(int skip, int size, Span order, JxlBitR Span temp = stackalloc uint[size * 2]; temp.Clear(); - uint end = reader.ReadHybridUnsignedInteger(CoeffOrderContext((uint)size), bitReader, contextMap) + skip; + // Borrow the reader's window once; all coefficient symbols advance the same entropy and LZ77 state. + Span window = reader.Lz77Window; + uint end = (uint)reader.ReadHybridUInt((int)CoeffOrderContext((uint)size), bitReader, contextMap, window) + (uint)skip; if (end > size) { @@ -72,7 +84,7 @@ public static bool ReadPermutation(int skip, int size, Span order, JxlBitR for (int i = skip; i < end; i++) { - lehmer[i] = reader.ReadHybridUnsignedInteger(CoeffOrderContext(last), bitReader, contextMap); + lehmer[i] = (uint)reader.ReadHybridUInt((int)CoeffOrderContext(last), bitReader, contextMap, window); last = lehmer[i]; if (lehmer[i] >= size - i) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs index 8c56b240c6..ae426f9d7e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs @@ -39,7 +39,11 @@ internal class JxlPassesSharedState public int CoeffOrderSize { get; set; } - public List CoeffOrders { get; set; } = []; + /// + /// Gets or sets coefficient indices, arranged as consecutive pass-sized orders. + /// + /// Large transforms need indices greater than 255. Keep the indices as integers until coefficient lookup. + public List CoeffOrders { get; set; } = []; public JxlPlane QuantDc { get; set; } @@ -74,11 +78,11 @@ public JxlPassesSharedState(Configuration configuration, JxlFrameHeader frameHea this.CoeffOrderSize = JxlCoefficientOrder.CoefficientOrderMaxSize; if (encoder && - this.CoeffOrders.Count < (frameHeader.Passes.NumPasses & JxlCoefficientOrder.CoefficientOrderMaxSize) && + this.CoeffOrders.Count < ((int)frameHeader.Passes.NumberOfPasses * JxlCoefficientOrder.CoefficientOrderMaxSize) && frameHeader.Encoding == JxlFrameEncoding.VarDct) { - // we add the padding to CoeffOrders so its length is equal to the variable upperBound - int upperBound = frameHeader.Passes.NumPasses & JxlCoefficientOrder.CoefficientOrderMaxSize; + // Each progressive pass has one complete coefficient order. Reserve consecutive pass-sized ranges. + int upperBound = (int)frameHeader.Passes.NumberOfPasses * JxlCoefficientOrder.CoefficientOrderMaxSize; int length = this.CoeffOrders.Count; int delta = upperBound - length; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaDecoder.cs index cfe5be459e..6af3149ca7 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaDecoder.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; @@ -93,12 +94,24 @@ public static void ValidateTree(Tree tree) } } - public static void DecodeTree(JxlBitReader br, JxlAnsSymbolReader reader, List contextMap, Tree tree, int treeSizeLimit) + /// + /// Reads a modular prediction tree using an initialized entropy reader. + /// + /// The source bit reader. + /// The entropy reader for the tree symbols. + /// The mapping from tree fields to entropy contexts. + /// The destination tree nodes. + /// The maximum permitted number of nodes. + public static void DecodeTree(JxlBitReader bitReader, JxlAnsSymbolReader symbolReader, List contextMap, Tree tree, int treeSizeLimit) { int leafId = 0; int toDecode = 1; tree.Clear(); + // The reader remains the window owner. Neither this loop nor the context map resizes its borrowed storage. + Span window = symbolReader.Lz77Window; + ReadOnlySpan contexts = CollectionsMarshal.AsSpan(contextMap); + while (toDecode > 0) { if (tree.Count > treeSizeLimit) @@ -107,8 +120,8 @@ public static void DecodeTree(JxlBitReader br, JxlAnsSymbolReader reader, List 256) + int prop1 = symbolReader.ReadHybridUInt((int)JxlMaTreeContext.Property, bitReader, contexts, window); + if ((uint)prop1 > 256) { throw new InvalidOperationException("Invalid tree property value"); } @@ -116,24 +129,24 @@ public static void DecodeTree(JxlBitReader br, JxlAnsSymbolReader reader, List= JxlPredictorFacts.ModularPredictors) + if ((uint)predictor >= JxlPredictorFacts.ModularPredictors) { throw new InvalidOperationException("Invalid predictor"); } - int predictor_offset = JxlPackSigned.UnpackSigned(reader.ReadHybridUint(JxlMaTreeContext.Offset, br, contextMap)); - int mul_log = reader.ReadHybridUint(JxlMaTreeContext.MultiplierLog, br, contextMap); + int predictor_offset = JxlPackSigned.UnpackSigned((uint)symbolReader.ReadHybridUInt((int)JxlMaTreeContext.Offset, bitReader, contexts, window)); + int mul_log = symbolReader.ReadHybridUInt((int)JxlMaTreeContext.MultiplierLog, bitReader, contexts, window); - if (mul_log >= 31) + if ((uint)mul_log >= 31) { throw new InvalidOperationException("Invalid multiplier logarithm"); } - int mul_bits = reader.ReadHybridUint(JxlMaTreeContext.MultiplierBits, br, contextMap); + int mul_bits = symbolReader.ReadHybridUInt((int)JxlMaTreeContext.MultiplierBits, bitReader, contexts, window); - if (mul_bits >= (1 << (31 - mul_log)) - 1) + if ((uint)mul_bits >= (1u << (31 - mul_log)) - 1) { throw new InvalidOperationException("Invalid multiplier"); } @@ -146,7 +159,7 @@ public static void DecodeTree(JxlBitReader br, JxlAnsSymbolReader reader, List + /// Reads the entropy tables and modular prediction tree. + /// + /// The configuration used to decode entropy tables. + /// The source bit reader. + /// The destination tree nodes. + /// The maximum permitted number of nodes. + public static void DecodeTree(Configuration configuration, JxlBitReader bitReader, Tree tree, int treeSizeLimit) { List treeContextMap = []; JxlAnsCode code = new(); - JxlAnsReader.DecodeHistograms(configuration, reader, JxlMaConstants.NumTreeContexts, code, treeContextMap); + JxlAnsReader.DecodeHistograms(configuration, bitReader, JxlMaConstants.NumTreeContexts, code, ref treeContextMap, false); if (code.DegenerateSymbols[treeContextMap[(int)JxlMaTreeContext.Property]] > 0) { throw new InvalidOperationException("Infinite tree"); } - JxlAnsSymbolReader symbolReader = JxlAnsSymbolReader.Create(code, reader); - DecodeTree(reader, symbolReader, treeContextMap, tree, Math.Min(treeSizeLimit, JxlMaConstants.MaxTreeSize)); + // Tree fields use direct LZ77 distances. A zero multiplier disables image-row distance shortcuts. + using JxlAnsSymbolReader symbolReader = JxlAnsSymbolReader.Create(configuration, code, bitReader, 0); + DecodeTree(bitReader, symbolReader, treeContextMap, tree, Math.Min(treeSizeLimit, JxlMaConstants.MaxTreeSize)); if (!symbolReader.CheckAnsFinalState()) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncoding.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncoding.cs index 5cd4f530d8..d45e18dce3 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncoding.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncoding.cs @@ -234,7 +234,38 @@ void MarkProperty(int p, ref bool gradientOnly) return output; } - public static void DecodeModularChannelMAANS(Configuration configuration, bool usesLz77, JxlBitReader bitReader, JxlAnsSymbolReader reader, List contextMap, Tree globalTree, JxlModularHeader wpHeader, int channelIdx, int groupId, JxlTreeLut treeLookup, JxlModularImage image, ref uint flRun, ref uint flV) + /// + /// Decodes one modular channel using the prediction tree and an initialized entropy stream. + /// + /// The configuration that owns prediction storage. + /// Whether this entropy stream enables LZ77 references. + /// The encoded channel data. + /// The caller-owned entropy reader. + /// The stable map used to resolve prediction-tree contexts. + /// The reader's LZ77 storage, borrowed before the channel loop. + /// The shared prediction tree. + /// The weighted-prediction settings. + /// The channel to decode. + /// The group used for static prediction properties. + /// The reusable prediction lookup storage. + /// The image that owns the channel. + /// The remaining prefix-coded repetition count. + /// The repeated prefix-coded value. + public static void DecodeModularChannelMAANS( + Configuration configuration, + bool usesLz77, + JxlBitReader bitReader, + JxlAnsSymbolReader reader, + ReadOnlySpan contextMap, + Span window, + Tree globalTree, + JxlModularHeader wpHeader, + int channelIdx, + int groupId, + JxlTreeLut treeLookup, + JxlModularImage image, + ref uint flRun, + ref uint flV) { JxlModularChannel channel = image.Channels[channelIdx]; @@ -300,7 +331,7 @@ bool GlobalTreeIsAllGradientNoOp() if (predictor == JxlPredictor.Zero) { - if (reader.IsSingleValueAndAdvance(ctx_id, channel.Width * channel.Height, out uint value)) + if (reader.IsSingleValueAndAdvance((int)ctx_id, channel.Width * channel.Height, out uint value, window)) { int v = MakePixel(value, multiplier, offset); @@ -320,7 +351,7 @@ bool GlobalTreeIsAllGradientNoOp() for (int x = 0; x < channel.Width; x++) { - uint v = reader.ReadHybridUintClusteredInlined(usesLz77, ctx_id, bitReader); + uint v = (uint)reader.ReadHybridUIntClustered(usesLz77, (int)ctx_id, bitReader, window); r[x] = JxlPackSigned.UnpackSigned(v); } @@ -334,7 +365,7 @@ bool GlobalTreeIsAllGradientNoOp() for (int x = 0; x < channel.Width; x++) { - uint v = reader.ReadHybridUintClusteredMaybeInlined(usesLz77, ctx_id, bitReader); + uint v = (uint)reader.ReadHybridUIntClustered(usesLz77, (int)ctx_id, bitReader, window); r[x] = MakePixel(v, multiplier, offset); } @@ -357,7 +388,7 @@ bool GlobalTreeIsAllGradientNoOp() if (flRun == 0) { - reader.ReadHybridUintClusteredHuffRleOnly(ctx_id, bitReader, ref flV, ref flRun); + reader.ReadHybridUIntClusteredHuffRleOnly((int)ctx_id, bitReader, ref flV, ref flRun); sv = JxlPackSigned.UnpackSigned(flV); } else @@ -376,7 +407,7 @@ bool GlobalTreeIsAllGradientNoOp() if (flRun == 0) { - reader.ReadHybridUintClusteredHuffRleOnly(ctx_id, bitReader, ref flV, ref flRun); + reader.ReadHybridUIntClusteredHuffRleOnly((int)ctx_id, bitReader, ref flV, ref flRun); sv = JxlPackSigned.UnpackSigned(flV); } else @@ -406,10 +437,7 @@ bool GlobalTreeIsAllGradientNoOp() int topleft = x > 0 && y > 0 ? channel.GetRowMinus(y, x - 1 - onerow)[0] : left; int guess = JxlContextPrediction.ClampedGradient(top, left, topleft); - uint v = reader.ReadHybridUintClusteredMaybeInlined>( - usesLz77, - ctx_id, - bitReader); + uint v = (uint)reader.ReadHybridUIntClustered(usesLz77, (int)ctx_id, bitReader, window); r[x] = MakePixel(v, 1, guess); } @@ -452,7 +480,7 @@ bool GlobalTreeIsAllGradientNoOp() JxlMaConstants.PropertyRangeFast - 1); uint ctx_id = treeLookup.ContextLookup[pos]; - uint v = reader.ReadHybridUintClusteredMaybeInlined(usesLz77, ctx_id, br); + uint v = (uint)reader.ReadHybridUIntClustered(usesLz77, (int)ctx_id, bitReader, window); r[x] = MakePixel(v, 1, guess); } } @@ -481,7 +509,7 @@ bool GlobalTreeIsAllGradientNoOp() int pos = JxlMaConstants.PropertyRangeFast + Math.Clamp(properties[0], -JxlMaConstants.PropertyRangeFast, JxlMaConstants.PropertyRangeFast - 1); uint ctx_id = treeLookup.ContextLookup[pos]; - uint v = reader.ReadHybridUintClusteredInlined(usesLz77, ctx_id, bitReader); + uint v = (uint)reader.ReadHybridUIntClustered(usesLz77, (int)ctx_id, bitReader, window); r[x] = MakePixel(v, 1, guess); wpState.UpdatePredictionErrors(r[x], x, y, channel.Width); @@ -494,7 +522,7 @@ bool GlobalTreeIsAllGradientNoOp() int pos = JxlMaConstants.PropertyRangeFast + Math.Clamp(properties[0], -JxlMaConstants.PropertyRangeFast, JxlMaConstants.PropertyRangeFast - 1); int ctx_id = treeLookup.ContextLookup[pos]; - uint v = reader.ReadHybridUintClusteredInlined(usesLz77, ctx_id, bitReader); + uint v = (uint)reader.ReadHybridUIntClustered(usesLz77, (int)ctx_id, bitReader, window); r[x] = MakePixel(v, 1, guess); wpState.UpdatePredictionErrors(r[x], x, y, channel.Width); @@ -506,7 +534,7 @@ bool GlobalTreeIsAllGradientNoOp() int pos = JxlMaConstants.PropertyRangeFast + Math.Clamp(properties[0], -JxlMaConstants.PropertyRangeFast, JxlMaConstants.PropertyRangeFast - 1); int ctx_id = treeLookup.ContextLookup[pos]; - uint v = reader.ReadHybridUintClusteredInlined(usesLz77, ctx_id, bitReader); + uint v = (uint)reader.ReadHybridUIntClustered(usesLz77, (int)ctx_id, bitReader, window); r[x] = MakePixel(v, 1, guess); wpState.UpdatePredictionErrors(r[x], x, y, channel.Width); @@ -532,21 +560,21 @@ bool GlobalTreeIsAllGradientNoOp() for (int x = 0; x < 2; x++) { JxlPredictionResult res = JxlContextPrediction.PredictTreeNoWeightedPrediction(properties, channel.Width, ref p[x], onerow, x, y, tree_lookup, references); - uint v = reader.ReadHybridUintClustered(usesLz77, res.Context, bitReader); + uint v = (uint)reader.ReadHybridUIntClustered(usesLz77, res.Context, bitReader, window); p[x] = MakePixel(v, res.Multiplier, res.Guess); } for (int x = 2; x < channel.Width - 2; x++) { JxlPredictionResult res = JxlContextPrediction.PredictTreeNoWeightedPredictionNoEdgeCases(properties, channel.Width, ref p[x], onerow, x, y, tree_lookup, references); - uint v = reader.ReadHybridUintClusteredInlined(usesLz77, res.Context, bitReader); + uint v = (uint)reader.ReadHybridUIntClustered(usesLz77, res.Context, bitReader, window); p[x] = MakePixel(v, res.Multiplier, res.Guess); } for (int x = channel.Width - 2; x < channel.Width; x++) { JxlPredictionResult res = JxlContextPrediction.PredictTreeNoWeightedPrediction(properties, channel.Width, ref p[x], onerow, x, y, tree_lookup, references); - uint v = reader.ReadHybridUintClustered(usesLz77, res.Context, bitReader); + uint v = (uint)reader.ReadHybridUIntClustered(usesLz77, res.Context, bitReader, window); p[x] = MakePixel(v, res.Multiplier, res.Guess); } } @@ -555,7 +583,7 @@ bool GlobalTreeIsAllGradientNoOp() for (int x = 0; x < channel.Width; x++) { JxlPredictionResult res = JxlContextPrediction.PredictTreeNoWeightedPrediction(properties, channel.Width, ref p[x], onerow, x, y, tree_lookup, references); - uint v = reader.ReadHybridUintClusteredMaybeInlined(usesLz77, res.Context, bitReader); + uint v = (uint)reader.ReadHybridUIntClustered(usesLz77, res.Context, bitReader, window); p[x] = MakePixel(v, res.Multiplier, res.Guess); } } @@ -581,7 +609,7 @@ bool GlobalTreeIsAllGradientNoOp() for (int x = 0; x < 2; x++) { JxlPredictionResult res = JxlContextPrediction.PredictTreeWeightedPrediction(properties, channel.Width, ref p[x], onerow, x, y, tree_lookup, references, wpState); - uint v = reader.ReadHybridUintClustered(usesLz77, res.Context, bitReader); + uint v = (uint)reader.ReadHybridUIntClustered(usesLz77, res.Context, bitReader, window); p[x] = MakePixel(v, res.Multiplier, res.Guess); wpState.UpdatePredictionErrors(p[x], x, y, channel.Width); @@ -590,7 +618,7 @@ bool GlobalTreeIsAllGradientNoOp() for (int x = 2; x < channel.Width - 2; x++) { JxlPredictionResult res = JxlContextPrediction.PredictTreeWeightedPredictionNoEdgeCases(properties, channel.Width, ref p[x], onerow, x, y, tree_lookup, references, wpState); - uint v = reader.ReadHybridUintClusteredInlined(usesLz77, res.Context, bitReader); + uint v = (uint)reader.ReadHybridUIntClustered(usesLz77, res.Context, bitReader, window); p[x] = MakePixel(v, res.Multiplier, res.Guess); wpState.UpdatePredictionErrors(p[x], x, y, channel.Width); @@ -599,7 +627,7 @@ bool GlobalTreeIsAllGradientNoOp() for (int x = channel.Width - 2; x < channel.Width; x++) { JxlPredictionResult res = JxlContextPrediction.PredictTreeWeightedPrediction(properties, channel.Width, ref p[x], onerow, x, y, tree_lookup, references, wpState); - uint v = reader.ReadHybridUintClustered(usesLz77, res.Context, bitReader); + uint v = (uint)reader.ReadHybridUIntClustered(usesLz77, res.Context, bitReader, window); p[x] = MakePixel(v, res.Multiplier, res.Guess); wpState.UpdatePredictionErrors(p[x], x, y, channel.Width); @@ -610,7 +638,7 @@ bool GlobalTreeIsAllGradientNoOp() for (int x = 0; x < channel.Width; x++) { JxlPredictionResult res = JxlContextPrediction.PredictTreeWeightedPrediction(properties, channel.Width, ref p[x], onerow, x, y, tree_lookup, references, wpState); - uint v = reader.ReadHybridUintClustered(usesLz77, res.Context, bitReader); + uint v = (uint)reader.ReadHybridUIntClustered(usesLz77, res.Context, bitReader, window); p[x] = MakePixel(v, res.Multiplier, res.Guess); wpState.UpdatePredictionErrors(p[x], x, y, channel.Width); @@ -620,8 +648,51 @@ bool GlobalTreeIsAllGradientNoOp() } } - public static void DecodeModularChannelMAANS(JxlBitReader bitReader, JxlAnsSymbolReader reader, List contextMap, Tree globalTree, JxlModularHeader wpHeader, int channelIdx, int groupId, JxlTreeLut treeLut, JxlModularImage image, ref uint flRun, ref uint flV) - => DecodeModularChannelMAANS(reader.UsesLz77, bitReader, reader, contextMap, globalTree, wpHeader, channelIdx, groupId, treeLut, image, ref flRun, ref flV); + /// + /// Decodes one channel using the entropy reader's LZ77 selection and caller-borrowed storage. + /// + /// The configuration that owns prediction storage. + /// The encoded channel data. + /// The caller-owned entropy reader. + /// The stable prediction-context mapping. + /// The LZ77 window borrowed before decoding channels. + /// The shared prediction tree. + /// The weighted-prediction settings. + /// The channel index. + /// The group identifier. + /// The reusable prediction lookup storage. + /// The image that owns the channel. + /// The remaining prefix-coded repetition count. + /// The repeated prefix-coded value. + public static void DecodeModularChannelMAANS( + Configuration configuration, + JxlBitReader bitReader, + JxlAnsSymbolReader reader, + ReadOnlySpan contextMap, + Span window, + Tree globalTree, + JxlModularHeader wpHeader, + int channelIdx, + int groupId, + JxlTreeLut treeLut, + JxlModularImage image, + ref uint flRun, + ref uint flV) + => DecodeModularChannelMAANS( + configuration, + reader.UsesLz77, + bitReader, + reader, + contextMap, + window, + globalTree, + wpHeader, + channelIdx, + groupId, + treeLut, + image, + ref flRun, + ref flV); public static void ValidateChannelDimensions(JxlModularImage image, JxlModularOptions options) { @@ -664,7 +735,31 @@ public static void ValidateChannelDimensions(JxlModularImage image, JxlModularOp } } - public static bool ModularDecode(Configuration configuration, JxlBitReader br, JxlModularImage image, JxlGroupHeader header, int groupId, JxlModularOptions options, Tree globalTree, JxlAnsCode globalCode, List globalContextMap, bool allowTruncatedGroup) + /// + /// Reads one modular group's channels using local or shared entropy tables and prediction trees. + /// + /// The configuration that owns entropy and prediction storage. + /// The encoded group fields. + /// The destination modular channels. + /// The group header. + /// The group identifier used by prediction. + /// The channel and transform limits. + /// The prediction tree available to groups that request it. + /// The entropy tables available to groups that request them. + /// The context mapping for the shared entropy tables. + /// Whether to keep partial channel data if decoding fails. + /// Whether the group completed its entropy checks. + public static bool ModularDecode( + Configuration configuration, + JxlBitReader br, + JxlModularImage image, + JxlGroupHeader header, + int groupId, + JxlModularOptions options, + Tree globalTree, + JxlAnsCode globalCode, + List globalContextMap, + bool allowTruncatedGroup) { if (image.Channels.Count == 0) { @@ -759,13 +854,8 @@ public static bool ModularDecode(Configuration configuration, JxlBitReader br, J } maxTreeSize = Math.Min(1uL << 20, maxTreeSize); - DecodeTree(configuration, br, treeStorage, maxTreeSize); - DecodeHistograms( - configuration, - br, - (treeStorage.Count + 1) / 2, - codeStorage, - contextMapStorage); + JxlMaDecoder.DecodeTree(configuration, br, treeStorage, (int)maxTreeSize); + JxlAnsReader.DecodeHistograms(configuration, br, (treeStorage.Count + 1) / 2, codeStorage, ref contextMapStorage, false); } else { @@ -779,9 +869,14 @@ public static bool ModularDecode(Configuration configuration, JxlBitReader br, J contextMap = globalContextMap; } - JxlAnsSymbolReader reader = new(code, br, distanceMultiplier); + using JxlAnsSymbolReader reader = JxlAnsSymbolReader.Create(configuration, code, br, distanceMultiplier); JxlTreeLut treeLut = new(false, false); + // All channels share this reader and distance history. Borrow storage before the channel loop, + // then pass the same spans through prediction loops without acquiring Memory.Span for each symbol. + Span window = reader.Lz77Window; + ReadOnlySpan contexts = CollectionsMarshal.AsSpan(contextMap); + uint flRun = 0; uint flV = 0; @@ -800,7 +895,21 @@ public static bool ModularDecode(Configuration configuration, JxlBitReader br, J continue; // skip empty channels } - DecodeModularChannelMAANS(br, reader, contextMap, tree, header.WeightedHeader, nextChannel, groupId, treeLut, image, ref flRun, ref flV); + DecodeModularChannelMAANS( + configuration, + br, + reader, + contexts, + window, + tree, + header.WeightedHeader, + nextChannel, + groupId, + treeLut, + image, + ref flRun, + ref flV); + } clearGuard.Disarm(); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs index e87f08a09a..def316e5f8 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs @@ -241,15 +241,27 @@ public bool Dequantize( return true; } + /// + /// Reads one spline's control-point deltas and DCT coefficients. + /// + /// The configuration that owns control-point storage. + /// The stable map from spline contexts to histograms. + /// The entropy reader shared by all splines in the stream. + /// The encoded spline fields. + /// The decoder's LZ77 window, borrowed by the owning spline stream. + /// The maximum permitted total control-point count. + /// The accumulated control-point count. + /// Whether all coefficient groups were decoded. public bool TryDecode( Configuration configuration, - Span contextMap, + ReadOnlySpan contextMap, JxlAnsSymbolReader decoder, JxlBitReader br, + Span window, int maxControlPoints, ref int totalControlPoints) { - int numControlPoints = decoder.ReadHybridUnsignedInteger(NumControlPointsContext, br, contextMap); + int numControlPoints = decoder.ReadHybridUInt((int)JxlSplineEntropyContext.NumControlPoints, br, contextMap, window); if (numControlPoints > maxControlPoints) { throw new InvalidOperationException("Too many control points"); @@ -273,8 +285,8 @@ public bool TryDecode( { ref JxlControlPoint controlPoint = ref controlPoints[i]; - controlPoint.First = JxlPackSigned.UnpackSigned(decoder.ReadHybridUnsignedInteger(ControlPointsContext, br, contextMap)); - controlPoint.Second = JxlPackSigned.UnpackSigned(decoder.ReadHybridUnsignedInteger(ControlPointsContext, br, contextMap)); + controlPoint.First = JxlPackSigned.UnpackSigned((uint)decoder.ReadHybridUInt((int)JxlSplineEntropyContext.ControlPoints, br, contextMap, window)); + controlPoint.Second = JxlPackSigned.UnpackSigned((uint)decoder.ReadHybridUInt((int)JxlSplineEntropyContext.ControlPoints, br, contextMap, window)); if (controlPoint.First >= deltaLimit || controlPoint.First <= -deltaLimit || controlPoint.Second >= deltaLimit || controlPoint.Second <= -deltaLimit) @@ -285,34 +297,48 @@ public bool TryDecode( for (int i = 0; i < this.ColorDct.Length; i++) { - if (!TryDecodeDct(contextMap, this.ColorDct[i])) + if (!TryDecodeDct(contextMap, this.ColorDct[i], decoder, br, window)) { return false; } } - if (!TryDecodeDct(contextMap, this.SigmaDct)) + if (!TryDecodeDct(contextMap, this.SigmaDct, decoder, br, window)) { return false; } return true; + } - bool TryDecodeDct(ReadOnlySpan contextMap, Span dct) - { - const int invalidCoefficient = int.MinValue; + /// + /// Reads one DCT coefficient group without capturing the stream's borrowed spans. + /// + /// The spline histogram mapping. + /// The destination coefficients. + /// The owning entropy reader. + /// The encoded coefficient fields. + /// The window borrowed before the spline loops. + /// Whether the coefficient group is valid. + private static bool TryDecodeDct( + ReadOnlySpan contextMap, + Span dct, + JxlAnsSymbolReader decoder, + JxlBitReader br, + Span window) + { + const int invalidCoefficient = int.MinValue; - for (int i = 0; i < 32; i++) + for (int i = 0; i < 32; i++) + { + dct[i] = JxlPackSigned.UnpackSigned((uint)decoder.ReadHybridUInt((int)JxlSplineEntropyContext.Dct, br, contextMap, window)); + if (dct[i] == invalidCoefficient) { - dct[i] = JxlPackSigned.UnpackSigned(decoder.ReadHybridUnsignedInteger(DctContext, br, contextMap)); - if (dct[i] == invalidCoefficient) - { - throw new InvalidOperationException("The DCT coefficient is invalid"); - } + throw new InvalidOperationException("The DCT coefficient is invalid"); } - - return true; } + + return true; } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplines.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplines.cs index 52abd7c410..4783581e52 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplines.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplines.cs @@ -44,14 +44,26 @@ public void Clear() this.segmentYStart = EmptyMemoryOwner.Instance; } + /// + /// Reads spline positions, quantization, and control-point coefficients from the entropy stream. + /// + /// The configuration that owns entropy and control-point storage. + /// The encoded spline fields. + /// The image pixel count used to limit spline data. public void Decode(Configuration configuration, JxlBitReader reader, int numPixels) { List contextMap = []; - JxlAnsReader.DecodeHistograms(reader, NumSplineContexts, out JxlAnsCode code, contextMap); - JxlAnsSymbolReader decoder = new(code, reader); + JxlAnsCode code = new(); + JxlAnsReader.DecodeHistograms(configuration, reader, (int)JxlSplineEntropyContext.NumSplineContexts, code, ref contextMap, false); - int numSplines = decoder.ReadHybridUint(NumSplinesContext, reader, contextMap); + // Spline fields use direct LZ77 distances; multiplier zero selects that native stream rule. + using JxlAnsSymbolReader decoder = JxlAnsSymbolReader.Create(configuration, code, reader, 0); + + // One stream covers starting positions and every spline. Keep its borrowed window and map stable across those loops. + Span window = decoder.Lz77Window; + ReadOnlySpan contexts = CollectionsMarshal.AsSpan(contextMap); + int numSplines = decoder.ReadHybridUInt((int)JxlSplineEntropyContext.NumSplines, reader, contexts, window); int maxControlPoints = Math.Min(MaxNumControlPoints, numPixels / MaxNumControlPointsPerPixelRatio); if (numSplines > maxControlPoints || numSplines + 1 > maxControlPoints) @@ -63,7 +75,8 @@ public void Decode(Configuration configuration, JxlBitReader reader, int numPixe DecodeAllStartingPoints(this.startingPointsStorage, reader, decoder, contextMap, numSplines); - this.QuantizationAdjustment = JxlPackSigned.UnpackSigned(decoder.ReadHybridUint(QuantizationAdjustmentContext, reader, contextMap)); + int quantizationAdjustment = decoder.ReadHybridUInt((int)JxlSplineEntropyContext.QuantizationAdjustment, reader, contexts, window); + this.QuantizationAdjustment = JxlPackSigned.UnpackSigned((uint)quantizationAdjustment); this.splinesStorage = new List(numSplines); int numControlPoints = numSplines; @@ -73,9 +86,10 @@ public void Decode(Configuration configuration, JxlBitReader reader, int numPixe JxlQuantizedSpline spline = new(); if (!spline.TryDecode( configuration, - CollectionsMarshal.AsSpan(contextMap), + contexts, decoder, reader, + window, maxControlPoints, ref numControlPoints)) { diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/AnsCommonTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/AnsCommonTests.cs index 87f277e095..577e45eb7c 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/AnsCommonTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/AnsCommonTests.cs @@ -51,7 +51,7 @@ public void CheckAnsFinalState_UsesFormatSignature(byte signature, bool expected List contextMap = []; JxlAnsReader.DecodeHistograms(Configuration.Default, bitReader, 1, code, ref contextMap, false); - using JxlAnsSymbolReader reader = JxlAnsSymbolReader.Create(code, bitReader, 0); + using JxlAnsSymbolReader reader = JxlAnsSymbolReader.Create(Configuration.Default, code, bitReader, 0); Assert.Equal(expected, reader.CheckAnsFinalState()); } diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/EntropyCoderTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/EntropyCoderTests.cs index d3dce2c713..2cba3f429b 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/EntropyCoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/EntropyCoderTests.cs @@ -36,15 +36,15 @@ public MockBitReader(Stream stream) { } - public override uint PeekBits32(uint n) + public override uint PeekBits32(uint bitCount) { - Assert.Equal(this.NBits, n); + Assert.Equal(this.NBits, bitCount); return (uint)this.Bits; } - public override uint ReadBits32(uint n) + public override uint ReadBits32(uint bitCount) { - Assert.Equal(this.NBits, n); + Assert.Equal(this.NBits, bitCount); return (uint)this.Bits; } } @@ -82,10 +82,7 @@ private static void HybridUintRoundtrip( Assert.Equal( integers[i], - JxlAnsSymbolReader.ReadHybridUintConfig( - config, - token[i], - ref br)); + (uint)JxlAnsSymbolReader.ReadHybridUIntConfiguration(config, (int)token[i], br)); } } From 8f4232f380a601e171f14fe8da2a1926bd30d4a9 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 12:32:58 +1000 Subject: [PATCH 243/328] Decode JPEG XL compressed ICC profiles through shared validation --- .../Decoder/Ans/JxlAnsSymbolReader.cs | 3 - .../Processing/Decoder/JxlDecoderInternal.cs | 12 +- .../Jxl/Processing/Decoder/JxlIccReader.cs | 627 ++++++++++++++++++ .../Metadata/Profiles/ICC/IccReader.cs | 78 ++- .../Formats/Jxl/Processing/AnsCommonTests.cs | 141 ++++ .../Metadata/Profiles/ICC/IccReaderTests.cs | 34 +- 6 files changed, 856 insertions(+), 39 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlIccReader.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsSymbolReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsSymbolReader.cs index 89b4afb18b..c5b6532f69 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsSymbolReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsSymbolReader.cs @@ -172,9 +172,6 @@ public int ReadSymbolAnsWithoutRefill(int histogramIndex, JxlBitReader br) this.state = newState; } - uint nextRes = this.state & (JxlAnsConstants.AnsTableSize - 1); - JxlAnsHelper.Prefetch(table, nextRes, this.logEntrySize); - return symbol.Value; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderInternal.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderInternal.cs index 542aa710fb..c04c53fb21 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderInternal.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderInternal.cs @@ -1572,14 +1572,10 @@ public void ReadAllHeaders() if (this.metadata.ImageMetadata.ColorEncoding.NeedsIcc) { - // TODO: Decode the JPEG XL compressed ICC payload before constructing the profile. - IccDataReader reader = new(sp.ToArray()); - IccProfileHeader header = IccReader.ReadHeader(reader); - IccTagDataEntry[] tagData = IccReader.ReadTagData(reader); - - // Keep the profile on its original encoding. Frame bundles and JPEG reconstruction share this owner. - this.metadata.ImageMetadata.ColorEncoding.IccProfile = new IccProfile(header, tagData); - sp = sp[reader.Index..]; + // ICC compression continues at this exact bit position. The codec reconstructs bytes; + // the shared ICC reader validates and retains them on the original color encoding. + byte[] profileData = JxlIccReader.Read(this.Options.Configuration, bitReader); + this.metadata.ImageMetadata.ColorEncoding.IccProfile = IccReader.ToProfile(profileData); } this.gotAllHeaders = true; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlIccReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlIccReader.cs new file mode 100644 index 0000000000..f7a83259ea --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlIccReader.cs @@ -0,0 +1,627 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Buffers.Binary; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +/// +/// Reconstructs the ICC profile bytes stored in a JPEG XL entropy stream. +/// +/// +/// This reader reverses the codec's byte predictions. The shared ICC reader owns profile validation and interpretation. +/// +internal static class JxlIccReader +{ + private const int ContextCount = 41; + private const int HeaderSize = 128; + private const int PreambleSize = 20; + private const int MaximumEncodedSize = 1 << 28; + private const int MaximumEncodedOverhead = 65536; + private const int ExpansionCheckInterval = 65536; + private const int MaximumEncodedBytesPerInputByte = 256; + private const int BitsPerByte = 8; + private const uint BlackMatrixColumn = 0x6B58595A; + private const uint GamutBoundaryDescription = 0x67626420; + + /// + /// Gets tag signatures in the order assigned by the compressed ICC syntax. + /// + private static ReadOnlySpan TagSignatures => + [ + (uint)IccProfileTag.Copyright, + (uint)IccProfileTag.MediaWhitePoint, + (uint)IccProfileTag.MediaBlackPoint, + (uint)IccProfileTag.RedMatrixColumn, + (uint)IccProfileTag.GreenMatrixColumn, + (uint)IccProfileTag.BlueMatrixColumn, + BlackMatrixColumn, + (uint)IccProfileTag.RedTrc, + (uint)IccProfileTag.GreenTrc, + (uint)IccProfileTag.BlueTrc, + (uint)IccProfileTag.GrayTrc, + (uint)IccProfileTag.ChromaticAdaptation, + (uint)IccProfileTag.ProfileDescription, + (uint)IccProfileTag.Chromaticity, + (uint)IccProfileTag.DeviceManufacturerDescription, + (uint)IccProfileTag.DeviceModelDescription, + (uint)IccProfileTag.Luminance + ]; + + /// + /// Gets type signatures in the order assigned by the compressed ICC syntax. + /// + private static ReadOnlySpan TypeSignatures => + [ + (uint)IccTypeSignature.Xyz, + (uint)IccTypeSignature.TextDescription, + (uint)IccTypeSignature.Text, + (uint)IccTypeSignature.MultiLocalizedUnicode, + (uint)IccTypeSignature.ParametricCurve, + (uint)IccTypeSignature.Curve, + (uint)IccTypeSignature.S15Fixed16Array, + GamutBoundaryDescription + ]; + + /// + /// Reads and reconstructs one compressed ICC profile without interpreting its tags. + /// + /// The configuration whose allocator owns temporary encoded-byte storage. + /// The reader positioned at the compressed profile's encoded size. + /// The reconstructed profile bytes. + /// The entropy stream or compressed profile commands are invalid. + public static byte[] Read(Configuration configuration, JxlBitReader bitReader) + { + long initialBitPosition = bitReader.TotalBitsConsumed; + ulong encodedSize = JxlU64Coder.Read(bitReader); + if (encodedSize > MaximumEncodedSize) + { + throw new InvalidImageContentException("JPEG XL compressed ICC data is too large."); + } + + JxlAnsCode code = new(); + List contextMap = []; + JxlAnsReader.DecodeHistograms(configuration, bitReader, ContextCount, code, ref contextMap, disallowLz77: false); + + using JxlAnsSymbolReader reader = JxlAnsSymbolReader.Create(configuration, code, bitReader, distanceMultiplier: 0); + Span window = reader.Lz77Window; + ReadOnlySpan contexts = CollectionsMarshal.AsSpan(contextMap); + + // Two UInt64 varints occupy at most twenty bytes. Decode them before trusting either allocation size. + // These first symbols all use context zero, regardless of their previous bytes. + Span preamble = stackalloc byte[PreambleSize]; + int preambleLength = Math.Min((int)encodedSize, PreambleSize); + for (int i = 0; i < preambleLength; i++) + { + preamble[i] = unchecked((byte)reader.ReadHybridUInt(0, bitReader, contexts, window)); + } + + int preamblePosition = 0; + ulong outputSize = ReadVarInt(preamble[..preambleLength], ref preamblePosition); + ulong commandSize = ReadVarInt(preamble[..preambleLength], ref preamblePosition); + if (outputSize >= IccProfile.MaxProfileSize || commandSize > encodedSize - (ulong)preamblePosition) + { + throw new InvalidImageContentException("JPEG XL compressed ICC sizes are invalid."); + } + + // Every header byte needs one residual byte. Reject an impossible header before allocating either declared buffer. + ulong dataSize = encodedSize - (ulong)preamblePosition - commandSize; + if (Math.Min(outputSize, HeaderSize) > dataSize) + { + throw new InvalidImageContentException("JPEG XL compressed ICC header is truncated."); + } + + // Encoded data can exceed reconstructed data by at most 65,536 bytes. + // This check bounds temporary storage before the allocator receives the encoded size. + if (outputSize + MaximumEncodedOverhead < encodedSize) + { + throw new InvalidImageContentException("JPEG XL compressed ICC data exceeds its reconstructed size limit."); + } + + using IMemoryOwner encodedOwner = configuration.MemoryAllocator.Allocate((int)encodedSize); + Span encoded = encodedOwner.GetSpan(); + preamble[..preambleLength].CopyTo(encoded); + + // Keep the allocator span and LZ77 span outside the symbol loop. Each context depends on the last two encoded bytes. + for (int i = preambleLength; i < encoded.Length; i++) + { + if ((i & (ExpansionCheckInterval - 1)) == 0) + { + // The native corruption limit includes length and entropy headers. Compare in bits to retain partial input bytes. + // Both sides use Int64: the encoded-byte limit bounds these products well below its maximum. + long consumedInputBits = bitReader.TotalBitsConsumed - initialBitPosition; + if ((long)i * BitsPerByte > consumedInputBits * MaximumEncodedBytesPerInputByte) + { + throw new InvalidImageContentException("JPEG XL compressed ICC entropy expansion exceeds its limit."); + } + } + + int context = i <= HeaderSize ? 0 : 1 + GetByteKind(encoded[i - 1], false) + (8 * GetByteKind(encoded[i - 2], true)); + encoded[i] = unchecked((byte)reader.ReadHybridUInt(context, bitReader, contexts, window)); + } + + if (!reader.CheckAnsFinalState()) + { + throw new InvalidImageContentException("JPEG XL compressed ICC entropy state is invalid."); + } + + byte[] output = new byte[(int)outputSize]; + Reconstruct(encoded, preamblePosition, (int)commandSize, output); + return output; + } + + /// + /// Classifies one preceding byte for the first or second ICC entropy context component. + /// + /// The preceding encoded byte. + /// Whether this is the second preceding byte. + /// The context component assigned by the compressed ICC syntax. + private static int GetByteKind(byte value, bool second) + { + if (value is >= (byte)'a' and <= (byte)'z' or >= (byte)'A' and <= (byte)'Z') + { + return 0; + } + + if (value is >= (byte)'0' and <= (byte)'9' or (byte)'.' or (byte)',') + { + return 1; + } + + if (second) + { + return value < 16 ? 2 : value > 240 ? 3 : 4; + } + + return value switch + { + 0 => 2, + 1 => 3, + < 16 => 4, + 255 => 6, + > 240 => 5, + _ => 7 + }; + } + + /// + /// Reads a base-128 UInt64 without accepting truncated, unterminated, or overflowing encodings. + /// + /// The bounded preamble or command stream. + /// The next byte, updated to the first byte after the integer. + /// The decoded integer. + private static ulong ReadVarInt(ReadOnlySpan source, ref int position) + { + ulong value = 0; + for (int i = 0; i < 10; i++) + { + if (position >= source.Length) + { + throw new InvalidImageContentException("JPEG XL compressed ICC integer is truncated."); + } + + byte next = source[position++]; + if (i == 9 && next > 1) + { + throw new InvalidImageContentException("JPEG XL compressed ICC integer exceeds UInt64."); + } + + value |= (ulong)(next & 127) << (7 * i); + if (next < 128) + { + return value; + } + } + + throw new InvalidImageContentException("JPEG XL compressed ICC integer is unterminated."); + } + + /// + /// Reverses header, tag-table, shuffle, and integer predictions into the final byte array. + /// + /// The complete entropy-decoded payload. + /// The first command byte, after the two preamble integers. + /// The command-stream length. + /// The final profile storage. + private static void Reconstruct(ReadOnlySpan encoded, int commandStart, int commandSize, Span output) + { + ReadOnlySpan commands = encoded.Slice(commandStart, commandSize); + int commandPosition = 0; + int dataPosition = commandStart + commandSize; + int outputPosition = 0; + + // This is the codec's header prediction, not a default ICC profile. All unspecified bytes are zero. + Span header = stackalloc byte[HeaderSize]; + header.Clear(); + BinaryPrimitives.WriteUInt32BigEndian(header, (uint)output.Length); + header[8] = 4; + BinaryPrimitives.WriteUInt32BigEndian(header[12..], (uint)IccProfileClass.DisplayDevice); + BinaryPrimitives.WriteUInt32BigEndian(header[16..], (uint)IccColorSpaceType.Rgb); + BinaryPrimitives.WriteUInt32BigEndian(header[20..], (uint)IccColorSpaceType.CieXyz); + "acsp"u8.CopyTo(header[36..]); + + // These D50 words are exact header-prediction bytes from the format. Do not recompute them from floating-point primaries. + BinaryPrimitives.WriteUInt32BigEndian(header[68..], 0x0000F6D6); + BinaryPrimitives.WriteUInt32BigEndian(header[72..], 0x00010000); + BinaryPrimitives.WriteUInt32BigEndian(header[76..], 0x0000D32D); + + int headerLength = Math.Min(HeaderSize, output.Length); + for (; outputPosition < headerLength; outputPosition++) + { + // The decoded CMM and platform bytes predict later header fields. Read only bytes already reconstructed. + if (outputPosition == 8) + { + output.Slice(4, 4).CopyTo(header[80..]); + } + else if (outputPosition == 41) + { + if (output[40] == (byte)'A') + { + "PPL"u8.CopyTo(header[41..]); + } + else if (output[40] == (byte)'M') + { + "SFT"u8.CopyTo(header[41..]); + } + } + else if (outputPosition == 42 && output[40] == (byte)'S') + { + if (output[41] == (byte)'G') + { + "I "u8.CopyTo(header[42..]); + } + else if (output[41] == (byte)'U') + { + "NW"u8.CopyTo(header[42..]); + } + } + + output[outputPosition] = unchecked((byte)(encoded[dataPosition++] + header[outputPosition])); + } + + if (outputPosition != output.Length) + { + ReadTagTable(commands, ref commandPosition, encoded, ref dataPosition, output, ref outputPosition); + ReadCommands(commands, ref commandPosition, encoded, ref dataPosition, output, ref outputPosition); + } + + if (commandPosition != commands.Length || dataPosition != encoded.Length || outputPosition != output.Length) + { + throw new InvalidImageContentException("JPEG XL compressed ICC commands do not match their declared sizes."); + } + } + + /// + /// Restores predicted tag-table entries, including shared TRCs and consecutive XYZ tags. + /// + /// The bounded command stream. + /// The next command byte. + /// The complete encoded payload. + /// The next residual-data byte. + /// The final profile storage. + /// The next output byte. + private static void ReadTagTable( + ReadOnlySpan commands, + ref int commandPosition, + ReadOnlySpan encoded, + ref int dataPosition, + Span output, + ref int outputPosition) + { + ulong tagCount = ReadVarInt(commands, ref commandPosition); + if (tagCount == 0) + { + return; + } + + tagCount--; + WriteUInt32(output, ref outputPosition, tagCount); + + // The syntax predicts the first offset from 128, not from the 132-byte start of the actual tag table. + ulong previousStart = HeaderSize + (tagCount * 12); + ulong previousSize = 0; + while (commandPosition < commands.Length) + { + byte command = commands[commandPosition++]; + int tagCode = command & 63; + if (tagCode == 0) + { + break; + } + + uint tag; + if (tagCode == 1) + { + RequireLength(4, encoded.Length - dataPosition); + tag = BinaryPrimitives.ReadUInt32BigEndian(encoded[dataPosition..]); + dataPosition += 4; + } + else if (tagCode == 2) + { + tag = (uint)IccProfileTag.RedTrc; + } + else if (tagCode == 3) + { + tag = (uint)IccProfileTag.RedMatrixColumn; + } + else if (tagCode - 4 < TagSignatures.Length) + { + tag = TagSignatures[tagCode - 4]; + } + else + { + throw new InvalidImageContentException("JPEG XL compressed ICC tag code is invalid."); + } + + ulong size = tag is (uint)IccProfileTag.RedMatrixColumn or (uint)IccProfileTag.GreenMatrixColumn + or (uint)IccProfileTag.BlueMatrixColumn or BlackMatrixColumn or (uint)IccProfileTag.MediaWhitePoint + or (uint)IccProfileTag.MediaBlackPoint or (uint)IccProfileTag.Luminance ? 20UL : previousSize; + + ulong start = (command & 64) != 0 ? ReadVarInt(commands, ref commandPosition) : previousStart + previousSize; + if ((command & 128) != 0) + { + size = ReadVarInt(commands, ref commandPosition); + } + + WriteUInt32(output, ref outputPosition, tag); + WriteUInt32(output, ref outputPosition, start); + WriteUInt32(output, ref outputPosition, size); + previousStart = start; + previousSize = size; + + if (tagCode is 2 or 3) + { + // TRCs share one payload. XYZ tags instead occupy three consecutive payloads of equal size. + bool xyz = tagCode == 3; + WriteUInt32(output, ref outputPosition, xyz ? (uint)IccProfileTag.GreenMatrixColumn : (uint)IccProfileTag.GreenTrc); + WriteUInt32(output, ref outputPosition, xyz ? start + size : start); + WriteUInt32(output, ref outputPosition, size); + WriteUInt32(output, ref outputPosition, xyz ? (uint)IccProfileTag.BlueMatrixColumn : (uint)IccProfileTag.BlueTrc); + WriteUInt32(output, ref outputPosition, xyz ? start + (2 * size) : start); + WriteUInt32(output, ref outputPosition, size); + } + } + } + + /// + /// Restores profile payload bytes from the remaining commands. + /// + /// The bounded command stream. + /// The next command byte. + /// The complete encoded payload. + /// The next residual-data byte. + /// The final profile storage. + /// The next output byte. + private static void ReadCommands( + ReadOnlySpan commands, + ref int commandPosition, + ReadOnlySpan encoded, + ref int dataPosition, + Span output, + ref int outputPosition) + { + while (commandPosition < commands.Length) + { + byte command = commands[commandPosition++]; + if (command is 1 or 2 or 3) + { + ulong length = ReadVarInt(commands, ref commandPosition); + RequireLength(length, encoded.Length - dataPosition); + RequireLength(length, output.Length - outputPosition); + ReadOnlySpan source = encoded.Slice(dataPosition, (int)length); + Span destination = output.Slice(outputPosition, (int)length); + if (command == 1) + { + source.CopyTo(destination); + } + else + { + Shuffle(source, destination, command == 2 ? 2 : 4); + } + + dataPosition += (int)length; + outputPosition += (int)length; + } + else if (command == 4) + { + RequireLength(2, commands.Length - commandPosition); + byte flags = commands[commandPosition++]; + int width = (flags & 3) + 1; + int order = (flags & 12) >> 2; + if (width == 3 || order == 3) + { + throw new InvalidImageContentException("JPEG XL compressed ICC predictor is invalid."); + } + + ulong stride = (flags & 16) != 0 ? ReadVarInt(commands, ref commandPosition) : (ulong)width; + if (stride < (ulong)width || outputPosition == 0 || stride > (ulong)((outputPosition - 1) >> 2)) + { + throw new InvalidImageContentException("JPEG XL compressed ICC predictor stride is invalid."); + } + + ulong length = ReadVarInt(commands, ref commandPosition); + RequireLength(length, encoded.Length - dataPosition); + RequireLength(length, output.Length - outputPosition); + Predict(encoded.Slice(dataPosition, (int)length), output, outputPosition, (int)stride, width, order); + dataPosition += (int)length; + outputPosition += (int)length; + } + else if (command == 10) + { + RequireLength(12, encoded.Length - dataPosition); + RequireLength(20, output.Length - outputPosition); + WriteUInt32(output, ref outputPosition, (uint)IccTypeSignature.Xyz); + WriteUInt32(output, ref outputPosition, 0); + encoded.Slice(dataPosition, 12).CopyTo(output[outputPosition..]); + dataPosition += 12; + outputPosition += 12; + } + else if (command >= 16 && command - 16 < TypeSignatures.Length) + { + WriteUInt32(output, ref outputPosition, TypeSignatures[command - 16]); + WriteUInt32(output, ref outputPosition, 0); + } + else + { + throw new InvalidImageContentException("JPEG XL compressed ICC command is invalid."); + } + } + } + + /// + /// Interleaves two or four byte planes, including incomplete columns at the end of the encoded data. + /// + /// The consecutive encoded byte planes. + /// The equally sized interleaved output. + /// The component count, either two or four. + private static void Shuffle(ReadOnlySpan source, Span destination, int width) + { + int height = (source.Length + width - 1) / width; + int column = 0; + if (width == 4) + { + // Each plane has 'height' bytes, except the final plane. SIMD reads only columns present in all four planes. + // The shared helper writes consecutive four-byte groups and accepts unaligned input and output. + int completeColumns = source.Length - (3 * height); + ref byte input = ref MemoryMarshal.GetReference(source); + if (Vector512.IsHardwareAccelerated) + { + for (; column <= completeColumns - Vector512.Count; column += Vector512.Count) + { + SimdUtils.Interleave4( + Vector512.LoadUnsafe(ref input, (nuint)column), + Vector512.LoadUnsafe(ref input, (nuint)(column + height)), + Vector512.LoadUnsafe(ref input, (nuint)(column + (2 * height))), + Vector512.LoadUnsafe(ref input, (nuint)(column + (3 * height))), + ref destination[4 * column]); + } + } + + if (Vector256.IsHardwareAccelerated) + { + for (; column <= completeColumns - Vector256.Count; column += Vector256.Count) + { + SimdUtils.Interleave4( + Vector256.LoadUnsafe(ref input, (nuint)column), + Vector256.LoadUnsafe(ref input, (nuint)(column + height)), + Vector256.LoadUnsafe(ref input, (nuint)(column + (2 * height))), + Vector256.LoadUnsafe(ref input, (nuint)(column + (3 * height))), + ref destination[4 * column]); + } + } + } + + // Walk the remaining matrix columns in native order. Missing cells are skipped, rather than padded or read. + // Start after complete SIMD columns; a short or two-plane shuffle uses this same scalar traversal from column zero. + int sourcePosition = column; + for (int i = width * column; i < destination.Length; i++) + { + destination[i] = source[sourcePosition]; + sourcePosition += height; + if (sourcePosition >= source.Length) + { + sourcePosition = ++column; + } + } + } + + /// + /// Adds shuffled residual bytes to wrapping integer predictions from previously reconstructed output. + /// + /// The encoded residual planes. + /// The profile storage, including the predictor's completed history. + /// The first output byte for this command. + /// The byte distance between preceding predictor samples. + /// The integer width, one, two, or four bytes. + /// The predictor order, zero, one, or two. + private static void Predict(ReadOnlySpan source, Span output, int start, int stride, int width, int order) + { + int height = (source.Length + width - 1) / width; + int column = 0; + int sourcePosition = 0; + for (int i = 0; i < source.Length; i++) + { + // Samples can depend on bytes produced earlier in this command. Preserve that dependency instead of predicting a full block at once. + // Multi-byte samples are big-endian. Align within this command, not within the whole profile. + int position = start + (i & -width); + uint first; + uint second; + uint third; + if (width == 1) + { + first = output[position - stride]; + second = output[position - (2 * stride)]; + third = output[position - (3 * stride)]; + } + else if (width == 2) + { + first = BinaryPrimitives.ReadUInt16BigEndian(output[(position - stride)..]); + second = BinaryPrimitives.ReadUInt16BigEndian(output[(position - (2 * stride))..]); + third = BinaryPrimitives.ReadUInt16BigEndian(output[(position - (3 * stride))..]); + } + else + { + // The command boundary requires stride >= 4 and four prior samples. Every word ends before the current output byte. + // Thus the native incomplete-word rule cannot apply after the stride check succeeds. + first = BinaryPrimitives.ReadUInt32BigEndian(output[(position - stride)..]); + second = BinaryPrimitives.ReadUInt32BigEndian(output[(position - (2 * stride))..]); + third = BinaryPrimitives.ReadUInt32BigEndian(output[(position - (3 * stride))..]); + } + + uint predicted = order switch + { + 0 => first, + 1 => unchecked((2 * first) - second), + _ => unchecked((3 * first) - (3 * second) + third) + }; + + int shift = (width - 1 - (i & (width - 1))) * 8; + output[start + i] = unchecked((byte)((predicted >> shift) + source[sourcePosition])); + + // Consume shuffled residuals directly. This avoids allocating and copying a temporary array for each predictor command. + sourcePosition += height; + if (sourcePosition >= source.Length) + { + sourcePosition = ++column; + } + } + } + + /// + /// Checks an external command length before narrowing it or taking a source or destination slice. + /// + /// The encoded length. + /// The remaining source or destination bytes. + private static void RequireLength(ulong length, int available) + { + if (length > (ulong)available) + { + throw new InvalidImageContentException("JPEG XL compressed ICC command exceeds its data or output bounds."); + } + } + + /// + /// Appends one big-endian UInt32 after checking its wire range and the declared output length. + /// + /// The final profile storage. + /// The next output byte, advanced by four. + /// The reconstructed signature, size, or offset. + private static void WriteUInt32(Span output, ref int position, ulong value) + { + if (value > uint.MaxValue) + { + throw new InvalidImageContentException("JPEG XL compressed ICC value exceeds UInt32."); + } + + RequireLength(4, output.Length - position); + BinaryPrimitives.WriteUInt32BigEndian(output[position..], (uint)value); + position += 4; + } +} diff --git a/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs b/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs index e0282893a6..366cacf544 100644 --- a/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs +++ b/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs @@ -71,30 +71,25 @@ public int Read(Stream stream, int length) public IccProfile ToProfile() { ReadOnlySpan data = this.profileData.WrittenSpan; + int declaredSize = GetValidatedProfileLength(data); - // Even a profile with no tags requires the 128-byte ICC header and the 4-byte tag count. - if (data.Length < 132) - { - throw new InvalidIccProfileException("Invalid ICC profile."); - } - - uint declaredSize = BinaryPrimitives.ReadUInt32BigEndian(data); - - // The declared size must also include the header and tag count and fit within the bytes read. - if (declaredSize < 132 || declaredSize > data.Length || declaredSize >= IccProfile.MaxProfileSize || - !IsTagTableValid(data[..(int)declaredSize])) - { - throw new InvalidIccProfileException("Invalid ICC profile."); - } + // The accumulator remains reusable. Copy only the declared profile into the retained array. + return CreateProfile(data[..declaredSize].ToArray()); + } - // Retain only the declared profile bytes so lazy tag parsing uses the same boundary as validation. - IccProfile profile = new(data[..(int)declaredSize].ToArray()); - if (!profile.CheckIsValid()) - { - throw new InvalidIccProfileException("Invalid ICC profile."); - } + /// + /// Creates a validated profile from a complete, caller-owned byte array. + /// + /// The complete profile bytes. The caller must not change these bytes after this call. + /// The validated profile, retaining the array when it contains exactly the declared profile. + /// Trailing bytes are excluded from the retained profile. This requires a copy only when trailing bytes are present. + public static IccProfile ToProfile(byte[] data) + { + int declaredSize = GetValidatedProfileLength(data); - return profile; + // Decompressors can supply the final array directly. Preserve the same declared-size boundary + // as the segmented reader without copying an already complete profile. + return CreateProfile(declaredSize == data.Length ? data : data.AsSpan(0, declaredSize).ToArray()); } /// @@ -213,6 +208,47 @@ private static IccTagTableEntry[] ReadTagTable(IccDataReader reader) return [.. table]; } + /// + /// Checks profile-length and tag-table boundaries before retaining profile bytes. + /// + /// The available profile bytes, which can include trailing data. + /// The declared profile length. + private static int GetValidatedProfileLength(ReadOnlySpan data) + { + // Even a profile with no tags requires the 128-byte ICC header and the 4-byte tag count. + if (data.Length < 132) + { + throw new InvalidIccProfileException("Invalid ICC profile."); + } + + uint declaredSize = BinaryPrimitives.ReadUInt32BigEndian(data); + + // Bound the table by the declared profile, not by any readable bytes after that profile. + if (declaredSize < 132 || declaredSize > data.Length || declaredSize >= IccProfile.MaxProfileSize || + !IsTagTableValid(data[..(int)declaredSize])) + { + throw new InvalidIccProfileException("Invalid ICC profile."); + } + + return (int)declaredSize; + } + + /// + /// Creates a profile from validated length and table boundaries, then checks its parsed header. + /// + /// The owned array containing exactly the declared profile bytes. + /// The profile with a valid header. + private static IccProfile CreateProfile(byte[] data) + { + IccProfile profile = new(data); + if (!profile.CheckIsValid()) + { + throw new InvalidIccProfileException("Invalid ICC profile."); + } + + return profile; + } + /// /// Checks that the count and complete fixed-size tag table fit in the payload. /// diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/AnsCommonTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/AnsCommonTests.cs index 577e45eb7c..716a50c4c5 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/AnsCommonTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/AnsCommonTests.cs @@ -6,12 +6,153 @@ using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Tests.Memory; namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; public class AnsCommonTests { + /// + /// The fixed compressed ICC stream from libjxl's IccCodecTest.EncodedIccProfile regression. + /// + private static readonly byte[] EncodedIccProfile = + [ + 0x1f, 0x8b, 0x01, 0x13, 0x10, 0x00, 0x00, 0x00, 0x20, 0x4c, 0xcc, 0x03, + 0xe7, 0xa0, 0xa5, 0xa2, 0x90, 0xa4, 0x27, 0xe8, 0x79, 0x1d, 0xe3, 0x26, + 0x57, 0x54, 0xef, 0x00, 0xe8, 0x97, 0x02, 0xce, 0xa1, 0xd7, 0x85, 0x16, + 0xb4, 0x29, 0x94, 0x58, 0xf2, 0x56, 0xc0, 0x76, 0xea, 0x23, 0xec, 0x7c, + 0x73, 0x51, 0x41, 0x40, 0x23, 0x21, 0x95, 0x04, 0x75, 0x12, 0xc9, 0xcc, + 0x16, 0xbd, 0xb6, 0x99, 0xad, 0xf8, 0x75, 0x35, 0xb6, 0x42, 0xae, 0xae, + 0xae, 0x86, 0x56, 0xf8, 0xcc, 0x16, 0x30, 0xb3, 0x45, 0xad, 0x0d, 0x40, + 0xd6, 0xd1, 0xd6, 0x99, 0x40, 0xbe, 0xe2, 0xdc, 0x31, 0x07, 0xa6, 0xb9, + 0x27, 0x92, 0x38, 0x00, 0x03, 0x5e, 0x2c, 0xbe, 0xe6, 0xfb, 0x19, 0xbf, + 0xf3, 0x6d, 0xbc, 0x4d, 0x64, 0xe5, 0xba, 0x76, 0xde, 0x31, 0x65, 0x66, + 0x14, 0xa6, 0x3a, 0xc5, 0x8f, 0xb1, 0xb4, 0xba, 0x1f, 0xb1, 0xb8, 0xd4, + 0x75, 0xba, 0x18, 0x86, 0x95, 0x3c, 0x26, 0xf6, 0x25, 0x62, 0x53, 0xfd, + 0x9c, 0x94, 0x76, 0xf6, 0x95, 0x2c, 0xb1, 0xfd, 0xdc, 0xc0, 0xe4, 0x3f, + 0xb3, 0xff, 0x67, 0xde, 0xd5, 0x94, 0xcc, 0xb0, 0x83, 0x2f, 0x28, 0x93, + 0x92, 0x03, 0xa1, 0x41, 0x64, 0x60, 0x62, 0x70, 0x80, 0x87, 0xaf, 0xe7, + 0x60, 0x4a, 0x20, 0x23, 0xb3, 0x11, 0x07, 0x38, 0x38, 0xd4, 0x0a, 0x66, + 0xb5, 0x93, 0x41, 0x90, 0x19, 0x17, 0x18, 0x60, 0xa5, 0x0b, 0x7a, 0x24, + 0xaa, 0x20, 0x81, 0xac, 0xa9, 0xa1, 0x70, 0xa6, 0x12, 0x8a, 0x4a, 0xa3, + 0xa0, 0xf9, 0x9a, 0x97, 0xe7, 0xa8, 0xac, 0x08, 0xa8, 0xc4, 0x2a, 0x86, + 0xa7, 0x69, 0x1e, 0x67, 0xe6, 0xbe, 0xa4, 0xd3, 0xff, 0x91, 0x61, 0xf6, + 0x8a, 0xe6, 0xb5, 0xb3, 0x61, 0x9f, 0x19, 0x17, 0x98, 0x27, 0x6b, 0xe9, + 0x08, 0x98, 0xe1, 0x21, 0x4a, 0x09, 0xb5, 0xd7, 0xca, 0xfa, 0x94, 0xd0, + 0x69, 0x1a, 0xeb, 0x52, 0x01, 0x4e, 0xf5, 0xf6, 0xdf, 0x7f, 0xe7, 0x29, + 0x70, 0xee, 0x04, 0xda, 0x2f, 0xa4, 0xff, 0xfe, 0xbb, 0x6f, 0xa8, 0xff, + 0xfe, 0xdb, 0xaf, 0x08, 0xf6, 0x72, 0xa1, 0x40, 0x5d, 0xf0, 0x2d, 0x08, + 0x82, 0x5b, 0x87, 0xbd, 0x10, 0x08, 0xe9, 0x07, 0xee, 0x4b, 0x80, 0xda, + 0x4a, 0x04, 0xc5, 0x5e, 0xa0, 0xb7, 0x1e, 0x60, 0xb0, 0x59, 0x76, 0x60, + 0x0b, 0x2e, 0x19, 0x8a, 0x2e, 0x1c, 0xe6, 0x06, 0x20, 0xb8, 0x64, 0x18, + 0x2a, 0xcf, 0x51, 0x94, 0xd4, 0xee, 0xc3, 0xfe, 0x39, 0x74, 0xd4, 0x2b, + 0x48, 0xc9, 0x83, 0x4c, 0x9b, 0xd0, 0x4c, 0x35, 0x10, 0xe3, 0x09, 0xf7, + 0x72, 0xf0, 0x7a, 0x0e, 0xbf, 0x7d, 0x36, 0x2e, 0x19, 0x7e, 0x3f, 0x0c, + 0xf7, 0x93, 0xe7, 0xf4, 0x1d, 0x32, 0xc6, 0xb0, 0x89, 0xad, 0xe0, 0x28, + 0xc1, 0xa7, 0x59, 0xe3, 0x00 + ]; + + /// + /// Gets the complete ICC bytes paired with the compressed stream by libjxl. + /// + private static ReadOnlySpan ExpectedIccProfile => + [ + 0x00, 0x00, 0x03, 0x80, 0x6c, 0x63, 0x6d, 0x73, 0x04, 0x30, 0x00, 0x00, + 0x6d, 0x6e, 0x74, 0x72, 0x52, 0x47, 0x42, 0x20, 0x58, 0x59, 0x5a, 0x20, + 0x07, 0xe3, 0x00, 0x04, 0x00, 0x1d, 0x00, 0x0f, 0x00, 0x32, 0x00, 0x2e, + 0x61, 0x63, 0x73, 0x70, 0x41, 0x50, 0x50, 0x4c, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0xf6, 0xd6, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xd3, 0x2d, 0x6c, 0x63, 0x6d, 0x73, + 0x5f, 0x07, 0x0d, 0x3e, 0x4d, 0x32, 0xf2, 0x6e, 0x5d, 0x77, 0x26, 0xcc, + 0x23, 0xb0, 0x6a, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, + 0x64, 0x65, 0x73, 0x63, 0x00, 0x00, 0x01, 0x20, 0x00, 0x00, 0x00, 0x42, + 0x63, 0x70, 0x72, 0x74, 0x00, 0x00, 0x01, 0x64, 0x00, 0x00, 0x01, 0x00, + 0x77, 0x74, 0x70, 0x74, 0x00, 0x00, 0x02, 0x64, 0x00, 0x00, 0x00, 0x14, + 0x63, 0x68, 0x61, 0x64, 0x00, 0x00, 0x02, 0x78, 0x00, 0x00, 0x00, 0x2c, + 0x72, 0x58, 0x59, 0x5a, 0x00, 0x00, 0x02, 0xa4, 0x00, 0x00, 0x00, 0x14, + 0x62, 0x58, 0x59, 0x5a, 0x00, 0x00, 0x02, 0xb8, 0x00, 0x00, 0x00, 0x14, + 0x67, 0x58, 0x59, 0x5a, 0x00, 0x00, 0x02, 0xcc, 0x00, 0x00, 0x00, 0x14, + 0x72, 0x54, 0x52, 0x43, 0x00, 0x00, 0x02, 0xe0, 0x00, 0x00, 0x00, 0x20, + 0x67, 0x54, 0x52, 0x43, 0x00, 0x00, 0x02, 0xe0, 0x00, 0x00, 0x00, 0x20, + 0x62, 0x54, 0x52, 0x43, 0x00, 0x00, 0x02, 0xe0, 0x00, 0x00, 0x00, 0x20, + 0x63, 0x68, 0x72, 0x6d, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x24, + 0x64, 0x6d, 0x6e, 0x64, 0x00, 0x00, 0x03, 0x24, 0x00, 0x00, 0x00, 0x28, + 0x64, 0x6d, 0x64, 0x64, 0x00, 0x00, 0x03, 0x4c, 0x00, 0x00, 0x00, 0x32, + 0x6d, 0x6c, 0x75, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x0c, 0x65, 0x6e, 0x55, 0x53, 0x00, 0x00, 0x00, 0x26, + 0x00, 0x00, 0x00, 0x1c, 0x00, 0x52, 0x00, 0x47, 0x00, 0x42, 0x00, 0x5f, + 0x00, 0x44, 0x00, 0x36, 0x00, 0x35, 0x00, 0x5f, 0x00, 0x53, 0x00, 0x52, + 0x00, 0x47, 0x00, 0x5f, 0x00, 0x52, 0x00, 0x65, 0x00, 0x6c, 0x00, 0x5f, + 0x00, 0x37, 0x00, 0x30, 0x00, 0x39, 0x00, 0x00, 0x6d, 0x6c, 0x75, 0x63, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0c, + 0x65, 0x6e, 0x55, 0x53, 0x00, 0x00, 0x00, 0xe4, 0x00, 0x00, 0x00, 0x1c, + 0x00, 0x43, 0x00, 0x6f, 0x00, 0x70, 0x00, 0x79, 0x00, 0x72, 0x00, 0x69, + 0x00, 0x67, 0x00, 0x68, 0x00, 0x74, 0x00, 0x20, 0x00, 0x32, 0x00, 0x30, + 0x00, 0x31, 0x00, 0x38, 0x00, 0x20, 0x00, 0x47, 0x00, 0x6f, 0x00, 0x6f, + 0x00, 0x67, 0x00, 0x6c, 0x00, 0x65, 0x00, 0x20, 0x00, 0x4c, 0x00, 0x4c, + 0x00, 0x43, 0x00, 0x2c, 0x00, 0x20, 0x00, 0x43, 0x00, 0x43, 0x00, 0x2d, + 0x00, 0x42, 0x00, 0x59, 0x00, 0x2d, 0x00, 0x53, 0x00, 0x41, 0x00, 0x20, + 0x00, 0x33, 0x00, 0x2e, 0x00, 0x30, 0x00, 0x20, 0x00, 0x55, 0x00, 0x6e, + 0x00, 0x70, 0x00, 0x6f, 0x00, 0x72, 0x00, 0x74, 0x00, 0x65, 0x00, 0x64, + 0x00, 0x20, 0x00, 0x6c, 0x00, 0x69, 0x00, 0x63, 0x00, 0x65, 0x00, 0x6e, + 0x00, 0x73, 0x00, 0x65, 0x00, 0x28, 0x00, 0x68, 0x00, 0x74, 0x00, 0x74, + 0x00, 0x70, 0x00, 0x73, 0x00, 0x3a, 0x00, 0x2f, 0x00, 0x2f, 0x00, 0x63, + 0x00, 0x72, 0x00, 0x65, 0x00, 0x61, 0x00, 0x74, 0x00, 0x69, 0x00, 0x76, + 0x00, 0x65, 0x00, 0x63, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x6d, 0x00, 0x6f, + 0x00, 0x6e, 0x00, 0x73, 0x00, 0x2e, 0x00, 0x6f, 0x00, 0x72, 0x00, 0x67, + 0x00, 0x2f, 0x00, 0x6c, 0x00, 0x69, 0x00, 0x63, 0x00, 0x65, 0x00, 0x6e, + 0x00, 0x73, 0x00, 0x65, 0x00, 0x73, 0x00, 0x2f, 0x00, 0x62, 0x00, 0x79, + 0x00, 0x2d, 0x00, 0x73, 0x00, 0x61, 0x00, 0x2f, 0x00, 0x33, 0x00, 0x2e, + 0x00, 0x30, 0x00, 0x2f, 0x00, 0x6c, 0x00, 0x65, 0x00, 0x67, 0x00, 0x61, + 0x00, 0x6c, 0x00, 0x63, 0x00, 0x6f, 0x00, 0x64, 0x00, 0x65, 0x00, 0x29, + 0x58, 0x59, 0x5a, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf6, 0xd6, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xd3, 0x2d, 0x73, 0x66, 0x33, 0x32, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x0c, 0x42, 0x00, 0x00, 0x05, 0xde, + 0xff, 0xff, 0xf3, 0x25, 0x00, 0x00, 0x07, 0x93, 0x00, 0x00, 0xfd, 0x90, + 0xff, 0xff, 0xfb, 0xa1, 0xff, 0xff, 0xfd, 0xa2, 0x00, 0x00, 0x03, 0xdc, + 0x00, 0x00, 0xc0, 0x6e, 0x58, 0x59, 0x5a, 0x20, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x6f, 0xa0, 0x00, 0x00, 0x38, 0xf5, 0x00, 0x00, 0x03, 0x90, + 0x58, 0x59, 0x5a, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x9f, + 0x00, 0x00, 0x0f, 0x84, 0x00, 0x00, 0xb6, 0xc4, 0x58, 0x59, 0x5a, 0x20, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x62, 0x97, 0x00, 0x00, 0xb7, 0x87, + 0x00, 0x00, 0x18, 0xd9, 0x70, 0x61, 0x72, 0x61, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x03, 0x00, 0x00, 0x00, 0x02, 0x38, 0xe4, 0x00, 0x00, 0xe8, 0xf0, + 0x00, 0x00, 0x17, 0x10, 0x00, 0x00, 0x38, 0xe4, 0x00, 0x00, 0x14, 0xbc, + 0x63, 0x68, 0x72, 0x6d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, + 0x00, 0x00, 0xa3, 0xd7, 0x00, 0x00, 0x54, 0x7c, 0x00, 0x00, 0x4c, 0xcd, + 0x00, 0x00, 0x99, 0x9a, 0x00, 0x00, 0x26, 0x67, 0x00, 0x00, 0x0f, 0x5c, + 0x6d, 0x6c, 0x75, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x0c, 0x65, 0x6e, 0x55, 0x53, 0x00, 0x00, 0x00, 0x0c, + 0x00, 0x00, 0x00, 0x1c, 0x00, 0x47, 0x00, 0x6f, 0x00, 0x6f, 0x00, 0x67, + 0x00, 0x6c, 0x00, 0x65, 0x6d, 0x6c, 0x75, 0x63, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0c, 0x65, 0x6e, 0x55, 0x53, + 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x49, 0x00, 0x6d, + 0x00, 0x61, 0x00, 0x67, 0x00, 0x65, 0x00, 0x20, 0x00, 0x63, 0x00, 0x6f, + 0x00, 0x64, 0x00, 0x65, 0x00, 0x63, 0x00, 0x00 + ]; + + /// + /// Checks compressed ICC reconstruction against independently stored wire and profile bytes. + /// + [Fact] + public void ReadIcc_ReconstructsNativeProfile() + { + // Both vectors are copied unchanged from libjxl/lib/jxl/icc_codec_test.cc: + // kEncodedTestProfile and kTestProfile. Neither expectation is produced by this decoder. + using MemoryStream input = new(EncodedIccProfile, writable: false); + byte[] decoded = JxlIccReader.Read(Configuration.Default, new JxlBitReader(input)); + + Assert.True(ExpectedIccProfile.SequenceEqual(decoded)); + + // Parse the reconstructed bytes through the shared ICC reader, not a codec-specific profile model. + IccProfile profile = IccReader.ToProfile(decoded); + Assert.Equal(IccColorSpaceType.Rgb, profile.Header.DataColorSpace); + } + /// /// Checks hybrid-integer expansion against fixed tokens and bit payloads. /// diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/ICC/IccReaderTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/ICC/IccReaderTests.cs index 9f5e22c9c8..4eb09b348e 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/ICC/IccReaderTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/ICC/IccReaderTests.cs @@ -66,6 +66,7 @@ public void ToProfile_WithIncompleteHeader_Throws(int length) Assert.Equal(length, reader.Read(stream, length)); Assert.Throws(() => reader.ToProfile()); + Assert.Throws(() => IccReader.ToProfile(data[..length])); } /// @@ -88,6 +89,7 @@ public void ToProfile_WithIncompleteTagTable_Throws() Assert.Equal(length, reader.Read(stream, length)); Assert.Throws(() => reader.ToProfile()); + Assert.Throws(() => IccReader.ToProfile(data[..length])); } [Theory] @@ -135,6 +137,11 @@ public void ToProfile_WithTrailingData_RetainsDeclaredProfileOnly() Assert.Equal(profileData, profile.ToByteArray()); Assert.Equal(source.Entries.Length, profile.Entries.Length); + + // The complete-buffer entry point must use the same declared boundary as segmented reads. + IccProfile completeProfile = IccReader.ToProfile(data); + Assert.Equal(profileData, completeProfile.ToByteArray()); + Assert.Equal(source.Entries.Length, completeProfile.Entries.Length); } /// @@ -164,15 +171,28 @@ public void ToProfile_WithTagOutsideDeclaredSize_IgnoresTag() Assert.Equal(source.Entries.Length - 1, profile.Entries.Length); } - [Fact] - public void ReadProfile_NoEntries() + /// + /// Checks a stored profile with no tags through both validated input paths. + /// + /// Whether the input arrives as one complete owned array. + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ReadProfile_NoEntries(bool completeBuffer) { byte[] data = IccTestDataProfiles.HeaderRandomArray; - using MemoryStream stream = new(data); - IccReader reader = new(); - - reader.Read(stream, data.Length); - IccProfile output = reader.ToProfile(); + IccProfile output; + if (completeBuffer) + { + output = IccReader.ToProfile(data); + } + else + { + using MemoryStream stream = new(data); + IccReader reader = new(); + reader.Read(stream, data.Length); + output = reader.ToProfile(); + } Assert.Equal(0, output.Entries.Length); Assert.NotNull(output.Header); From 9a2b415b0c6c45840f64481c815ed0db906380bd Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 12:36:46 +1000 Subject: [PATCH 244/328] Consolidate JPEG XL field defaults and reuse half conversion --- .../Cms/TransferFunctions/JxlTransferUtils.cs | 71 ------------------- .../Formats/Jxl/Fields/JxlBundle.cs | 4 +- .../Formats/Jxl/Fields/JxlF16Coder.cs | 38 ++++------ .../Formats/Jxl/Fields/JxlInitVisitor.cs | 51 ------------- .../Jxl/Fields/JxlSetDefaultVisitor.cs | 19 ++++- .../Formats/Jxl/IO/FieldsTests.cs | 38 +++++++++- 6 files changed, 69 insertions(+), 152 deletions(-) delete mode 100644 src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlTransferUtils.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlInitVisitor.cs diff --git a/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlTransferUtils.cs b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlTransferUtils.cs deleted file mode 100644 index 8b9760c73d..0000000000 --- a/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlTransferUtils.cs +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; - -namespace SixLabors.ImageSharp.Formats.Jxl.Cms.TransferFunctions; - -internal static class JxlTransferUtils -{ - public static Vector FastLinearToSrgb(Vector v) - { - Vector v025_05 = ((v.As() & new Vector(0x3effffff)) | new Vector(0x3e800000)).As(); - - Vector d1 = (v025_05 * Vector.Create(0.059914046f)) - Vector.Create(0.108894556f); - Vector d2 = (d1 * v025_05) + Vector.Create(0.107963754f); - Vector pow = (d2 * v025_05) + Vector.Create(0.018092343f); - - const uint baseBits = 0x40000000; - - ReadOnlySpan powers25to18 = - [ - 0x00, 0x0a, 0x19, 0x26, - 0x32, 0x41, 0x4d, 0x5c, - 0x68, 0x75, 0x83, 0x8f, - 0xa0, 0xaa, 0xb9, 0xc6 - ]; - - ReadOnlySpan powers17to10 = - [ - 0x00, 0xb7, 0x04, 0x0d, - 0xcb, 0xe7, 0x41, 0x68, - 0x51, 0xd1, 0xeb, 0xf2, - 0x00, 0xb7, 0x04, 0x0d - ]; - - Vector bits = Vector.AsVectorInt32(v); - Vector exp = (bits >> 23) - Vector.Create(118); - - exp &= new Vector(0xf); - - Vector p25to18 = GatherByteTable(exp, powers25to18); - Vector p17to10 = GatherByteTable(exp, powers17to10); - - Vector mulBits = - (p25to18 << 18) | - (p17to10 << 10) | - new Vector((int)baseBits); - - Vector mul = Vector.AsVectorSingle(mulBits); - - Vector cutoff = new(0.0031308f); - - return Vector.ConditionalSelect( - Vector.LessThan(v, cutoff), - v * new Vector(12.92f), - (pow * mul) - new Vector(0.055f)); - } - - private static Vector GatherByteTable(Vector indices, ReadOnlySpan table) - { - int count = Vector.Count; - Span result = stackalloc int[count]; - - for (int i = 0; i < count; i++) - { - result[i] = table[indices[i]]; - } - - return new Vector(result); - } -} diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs index a642da2364..02df2bd947 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs @@ -27,7 +27,7 @@ internal static class JxlBundle /// The JXL fields. public static void Init(IJxlFields fields) { - JxlInitVisitor initVisitor = new(); + JxlSetDefaultVisitor initVisitor = new(visitNested: false); if (!initVisitor.Visit(fields)) { @@ -41,7 +41,7 @@ public static void Init(IJxlFields fields) /// The JXL fields. public static void SetDefault(IJxlFields fields) { - JxlSetDefaultVisitor visitor = new(); + JxlSetDefaultVisitor visitor = new(visitNested: true); if (!visitor.Visit(fields)) { diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs index d5b0d30fd2..ce7bcfc925 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs @@ -3,11 +3,12 @@ using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; +using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Jxl.Fields; /// -/// Represents the Half-precision Floating-point number coder. +/// Reads and writes JPEG XL half-precision fields. /// internal static class JxlF16Coder { @@ -34,37 +35,24 @@ public static bool CanEncode(float value, ref int encodedBits) return MathF.Abs(value) <= 65504.0f; } + /// + /// Reads one half-precision field, rejecting the NaN and infinity encodings forbidden by JPEG XL. + /// + /// The reader positioned at the 16-bit field. + /// The decoded value, unchanged when its encoding is invalid. + /// Whether the field contains a finite value. public static bool Read(JxlBitReader reader, ref float value) { - uint bits16 = reader.ReadBits32(16u); - uint sign = bits16 >> 15; - uint biasedExponent = (bits16 >> 10) & 0x1Fu; - uint mantissa = bits16 & 0x3FFu; + ushort bits = (ushort)reader.ReadBits32(16u); - if (biasedExponent == 31u) + // The wire format forbids an all-ones exponent. All other bit patterns, including signed zero + // and subnormal values, use the existing half-storage conversion without color-range clamping. + if ((bits & 0x7C00) == 0x7C00) { - // NaN and Infinity are not valid return false; } - if (biasedExponent == 0u) - { - // Subnormal or zero. - value = (1.0f / 16384) * (mantissa * (1.0f / 1024)); - if (sign != 0u) - { - value = -value; - } - - return true; - } - - uint biasedExp32 = biasedExponent + (127u - 15u); - uint mantissa32 = mantissa << (23 - 10); - uint bits32 = (sign << 31) | (biasedExp32 << 23) | mantissa32; - - value = BitConverter.UInt32BitsToSingle(bits32); - + value = HalfTypeHelper.Unpack(bits); return true; } diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlInitVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlInitVisitor.cs deleted file mode 100644 index 347c56dc07..0000000000 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlInitVisitor.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Jxl.Fields; - -/// -/// Initializes a bundle's own fields. Nested bundles initialize their fields in their constructors. -/// -internal sealed class JxlInitVisitor : JxlVisitor -{ - public override bool Bits(int bits, uint defaultValue, ref uint value) - { - value = defaultValue; - return true; - } - - /// - public override bool U32(JxlU32Enc enc, uint defaultValue, ref uint value) - { - value = defaultValue; - return true; - } - - public override bool U64(ulong defaultValue, ref ulong value) - { - value = defaultValue; - return true; - } - - public override bool Boolean(bool defaultValue, ref bool value) - { - value = defaultValue; - return true; - } - - public override bool F16(float defaultValue, ref float value) - { - value = defaultValue; - return true; - } - - public override bool Conditional(bool condition) => true; - - public override bool AllDefault(IJxlFields fields, ref bool allDefault) - { - _ = this.Boolean(true, ref allDefault); - return false; - } - - public override bool VisitNested(IJxlFields fields) => true; -} diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlSetDefaultVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlSetDefaultVisitor.cs index b329d09896..0b2e2a1014 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlSetDefaultVisitor.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlSetDefaultVisitor.cs @@ -4,10 +4,12 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Fields; /// -/// Restores serialized defaults in a bundle and its nested bundles. +/// Sets serialized fields to their defaults, with explicit control of nested-bundle traversal. /// -internal sealed class JxlSetDefaultVisitor : JxlVisitor +/// Whether to reset existing nested bundles as well as the current bundle's own fields. +internal sealed class JxlSetDefaultVisitor(bool visitNested) : JxlVisitor { + /// public override bool Bits(int bits, uint defaultValue, ref uint value) { value = defaultValue; @@ -21,26 +23,39 @@ public override bool U32(JxlU32Enc enc, uint defaultValue, ref uint value) return true; } + /// public override bool U64(ulong defaultValue, ref ulong value) { value = defaultValue; return true; } + /// public override bool Boolean(bool defaultValue, ref bool value) { value = defaultValue; return true; } + /// public override bool F16(float defaultValue, ref float value) { value = defaultValue; return true; } + /// public override bool Conditional(bool condition) => true; + /// + public override bool VisitNested(IJxlFields fields) + { + // Initialization touches only the parent's fields: each nested bundle has its own constructor. + // An explicit reset instead visits existing children in their normal extension scopes. + return !visitNested || base.VisitNested(fields); + } + + /// public override bool AllDefault(IJxlFields fields, ref bool allDefault) { _ = this.Boolean(true, ref allDefault); diff --git a/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs index b2e413aefb..75f8686116 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs @@ -265,6 +265,42 @@ public void BundleDefaults_RetainEarlierNonDefaultFields() Assert.Equal(1U, bundle.OldSmall); Assert.True(JxlBundle.AllDefault(bundle)); + + // Constructed children keep their own values when the parent's fields are initialized. + JxlImageMetadata metadata = new(); + metadata.ToneMapping.IntensityTarget = 123F; + JxlBundle.Init(metadata); + Assert.Equal(123F, metadata.ToneMapping.IntensityTarget); + + // An explicit default reset must also restore nested fields. + JxlBundle.SetDefault(metadata); + Assert.Equal(JxlMath.DefaultIntensityTarget, metadata.ToneMapping.IntensityTarget); + } + + /// + /// Checks finite half fields and the wire rejection of nonfinite encodings. + /// + /// The stored half-precision field. + /// The exact single-precision result, or the unchanged initial value after rejection. + /// Whether the wire encoding is valid. + [Theory] + [InlineData((ushort)0x0000, 0x00000000U, true)] + [InlineData((ushort)0x8000, 0x80000000U, true)] + [InlineData((ushort)0x0001, 0x33800000U, true)] + [InlineData((ushort)0x0400, 0x38800000U, true)] + [InlineData((ushort)0x7BFF, 0x477FE000U, true)] + [InlineData((ushort)0x7C00, 0x3F900000U, false)] + [InlineData((ushort)0xFC00, 0x3F900000U, false)] + [InlineData((ushort)0x7E00, 0x3F900000U, false)] + public void ReadF16_PreservesFiniteBitsAndRejectsNonfinite(ushort bits, uint expectedBits, bool valid) + { + // JXL fields use least-significant-bit-first bytes. The initial 1.125F must survive an invalid encoding. + using MemoryStream input = new([(byte)bits, (byte)(bits >> 8)]); + JxlBitReader reader = new(input); + float value = 1.125F; + + Assert.Equal(valid, JxlF16Coder.Read(reader, ref value)); + Assert.Equal(expectedBits, BitConverter.SingleToUInt32Bits(value)); } /// @@ -593,7 +629,7 @@ private static void TestU64Coder(ulong value, int expectedBitsWritten) private static void TestF16Coder(float value) { int maxEncodedBits = 0; - if (JxlF16Coder.CanEncode(value, ref maxEncodedBits)) + if (!JxlF16Coder.CanEncode(value, ref maxEncodedBits)) { throw new InvalidOperationException("This F16 value cannot be encoded: " + value); } From 457a6988247219d03cf949d00ae4dbc16a2f6f8f Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 12:52:33 +1000 Subject: [PATCH 245/328] Use shared nearest rounding for JPEG XL half fields --- .../Formats/Jxl/Fields/JxlBundle.cs | 4 +- .../Formats/Jxl/Fields/JxlF16Coder.cs | 85 +++++-------------- .../Jxl/Processing/Decoder/JxlIccReader.cs | 4 +- .../Formats/Jxl/IO/FieldsTests.cs | 48 ++++++++++- .../Formats/Jxl/Processing/AnsCommonTests.cs | 2 +- 5 files changed, 70 insertions(+), 73 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs index 02df2bd947..60f4c295e5 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs @@ -27,7 +27,7 @@ internal static class JxlBundle /// The JXL fields. public static void Init(IJxlFields fields) { - JxlSetDefaultVisitor initVisitor = new(visitNested: false); + JxlSetDefaultVisitor initVisitor = new(false); if (!initVisitor.Visit(fields)) { @@ -41,7 +41,7 @@ public static void Init(IJxlFields fields) /// The JXL fields. public static void SetDefault(IJxlFields fields) { - JxlSetDefaultVisitor visitor = new(visitNested: true); + JxlSetDefaultVisitor visitor = new(true); if (!visitor.Visit(fields)) { diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs index ce7bcfc925..7834a32113 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs @@ -19,20 +19,18 @@ internal static class JxlF16Coder public static int MaxEncodedBits() => 16; /// - /// Returns a boolean indicating whether the input float - /// can be represented properly when encoded into a bit-stream. - /// Also stores the maximum encodeable bits into encodedBits (which is - /// always 16). + /// Checks whether a value is finite and within the half-precision range. /// + /// The value to encode. + /// The field width, set to 16 even when the value is invalid. + /// Whether the value can be rounded to a finite half without saturation. public static bool CanEncode(float value, ref int encodedBits) { encodedBits = MaxEncodedBits(); - if (float.IsNaN(value) || float.IsInfinity(value)) - { - return false; // NaN and Infinity are not valid - } - return MathF.Abs(value) <= 65504.0f; + // Use the shared finite-range rules. A changed value is outside the wire field's accepted range; + // NaN also fails equality. In-range values, including either signed zero, remain valid. + return value == ColorNumerics.ClampToFinite(value, (float)Half.MinValue, (float)Half.MaxValue); } /// @@ -56,69 +54,24 @@ public static bool Read(JxlBitReader reader, ref float value) return true; } + /// + /// Rounds a finite value to the nearest half-precision value and writes its 16-bit field. + /// + /// The value to encode. + /// The output bit writer. + /// Whether the value is finite and within the half-precision range. Invalid values write no bits. + /// Halfway values round to the value with an even low mantissa bit. Signed zero is preserved. public static bool Write(float value, JxlBitWriter writer) { - uint bits32 = BitConverter.SingleToUInt32Bits(value); - - uint sign = bits32 >> 31; - uint biasedExp32 = (bits32 >> 23) & 0xFF; - uint mantissa32 = bits32 & 0x7FFFFF; - - int exp = (int)biasedExp32 - 127; - if (exp > 15) - { - throw new InvalidOperationException("Too big to encode, CanEncode should return false"); - } - - // Tiny or zero => zero. - if (exp < -24) - { - writer.Write(16, 0); - return true; - } - - uint biasedExp16 = 0; - uint mantissa16 = 0; - - if (exp < -14) - { - biasedExp16 = 0; - - uint subExp = unchecked((uint)(-14 - exp)); - - if (subExp is not (>= 1 and < 11)) - { - return false; - } - - mantissa16 = (1u << (int)(10 - subExp)) + (mantissa32 >> (int)(13 + subExp)); - } - else - { - // exp = [-14, 15] - biasedExp16 = unchecked((uint)(exp + 15)); - - if (biasedExp16 is not (>= 1 and < 31)) - { - return false; - } - - mantissa16 = mantissa32 >> 13; - } - - if (mantissa16 >= 1024) - { - return false; - } - - uint bits16 = (sign << 15) | (biasedExp16 << 10) | mantissa16; - - if (bits16 >= 0x10000) + int encodedBits = 0; + if (!CanEncode(value, ref encodedBits)) { return false; } - writer.Write(16, bits16); + // Shared half packing supplies nearest rounding, including subnormal boundaries and signed zero. + // The range check excludes values that can produce a nonfinite wire encoding. + writer.Write(encodedBits, HalfTypeHelper.Pack(value)); return true; } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlIccReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlIccReader.cs index f7a83259ea..25b2045f7f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlIccReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlIccReader.cs @@ -88,9 +88,9 @@ public static byte[] Read(Configuration configuration, JxlBitReader bitReader) JxlAnsCode code = new(); List contextMap = []; - JxlAnsReader.DecodeHistograms(configuration, bitReader, ContextCount, code, ref contextMap, disallowLz77: false); + JxlAnsReader.DecodeHistograms(configuration, bitReader, ContextCount, code, ref contextMap, false); - using JxlAnsSymbolReader reader = JxlAnsSymbolReader.Create(configuration, code, bitReader, distanceMultiplier: 0); + using JxlAnsSymbolReader reader = JxlAnsSymbolReader.Create(configuration, code, bitReader, 0); Span window = reader.Lz77Window; ReadOnlySpan contexts = CollectionsMarshal.AsSpan(contextMap); diff --git a/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs index 75f8686116..ee58350f81 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers.Binary; using SixLabors.ImageSharp.Formats.Jxl.Cms; using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; @@ -303,6 +304,49 @@ public void ReadF16_PreservesFiniteBitsAndRejectsNonfinite(ushort bits, uint exp Assert.Equal(expectedBits, BitConverter.SingleToUInt32Bits(value)); } + /// + /// Checks nearest rounding and ties-to-even against fixed single- and half-precision bit patterns. + /// + /// The exact single-precision input. + /// The nearest half-precision encoding. + [Theory] + [InlineData(0x3F801000U, (ushort)0x3C00)] + [InlineData(0x3F803000U, (ushort)0x3C02)] + [InlineData(0x3F801001U, (ushort)0x3C01)] + [InlineData(0xBF801001U, (ushort)0xBC01)] + [InlineData(0x33000001U, (ushort)0x0001)] + [InlineData(0x80000000U, (ushort)0x8000)] + [InlineData(0x477FE000U, (ushort)0x7BFF)] + public void WriteF16_RoundsToNearestHalf(uint sourceBits, ushort expectedBits) + { + // Around 1, halfway mantissas select an even low bit. Just past a midpoint must round toward the nearer value. + using MemoryStream output = new(); + JxlBitWriter writer = new(output); + + Assert.True(JxlF16Coder.Write(BitConverter.UInt32BitsToSingle(sourceBits), writer)); + Assert.Equal(16, writer.BitsWritten); + Assert.Equal(expectedBits, BinaryPrimitives.ReadUInt16LittleEndian(output.GetBuffer())); + } + + /// + /// Checks that invalid half fields are rejected before any output is written. + /// + /// A nonfinite value or a finite value outside the half-precision range. + [Theory] + [InlineData(float.NaN)] + [InlineData(float.PositiveInfinity)] + [InlineData(float.NegativeInfinity)] + [InlineData(65505F)] + [InlineData(-65505F)] + public void WriteF16_RejectsValuesOutsideFiniteRange(float value) + { + using MemoryStream output = new(); + JxlBitWriter writer = new(output); + + Assert.False(JxlF16Coder.Write(value, writer)); + Assert.Equal(0, writer.BitsWritten); + } + /// /// Checks that probing an incomplete header reports failure without accepting default field status. /// @@ -767,8 +811,8 @@ public void F16CoderTest() // These values are out of range and cannot be represented // by an F16 coder, so they're expected to throw. - Assert.Throws(() => TestF16Coder(65504.01f)); - Assert.Throws(() => TestF16Coder(-65505.0f)); + Assert.Throws(() => TestF16Coder(65504.01f)); + Assert.Throws(() => TestF16Coder(-65505.0f)); } [Fact] diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/AnsCommonTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/AnsCommonTests.cs index 716a50c4c5..85fb8b2ae0 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/AnsCommonTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/AnsCommonTests.cs @@ -143,7 +143,7 @@ public void ReadIcc_ReconstructsNativeProfile() { // Both vectors are copied unchanged from libjxl/lib/jxl/icc_codec_test.cc: // kEncodedTestProfile and kTestProfile. Neither expectation is produced by this decoder. - using MemoryStream input = new(EncodedIccProfile, writable: false); + using MemoryStream input = new(EncodedIccProfile, false); byte[] decoded = JxlIccReader.Read(Configuration.Default, new JxlBitReader(input)); Assert.True(ExpectedIccProfile.SequenceEqual(decoded)); From 6d72744140ffce157496016115a98fe20b383787 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 13:11:26 +1000 Subject: [PATCH 246/328] Share partial span selection for JPEG XL quantization --- .../Common/Extensions/SpanExtensions.cs | 72 +++++++++++++++++++ .../Formats/Jxl/Memory/JxlSpanHelper.cs | 50 ------------- .../Common/SpanExtensionsTests.cs | 49 +++++++++++++ 3 files changed, 121 insertions(+), 50 deletions(-) create mode 100644 src/ImageSharp/Common/Extensions/SpanExtensions.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Memory/JxlSpanHelper.cs create mode 100644 tests/ImageSharp.Tests/Common/SpanExtensionsTests.cs diff --git a/src/ImageSharp/Common/Extensions/SpanExtensions.cs b/src/ImageSharp/Common/Extensions/SpanExtensions.cs new file mode 100644 index 0000000000..0c25fa646e --- /dev/null +++ b/src/ImageSharp/Common/Extensions/SpanExtensions.cs @@ -0,0 +1,72 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp; + +/// +/// Provides partial selection for mutable spans. +/// +internal static class SpanExtensions +{ + /// + /// Partitions a nonempty span and returns the value at the requested position in sorted order. + /// + /// The comparable sample type. + /// The workspace to partition. Its sample values are retained but their order can change. + /// The zero-based position, within the supplied span. + /// The selected value. Samples before and after it are partitioned rather than fully sorted. + public static T NthElement(this Span span, int n) + where T : IComparable + { + int left = 0; + int right = span.Length - 1; + + while (left < right) + { + // Repeated values share one partition. Partition into values less than, equal to, + // and greater than the pivot. Selecting within the equal range finishes without rescanning each equal sample. + T pivot = span[left + ((right - left) / 2)]; + int lower = left; + int current = left; + int upper = right; + + while (current <= upper) + { + int comparison = span[current].CompareTo(pivot); + if (comparison < 0) + { + (span[lower], span[current]) = (span[current], span[lower]); + lower++; + current++; + } + else if (comparison > 0) + { + // The sample moved from the upper end has not been classified. Examine it before advancing current. + (span[current], span[upper]) = (span[upper], span[current]); + upper--; + } + else + { + current++; + } + } + + // Only the partition containing n can affect the result. The equal partition already occupies + // its final range of sorted positions, even though values on either side are not fully sorted. + if (n < lower) + { + right = lower - 1; + } + else if (n > upper) + { + left = upper + 1; + } + else + { + return span[n]; + } + } + + return span[left]; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlSpanHelper.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlSpanHelper.cs deleted file mode 100644 index fcf9b61aa5..0000000000 --- a/src/ImageSharp/Formats/Jxl/Memory/JxlSpanHelper.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Jxl.Memory; - -internal static class JxlSpanHelper -{ - public static T NthElement(this Span span, int n) - where T : IComparable - { - int left = 0; - int right = span.Length - 1; - - while (true) - { - int pivotIndex = Partition(span, left, right); - if (pivotIndex == n) - { - return span[pivotIndex]; - } - else if (n < pivotIndex) - { - right = pivotIndex - 1; - } - else - { - left = pivotIndex + 1; - } - } - } - - private static int Partition(Span span, int left, int right) - where T : IComparable - { - T pivot = span[right]; - int storeIndex = left; - - for (int i = left; i < right; i++) - { - if (span[i].CompareTo(pivot) < 0) - { - (span[i], span[storeIndex]) = (span[storeIndex], span[i]); - storeIndex++; - } - } - - (span[storeIndex], span[right]) = (span[right], span[storeIndex]); - return storeIndex; - } -} diff --git a/tests/ImageSharp.Tests/Common/SpanExtensionsTests.cs b/tests/ImageSharp.Tests/Common/SpanExtensionsTests.cs new file mode 100644 index 0000000000..babd7cdd14 --- /dev/null +++ b/tests/ImageSharp.Tests/Common/SpanExtensionsTests.cs @@ -0,0 +1,49 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Tests.Common; + +public class SpanExtensionsTests +{ + /// + /// Checks partial selection against full sorting for fixed numeric inputs. + /// + /// The fixed input values, including repeated and extreme finite values. + [Theory] + [InlineData(new float[] { 7 })] + [InlineData(new float[] { 1, 2, 3, 4, 5 })] + [InlineData(new float[] { 5, 4, 3, 2, 1 })] + [InlineData(new float[] { 2, 2, 2, 2, 2 })] + [InlineData(new float[] { 2, 1, 3, 2, 3, 1 })] + [InlineData(new float[] { -9, 6, 0, float.MinValue, float.MaxValue })] + public void NthElement_SelectsOrderStatisticAndPreservesValues(float[] input) + { + float[] expected = input.ToArray(); + expected.AsSpan().Sort(); + + for (int n = 0; n < input.Length; n++) + { + // Each selection starts with the same fixed input. Full sorting supplies an independent order statistic. + float[] actual = input.ToArray(); + float selected = actual.AsSpan().NthElement(n); + + Assert.Equal(expected[n], selected); + Assert.Equal(selected, actual[n]); + + // Selection partitions around n but does not sort either side. + for (int i = 0; i < n; i++) + { + Assert.True(actual[i] <= selected); + } + + for (int i = n + 1; i < actual.Length; i++) + { + Assert.True(actual[i] >= selected); + } + + // Sorting the partitioned values checks that selection did not lose, duplicate, or change a value. + actual.AsSpan().Sort(); + Assert.Equal(expected, actual); + } + } +} From 7427b61a3c46d19a5ace079e154e368bc2239261 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 13:11:27 +1000 Subject: [PATCH 247/328] Write JPEG XL output through shared typed pixel conversion --- .../Formats/Jxl/Memory/JxlMemoryHelpers.cs | 64 -- .../Jxl/Processing/Image/IJxlImageOutput.cs | 65 -- .../Jxl/Processing/Primitives/JxlHalfUtils.cs | 178 ---- .../RenderPipeline/WriteToOutputStage.cs | 777 ------------------ .../WriteToOutputStage{TPixel}.cs | 198 +++++ 5 files changed, 198 insertions(+), 1084 deletions(-) delete mode 100644 src/ImageSharp/Formats/Jxl/Memory/JxlMemoryHelpers.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Image/IJxlImageOutput.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlHalfUtils.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage{TPixel}.cs diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlMemoryHelpers.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlMemoryHelpers.cs deleted file mode 100644 index 54c7bd7560..0000000000 --- a/src/ImageSharp/Formats/Jxl/Memory/JxlMemoryHelpers.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; -using System.Runtime.InteropServices; -using System.Security.AccessControl; - -namespace SixLabors.ImageSharp.Formats.Jxl.Memory; - -/// -/// Memory management utilities. -/// -internal static class JxlMemoryHelpers -{ - public static Memory CastMemory(Memory memory) - where TSourceType : unmanaged - where TOutputType : unmanaged - { - CastingMemoryManager memoryManager = new(memory); - return memoryManager.Memory; - } - - public static Memory MemoryFromList(List list) - where T : unmanaged - { - ListFactoryMemoryManager lf = new(list); - return lf.Memory; - } - - private sealed class CastingMemoryManager(Memory memory) - : MemoryManager - where TSourceType : unmanaged - where TOutputType : unmanaged - { - public override Span GetSpan() => MemoryMarshal.Cast(memory.Span); - - // We don't use these. - public override MemoryHandle Pin(int elementIndex = 0) => throw new NotImplementedException(); - - public override void Unpin() => throw new NotImplementedException(); - - protected override void Dispose(bool disposing) - { - // blank - } - } - - private sealed class ListFactoryMemoryManager(List list) - : MemoryManager - where T : unmanaged - { - public override Span GetSpan() => CollectionsMarshal.AsSpan(list); - - // We don't use these. - public override MemoryHandle Pin(int elementIndex = 0) => throw new NotImplementedException(); - - public override void Unpin() => throw new NotImplementedException(); - - protected override void Dispose(bool disposing) - { - // blank - } - } -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Image/IJxlImageOutput.cs b/src/ImageSharp/Formats/Jxl/Processing/Image/IJxlImageOutput.cs deleted file mode 100644 index 2bc74ef17b..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Image/IJxlImageOutput.cs +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Image; - -/// -/// Abstracts a way for the codec to output images during decoding. -/// -internal interface IJxlImageOutput : IDisposable -{ - /// - /// Gets or sets the index of this channel. - /// - public int ChannelIndex { get; set; } - - /// - /// Gets or sets the pixel format for the output pixels. - /// - public JxlPixelFormat PixelFormat { get; set; } - - /// - /// Gets or sets the output bit depth for unsigned data types. - /// - public int BitsPerSample { get; set; } - - /// - /// Gets or sets the pixel buffer for image output. - /// - public Memory Buffer { get; set; } - - /// - /// Gets or sets length of a row of image buffer in bytes. - /// - public int Stride { get; set; } - - /// - /// Gets or sets a value indicating whether endianness must be - /// swapped. - /// - public bool SwapEndianness { get; set; } - - /// - /// Outputs the image. - /// - /// Data where the image should be output. - /// X offset - /// Y offset - /// Pixels to output. - public void Output( - Span data, - int x, - int y, - Span pixels); - - /// - /// Initializes image data. - /// - /// Output image data. - /// Number of pixels a thread processes. - public void Initialize( - Span data, - int numPixelsPerThread); -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlHalfUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlHalfUtils.cs deleted file mode 100644 index 49faab821a..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlHalfUtils.cs +++ /dev/null @@ -1,178 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; - -internal static class JxlHalfUtils -{ - /* - * SIMD version of the following bit-twiddling implementation - * of float to Half conversion, because Vector doesn't - * have a method like this (only reinterpretation, we want - * conversion). - public static ushort ConvertSingleToHalf(float f) - { - uint x = BitConverter.SingleToUInt32Bits(f); - - uint sign = (x >> 16) & 0x8000; // sign bit - int exp = (int)((x >> 23) & 0xFF) - 127 + 15; // exponent rebias - uint mantissa = x & 0x007FFFFF; // mantissa bits - - if (exp <= 0) - { - // Subnormal or zero - if (exp < -10) - { - return (ushort)sign; - } - - mantissa = (mantissa | 0x00800000) >> (1 - exp); - return (ushort)(sign | (mantissa >> 13)); - } - else if (exp >= 31) - { - // Inf or NaN - return (ushort)(sign | 0x7C00 | (mantissa >> 13)); - } - else - { - // Normalized - return (ushort)(sign | ((uint)exp << 10) | (mantissa >> 13)); - } - } - */ - public static Vector ConvertSingleToHalf(Vector f) - { - Vector x = f.As(); - - Vector sign = (x >> 16) & Vector.Create(0x8000u); - Vector exponent = ((x >> 23) & Vector.Create(0xFFu)).As() - Vector.Create(127) + Vector.Create(15); - Vector mantissa = x & Vector.Create(0x007FFFFFu); - - Vector results = Vector.Zero; - - Vector lessThanMinus10 = Vector.LessThan(exponent, Vector.Create(-10)).As(); - Vector gte31 = Vector.GreaterThanOrEqual(exponent, Vector.Create(31)).As(); - Vector notLtMinus10OrGte31 = ~(lessThanMinus10 | gte31); - - // Subnormal or zero (exp < -10) = sign - results = Vector.ConditionalSelect(lessThanMinus10, sign, results); - - // exp <= 0 = (ushort)(sign | (((mantissa | 0x00800000) >> (1 - exp)) >> 13)); - results = Vector.ConditionalSelect( - Vector.LessThanOrEqual(exponent, Vector.Zero).As(), - sign | (ShiftRightAll(mantissa.As() | Vector.Create(0x00800000), Vector.One - exponent) >> 13).As(), - results); - - // exp >= 31 = (ushort)(sign | 0x7C00 | (mantissa >> 13)) - results = Vector.ConditionalSelect( - gte31, - sign | Vector.Create(0x7C00u) | (mantissa >> 13), - results); - - // anything else - normalized - results = Vector.ConditionalSelect( - notLtMinus10OrGte31, - sign | (exponent.As() << 10) | (mantissa >> 13), - results); - - return VectorUInt32ToUInt16(results); - } - - private static Vector VectorUInt32ToUInt16(Vector uint32) - { - Vector clipped = uint32 & Vector.Create(0xFFFFu); - return clipped.As(); - } - - /// - /// Vectors don't support shifting right using shift value as vector. - /// This is a hack to do this. - /// - /// Kind of vector - /// Input vector - /// Vectors with shift value for each corresponding item. - /// Shifted vectors - private static Vector ShiftRightAll(Vector vec, Vector shiftVec) - where T : unmanaged, IShiftOperators - { - Span firstVec = stackalloc T[Vector.Count]; - Span secondVec = stackalloc T[Vector.Count]; - - vec.CopyTo(firstVec); - shiftVec.CopyTo(secondVec); - - ref T firstRef = ref MemoryMarshal.GetReference(firstVec); - ref T secondRef = ref MemoryMarshal.GetReference(secondVec); - - int count = Vector.Count; - - if (count == 1) - { - return Vector.Create(firstRef >>> secondRef); - } - else if (count == 2) - { - firstRef >>>= secondRef; - Unsafe.Add(ref firstRef, 1) >>>= Unsafe.Add(ref secondRef, 1); - return Vector.Create(firstVec); - } - else if (count == 4) - { - firstRef >>>= secondRef; - Unsafe.Add(ref firstRef, 1) >>>= Unsafe.Add(ref secondRef, 1); - Unsafe.Add(ref firstRef, 2) >>>= Unsafe.Add(ref secondRef, 2); - Unsafe.Add(ref firstRef, 3) >>>= Unsafe.Add(ref secondRef, 3); - return Vector.Create(firstVec); - } - else if (count == 8) - { - firstRef >>>= secondRef; - Unsafe.Add(ref firstRef, 1) >>>= Unsafe.Add(ref secondRef, 1); - Unsafe.Add(ref firstRef, 2) >>>= Unsafe.Add(ref secondRef, 2); - Unsafe.Add(ref firstRef, 3) >>>= Unsafe.Add(ref secondRef, 3); - Unsafe.Add(ref firstRef, 4) >>>= Unsafe.Add(ref secondRef, 4); - Unsafe.Add(ref firstRef, 5) >>>= Unsafe.Add(ref secondRef, 5); - Unsafe.Add(ref firstRef, 6) >>>= Unsafe.Add(ref secondRef, 6); - Unsafe.Add(ref firstRef, 7) >>>= Unsafe.Add(ref secondRef, 7); - return Vector.Create(firstVec); - } - else if (count == 16) - { - firstRef >>>= secondRef; - Unsafe.Add(ref firstRef, 1) >>>= Unsafe.Add(ref secondRef, 1); - Unsafe.Add(ref firstRef, 2) >>>= Unsafe.Add(ref secondRef, 2); - Unsafe.Add(ref firstRef, 3) >>>= Unsafe.Add(ref secondRef, 3); - Unsafe.Add(ref firstRef, 4) >>>= Unsafe.Add(ref secondRef, 4); - Unsafe.Add(ref firstRef, 5) >>>= Unsafe.Add(ref secondRef, 5); - Unsafe.Add(ref firstRef, 6) >>>= Unsafe.Add(ref secondRef, 6); - Unsafe.Add(ref firstRef, 7) >>>= Unsafe.Add(ref secondRef, 7); - Unsafe.Add(ref firstRef, 8) >>>= Unsafe.Add(ref secondRef, 8); - Unsafe.Add(ref firstRef, 9) >>>= Unsafe.Add(ref secondRef, 9); - Unsafe.Add(ref firstRef, 10) >>>= Unsafe.Add(ref secondRef, 10); - Unsafe.Add(ref firstRef, 11) >>>= Unsafe.Add(ref secondRef, 11); - Unsafe.Add(ref firstRef, 12) >>>= Unsafe.Add(ref secondRef, 12); - Unsafe.Add(ref firstRef, 13) >>>= Unsafe.Add(ref secondRef, 13); - Unsafe.Add(ref firstRef, 14) >>>= Unsafe.Add(ref secondRef, 14); - Unsafe.Add(ref firstRef, 15) >>>= Unsafe.Add(ref secondRef, 15); - return Vector.Create(firstVec); - } - else - { - // Vector too large, use a (slightly) slower loop - // instead of duplicating too much. - firstRef >>>= secondRef; - - for (int i = 1; i < count; i++) - { - Unsafe.Add(ref firstRef, i) >>>= Unsafe.Add(ref secondRef, i); - } - - return Vector.Create(firstVec); - } - } -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs deleted file mode 100644 index 786dddfa6b..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs +++ /dev/null @@ -1,777 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; -using System.Buffers.Binary; -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using SixLabors.ImageSharp.Common.Helpers; -using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; -using SixLabors.ImageSharp.Formats.Jxl.Memory; -using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; -using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; -using SixLabors.ImageSharp.Memory; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; - -internal sealed class WriteToOutputStage : RenderPipelineStageBase, IDisposable -{ - private const int ChunkSize = 1024; - - private readonly int width; - private readonly int height; - private readonly IJxlImageOutput main; - private readonly int numColors; - private readonly bool wantAlpha; - private readonly bool hasAlpha; - private readonly bool unpremultiplyAlpha; - private readonly int alphaC; - private readonly bool flipX; - private readonly bool flipY; - private readonly bool transpose; - private readonly List extraChannels = []; - private readonly List opaqueAlpha = []; - private readonly List> tempIn = []; - private readonly List> tempOut = []; - - // For Dispose() - private readonly List> disposables = []; - - public WriteToOutputStage(Configuration configuration, IJxlImageOutput output, int width, int height, bool hasAlpha, bool unpremultiplyAlpha, int alphaC, JxlExifOrientation undoOrientation, List extraChannel, Func extraFactory) - : base(configuration) - { - this.width = width; - this.height = height; - this.main = output; - this.numColors = this.main.PixelFormat.Channels < 3 ? 1 : 3; - this.wantAlpha = this.main.PixelFormat.Channels is 2 or 4; - this.hasAlpha = hasAlpha; - this.unpremultiplyAlpha = unpremultiplyAlpha; - this.alphaC = alphaC; - this.flipX = ShouldFlipX(undoOrientation); - this.flipY = ShouldFlipY(undoOrientation); - this.transpose = ShouldTranspose(undoOrientation); - this.opaqueAlpha = new(ChunkSize); - CollectionsMarshal.AsSpan(this.opaqueAlpha).Fill(1.0f); - - for (int ec = 0; ec < this.extraChannels.Count; ec++) - { - if (extraChannel[ec].IsPresent) - { - IJxlImageOutput extra = extraFactory(extraChannel[ec]); - extra.ChannelIndex = 3 + ec; - this.extraChannels.Add(extra); - } - } - } - - public override string Name => "WritePixelCB"; - - /// - /// Gets the 32x32 blue noise dithering pattern lookup - /// table (from ), - /// scaled to have an average of 0 and be fully contained in (0.49219 - /// to -0.49219). Rows are padded to 48 (32 + 16) to allow SIMD to wrap around - /// horizontally. - /// - private static ReadOnlySpan DitheringPattern => - [ - -0.26057f, 0.32619f, 0.21039f, -0.03281f, -0.10616f, 0.16792f, 0.43042f, -0.48061f, - -0.00965f, -0.31075f, 0.24899f, -0.35322f, -0.02509f, -0.25285f, 0.02895f, 0.10230f, - -0.28373f, -0.00193f, 0.23355f, 0.43428f, -0.23741f, 0.18336f, -0.31847f, -0.11002f, - -0.36094f, 0.26057f, -0.19108f, -0.29531f, 0.40726f, -0.09458f, 0.11002f, -0.48833f, - -0.26057f, 0.32619f, 0.21039f, -0.03281f, -0.10616f, 0.16792f, 0.43042f, -0.48061f, - -0.00965f, -0.31075f, 0.24899f, -0.35322f, -0.02509f, -0.25285f, 0.02895f, 0.10230f, - 0.16020f, -0.35708f, -0.18336f, 0.36094f, -0.28373f, -0.34550f, -0.20267f, 0.07914f, - 0.35708f, -0.41498f, 0.47675f, -0.21811f, -0.12546f, 0.44200f, -0.41884f, -0.17178f, - 0.39954f, 0.33778f, -0.33778f, 0.04053f, -0.46517f, 0.27215f, -0.16792f, 0.39182f, - 0.20653f, -0.43814f, -0.02895f, 0.17950f, -0.41498f, 0.01737f, 0.24899f, 0.49219f, - 0.16020f, -0.35708f, -0.18336f, 0.36094f, -0.28373f, -0.34550f, -0.20267f, 0.07914f, - 0.35708f, -0.41498f, 0.47675f, -0.21811f, -0.12546f, 0.44200f, -0.41884f, -0.17178f, - -0.00965f, 0.08300f, 0.41112f, -0.46903f, 0.04053f, 0.47289f, 0.26057f, -0.05983f, - -0.13704f, 0.14862f, 0.03281f, 0.29531f, -0.45744f, 0.22583f, 0.14862f, -0.09072f, - -0.37638f, 0.19881f, -0.14476f, 0.14476f, -0.09072f, 0.48447f, -0.39954f, 0.06369f, - -0.05983f, -0.26829f, 0.43428f, -0.12546f, 0.28759f, -0.22969f, -0.32619f, -0.15248f, - -0.00965f, 0.08300f, 0.41112f, -0.46903f, 0.04053f, 0.47289f, 0.26057f, -0.05983f, - -0.13704f, 0.14862f, 0.03281f, 0.29531f, -0.45744f, 0.22583f, 0.14862f, -0.09072f, - -0.42270f, 0.23741f, -0.23355f, -0.11774f, 0.18722f, 0.11388f, -0.43814f, -0.24899f, - 0.41884f, 0.21039f, -0.28373f, -0.06756f, 0.07914f, 0.36480f, -0.31075f, 0.30303f, - -0.03281f, 0.07142f, -0.42656f, 0.38024f, -0.27987f, 0.00579f, 0.12546f, -0.22197f, - 0.29917f, 0.36866f, 0.13704f, -0.47289f, 0.09072f, 0.35708f, -0.04825f, 0.38796f, - -0.42270f, 0.23741f, -0.23355f, -0.11774f, 0.18722f, 0.11388f, -0.43814f, -0.24899f, - 0.41884f, 0.21039f, -0.28373f, -0.06756f, 0.07914f, 0.36480f, -0.31075f, 0.30303f, - -0.28759f, -0.07142f, 0.44200f, 0.27601f, -0.38024f, -0.16020f, -0.01737f, 0.30303f, - -0.33006f, -0.40340f, -0.16792f, 0.40726f, -0.36480f, -0.00579f, -0.19108f, 0.41498f, - -0.26443f, 0.46903f, -0.21811f, 0.28759f, -0.04053f, 0.22197f, 0.34550f, -0.44972f, - -0.14476f, -0.34164f, 0.04053f, -0.19494f, 0.45358f, -0.37252f, 0.21425f, 0.05597f, - -0.28759f, -0.07142f, 0.44200f, 0.27601f, -0.38024f, -0.16020f, -0.01737f, 0.30303f, - -0.33006f, -0.40340f, -0.16792f, 0.40726f, -0.36480f, -0.00579f, -0.19108f, 0.41498f, - 0.31075f, 0.14090f, -0.33778f, 0.00579f, 0.34550f, -0.29917f, 0.38796f, 0.13704f, - 0.05983f, -0.10230f, 0.34164f, 0.10616f, -0.23741f, 0.19494f, -0.47675f, 0.04439f, - -0.39568f, 0.24127f, 0.10616f, -0.49219f, -0.17950f, -0.36094f, -0.30303f, 0.45744f, - -0.01351f, 0.24513f, -0.39182f, -0.07528f, 0.18722f, -0.26057f, -0.11002f, -0.45358f, - 0.31075f, 0.14090f, -0.33778f, 0.00579f, 0.34550f, -0.29917f, 0.38796f, 0.13704f, - 0.05983f, -0.10230f, 0.34164f, 0.10616f, -0.23741f, 0.19494f, -0.47675f, 0.04439f, - 0.46903f, -0.17178f, -0.41112f, 0.07528f, -0.09458f, 0.21811f, -0.20267f, -0.48833f, - 0.44972f, 0.00965f, 0.24127f, -0.42656f, 0.48447f, -0.11774f, 0.26443f, 0.14090f, - -0.15634f, -0.07142f, -0.32233f, 0.36094f, 0.42270f, 0.19108f, 0.07142f, -0.11002f, - 0.15634f, 0.38024f, -0.28759f, 0.27987f, -0.00193f, 0.33006f, 0.11388f, -0.21039f, - 0.46903f, -0.17178f, -0.41112f, 0.07528f, -0.09458f, 0.21811f, -0.20267f, -0.48833f, - 0.44972f, 0.00965f, 0.24127f, -0.42656f, 0.48447f, -0.11774f, 0.26443f, 0.14090f, - 0.02123f, 0.17950f, 0.38024f, -0.24127f, -0.44586f, 0.48833f, -0.03667f, 0.26829f, - -0.36866f, -0.22583f, 0.17178f, -0.30689f, 0.29145f, -0.04825f, -0.35322f, 0.43042f, - 0.34936f, 0.00193f, 0.16792f, -0.12932f, 0.03667f, -0.06756f, 0.31847f, -0.40726f, - -0.24513f, 0.09458f, -0.17564f, 0.47675f, -0.43042f, -0.32233f, 0.40340f, 0.26057f, - 0.02123f, 0.17950f, 0.38024f, -0.24127f, -0.44586f, 0.48833f, -0.03667f, 0.26829f, - -0.36866f, -0.22583f, 0.17178f, -0.30689f, 0.29145f, -0.04825f, -0.35322f, 0.43042f, - -0.47675f, -0.12160f, -0.04825f, 0.28759f, 0.10230f, 0.15634f, -0.14862f, -0.27601f, - 0.36094f, -0.12932f, -0.05983f, -0.45358f, -0.17950f, 0.01737f, 0.09458f, -0.29145f, - -0.22969f, -0.43428f, 0.45744f, -0.38796f, -0.27601f, -0.21039f, -0.46131f, 0.22969f, - 0.41112f, -0.05211f, -0.48061f, 0.16406f, 0.05211f, -0.14862f, -0.03281f, -0.36866f, - -0.47675f, -0.12160f, -0.04825f, 0.28759f, 0.10230f, 0.15634f, -0.14862f, -0.27601f, - 0.36094f, -0.12932f, -0.05983f, -0.45358f, -0.17950f, 0.01737f, 0.09458f, -0.29145f, - -0.27215f, 0.34164f, -0.31075f, 0.42656f, -0.38410f, -0.32619f, 0.02895f, 0.19881f, - 0.08300f, 0.42270f, 0.31461f, 0.13318f, 0.45744f, 0.37638f, -0.40726f, 0.31847f, - -0.08686f, 0.21425f, 0.29917f, 0.07914f, 0.26829f, 0.13704f, 0.48447f, -0.15248f, - 0.02509f, -0.34936f, 0.34936f, -0.10230f, 0.42656f, -0.23741f, 0.22583f, 0.09072f, - -0.27215f, 0.34164f, -0.31075f, 0.42656f, -0.38410f, -0.32619f, 0.02895f, 0.19881f, - 0.08300f, 0.42270f, 0.31461f, 0.13318f, 0.45744f, 0.37638f, -0.40726f, 0.31847f, - 0.44972f, 0.20267f, 0.04825f, -0.21425f, 0.24513f, -0.07142f, 0.39954f, -0.46131f, - -0.39568f, -0.01351f, -0.33392f, 0.05597f, -0.26443f, 0.22197f, -0.20653f, 0.15248f, - 0.04439f, -0.46517f, -0.16406f, -0.04439f, -0.34936f, 0.37252f, -0.01351f, -0.30689f, - 0.29917f, 0.20653f, -0.26829f, 0.26443f, 0.13318f, -0.39954f, 0.30303f, -0.08686f, - 0.44972f, 0.20267f, 0.04825f, -0.21425f, 0.24513f, -0.07142f, 0.39954f, -0.46131f, - -0.39568f, -0.01351f, -0.33392f, 0.05597f, -0.26443f, 0.22197f, -0.20653f, 0.15248f, - -0.42656f, 0.12932f, -0.14476f, -0.46903f, -0.00579f, 0.34936f, -0.18722f, 0.28373f, - -0.23741f, 0.22969f, -0.16020f, -0.38024f, -0.08300f, -0.48447f, -0.02123f, -0.14862f, - 0.48061f, -0.31847f, 0.39568f, -0.24899f, 0.18722f, -0.41884f, 0.10230f, -0.08300f, - -0.38796f, 0.06369f, -0.19881f, -0.44972f, 0.00579f, -0.33392f, 0.37252f, -0.19108f, - -0.42656f, 0.12932f, -0.14476f, -0.46903f, -0.00579f, 0.34936f, -0.18722f, 0.28373f, - -0.23741f, 0.22969f, -0.16020f, -0.38024f, -0.08300f, -0.48447f, -0.02123f, -0.14862f, - -0.02509f, -0.35708f, 0.32619f, 0.46517f, 0.17178f, -0.28373f, 0.10616f, 0.47675f, - -0.09458f, 0.15248f, 0.43428f, 0.35322f, 0.17564f, 0.27215f, 0.41112f, -0.36480f, - 0.24899f, 0.11774f, 0.01351f, 0.33006f, -0.11388f, -0.18336f, 0.41884f, -0.23355f, - 0.16406f, 0.46131f, 0.38410f, -0.04825f, -0.15634f, 0.49219f, 0.17564f, 0.03667f, - -0.02509f, -0.35708f, 0.32619f, 0.46517f, 0.17178f, -0.28373f, 0.10616f, 0.47675f, - -0.09458f, 0.15248f, 0.43428f, 0.35322f, 0.17564f, 0.27215f, 0.41112f, -0.36480f, - 0.40726f, 0.23355f, -0.25285f, -0.08300f, -0.41112f, -0.12160f, -0.35708f, 0.05211f, - -0.41884f, -0.29531f, 0.02123f, -0.21425f, 0.09844f, -0.30689f, -0.11388f, 0.34550f, - -0.26443f, -0.07142f, -0.39954f, 0.44586f, 0.05983f, -0.48833f, 0.24127f, 0.34936f, - -0.44200f, -0.12546f, 0.12160f, -0.30303f, 0.27215f, 0.07528f, -0.48447f, -0.29145f, - 0.40726f, 0.23355f, -0.25285f, -0.08300f, -0.41112f, -0.12160f, -0.35708f, 0.05211f, - -0.41884f, -0.29531f, 0.02123f, -0.21425f, 0.09844f, -0.30689f, -0.11388f, 0.34550f, - 0.28373f, -0.17564f, 0.09458f, 0.02123f, 0.30689f, 0.41884f, 0.20653f, -0.03667f, - 0.32233f, 0.25671f, -0.45744f, -0.05597f, 0.46517f, -0.41498f, 0.00965f, 0.07142f, - -0.44586f, 0.16406f, -0.20653f, 0.21811f, -0.29917f, 0.28759f, -0.05597f, 0.03281f, - -0.32619f, -0.00965f, 0.31847f, -0.37252f, 0.18722f, -0.11002f, -0.22969f, -0.06369f, - 0.28373f, -0.17564f, 0.09458f, 0.02123f, 0.30689f, 0.41884f, 0.20653f, -0.03667f, - 0.32233f, 0.25671f, -0.45744f, -0.05597f, 0.46517f, -0.41498f, 0.00965f, 0.07142f, - -0.39568f, 0.36866f, -0.45744f, -0.31847f, 0.14476f, -0.22583f, -0.49219f, 0.37638f, - -0.19494f, -0.13318f, 0.39182f, -0.35322f, 0.29531f, -0.24127f, 0.21039f, -0.18722f, - 0.45358f, 0.31461f, -0.13318f, -0.01737f, -0.36094f, 0.12932f, -0.25671f, 0.43814f, - -0.16792f, 0.23355f, -0.22197f, 0.44972f, -0.42270f, 0.33392f, 0.42656f, 0.11774f, - -0.39568f, 0.36866f, -0.45744f, -0.31847f, 0.14476f, -0.22583f, -0.49219f, 0.37638f, - -0.19494f, -0.13318f, 0.39182f, -0.35322f, 0.29531f, -0.24127f, 0.21039f, -0.18722f, - -0.13318f, 0.19494f, -0.03667f, 0.44972f, 0.24513f, -0.15248f, 0.08300f, -0.33006f, - 0.00579f, 0.12546f, 0.19494f, 0.05983f, -0.15634f, 0.14476f, 0.36480f, -0.04053f, - -0.33006f, 0.25671f, -0.46903f, 0.37252f, 0.48833f, -0.09458f, -0.41112f, 0.19108f, - 0.08686f, -0.46903f, -0.07528f, 0.04053f, -0.26829f, -0.02895f, 0.22197f, -0.34164f, - -0.13318f, 0.19494f, -0.03667f, 0.44972f, 0.24513f, -0.15248f, 0.08300f, -0.33006f, - 0.00579f, 0.12546f, 0.19494f, 0.05983f, -0.15634f, 0.14476f, 0.36480f, -0.04053f, - 0.47289f, -0.21811f, 0.06756f, -0.38410f, -0.27987f, -0.06369f, 0.27987f, 0.43814f, - -0.25671f, -0.39182f, 0.49219f, -0.27601f, -0.07914f, -0.48061f, 0.42656f, -0.38410f, - 0.11002f, 0.03667f, -0.27215f, 0.15634f, 0.07528f, -0.22197f, 0.33006f, 0.38410f, - -0.34936f, 0.27987f, 0.15248f, 0.40340f, 0.09844f, -0.16406f, -0.46131f, 0.03281f, - 0.47289f, -0.21811f, 0.06756f, -0.38410f, -0.27987f, -0.06369f, 0.27987f, 0.43814f, - -0.25671f, -0.39182f, 0.49219f, -0.27601f, -0.07914f, -0.48061f, 0.42656f, -0.38410f, - -0.29531f, 0.31461f, -0.10616f, 0.39954f, 0.01351f, 0.33778f, -0.43814f, 0.17178f, - -0.08686f, 0.23741f, -0.44586f, 0.33778f, -0.00193f, -0.31461f, 0.23741f, -0.12932f, - -0.22583f, -0.06756f, 0.40340f, -0.16792f, -0.43428f, 0.01351f, -0.14476f, -0.04053f, - -0.29145f, 0.46517f, -0.13704f, -0.39182f, -0.32233f, 0.29531f, 0.38410f, 0.16020f, - -0.29531f, 0.31461f, -0.10616f, 0.39954f, 0.01351f, 0.33778f, -0.43814f, 0.17178f, - -0.08686f, 0.23741f, -0.44586f, 0.33778f, -0.00193f, -0.31461f, 0.23741f, -0.12932f, - -0.44200f, 0.26443f, 0.12546f, -0.42270f, 0.21425f, -0.19881f, -0.35708f, 0.04825f, - 0.36480f, -0.02895f, -0.21425f, 0.09072f, 0.41498f, 0.18336f, 0.04439f, 0.29917f, - 0.47675f, -0.40340f, 0.27601f, -0.31461f, 0.31075f, 0.17564f, 0.24899f, -0.45744f, - 0.05597f, -0.19494f, 0.00193f, 0.36094f, 0.24127f, -0.09844f, -0.24513f, -0.00965f, - -0.44200f, 0.26443f, 0.12546f, -0.42270f, 0.21425f, -0.19881f, -0.35708f, 0.04825f, - 0.36480f, -0.02895f, -0.21425f, 0.09072f, 0.41498f, 0.18336f, 0.04439f, 0.29917f, - -0.17564f, -0.05597f, -0.34550f, -0.24899f, 0.48061f, 0.15248f, -0.11388f, 0.45358f, - -0.16406f, -0.32233f, 0.31461f, -0.11774f, -0.36866f, -0.18722f, -0.25671f, -0.44200f, - 0.13318f, -0.02123f, 0.19881f, -0.10616f, 0.43042f, -0.36866f, -0.24899f, 0.41112f, - 0.11002f, 0.21425f, -0.25671f, -0.47675f, -0.04439f, 0.13704f, -0.37252f, 0.43814f, - -0.17564f, -0.05597f, -0.34550f, -0.24899f, 0.48061f, 0.15248f, -0.11388f, 0.45358f, - -0.16406f, -0.32233f, 0.31461f, -0.11774f, -0.36866f, -0.18722f, -0.25671f, -0.44200f, - 0.19108f, 0.03667f, 0.35708f, -0.14090f, 0.08300f, -0.02123f, -0.30303f, -0.48061f, - 0.11774f, 0.20267f, -0.43042f, 0.25285f, 0.14090f, -0.04439f, 0.38796f, 0.34550f, - -0.34164f, -0.19494f, 0.05983f, -0.48447f, 0.09844f, -0.00579f, -0.07914f, 0.33778f, - -0.41498f, -0.10230f, 0.30689f, 0.17178f, 0.48833f, -0.20267f, 0.07914f, 0.33392f, - 0.19108f, 0.03667f, 0.35708f, -0.14090f, 0.08300f, -0.02123f, -0.30303f, -0.48061f, - 0.11774f, 0.20267f, -0.43042f, 0.25285f, 0.14090f, -0.04439f, 0.38796f, 0.34550f, - -0.48833f, -0.30689f, 0.41498f, 0.22969f, -0.44586f, 0.32233f, 0.25285f, 0.39182f, - -0.23355f, 0.01737f, 0.42270f, -0.27987f, 0.46903f, -0.47289f, 0.02123f, -0.09072f, - 0.21811f, 0.44586f, -0.25285f, 0.36480f, -0.29145f, 0.47289f, -0.18722f, 0.14476f, - -0.31461f, 0.43814f, -0.36094f, 0.04439f, -0.29917f, -0.41884f, 0.25285f, -0.11774f, - -0.48833f, -0.30689f, 0.41498f, 0.22969f, -0.44586f, 0.32233f, 0.25285f, 0.39182f, - -0.23355f, 0.01737f, 0.42270f, -0.27987f, 0.46903f, -0.47289f, 0.02123f, -0.09072f, - 0.46131f, 0.11388f, -0.21039f, -0.07528f, -0.38024f, -0.26057f, 0.06369f, -0.05983f, - 0.29145f, -0.40340f, -0.09072f, 0.06756f, -0.16020f, 0.27601f, -0.31075f, 0.10616f, - -0.14090f, -0.43042f, 0.25671f, -0.05211f, -0.13318f, 0.23355f, -0.44972f, 0.02895f, - 0.26829f, -0.02895f, -0.17950f, 0.37252f, -0.13704f, 0.40726f, 0.01351f, -0.26443f, - 0.46131f, 0.11388f, -0.21039f, -0.07528f, -0.38024f, -0.26057f, 0.06369f, -0.05983f, - 0.29145f, -0.40340f, -0.09072f, 0.06756f, -0.16020f, 0.27601f, -0.31075f, 0.10616f, - -0.03281f, -0.40340f, 0.27987f, 0.17564f, 0.02509f, 0.44200f, -0.15248f, -0.34550f, - 0.14862f, -0.19881f, -0.01351f, 0.36866f, -0.38796f, 0.19494f, -0.22197f, 0.32619f, - -0.37638f, 0.00193f, 0.30689f, 0.12160f, -0.39182f, 0.16792f, -0.34550f, 0.39954f, - -0.23355f, 0.09072f, -0.43428f, 0.22969f, -0.06369f, 0.12546f, -0.35322f, 0.30689f, - -0.03281f, -0.40340f, 0.27987f, 0.17564f, 0.02509f, 0.44200f, -0.15248f, -0.34550f, - 0.14862f, -0.19881f, -0.01351f, 0.36866f, -0.38796f, 0.19494f, -0.22197f, 0.32619f, - -0.09844f, 0.06756f, 0.38410f, -0.33392f, -0.18336f, 0.35322f, 0.21039f, -0.42270f, - 0.48833f, 0.33006f, 0.21811f, -0.33392f, 0.12932f, -0.05211f, 0.39568f, 0.04825f, - 0.48061f, 0.17950f, -0.31847f, -0.21811f, 0.38024f, 0.05211f, 0.32233f, -0.06756f, - -0.12546f, 0.46131f, 0.16020f, -0.25285f, 0.29531f, -0.44972f, 0.17950f, -0.16406f, - -0.09844f, 0.06756f, 0.38410f, -0.33392f, -0.18336f, 0.35322f, 0.21039f, -0.42270f, - 0.48833f, 0.33006f, 0.21811f, -0.33392f, 0.12932f, -0.05211f, 0.39568f, 0.04825f, - 0.22583f, -0.46131f, -0.27601f, -0.00579f, 0.12932f, -0.47289f, -0.09844f, 0.10230f, - -0.28759f, -0.12160f, -0.49219f, -0.24127f, 0.44586f, -0.11388f, -0.45358f, -0.27215f, - -0.17178f, -0.07528f, -0.47675f, 0.43042f, -0.02509f, -0.27215f, -0.19108f, 0.19881f, - -0.49219f, -0.37252f, 0.33392f, -0.00193f, -0.33006f, -0.20267f, 0.48061f, 0.34164f, - 0.22583f, -0.46131f, -0.27601f, -0.00579f, 0.12932f, -0.47289f, -0.09844f, 0.10230f, - -0.28759f, -0.12160f, -0.49219f, -0.24127f, 0.44586f, -0.11388f, -0.45358f, -0.27215f, - -0.22969f, 0.42270f, -0.12160f, 0.31075f, 0.46903f, -0.22583f, 0.27215f, -0.02509f, - 0.03281f, 0.40340f, 0.25671f, 0.08686f, 0.00965f, 0.29145f, -0.41112f, 0.14090f, - 0.24513f, 0.34164f, 0.08686f, -0.14862f, 0.27601f, -0.42656f, 0.48447f, 0.09844f, - 0.26443f, -0.27987f, 0.05597f, -0.10230f, 0.43428f, 0.08686f, 0.02895f, -0.38024f, - -0.22969f, 0.42270f, -0.12160f, 0.31075f, 0.46903f, -0.22583f, 0.27215f, -0.02509f, - 0.03281f, 0.40340f, 0.25671f, 0.08686f, 0.00965f, 0.29145f, -0.41112f, 0.14090f, - 0.15634f, 0.09458f, -0.36480f, 0.18336f, -0.05211f, -0.40726f, 0.36866f, -0.33778f, - -0.19881f, 0.16020f, -0.37638f, -0.16020f, -0.29917f, 0.20267f, 0.41884f, -0.01737f, - -0.34936f, -0.24127f, 0.02509f, 0.20653f, -0.36480f, -0.08686f, 0.01737f, -0.33778f, - 0.41498f, -0.03667f, 0.37638f, -0.17178f, -0.47289f, 0.26829f, -0.28759f, -0.05597f, - 0.15634f, 0.09458f, -0.36480f, 0.18336f, -0.05211f, -0.40726f, 0.36866f, -0.33778f, - -0.19881f, 0.16020f, -0.37638f, -0.16020f, -0.29917f, 0.20267f, 0.41884f, -0.01737f, - 0.35708f, 0.00193f, 0.25285f, -0.15634f, -0.30303f, 0.06369f, 0.22197f, 0.45358f, - -0.43814f, 0.30303f, -0.04053f, 0.46517f, 0.35322f, -0.21039f, 0.06756f, -0.14090f, - 0.37638f, -0.43042f, 0.45744f, -0.29531f, 0.39568f, 0.14862f, 0.23741f, -0.13704f, - -0.21425f, 0.16406f, -0.40726f, 0.22583f, 0.13318f, 0.38796f, -0.12932f, -0.43428f, - 0.35708f, 0.00193f, 0.25285f, -0.15634f, -0.30303f, 0.06369f, 0.22197f, 0.45358f, - -0.43814f, 0.30303f, -0.04053f, 0.46517f, 0.35322f, -0.21039f, 0.06756f, -0.14090f, - -0.31461f, -0.20653f, 0.46131f, -0.45358f, 0.39568f, -0.24513f, -0.14090f, 0.11002f, - -0.08300f, -0.26829f, 0.05211f, -0.46517f, -0.09844f, -0.39568f, -0.32619f, -0.06369f, - 0.16792f, 0.28373f, 0.11388f, -0.04439f, -0.18336f, -0.44200f, 0.35322f, -0.26057f, - -0.46517f, 0.31075f, -0.07914f, -0.34164f, -0.24513f, -0.02123f, 0.19108f, 0.44200f, - -0.31461f, -0.20653f, 0.46131f, -0.45358f, 0.39568f, -0.24513f, -0.14090f, 0.11002f, - -0.08300f, -0.26829f, 0.05211f, -0.46517f, -0.09844f, -0.39568f, -0.32619f, -0.06369f, - 0.04825f, -0.07914f, -0.39954f, 0.12160f, 0.29145f, 0.00965f, -0.37638f, 0.32233f, - 0.20267f, -0.17564f, 0.39182f, 0.12160f, 0.18336f, 0.32619f, 0.26057f, 0.49219f, - -0.48447f, -0.20653f, -0.10616f, -0.38796f, 0.31847f, 0.07528f, -0.01737f, 0.44586f, - 0.11774f, 0.02509f, 0.47289f, 0.07142f, 0.33392f, -0.38410f, -0.17950f, 0.28373f, - 0.04825f, -0.07914f, -0.39954f, 0.12160f, 0.29145f, 0.00965f, -0.37638f, 0.32233f, - 0.20267f, -0.17564f, 0.39182f, 0.12160f, 0.18336f, 0.32619f, 0.26057f, 0.49219f - ]; - - private static bool ShouldFlipX(JxlExifOrientation orientation) => - orientation is JxlExifOrientation.FlipHorizontal or - JxlExifOrientation.Rotate180 or - JxlExifOrientation.Rotate270 or - JxlExifOrientation.AntiTranspose; - - private static bool ShouldFlipY(JxlExifOrientation orientation) => - orientation is JxlExifOrientation.FlipVertical or - JxlExifOrientation.Rotate180 or - JxlExifOrientation.Rotate90 or - JxlExifOrientation.AntiTranspose; - - private static bool ShouldTranspose(JxlExifOrientation orientation) => - orientation is JxlExifOrientation.Transpose or - JxlExifOrientation.Rotate90 or - JxlExifOrientation.Rotate270 or - JxlExifOrientation.AntiTranspose; - - public override RenderPipelineChannelMode GetChannelMode(int channel) - { - if (channel < this.numColors || (this.hasAlpha && channel == this.alphaC)) - { - return RenderPipelineChannelMode.Input; - } - - foreach (IJxlImageOutput ec in this.extraChannels) - { - if (channel == ec.ChannelIndex) - { - return RenderPipelineChannelMode.Input; - } - } - - return RenderPipelineChannelMode.Ignored; - } - - public override void Dispose() - { - base.Dispose(); - - foreach (IDisposable disposables in this.disposables) - { - disposables.Dispose(); - } - } - - public override void ProcessRow(Buffer2D> inputRows, Buffer2D> outputRows, int xExtraLeft, int xExtraRight, int width, int xPos, int yPos) - { - // HACK: to pass thread ID we use the mask in xExtraLeft, which shall be 0 anyway - int threadId = xExtraLeft & ~7; - - if (yPos >= this.height) - { - return; - } - - if (xPos >= this.width) - { - return; - } - - if (this.flipY) - { - yPos = this.height - 1 - yPos; - } - - int limit = Math.Min(width, this.width - xPos); - - for (int x0 = 0; x0 < limit; x0 += ChunkSize) - { - int xStart = xPos + x0; - int length = Math.Min(ChunkSize, limit - x0); - - Span> lineBuffers = [Memory.Empty, Memory.Empty, Memory.Empty, Memory.Empty]; - - for (int c = 0; c < this.numColors; c++) - { - lineBuffers[c] = this.GetInputRowMemory(inputRows, c, 0)[x0..]; - } - - if (this.hasAlpha) - { - lineBuffers[this.numColors] = this.GetInputRowMemory(inputRows, this.alphaC, 0)[x0..]; - } - else - { - lineBuffers[this.numColors] = JxlMemoryHelpers.MemoryFromList(this.opaqueAlpha); - } - - if (this.hasAlpha && this.wantAlpha && this.unpremultiplyAlpha) - { - this.UnpremultiplyAlpha(threadId, length, lineBuffers); - } - - this.OutputBuffers(this.main, threadId, yPos, ref xStart, length, lineBuffers[0], lineBuffers[1], lineBuffers[2], lineBuffers[3]); - - foreach (IJxlImageOutput extra in this.extraChannels) - { - lineBuffers[0] = this.GetInputRowMemory(inputRows, extra.ChannelIndex, 0)[x0..]; - this.OutputBuffers(extra, threadId, yPos, ref xStart, length, lineBuffers[0], lineBuffers[1], lineBuffers[2], lineBuffers[3]); - } - } - } - - public void PrepareForThreads(Configuration configuration, int numThreads) - { - this.tempOut.Resize(numThreads); - int allocSize = sizeof(float) * ChunkSize; - - for (int i = 0; i < this.tempOut.Count; i++) - { - IMemoryOwner owner = configuration.MemoryAllocator.Allocate(allocSize * this.main.PixelFormat.Channels); - this.disposables.Add(owner); - this.tempOut[i] = owner.Memory; - } - - if ((this.hasAlpha && this.wantAlpha && this.unpremultiplyAlpha) || this.flipX) - { - this.tempIn.Resize(numThreads * this.main.PixelFormat.Channels); - - for (int i = 0; i < this.tempIn.Count; i++) - { - IMemoryOwner owner = configuration.MemoryAllocator.Allocate(allocSize); - this.disposables.Add(owner); - this.tempIn[i] = owner.Memory; - } - } - } - - private unsafe void UnpremultiplyAlpha(int threadId, int len, Span> lineBuffers) - { - // Highly unsafe code! ⚠️ - float** tempIn = stackalloc float*[4]; - Vector one = Vector.One; - - for (int c = 0; c < this.main.PixelFormat.Channels; ++c) - { - // size_t tix = thread_id * main_.num_channels_ + c; - // temp_in[c] = temp_in_[tix].address(); - // memcpy(temp_in[c], line_buffers[c], sizeof(float) * len); - int tix = (threadId * this.main.PixelFormat.Channels) + c; - - tempIn[c] = (float*)Unsafe.AsPointer(ref MemoryMarshal.Cast(this.tempIn[tix].Span)[0]); - - lineBuffers[c].Span[..len] - .CopyTo(MemoryMarshal.CreateSpan(ref Unsafe.AsRef(tempIn[c]), len)); - } - - Vector smallAlpha = Vector.Create(SmallAlpha); - - for (int ix = 0; ix < len; ix += Vector.Count) - { - float* ptr = tempIn[this.numColors + ix]; - - // Using an aligned and unaligned branch - // REVIEW: does the branch outweigh alignment? we will have to benchmark this - // when the codec can build - if (JxlUnsafe.IsSimdAligned(ptr)) - { - Vector alpha = Vector.LoadAlignedNonTemporal(tempIn[this.numColors] + ix); - Vector mul = one / Vector.Max(smallAlpha, alpha); - - for (int c = 0; c < this.numColors; ++c) - { - float* currPtr = tempIn[c] + ix; - - if (JxlUnsafe.IsSimdAligned(currPtr)) - { - Vector val = Vector.LoadAlignedNonTemporal(currPtr); - Vector.StoreAlignedNonTemporal(val * mul, currPtr); - } - else - { - Vector val = Vector.Load(currPtr); - Vector.Store(val * mul, currPtr); - } - } - } - else - { - Vector alpha = Vector.Load(tempIn[this.numColors] + ix); - Vector mul = one / Vector.Max(smallAlpha, alpha); - - for (int c = 0; c < this.numColors; ++c) - { - float* currPtr = tempIn[c] + ix; - - if (JxlUnsafe.IsSimdAligned(currPtr)) - { - Vector val = Vector.LoadAlignedNonTemporal(currPtr); - Vector.StoreAlignedNonTemporal(val * mul, currPtr); - } - else - { - Vector val = Vector.Load(currPtr); - Vector.Store(val * mul, currPtr); - } - } - } - } - - for (int c = 0; c < this.main.PixelFormat.Channels; c++) - { - lineBuffers[c] = JxlMemoryHelpers.CastMemory(this.tempIn[c]); - } - } - - private static void StoreFloatRow(IJxlImageOutput output, Span input0, Span input1, Span input2, Span input3, int length, Span result) - { - Span bufferSpan = MemoryMarshal.Cast(output.Buffer.Span); - - if (output.PixelFormat.Channels == 1) - { - input0.CopyTo(result); - } - else if (output.PixelFormat.Channels == 2) - { - for (int i = 0; i <= length - Vector.Count; i += Vector.Count) - { - JxlSimdUtils.StoreInterleaved( - Vector.Create(input0[i..]), - Vector.Create(input1[i..]), - ref bufferSpan[2 * i]); - } - } - else if (output.PixelFormat.Channels == 3) - { - for (int i = 0; i <= length - Vector.Count; i += Vector.Count) - { - JxlSimdUtils.StoreInterleaved( - Vector.Create(input0[i..]), - Vector.Create(input1[i..]), - Vector.Create(input2[i..]), - ref bufferSpan[3 * i]); - } - } - else if (output.PixelFormat.Channels == 4) - { - for (int i = 0; i <= length - Vector.Count; i += Vector.Count) - { - JxlSimdUtils.StoreInterleaved( - Vector.Create(input0[i..]), - Vector.Create(input1[i..]), - Vector.Create(input2[i..]), - Vector.Create(input3[i..]), - ref bufferSpan[4 * i]); - } - } - } - - private static void StoreFloat16Row(IJxlImageOutput output, Span input0, Span input1, Span input2, Span input3, int length, Span result) - { - if (output.PixelFormat.Channels == 1) - { - for (int i = 0; i < length; i += Vector.Count) - { - Vector v0 = Vector.Create(input0[i..]); - JxlHalfUtils.ConvertSingleToHalf(v0).CopyTo(result[i..]); - } - } - else if (output.PixelFormat.Channels == 2) - { - for (int i = 0; i < length; i += Vector.Count) - { - Vector v0 = Vector.Create(input0[i..]); - Vector v1 = Vector.Create(input1[i..]); - JxlSimdUtils.StoreInterleaved( - JxlHalfUtils.ConvertSingleToHalf(v0), - JxlHalfUtils.ConvertSingleToHalf(v1), - ref result[i * 2]); - } - } - else if (output.PixelFormat.Channels == 3) - { - for (int i = 0; i < length; i += Vector.Count) - { - Vector v0 = Vector.Create(input0[i..]); - Vector v1 = Vector.Create(input1[i..]); - Vector v2 = Vector.Create(input2[i..]); - JxlSimdUtils.StoreInterleaved( - JxlHalfUtils.ConvertSingleToHalf(v0), - JxlHalfUtils.ConvertSingleToHalf(v1), - JxlHalfUtils.ConvertSingleToHalf(v2), - ref result[i * 3]); - } - } - else if (output.PixelFormat.Channels == 4) - { - for (int i = 0; i < length; i += Vector.Count) - { - Vector v0 = Vector.Create(input0[i..]); - Vector v1 = Vector.Create(input1[i..]); - Vector v2 = Vector.Create(input2[i..]); - Vector v3 = Vector.Create(input3[i..]); - JxlSimdUtils.StoreInterleaved( - JxlHalfUtils.ConvertSingleToHalf(v0), - JxlHalfUtils.ConvertSingleToHalf(v1), - JxlHalfUtils.ConvertSingleToHalf(v2), - JxlHalfUtils.ConvertSingleToHalf(v3), - ref result[i * 4]); - } - } - } - - private static void StoreUnsignedRow(IJxlImageOutput output, Span input0, Span input1, Span input2, Span input3, int length, Span result, int xStart, int yPos) - where TUnsigned : unmanaged, INumber - { - Vector mul = Vector.Create((1 << output.BitsPerSample) - 1.0f); - - if (output.PixelFormat.Channels == 1) - { - for (int i = 0; i < length; i += Vector.Count) - { - MakeUnsigned( - Vector.Create(input0[i..]), - xStart + i, - yPos, - mul, - 0) - .CopyTo(result[i..]); - } - } - else if (output.PixelFormat.Channels == 2) - { - for (int i = 0; i < length; i += Vector.Count) - { - JxlSimdUtils.StoreInterleaved( - MakeUnsigned(Vector.Create(input0[i..]), xStart + i, yPos, mul, 0), - MakeUnsigned(Vector.Create(input1[i..]), xStart + i, yPos, mul, 1), - ref result[i * 2]); - } - } - else if (output.PixelFormat.Channels == 3) - { - for (int i = 0; i < length; i += Vector.Count) - { - JxlSimdUtils.StoreInterleaved( - MakeUnsigned(Vector.Create(input0[i..]), xStart + i, yPos, mul, 0), - MakeUnsigned(Vector.Create(input1[i..]), xStart + i, yPos, mul, 1), - MakeUnsigned(Vector.Create(input2[i..]), xStart + i, yPos, mul, 2), - ref result[i * 3]); - } - } - else if (output.PixelFormat.Channels == 4) - { - for (int i = 0; i < length; i += Vector.Count) - { - JxlSimdUtils.StoreInterleaved( - MakeUnsigned(Vector.Create(input0[i..]), xStart + i, yPos, mul, 0), - MakeUnsigned(Vector.Create(input1[i..]), xStart + i, yPos, mul, 1), - MakeUnsigned(Vector.Create(input2[i..]), xStart + i, yPos, mul, 2), - MakeUnsigned(Vector.Create(input3[i..]), xStart + i, yPos, mul, 3), - ref result[i * 4]); - } - } - } - - private static Vector MakeUnsigned(Vector v, int x0, int y0, Vector mul, int c) - where T : unmanaged - { - v *= mul; - - if (typeof(T) == typeof(byte)) - { - int xOff = (x0 + (c * 23)) % 32; - int yOff = (y0 + (c * 13)) % 32; - int pos = (yOff * 48) + xOff; - - Vector dither = Vector.Create(DitheringPattern[pos..]); - v += dither; - } - - v = Vector.Min(Vector.Max(Vector.Zero, v), mul); - - Vector ni = Vector.ConvertToInt32(v); - Vector nu = Vector.AsVectorUInt32(ni); - - if (typeof(T) == typeof(uint)) - { - return (Vector)(object)nu; - } - - return nu.As(); - } - - private void FlipX(IJxlImageOutput output, int threadId, int length, ref int xStart, Span> lineBuffers) - { - InlineArray4> temporaryInput = default; - - for (int c = 0; c < output.PixelFormat.Channels; c++) - { - int tix = (threadId * this.main.PixelFormat.Channels) + c; - temporaryInput[c] = JxlMemoryHelpers.CastMemory(this.tempIn[tix]); - - lineBuffers[c][..length].Span.CopyTo(temporaryInput[c].Span); - } - - int last = length - 1; - int num = length / 2; - - for (int i = 0; i < num; i++) - { - for (int c = 0; c < output.PixelFormat.Channels; c++) - { - RuntimeUtility.Swap(ref temporaryInput[c].Span[i], ref temporaryInput[c].Span[last - i]); - } - } - - for (int c = 0; c < output.PixelFormat.Channels; c++) - { - lineBuffers[c] = temporaryInput[c]; - } - - xStart = this.width - xStart - length; - } - - private unsafe void WriteToOutput(IJxlImageOutput output, int yPos, int xStart, int length, Span result) - where T : unmanaged - { - if (this.transpose) - { - int stride = output.PixelFormat.Channels * sizeof(T); - int offset = (xStart * output.Stride) + (yPos * stride); - - for (int i = 0, j = 0; i < length; i++, j += output.PixelFormat.Channels) - { - int ix = offset + (i * output.Stride); - MemoryMarshal.Cast(result).Slice(j, stride).CopyTo(output.Buffer.Span[ix..]); - } - } - else - { - int stride = output.PixelFormat.Channels * sizeof(T); - int offset = (yPos * output.Stride) + (xStart * stride); - MemoryMarshal.Cast(result)[..(length * stride)].CopyTo(output.Buffer.Span[offset..]); - } - } - - private void OutputBuffers(IJxlImageOutput output, int threadId, int ypos, ref int xstart, int length, Memory input0, Memory input1, Memory input2, Memory input3) - { - if (this.flipX) - { - Span> buffers = [input0, input1, input2, input3]; - this.FlipX(output, threadId, length, ref xstart, buffers); - } - - if (output.PixelFormat.DataType == IO.JxlDataType.Byte) - { - Span temp = this.tempOut[threadId].Span; - StoreUnsignedRow(output, input0.Span, input1.Span, input2.Span, input3.Span, length, temp, xstart, ypos); - this.WriteToOutput(output, ypos, xstart, length, temp); - } - else if (output.PixelFormat.DataType is IO.JxlDataType.UInt16 or IO.JxlDataType.Single) - { - Span temp = MemoryMarshal.Cast(this.tempOut[threadId].Span); - - if (output.PixelFormat.DataType == IO.JxlDataType.UInt16) - { - StoreUnsignedRow(output, input0.Span, input1.Span, input2.Span, input3.Span, length, temp, xstart, ypos); - } - else - { - StoreFloat16Row(output, input0.Span, input1.Span, input2.Span, input3.Span, length, temp); - } - - if (output.SwapEndianness) - { - int outputLength = length * output.PixelFormat.Channels; - for (int j = 0; j < outputLength; j += Vector.Count) - { - Vector v = Vector.Create(temp[j..]); - Vector vswap = Vector.ShiftRightLogical(v, 8) | (v << 8); - vswap.CopyTo(temp[j..]); - } - } - - this.WriteToOutput(output, ypos, xstart, length, temp); - } - else if (output.PixelFormat.DataType == IO.JxlDataType.Single) - { - Span temp = MemoryMarshal.Cast(this.tempOut[threadId].Span); - StoreFloatRow(output, input0.Span, input1.Span, input2.Span, input3.Span, length, temp); - - if (output.SwapEndianness) - { - int outputLength = length * output.PixelFormat.Channels; - - for (int j = 0; j < outputLength; j++) - { - temp[j] = ReverseEndianness(temp[j]); - } - } - - this.WriteToOutput(output, ypos, xstart, length, temp); - } - } - - private static float ReverseEndianness(float value) - { - int bits = BitConverter.SingleToInt32Bits(value); - bits = BinaryPrimitives.ReverseEndianness(bits); - return BitConverter.Int32BitsToSingle(bits); - } -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage{TPixel}.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage{TPixel}.cs new file mode 100644 index 0000000000..dbfd59ef9f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage{TPixel}.cs @@ -0,0 +1,198 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +/// +/// Writes final planar RGB and alpha samples into the destination frame's pixel buffer. +/// +/// The destination pixel type. +/// +/// The pipeline supplies three RGB planes, including for grayscale images. Extra channels start at channel three. +/// Color conversion and frame blending precede this stage. The destination pixel operations own alpha conversion, clamping and packing. +/// +internal sealed class WriteToOutputStage : RenderPipelineStageBase + where TPixel : unmanaged, IPixel +{ + /// + /// The borrowed destination. The decoded image owns and disposes this buffer. + /// + private readonly Buffer2D destination; + + /// + /// The visible source width before orientation, excluding pipeline border storage. + /// + private readonly int sourceWidth; + + /// + /// The visible source height before orientation, excluding pipeline border storage. + /// + private readonly int sourceHeight; + + /// + /// The pipeline alpha channel, or minus one for opaque input. + /// + private readonly int alphaChannel; + + /// + /// Whether the final source RGB planes already include multiplication by alpha. + /// + private readonly bool associatedAlpha; + + /// + /// Whether source columns run backwards before any coordinate transposition. + /// + private readonly bool flipX; + + /// + /// Whether source rows run backwards before any coordinate transposition. + /// + private readonly bool flipY; + + /// + /// Whether source rows become destination columns. + /// + private readonly bool transpose; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration used by the destination pixel operations. + /// The frame's pixel buffer, allocated with the oriented image dimensions. + /// The pipeline alpha-channel index, or minus one when no alpha channel is present. + /// Whether the final source RGB planes are associated with the supplied alpha plane. + /// The encoded image orientation to apply to final pixel coordinates. + public WriteToOutputStage(Configuration configuration, Buffer2D destination, int alphaChannel, bool associatedAlpha, JxlExifOrientation orientation) + : base(configuration) + { + this.destination = destination; + this.alphaChannel = alphaChannel; + this.associatedAlpha = associatedAlpha; + + // JPEG XL first reverses the source axes, then transposes them. These combinations cover all eight header orientations. + // Retain source extents here because the destination has exchanged width and height for transposed orientations. + this.flipX = orientation is JxlExifOrientation.FlipHorizontal or JxlExifOrientation.Rotate180 + or JxlExifOrientation.Rotate270 or JxlExifOrientation.AntiTranspose; + + this.flipY = orientation is JxlExifOrientation.FlipVertical or JxlExifOrientation.Rotate180 + or JxlExifOrientation.Rotate90 or JxlExifOrientation.AntiTranspose; + + this.transpose = orientation is JxlExifOrientation.Transpose or JxlExifOrientation.Rotate90 + or JxlExifOrientation.Rotate270 or JxlExifOrientation.AntiTranspose; + + this.sourceWidth = this.transpose ? destination.Height : destination.Width; + this.sourceHeight = this.transpose ? destination.Width : destination.Height; + } + + /// + public override string Name => "WritePixels"; + + /// + public override RenderPipelineChannelMode GetChannelMode(int channel) + => channel < 3 || channel == this.alphaChannel ? RenderPipelineChannelMode.Input : RenderPipelineChannelMode.Ignored; + + /// + /// Writes one final source-row segment into its oriented destination coordinates. + /// + /// The planar source rows with the pipeline's fixed left prefix. + /// Unused by this terminal stage, which writes the destination pixel buffer. + /// The terminal stage's left border, which is zero. + /// The terminal stage's right border, which is zero. + /// The submitted sample count, including any right padding beyond the visible image. + /// The source column before orientation. + /// The source row before orientation. + public override void ProcessRow( + Buffer2D> inputRows, + Buffer2D> outputRows, + int xExtraLeft, + int xExtraRight, + int width, + int xPos, + int yPos) + { + // The pipeline can submit padded rows and columns outside the visible image. They do not belong in the destination. + // This terminal stage has no filter border; xExtraLeft and xExtraRight do not carry thread indices or output samples. + if (yPos >= this.sourceHeight || xPos >= this.sourceWidth) + { + return; + } + + int length = Math.Min(width, this.sourceWidth - xPos); + + // Borrow each plane once. GetInputRow removes the pipeline's fixed left prefix; sample zero is already the submitted xPos. + // Grayscale enters the render pipeline as three color planes. Do not infer source channels from the destination pixel format. + ReadOnlySpan red = this.GetInputRow(inputRows, 0, 0)[..length]; + ReadOnlySpan green = this.GetInputRow(inputRows, 1, 0)[..length]; + ReadOnlySpan blue = this.GetInputRow(inputRows, 2, 0)[..length]; + ReadOnlySpan alpha = this.alphaChannel < 0 ? default : this.GetInputRow(inputRows, this.alphaChannel, 0)[..length]; + int orientedY = this.flipY ? this.sourceHeight - 1 - yPos : yPos; + Configuration configuration = this.GetConfiguration(); + PixelOperations operations = PixelOperations.Instance; + + if (!this.transpose) + { + // A horizontal reversal still occupies one contiguous destination slice. Pack directly, then reverse the final pixels in place. + // Vertical reversal only changes the selected row. Neither case needs copied planes or an additional pixel buffer. + int destinationX = this.flipX ? this.sourceWidth - xPos - length : xPos; + Span pixels = this.destination.DangerousGetRowSpan(orientedY).Slice(destinationX, length); + + // Association describes the source planes, even when the destination does not store alpha. + // The shared operation owns interleaving, alpha conversion, destination bounds and final packing. + if (this.associatedAlpha) + { + operations.PackFromAssociatedScaledFloatPlanes(configuration, red, green, blue, alpha, pixels); + } + else + { + operations.PackFromUnassociatedScaledFloatPlanes(configuration, red, green, blue, alpha, pixels); + } + + if (this.flipX) + { + pixels.Reverse(); + } + + return; + } + + // A transposed source row is a destination column, so the final stores cannot use one contiguous span. + // Pack bounded blocks through the same bulk API, then scatter the completed pixels. Pixel arithmetic stays in the shared bulk conversion. + // The block matches the shared planar packer's 64-pixel block. It uses stack storage and needs no allocator or thread-specific state. + const int BlockSize = 64; + Span packed = stackalloc TPixel[BlockSize]; + int destinationRow = this.flipX ? this.sourceWidth - 1 - xPos : xPos; + int rowStep = this.flipX ? -1 : 1; + + for (int offset = 0; offset < length; offset += BlockSize) + { + int count = Math.Min(BlockSize, length - offset); + Span pixels = packed[..count]; + ReadOnlySpan redBlock = red.Slice(offset, count); + ReadOnlySpan greenBlock = green.Slice(offset, count); + ReadOnlySpan blueBlock = blue.Slice(offset, count); + ReadOnlySpan alphaBlock = alpha.IsEmpty ? default : alpha.Slice(offset, count); + + // Use the same conversion for column output. Do not add a separate scalar packer for the scattered stores. + if (this.associatedAlpha) + { + operations.PackFromAssociatedScaledFloatPlanes(configuration, redBlock, greenBlock, blueBlock, alphaBlock, pixels); + } + else + { + operations.PackFromUnassociatedScaledFloatPlanes(configuration, redBlock, greenBlock, blueBlock, alphaBlock, pixels); + } + + // After transposition, the possibly reversed source y is the fixed destination x. + // Each source x advances one destination row; the same mapping applies to a final short block. + for (int i = 0; i < count; i++) + { + this.destination.DangerousGetRowSpan(destinationRow)[orientedY] = pixels[i]; + destinationRow += rowStep; + } + } + } +} From c40dcf4194713865a19f0b53d545309b63215d56 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 13:19:31 +1000 Subject: [PATCH 248/328] Use span sorting for JPEG XL quantization medians --- .../Common/Extensions/SpanExtensions.cs | 72 ------- .../Processing/Quantization/JxlQuantizer.cs | 181 +++++++++--------- .../Common/SpanExtensionsTests.cs | 49 ----- 3 files changed, 93 insertions(+), 209 deletions(-) delete mode 100644 src/ImageSharp/Common/Extensions/SpanExtensions.cs delete mode 100644 tests/ImageSharp.Tests/Common/SpanExtensionsTests.cs diff --git a/src/ImageSharp/Common/Extensions/SpanExtensions.cs b/src/ImageSharp/Common/Extensions/SpanExtensions.cs deleted file mode 100644 index 0c25fa646e..0000000000 --- a/src/ImageSharp/Common/Extensions/SpanExtensions.cs +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp; - -/// -/// Provides partial selection for mutable spans. -/// -internal static class SpanExtensions -{ - /// - /// Partitions a nonempty span and returns the value at the requested position in sorted order. - /// - /// The comparable sample type. - /// The workspace to partition. Its sample values are retained but their order can change. - /// The zero-based position, within the supplied span. - /// The selected value. Samples before and after it are partitioned rather than fully sorted. - public static T NthElement(this Span span, int n) - where T : IComparable - { - int left = 0; - int right = span.Length - 1; - - while (left < right) - { - // Repeated values share one partition. Partition into values less than, equal to, - // and greater than the pivot. Selecting within the equal range finishes without rescanning each equal sample. - T pivot = span[left + ((right - left) / 2)]; - int lower = left; - int current = left; - int upper = right; - - while (current <= upper) - { - int comparison = span[current].CompareTo(pivot); - if (comparison < 0) - { - (span[lower], span[current]) = (span[current], span[lower]); - lower++; - current++; - } - else if (comparison > 0) - { - // The sample moved from the upper end has not been classified. Examine it before advancing current. - (span[current], span[upper]) = (span[upper], span[current]); - upper--; - } - else - { - current++; - } - } - - // Only the partition containing n can affect the result. The equal partition already occupies - // its final range of sorted positions, even though values on either side are not fully sorted. - if (n < lower) - { - right = lower - 1; - } - else if (n > upper) - { - left = upper + 1; - } - else - { - return span[n]; - } - } - - return span[left]; - } -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs index e5edc6fdf8..f06965294c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Buffers; +using System.Numerics.Tensors; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.Memory; @@ -68,7 +69,7 @@ internal sealed class JxlQuantizer /// /// Quantizer DC /// - private int quantDc; + private int dcQuantization; /// /// The zero bias. @@ -76,34 +77,34 @@ internal sealed class JxlQuantizer private readonly InlineArray3 zeroBias; /// - /// The dequant matrices. + /// The dequantization matrices used by this quantizer. /// - private readonly JxlDequantMatrices? dequant; + private readonly JxlDequantMatrices dequantizationMatrices; /// /// Initializes a new instance of the class using the default /// DC quantizer & scale factors. /// - /// The dequant. - public JxlQuantizer(JxlDequantMatrices dequant) - : this(dequant, DefaultQuant, GlobalScaleDenominator / DefaultQuant) + /// The matrices used to restore coefficient magnitudes. + public JxlQuantizer(JxlDequantMatrices dequantizationMatrices) + : this(dequantizationMatrices, DefaultQuant, GlobalScaleDenominator / DefaultQuant) { } /// /// Initializes a new instance of the class. /// - /// The dequant. - /// The DC quantizer. + /// The matrices used to restore coefficient magnitudes. + /// The DC quantizer. /// The scale factor. - public JxlQuantizer(JxlDequantMatrices dequant, int quantDc, int globalScale) + public JxlQuantizer(JxlDequantMatrices dequantizationMatrices, int dcQuantization, int globalScale) { - this.dequant = dequant; - this.quantDc = quantDc; + this.dequantizationMatrices = dequantizationMatrices; + this.dcQuantization = dcQuantization; this.globalScale = globalScale; this.RecomputeFromGlobalScale(); - this.InverseQuantDc = this.InverseGlobalScale / this.quantDc; + this.InverseQuantDc = this.InverseGlobalScale / this.dcQuantization; ZeroBiasDefault.CopyTo(this.zeroBias); } @@ -184,46 +185,46 @@ public void RecomputeFromGlobalScale() { this.Scale = this.globalScale * (1.0f / GlobalScaleDenominator); this.InverseGlobalScale = 1.0f * GlobalScaleDenominator / this.globalScale; - this.InverseQuantDc = this.InverseGlobalScale / this.quantDc; + this.InverseQuantDc = this.InverseGlobalScale / this.dcQuantization; - for (int c = 0; c < 3; c++) + for (int channelIndex = 0; channelIndex < 3; channelIndex++) { - this.mulDc[c] = this.GetDcStep(c); - this.inverseMulDc[c] = this.GetInverseDcStep(c); + this.mulDc[channelIndex] = this.GetDcStep(channelIndex); + this.inverseMulDc[channelIndex] = this.GetInverseDcStep(channelIndex); } } /// - /// Returns the dequant matrix. + /// Returns the dequantization matrix. /// /// The quant kind - /// The quantization index - /// The dequant matrix. - public ReadOnlySpan DequantMatrix(JxlAcStrategyType strategy, int c) - => this.dequant!.GetMatrix(strategy, c); + /// The XYB channel index. + /// The dequantization matrix. + public ReadOnlySpan DequantMatrix(JxlAcStrategyType strategy, int channelIndex) + => this.dequantizationMatrices.GetMatrix(strategy, channelIndex); /// - /// Returns the inverse dequant matrix. + /// Returns the inverse dequantization matrix. /// /// The quant kind - /// The quantization index - /// The inverse dequant matrix. - public ReadOnlySpan InverseDequantMatrix(JxlAcStrategyType strategy, int c) - => this.dequant!.GetInverseMatrix(strategy, c); + /// The XYB channel index. + /// The inverse dequantization matrix. + public ReadOnlySpan InverseDequantMatrix(JxlAcStrategyType strategy, int channelIndex) + => this.dequantizationMatrices.GetInverseMatrix(strategy, channelIndex); /// /// Returns the DC quantization step. /// - /// The quantization index + /// The XYB channel index. /// The DC quantization step - public float GetDcStep(int c) => this.InverseQuantDc * this.dequant!.GetDcQuant(c); + public float GetDcStep(int channelIndex) => this.InverseQuantDc * this.dequantizationMatrices.GetDcQuant(channelIndex); /// /// Returns the inverse DC quantization step. /// - /// The quantization index + /// The XYB channel index. /// The inverse DC quantization step - public float GetInverseDcStep(int c) => this.dequant!.GetInverseDcQuant(c) * (this.Scale * this.quantDc); + public float GetInverseDcStep(int channelIndex) => this.dequantizationMatrices.GetInverseDcQuant(channelIndex) * (this.Scale * this.dcQuantization); /// /// Creates JXL quantizer parameters with values reflecting those in this quantizer instance. @@ -231,7 +232,7 @@ public ReadOnlySpan InverseDequantMatrix(JxlAcStrategyType strategy, int /// The quantizer parameters. public JxlQuantizerParameters GetParameters() => new() { - QuantDc = (uint)this.quantDc, + QuantDc = (uint)this.dcQuantization, GlobalScale = (uint)this.globalScale }; @@ -242,14 +243,14 @@ public ReadOnlySpan InverseDequantMatrix(JxlAcStrategyType strategy, int /// Thrown when it is not possible to parse the quantizer parameters. public void Decode(JxlBitReader reader) { - JxlQuantizerParameters qp = new(); - if (!JxlBundle.Read(reader, qp)) + JxlQuantizerParameters parameters = new(); + if (!JxlBundle.Read(reader, parameters)) { throw new IOException("Could not read quantizer parameters"); } - this.globalScale = (int)qp.GlobalScale; - this.quantDc = (int)qp.QuantDc; + this.globalScale = (int)parameters.GlobalScale; + this.dcQuantization = (int)parameters.QuantDc; this.RecomputeFromGlobalScale(); } @@ -257,15 +258,18 @@ public void Decode(JxlBitReader reader) /// /// Recomputes the scaling factors and quant. /// - public void ComputeGlobalScaleAndQuant(float quantDc, float quantMedian, float quantMedianAbsd) + /// The requested DC quantization scale. + /// The median of the per-block quantization values. + /// The median absolute deviation from that median. + public void ComputeGlobalScaleAndQuant(float dcQuantization, float medianQuantization, float medianAbsoluteDeviation) { const int quantFieldTarget = 5; - float scale = GlobalScaleDenominator * (quantMedian - quantMedianAbsd) / quantFieldTarget; + float scale = GlobalScaleDenominator * (medianQuantization - medianAbsoluteDeviation) / quantFieldTarget; scale = Math.Clamp(scale, 1, 1 << 15); int newGlobalScale = (int)scale; - int scaledQuantDc = (int)(quantDc * GlobalScaleNumerator * 1.6f); + int scaledQuantDc = (int)(dcQuantization * GlobalScaleNumerator * 1.6f); if (newGlobalScale > scaledQuantDc) { @@ -281,92 +285,93 @@ public void ComputeGlobalScaleAndQuant(float quantDc, float quantMedian, float q this.RecomputeFromGlobalScale(); - float valueF = (quantDc * this.InverseGlobalScale) + 0.5f; + float valueF = (dcQuantization * this.InverseGlobalScale) + 0.5f; float clipValueF = MathF.Min(1 << 16, valueF); int newQuant = (int)clipValueF; - this.quantDc = newQuant; + this.dcQuantization = newQuant; this.RecomputeFromGlobalScale(); } /// - /// Quantizes the specified rectangular selection. + /// Converts floating-point quantization values to integers within a block region. /// - public void SetQuantFieldRect(JxlPlane qf, in Rectangle rect, JxlPlane rawQuantField) + /// The source per-block quantization values. + /// The region in blocks, applied to both fields. + /// The destination integer quantization values. + public void SetQuantFieldRect(JxlPlane quantizationField, in Rectangle blockBounds, JxlPlane quantizedField) { - for (int y = 0; y < rect.Height; y++) + for (int y = 0; y < blockBounds.Height; y++) { - ReadOnlySpan rowQf = qf.GetRow(rect, y); - Span rowQi = rawQuantField.GetRow(rect, y); + ReadOnlySpan quantizationRow = quantizationField.GetRow(blockBounds, y); + Span quantizedRow = quantizedField.GetRow(blockBounds, y); - for (int x = 0; x < rect.Width; x++) + for (int x = 0; x < blockBounds.Width; x++) { - int val = Clamp((rowQf[x] * this.InverseGlobalScale) + 0.5f); + int val = Clamp((quantizationRow[x] * this.InverseGlobalScale) + 0.5f); - rowQi[x] = val; + quantizedRow[x] = val; } } } /// - /// Set the quant field. + /// Sets the global quantization scale and optionally writes the integer quantization field. /// - public bool SetQuantField(Configuration configuration, float quantDc, JxlPlane qf, JxlPlane? rawQuantField) + /// The configuration that owns temporary sample storage. + /// The requested DC quantization scale. + /// The source per-block quantization values. + /// The destination integer field, or null to update only the global scale. + /// Whether the destination field has matching dimensions. + public bool SetQuantField(Configuration configuration, float dcQuantization, JxlPlane quantizationField, JxlPlane? quantizedField) { - IMemoryOwner data = configuration.MemoryAllocator.Allocate(qf.XSize * qf.YSize); - Span dataSpan = data.Memory.Span; - - for (int y = 0; y < qf.YSize; y++) + if (quantizedField is not null && quantizedField.GetRectangle() != quantizationField.GetRectangle()) { - ReadOnlySpan rowQf = qf.GetRow(y); - - for (int x = 0; x < qf.XSize; y++) - { - float quant = rowQf[x]; - - dataSpan[(qf.XSize * y) + x] = quant; - } + return false; } - dataSpan[dataSpan.Length / 2] = JxlSpanHelper.NthElement(dataSpan, dataSpan.Length / 2); - float quantMedian = dataSpan[dataSpan.Length / 2]; + // A valid frame supplies one value per DCT block. Copy logical samples only: GetRow also exposes + // the plane's padded stride and following rows. Sorting must not include that storage or mutate the source field. + int sampleCount = quantizationField.XSize * quantizationField.YSize; + using IMemoryOwner samplesOwner = configuration.MemoryAllocator.Allocate(sampleCount); + Span samples = samplesOwner.Memory.Span[..sampleCount]; - IMemoryOwner deviations = configuration.MemoryAllocator.Allocate(dataSpan.Length); - Span deviationsSpan = deviations.Memory.Span; - for (int i = 0; i < dataSpan.Length; i++) + for (int y = 0; y < quantizationField.YSize; y++) { - deviationsSpan[i] = MathF.Abs(dataSpan[i] - quantMedian); + quantizationField.GetRow(y)[..quantizationField.XSize].CopyTo(samples.Slice(y * quantizationField.XSize, quantizationField.XSize)); } - deviationsSpan[deviationsSpan.Length / 2] = JxlSpanHelper.NthElement(deviationsSpan, deviationsSpan.Length / 2); - float quantMedianAbsd = deviationsSpan[deviationsSpan.Length / 2]; + samples.Sort(); + float medianQuantization = samples[sampleCount / 2]; - this.ComputeGlobalScaleAndQuant(quantDc, quantMedian, quantMedianAbsd); + // Reuse the copied samples for absolute deviations. These have a different order from the original + // values, so sort them before reading their median. Shared tensor operations calculate the deviations. + TensorPrimitives.Subtract(samples, medianQuantization, samples); + TensorPrimitives.Abs(samples, samples); + samples.Sort(); + float medianAbsoluteDeviation = samples[sampleCount / 2]; - if (rawQuantField != null) - { - if (rawQuantField.GetRectangle() != qf.GetRectangle()) - { - data.Dispose(); - deviations.Dispose(); + this.ComputeGlobalScaleAndQuant(dcQuantization, medianQuantization, medianAbsoluteDeviation); - return false; - } - - this.SetQuantFieldRect(qf, qf.GetRectangle(), rawQuantField); + if (quantizedField is not null) + { + this.SetQuantFieldRect(quantizationField, quantizationField.GetRectangle(), quantizedField); } - data.Dispose(); - deviations.Dispose(); - return true; } - public void SetQuant(float quantDc, float quantAc, JxlPlane rawQuantField) + /// + /// Sets uniform AC quantization and the requested DC quantization scale. + /// + /// The requested DC quantization scale. + /// The AC quantization value for every block. + /// The destination integer quantization field. + public void SetQuant(float dcQuantization, float acQuantization, JxlPlane quantizedField) { - this.ComputeGlobalScaleAndQuant(quantDc, quantAc, 0); + this.ComputeGlobalScaleAndQuant(dcQuantization, acQuantization, 0); - int value = Clamp((quantAc * this.InverseGlobalScale) + 0.5f); - JxlImageOperations.FillImage(value, rawQuantField); + int value = Clamp((acQuantization * this.InverseGlobalScale) + 0.5f); + JxlImageOperations.FillImage(value, quantizedField); } } diff --git a/tests/ImageSharp.Tests/Common/SpanExtensionsTests.cs b/tests/ImageSharp.Tests/Common/SpanExtensionsTests.cs deleted file mode 100644 index babd7cdd14..0000000000 --- a/tests/ImageSharp.Tests/Common/SpanExtensionsTests.cs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Tests.Common; - -public class SpanExtensionsTests -{ - /// - /// Checks partial selection against full sorting for fixed numeric inputs. - /// - /// The fixed input values, including repeated and extreme finite values. - [Theory] - [InlineData(new float[] { 7 })] - [InlineData(new float[] { 1, 2, 3, 4, 5 })] - [InlineData(new float[] { 5, 4, 3, 2, 1 })] - [InlineData(new float[] { 2, 2, 2, 2, 2 })] - [InlineData(new float[] { 2, 1, 3, 2, 3, 1 })] - [InlineData(new float[] { -9, 6, 0, float.MinValue, float.MaxValue })] - public void NthElement_SelectsOrderStatisticAndPreservesValues(float[] input) - { - float[] expected = input.ToArray(); - expected.AsSpan().Sort(); - - for (int n = 0; n < input.Length; n++) - { - // Each selection starts with the same fixed input. Full sorting supplies an independent order statistic. - float[] actual = input.ToArray(); - float selected = actual.AsSpan().NthElement(n); - - Assert.Equal(expected[n], selected); - Assert.Equal(selected, actual[n]); - - // Selection partitions around n but does not sort either side. - for (int i = 0; i < n; i++) - { - Assert.True(actual[i] <= selected); - } - - for (int i = n + 1; i < actual.Length; i++) - { - Assert.True(actual[i] >= selected); - } - - // Sorting the partitioned values checks that selection did not lose, duplicate, or change a value. - actual.AsSpan().Sort(); - Assert.Equal(expected, actual); - } - } -} From e1009944b65e4d94cec8751ac6b2601a98ce9dc6 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 13:22:02 +1000 Subject: [PATCH 249/328] Implement JPEG XL field writing with nested extension budgets --- .../Formats/Jxl/Fields/JxlBitsCoder.cs | 49 ++++--- .../Formats/Jxl/Fields/JxlBundle.cs | 88 +++++++++--- .../Formats/Jxl/Fields/JxlCanEncodeVisitor.cs | 75 +++++++++- .../Formats/Jxl/Fields/JxlReadVisitor.cs | 22 +++ .../Formats/Jxl/Fields/JxlU32Coder.cs | 41 +++--- .../Formats/Jxl/Fields/JxlWriteVisitor.cs | 103 ++++++++++++++ .../Jxl/Processing/Encoder/JxlBitWriter.cs | 18 ++- .../Formats/Jxl/IO/FieldsTests.cs | 129 ++++++++++++++++++ 8 files changed, 459 insertions(+), 66 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlWriteVisitor.cs diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs index 787d16d088..0ebe31c3e2 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs @@ -7,44 +7,59 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Fields; /// -/// Raw bits coder +/// Reads and writes fixed-width unsigned fields. /// internal static class JxlBitsCoder { /// - /// Maximum number of encodeable bits. Since this coder encodes - /// bits raw, this happens to be whatever is passed to it. + /// Returns the wire length of a fixed-width field. /// - // Looks like that's what the function does (fields.cc:406): - // it returns whatever is passed to it. + /// The field width, from zero through 32. + /// The unchanged field width. public static int MaxEncodedBits(int bits) => bits; + /// + /// Checks whether an unsigned value fits the supplied field width. + /// + /// The field width, from zero through 32. + /// The value to encode. + /// The field width, including when the value cannot be encoded. + /// Whether the value fits the field. public static bool CanEncode(int bits, uint value, ref int encodedBits) { encodedBits = bits; - if (value >= (1 << bits)) - { - DebugGuard.IsTrue(false, "Value is too large"); - return false; - } - - return true; + // A UInt32 shift by 32 wraps its count to zero. Use UInt64 for the exclusive endpoint so a full-width field accepts every UInt32. + return value < (1UL << bits); } - public static void Write(int bits, uint value, JxlBitWriter writer) + /// + /// Writes a fixed-width field when the value fits without truncation. + /// + /// The field width, from zero through 32. + /// The value to encode. + /// The output bit writer. + /// Whether the value fits the field. An invalid value writes no bits. + public static bool Write(int bits, uint value, JxlBitWriter writer) { - if (value >= (1u << bits)) + int encodedBits = 0; + if (!CanEncode(bits, value, ref encodedBits)) { - throw new InvalidOperationException("Value is too large to encode in " + bits + " bits"); + return false; } writer.Write(bits, value); + return true; } - // NOTE: BitsCoder::Read (fields.cc:418) returns a uint32_t, - // suggesting the input bit size does not exceed 32 bits. + /// + /// Reads a fixed-width unsigned field. + /// + /// The field width, from zero through 32. + /// The input bit reader. + /// The unsigned field value. public static uint Read(uint bits, JxlBitReader reader) => reader.ReadBits32(bits); + /// public static uint Read(int bits, JxlBitReader reader) => reader.ReadBits32((uint)bits); } diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs index 60f4c295e5..6f6c821da1 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs @@ -85,21 +85,18 @@ public static bool Read(JxlBitReader reader, IJxlFields fields) return visitor.OK; } + /// + /// Measures the complete serialized length of a bundle without writing it. + /// + /// The fields to measure. + /// The current bundle's extension-payload length in bits. + /// The complete wire length, including nested bundles and extension-length fields. + /// Whether every field can be encoded. public static bool CanEncode(IJxlFields fields, ref int extensionBits, ref long totalBits) { JxlCanEncodeVisitor canEncodeVisitor = new(); - if (!canEncodeVisitor.Visit(fields)) - { - return false; - } - - if (!canEncodeVisitor.GetSizes(ref extensionBits, ref totalBits)) - { - return false; - } - - return true; + return canEncodeVisitor.Measure(fields, ref extensionBits, ref totalBits); } /// @@ -123,32 +120,48 @@ public static bool CanRead(JxlBitReader reader, IJxlFields fields) } } - public static bool Write(IJxlFields fields, JxlBitWriter writer, JxlLayerType layer, JxlAuxiliaryOutput auxOutput) + /// + /// Writes one validated bundle and charges its actual bit count to the supplied encoder layer. + /// + /// The fields to write. + /// The output bit writer. + /// The statistics layer for these fields. + /// The optional encoder statistics. + /// Whether all fields can be encoded and were written successfully. + public static bool Write(IJxlFields fields, JxlBitWriter writer, JxlLayerType layer, JxlAuxiliaryOutput? auxOutput) { int extensionBits = 0; long totalBits = 0; - if (!CanEncode(fields, ref extensionBits, ref totalBits)) + JxlCanEncodeVisitor sizeVisitor = new(); + if (!sizeVisitor.Measure(fields, ref extensionBits, ref totalBits)) { return false; } - return writer.WithMaxBits(totalBits, () => + return writer.WithMaxBits((ulong)totalBits, layer, auxOutput, () => { - JxlWriteVisitor visitor = new(extensionBits, writer); + JxlWriteVisitor visitor = new(extensionBits, writer, sizeVisitor); if (!visitor.Visit(fields)) { return false; } - return visitor.OK(); + return visitor.OK; }); } - public static bool WriteCodestreamHeaders(JxlCodecMetadata metadata, JxlBitWriter writer, JxlAuxiliaryOutput auxOut) + /// + /// Writes the codestream signature, image dimensions, image metadata and custom transform fields. + /// + /// The codestream metadata. + /// The output bit writer. + /// The optional encoder statistics. + /// Whether all header fields can be encoded and were written successfully. + public static bool WriteCodestreamHeaders(JxlCodecMetadata metadata, JxlBitWriter writer, JxlAuxiliaryOutput? auxOut) { // Marker/signature - if (!writer.WithMaxBits(16, () => + if (!writer.WithMaxBits(16, JxlLayerType.Header, auxOut, () => { writer.Write(8, 0xFF); writer.Write(8, JxlShared.CodestreamMarker); @@ -173,15 +186,46 @@ public static bool WriteCodestreamHeaders(JxlCodecMetadata metadata, JxlBitWrite return Write(metadata.CustomTransformData, writer, JxlLayerType.Header, auxOut); } - public static bool WriteFrameHeader(JxlFrameHeader frame, JxlBitWriter writer, JxlAuxiliaryOutput auxOut) + /// + /// Writes one frame header. + /// + /// The frame fields. + /// The output bit writer. + /// The optional encoder statistics. + /// Whether the frame fields can be encoded and were written successfully. + public static bool WriteFrameHeader(JxlFrameHeader frame, JxlBitWriter writer, JxlAuxiliaryOutput? auxOut) => Write(frame, writer, JxlLayerType.Header, auxOut); - public static bool WriteImageMetadata(JxlImageMetadata metadata, JxlBitWriter writer, JxlLayerType layer, JxlAuxiliaryOutput auxOut) + /// + /// Writes the image metadata fields. + /// + /// The metadata fields. + /// The output bit writer. + /// The encoder statistics layer. + /// The optional encoder statistics. + /// Whether the metadata fields can be encoded and were written successfully. + public static bool WriteImageMetadata(JxlImageMetadata metadata, JxlBitWriter writer, JxlLayerType layer, JxlAuxiliaryOutput? auxOut) => Write(metadata, writer, layer, auxOut); - public static bool WriteQuantizerParameters(JxlQuantizerParameters metadata, JxlBitWriter writer, JxlLayerType layer, JxlAuxiliaryOutput auxOut) + /// + /// Writes the quantizer parameters. + /// + /// The quantizer fields. + /// The output bit writer. + /// The encoder statistics layer. + /// The optional encoder statistics. + /// Whether the quantizer fields can be encoded and were written successfully. + public static bool WriteQuantizerParameters(JxlQuantizerParameters metadata, JxlBitWriter writer, JxlLayerType layer, JxlAuxiliaryOutput? auxOut) => Write(metadata, writer, layer, auxOut); - public static bool WriteSizeHeader(JxlSizeHeader header, JxlBitWriter writer, JxlLayerType layer, JxlAuxiliaryOutput auxOut) + /// + /// Writes the image dimensions. + /// + /// The image dimension fields. + /// The output bit writer. + /// The encoder statistics layer. + /// The optional encoder statistics. + /// Whether the dimensions can be encoded and were written successfully. + public static bool WriteSizeHeader(JxlSizeHeader header, JxlBitWriter writer, JxlLayerType layer, JxlAuxiliaryOutput? auxOut) => Write(header, writer, layer, auxOut); } diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlCanEncodeVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlCanEncodeVisitor.cs index 75f2355e21..dc123f12f7 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlCanEncodeVisitor.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlCanEncodeVisitor.cs @@ -5,14 +5,21 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Fields; +/// +/// Measures serialized fields and their extension payloads without writing bits. +/// internal sealed class JxlCanEncodeVisitor : JxlVisitor { private long encodedBits; private ulong extensions; private long posAfterExt; - public bool OK { get; set; } = true; + /// + /// Gets whether every field in the current measurement can be encoded. + /// + public bool OK { get; private set; } = true; + /// public override bool Bits(int bits, uint defaultValue, ref uint value) { int enc = 0; @@ -21,6 +28,7 @@ public override bool Bits(int bits, uint defaultValue, ref uint value) return true; } + /// public override bool U32(JxlU32Enc enc, uint defaultValue, ref uint value) { int encBits = 0; @@ -29,6 +37,7 @@ public override bool U32(JxlU32Enc enc, uint defaultValue, ref uint value) return true; } + /// public override bool U64(ulong defaultValue, ref ulong value) { int encBits = 0; @@ -37,6 +46,7 @@ public override bool U64(ulong defaultValue, ref ulong value) return true; } + /// public override bool F16(float defaultValue, ref float value) { int encBits = 0; @@ -45,6 +55,7 @@ public override bool F16(float defaultValue, ref float value) return true; } + /// public override bool AllDefault(IJxlFields fields, ref bool allDefault) { allDefault = JxlBundle.AllDefault(fields); @@ -56,6 +67,21 @@ public override bool AllDefault(IJxlFields fields, ref bool allDefault) return allDefault; } + /// + public override bool VisitNested(IJxlFields fields) + { + int extensionBits = 0; + long totalBits = 0; + bool valid = this.Measure(fields, ref extensionBits, ref totalBits); + this.OK &= valid; + + // A nested bundle's complete wire length includes its own extension-length fields. + // Charge that complete length to the parent, including when the child occurs inside a parent extension. + this.encodedBits += totalBits; + return valid; + } + + /// public override bool BeginExtensions(ref ulong extensions) { if (!base.BeginExtensions(ref extensions)) @@ -83,6 +109,46 @@ public override bool BeginExtensions(ref ulong extensions) return true; } + /// + /// Measures one bundle while preserving the active parent's counters and extension budget. + /// + /// The fields to measure. + /// The current bundle's extension-payload length, excluding its length fields. + /// The complete bundle length, including nested bundles and extension-length fields. + /// Whether all fields can be encoded and their extension payload fits the supported count. + public bool Measure(IJxlFields fields, ref int extensionBits, ref long totalBits) + { + long parentBits = this.encodedBits; + ulong parentExtensions = this.extensions; + long parentExtensionPosition = this.posAfterExt; + bool parentValid = this.OK; + + // Reuse this visitor for child measurements. Ordinary call locals retain the parent's state without allocating a scope object. + // JxlVisitor.Visit owns the corresponding begun/ended flags and depth limit. + this.encodedBits = 0; + this.extensions = 0; + this.posAfterExt = 0; + this.OK = true; + + try + { + return this.Visit(fields) && this.GetSizes(ref extensionBits, ref totalBits); + } + finally + { + this.encodedBits = parentBits; + this.extensions = parentExtensions; + this.posAfterExt = parentExtensionPosition; + this.OK = parentValid; + } + } + + /// + /// Includes the current bundle's extension-length fields in its measured wire size. + /// + /// The payload length to assign to the first selected extension. + /// The complete wire length for this bundle. + /// Whether the measured fields are valid and the payload length fits Int32. public bool GetSizes(ref int extensionBits, ref long totalBits) { if (!this.OK) @@ -95,12 +161,15 @@ public bool GetSizes(ref int extensionBits, ref long totalBits) if (this.posAfterExt != 0) { - if (this.encodedBits < this.posAfterExt) + long payloadBits = this.encodedBits - this.posAfterExt; + if (payloadBits < 0 || payloadBits > int.MaxValue) { return false; } - extensionBits = (int)this.encodedBits - (int)this.posAfterExt; + // Match the existing encoder's layout: the first selected extension carries the complete payload. + // Further selected extensions have zero-length payloads, but each still needs its own U64 length field. + extensionBits = (int)payloadBits; int encodedBits = 0; this.OK &= JxlU64Coder.CanEncode((ulong)extensionBits, ref encodedBits); totalBits += encodedBits; diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs index dcd56f9229..74c8bef388 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs @@ -60,6 +60,28 @@ public override bool F16(float defaultValue, ref float value) /// public override void SetDefault(IJxlFields fields) => JxlBundle.SetDefault(fields); + /// + public override bool VisitNested(IJxlFields fields) + { + ulong parentExtensionBits = this.totalExtensionBits; + long parentExtensionPosition = this.posAfterExtSize; + + // Each nested bundle carries its own extension lengths. A child with no extensions must not skip the parent's remaining payload. + // Preserve both counters in call locals while JxlVisitor maintains the nested begun/ended flags. + this.totalExtensionBits = 0; + this.posAfterExtSize = 0; + + try + { + return base.VisitNested(fields); + } + finally + { + this.totalExtensionBits = parentExtensionBits; + this.posAfterExtSize = parentExtensionPosition; + } + } + /// public override bool BeginExtensions(ref ulong extensions) { diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs index 3fa3a3ab79..b98f2f5dff 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.Processing; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; @@ -72,16 +71,16 @@ public static uint Read(in JxlU32Enc enc, JxlBitReader reader) /// /// Tries to find the best one of the four selectors based on the value. /// + /// The four wire alternatives. + /// The unsigned value to encode. + /// The selected alternative. + /// The selected field length, including the two selector bits. + /// Whether at least one alternative can represent the value. public static bool ChooseSelector(in JxlU32Enc enc, uint value, ref uint selector, ref int totalBits) { - uint bitsRequired = 32 - JxlMath.Num0BitsAboveMS1Bit(value); - - if (bitsRequired > 32) - { - return false; - } - selector = 0; + + // Alternatives use at most 32 payload bits plus two selector bits. The larger sentinel cannot be a valid selected length. totalBits = 64; for (int s = 0; s < 4; s++) @@ -103,7 +102,9 @@ public static bool ChooseSelector(in JxlU32Enc enc, uint value, ref uint selecto uint extraBits = dist.ExtraBits; uint offset = dist.Offset; - if (value < offset || value >= offset + (1u << (int)extraBits)) + // Check the payload distance after the offset, without wrapping either the endpoint or a 32-bit shift. + // Full-width alternatives can represent every UInt32 at or above their offset. + if (value < offset || (ulong)(value - offset) >= (1UL << (int)extraBits)) { continue; } @@ -115,14 +116,16 @@ public static bool ChooseSelector(in JxlU32Enc enc, uint value, ref uint selecto } } - if (totalBits == 64) - { - throw new InvalidOperationException("No matching selector"); - } - - return true; + return totalBits != 64; } + /// + /// Writes the shortest matching alternative and its offset-adjusted payload. + /// + /// The four wire alternatives. + /// The value to encode. + /// The output bit writer. + /// Whether an alternative can represent the value. An invalid value writes no bits. public static bool Write(in JxlU32Enc enc, uint value, JxlBitWriter writer) { uint selector = 0; @@ -141,12 +144,8 @@ public static bool Write(in JxlU32Enc enc, uint value, JxlBitWriter writer) { uint offset = d.Offset; - if (value < offset) - { - return false; - } - - writer.WriteBits(totalBits - 2, value - offset); + // ChooseSelector already checked this subtraction and selected the exact payload width. + writer.Write(totalBits - 2, value - offset); } return true; diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlWriteVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlWriteVisitor.cs new file mode 100644 index 0000000000..fb04c75dcb --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlWriteVisitor.cs @@ -0,0 +1,103 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; + +namespace SixLabors.ImageSharp.Formats.Jxl.Fields; + +/// +/// Writes measured JPEG XL fields through the existing primitive field coders. +/// +/// The current bundle's measured extension-payload length in bits. +/// The bit writer whose active budget covers the complete bundle. +/// The reusable visitor used to measure nested extension payloads. +internal sealed class JxlWriteVisitor(int extensionBits, JxlBitWriter writer, JxlCanEncodeVisitor sizeVisitor) : JxlVisitor +{ + /// + /// The active bundle's extension payload, excluding its extension-length fields. + /// + private int extensionPayloadBits = extensionBits; + + /// + /// Gets whether every primitive coder accepted its supplied field value. + /// + public bool OK { get; private set; } = true; + + /// + public override bool Bits(int bits, uint defaultValue, ref uint value) + { + this.OK &= JxlBitsCoder.Write(bits, value, writer); + return this.OK; + } + + /// + public override bool U32(JxlU32Enc enc, uint defaultValue, ref uint value) + { + this.OK &= JxlU32Coder.Write(enc, value, writer); + return this.OK; + } + + /// + public override bool U64(ulong defaultValue, ref ulong value) + { + this.OK &= JxlU64Coder.Write(value, writer); + return this.OK; + } + + /// + public override bool F16(float defaultValue, ref float value) + { + this.OK &= JxlF16Coder.Write(value, writer); + return this.OK; + } + + /// + public override bool VisitNested(IJxlFields fields) + { + int nestedExtensionBits = 0; + long nestedTotalBits = 0; + if (!sizeVisitor.Measure(fields, ref nestedExtensionBits, ref nestedTotalBits)) + { + this.OK = false; + return false; + } + + // The parent's extension length already includes this child's complete wire size. + // Keep writing under the same bit budget; only the extension-payload count changes for the nested scope. + int parentExtensionBits = this.extensionPayloadBits; + this.extensionPayloadBits = nestedExtensionBits; + + try + { + return base.VisitNested(fields); + } + finally + { + this.extensionPayloadBits = parentExtensionBits; + } + } + + /// + public override bool BeginExtensions(ref ulong extensions) + { + if (!base.BeginExtensions(ref extensions)) + { + return false; + } + + if (extensions == 0) + { + return true; + } + + // The encoder assigns all payload bits to the lowest selected extension. + // Other selected extensions still serialize a zero-length field. These are bit counts, with no byte-alignment padding. + this.OK &= JxlU64Coder.Write((ulong)this.extensionPayloadBits, writer); + for (ulong remaining = extensions & (extensions - 1); remaining != 0; remaining &= remaining - 1) + { + this.OK &= JxlU64Coder.Write(0, writer); + } + + return this.OK; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlBitWriter.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlBitWriter.cs index 32960f5a9c..d34e253565 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlBitWriter.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlBitWriter.cs @@ -76,11 +76,23 @@ public void ZeroPadToByte() /// /// The output bit budget for the operation. /// The encoder layer associated with the operation. - /// The encoder statistics supplied by the caller. + /// The optional encoder statistics supplied by the caller. /// The operation to run. /// The result of the operation. - public bool WithMaxBits(ulong maxBits, JxlLayerType layer, JxlAuxiliaryOutput auxOut, Func func) - => this.WithMaxBits(maxBits, func); + public bool WithMaxBits(ulong maxBits, JxlLayerType layer, JxlAuxiliaryOutput? auxOut, Func func) + { + long initialBits = this.BitsWritten; + bool result = this.WithMaxBits(maxBits, func); + + // Record the bits actually written, rather than the maximum reservation. + // Padding written by this operation is included; later writes belong to their own operation. + if (auxOut is not null) + { + auxOut.Layers[(int)layer].TotalBits += (int)(this.BitsWritten - initialBits); + } + + return result; + } public bool WithMaxBits(ulong maxBits, Func func) { diff --git a/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs index ee58350f81..37c2494036 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs @@ -412,6 +412,135 @@ public void ReadExtensions_RejectsUnrepresentableEnd() Assert.False(visitor.EndExtensions()); } + /// + /// Checks full-width raw fields against explicit least-significant-bit-first wire bytes. + /// + [Theory] + [InlineData(32, 0U, new byte[] { 0x00, 0x00, 0x00, 0x00 })] + [InlineData(32, uint.MaxValue, new byte[] { 0xFF, 0xFF, 0xFF, 0xFF })] + [InlineData(31, 0x7FFFFFFFU, new byte[] { 0xFF, 0xFF, 0xFF, 0x7F })] + public void WriteRawBits_PreservesFullWidthValues(int bits, uint value, byte[] expected) + { + using MemoryStream output = new(); + JxlBitWriter writer = new(output); + int encodedBits = 0; + + Assert.True(JxlBitsCoder.CanEncode(bits, value, ref encodedBits)); + Assert.Equal(bits, encodedBits); + Assert.True(JxlBitsCoder.Write(bits, value, writer)); + Assert.Equal(bits, writer.BitsWritten); + + // Flush completes the final byte without adding another field bit. A 31-bit value leaves its last high bit clear. + writer.Flush(); + Assert.Equal(expected, output.ToArray()); + } + + /// + /// Checks that invalid raw values fail before the writer emits a partial field. + /// + [Theory] + [InlineData(0, 1U)] + [InlineData(31, uint.MaxValue)] + public void WriteRawBits_RejectsValuesOutsideWidth(int bits, uint value) + { + using MemoryStream output = new(); + JxlBitWriter writer = new(output); + int encodedBits = 0; + + Assert.False(JxlBitsCoder.CanEncode(bits, value, ref encodedBits)); + Assert.False(JxlBitsCoder.Write(bits, value, writer)); + Assert.Equal(0, writer.BitsWritten); + Assert.Equal(0, output.Length); + } + + /// + /// Checks nested extension lengths, scope restoration, statistics and bit alignment against one fixed wire vector. + /// + [Fact] + public void WriteNestedExtensions_PreservesEachBundleBitBudget() + { + NestedExtensionBundle inner = new(new OldBundle(), 3); + NestedExtensionBundle outer = new(inner, 1); + int extensionBits = 0; + long totalBits = 0; + + // The default OldBundle uses 2 + 16 + 9 + 2 = 29 bits. + // Inner flags 3 need 6 flag bits and lengths of 10 + 2 bits: its complete length is 47 bits. + // Outer flags 1 need 6 flag bits and a 10-bit length: its payload is 47 bits and its complete length is 63 bits. + Assert.True(JxlBundle.CanEncode(outer, ref extensionBits, ref totalBits)); + Assert.Equal(47, extensionBits); + Assert.Equal(63, totalBits); + + using MemoryStream output = new(); + JxlBitWriter writer = new(output); + JxlAuxiliaryOutput statistics = new(); + + Assert.True(JxlBundle.Write(outer, writer, JxlLayerType.Header, statistics)); + Assert.Equal(63, writer.BitsWritten); + Assert.Equal(63, statistics.Layers[(int)JxlLayerType.Header].TotalBits); + + // The following one-bit marker completes byte eight. Extension payloads have no intervening byte padding. + writer.Write(1, 1); + writer.Flush(); + Assert.Equal(new byte[] { 0x81, 0x1E, 0x89, 0x0C, 0x00, 0xC8, 0x03, 0x80 }, output.ToArray()); + + output.Position = 0; + JxlBitReader reader = new(output); + OldBundle decodedFields = new() { OldSmall = 7, OldF = -7F, OldLarge = 777 }; + NestedExtensionBundle decodedInner = new(decodedFields, 0); + NestedExtensionBundle decodedOuter = new(decodedInner, 0); + + Assert.True(JxlBundle.Read(reader, decodedOuter)); + Assert.Equal(63, reader.TotalBitsConsumed); + Assert.Equal(1UL, decodedOuter.Extensions); + Assert.Equal(3UL, decodedInner.Extensions); + Assert.Equal(1U, decodedFields.OldSmall); + Assert.Equal(1.125F, decodedFields.OldF); + Assert.Equal(0U, decodedFields.OldLarge); + Assert.Equal(1U, reader.ReadBits32(1)); + } + + /// + /// Carries a nested bundle inside the first extension payload. + /// + private sealed class NestedExtensionBundle : IJxlFields + { + private readonly IJxlFields nested; + private ulong extensions; + + /// + /// Initializes a new instance of the class. + /// + /// The nested fields present when extension bit zero is set. + /// The initial extension flags, overwritten when reading. + public NestedExtensionBundle(IJxlFields nested, ulong extensions) + { + this.nested = nested; + this.extensions = extensions; + } + + /// + /// Gets the extension flags read or written for this bundle. + /// + public ulong Extensions => this.extensions; + + /// + public bool Visit(JxlVisitor visitor) + { + if (!visitor.BeginExtensions(ref this.extensions)) + { + return false; + } + + if (visitor.Conditional((this.extensions & 1) != 0) && !visitor.VisitNested(this.nested)) + { + return false; + } + + return visitor.EndExtensions(); + } + } + private sealed class OldBundle : IJxlFields { private uint oldSmall; From b543e37c726e00407cdacec679f6cd875a342b50 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 13:26:06 +1000 Subject: [PATCH 250/328] Validate JPEG XL patch fields before narrowing or allocation --- .../Decoder/Patch/JxlPatchDictionary.cs | 144 +++++++++--------- 1 file changed, 74 insertions(+), 70 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchDictionary.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchDictionary.cs index a18236609b..1c681daf70 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchDictionary.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchDictionary.cs @@ -30,10 +30,20 @@ internal sealed class JxlPatchDictionary public void SetShared(JxlReferenceFrame[] referenceFrames) => this.referenceFrames = referenceFrames; + /// + /// Clears the decoded dictionary and its row index before it is reused for another frame. + /// public void Clear() { + // Remove frame-specific entries while retaining list capacity for the next frame. this.positions.Clear(); - this.ComputePatchTree(); + this.referencePositions.Clear(); + this.blendings.Clear(); + this.blendingsStride = 0; + this.patchTree.Clear(); + this.numPatches.Clear(); + this.sortedPatchesY0.Clear(); + this.sortedPatchesY1.Clear(); } public int GetReferences() @@ -65,8 +75,10 @@ public void Decode( ulong numExtraChannels, ref bool usesExtraChannels) { - this.positions.Clear(); - this.blendingsStride = (int)(numExtraChannels + 1); + this.Clear(); + + // The image metadata header bounds extra channels to 4096. Each patch adds one color blending entry. + this.blendingsStride = (int)numExtraChannels + 1; List contextMap = []; JxlAnsCode code = new(); @@ -80,13 +92,23 @@ public void Decode( ReadOnlySpan contexts = CollectionsMarshal.AsSpan(contextMap); Span window = decoder.Lz77Window; - ulong numRefPatch = (uint)decoder.ReadHybridUInt((int)JxlPatchContext.NumRefPatch, br, contexts, window); + uint numRefPatch = (uint)decoder.ReadHybridUInt((int)JxlPatchContext.NumRefPatch, br, contexts, window); ulong numPixels = xsize * ysize; ulong maxRefPatches = 1024 + (numPixels / 4); ulong maxPatches = maxRefPatches * 4; ulong maxBlendingInfos = maxPatches * 4; + // Retain the format's pixel-based quotas, but reject counts that cannot fit the managed lists. + // Bounding patch count by the blending stride also keeps every capacity multiplication within that limit. + maxRefPatches = Math.Min(maxRefPatches, (ulong)Array.MaxLength); + maxBlendingInfos = Math.Min(maxBlendingInfos, (ulong)Array.MaxLength); + maxPatches = Math.Min(maxPatches, maxBlendingInfos / (uint)this.blendingsStride); + + // The patch tree stores rectangle ends as Int32 values. Both the frame and that storage must contain each complete patch. + ulong destinationWidth = Math.Min(xsize, (ulong)int.MaxValue); + ulong destinationHeight = Math.Min(ysize, (ulong)int.MaxValue); + if (numRefPatch > maxRefPatches) { throw new InvalidOperationException("Too many patches in dictionary"); @@ -95,120 +117,102 @@ public void Decode( ulong totalPatches = 0; ulong nextSize = 1; - for (ulong id = 0; id < numRefPatch; id++) + for (uint id = 0; id < numRefPatch; id++) { - JxlPatchReferencePosition refPos = new() - { - Ref = decoder.ReadHybridUInt((int)JxlPatchContext.ReferenceFrame, br, contexts, window) - }; - - if (refPos.Ref >= kMaxNumReferenceFrames || this.referenceFrames[(int)refPos.Ref].Frame.XSize == 0) + uint reference = (uint)decoder.ReadHybridUInt((int)JxlPatchContext.ReferenceFrame, br, contexts, window); + if (reference >= JxlShared.MaximumNumberOfReferenceFrames || this.referenceFrames[(int)reference].Frame.XSize == 0) { throw new InvalidOperationException("Invalid reference frame ID"); } - if (!this.referenceFrames[refPos.Ref].ImageBundleIsInXyb) + if (!this.referenceFrames[(int)reference].ImageBundleIsInXyb) { throw new InvalidOperationException("Patches cannot use frames saved post color transforms"); } - JxlImageBundle ib = this.referenceFrames[refPos.Ref].Frame; + JxlImageBundle ib = this.referenceFrames[(int)reference].Frame; + ulong referenceX = (uint)decoder.ReadHybridUInt((int)JxlPatchContext.PatchReferencePosition, br, contexts, window); + ulong referenceY = (uint)decoder.ReadHybridUInt((int)JxlPatchContext.PatchReferencePosition, br, contexts, window); - refPos.X0 = decoder.ReadHybridUInt((int)JxlPatchContext.PatchReferencePosition, br, contexts, window); - refPos.Y0 = decoder.ReadHybridUInt((int)JxlPatchContext.PatchReferencePosition, br, contexts, window); - refPos.XSize = decoder.ReadHybridUInt((int)JxlPatchContext.PatchSize, br, contexts, window) + 1; - refPos.YSize = decoder.ReadHybridUInt((int)JxlPatchContext.PatchSize, br, contexts, window) + 1; + // A size field stores size minus one. Widen before adding one, then validate the complete reference rectangle. + ulong width = (ulong)(uint)decoder.ReadHybridUInt((int)JxlPatchContext.PatchSize, br, contexts, window) + 1; + ulong height = (ulong)(uint)decoder.ReadHybridUInt((int)JxlPatchContext.PatchSize, br, contexts, window) + 1; - if (refPos.X0 + refPos.XSize > ib.XSize) + if (referenceX + width > (ulong)ib.XSize || referenceY + height > (ulong)ib.YSize) { throw new InvalidOperationException("Invalid position specified in reference frame"); } - if (refPos.Y0 + refPos.YSize > ib.YSize) + JxlPatchReferencePosition refPos = new() { - throw new InvalidOperationException("Invalid position specified in reference frame"); - } + Ref = (int)reference, + X0 = (int)referenceX, + Y0 = (int)referenceY, + XSize = (int)width, + YSize = (int)height + }; - ulong idCount = (uint)decoder.ReadHybridUInt((int)JxlPatchContext.PatchCount, br, contexts, window); + ulong idCount = (ulong)(uint)decoder.ReadHybridUInt((int)JxlPatchContext.PatchCount, br, contexts, window) + 1; - if (idCount > maxPatches) + // Check against the remaining quota before accumulating. The wire's UInt32 maximum plus one must not wrap. + if (idCount > maxPatches - totalPatches) { throw new InvalidOperationException("Too many patches in dictionary"); } - idCount++; - totalPatches += idCount; - if (totalPatches > maxPatches) - { - throw new InvalidOperationException("Too many patches in dictionary"); - } - if (nextSize < totalPatches) { - nextSize *= 2; - nextSize = Math.Min(nextSize, maxPatches); + nextSize = Math.Min(Math.Max(totalPatches, nextSize * 2), maxPatches); } - if (nextSize * (ulong)this.blendingsStride > maxBlendingInfos) - { - throw new InvalidOperationException("Too many patches in dictionary"); - } - - _ = this.blendings.EnsureCapacity((int)nextSize); - _ = this.blendings.EnsureCapacity((int)(nextSize * (ulong)this.blendingsStride)); + _ = this.positions.EnsureCapacity((int)nextSize); + _ = this.blendings.EnsureCapacity((int)(nextSize * (uint)this.blendingsStride)); bool chooseAlpha = numExtraChannels > 1; for (ulong i = 0; i < idCount; i++) { - JxlPatchPosition pos = new() - { - ReferencePositionIndex = this.referencePositions.Count - }; + long x; + long y; if (i == 0) { - pos.X = decoder.ReadHybridUInt((int)JxlPatchContext.PatchPosition, br, contexts, window); - pos.Y = decoder.ReadHybridUInt((int)JxlPatchContext.PatchPosition, br, contexts, window); + x = (uint)decoder.ReadHybridUInt((int)JxlPatchContext.PatchPosition, br, contexts, window); + y = (uint)decoder.ReadHybridUInt((int)JxlPatchContext.PatchPosition, br, contexts, window); } else { + // Delta codes cover the complete signed Int32 range. Add in Int64 so neither direction wraps before validation. long deltaX = JxlPackSigned.UnpackSigned((uint)decoder.ReadHybridUInt((int)JxlPatchContext.PatchOffset, br, contexts, window)); - - if (deltaX < 0 && (int)(-deltaX) > this.positions[^1].X) - { - throw new InvalidOperationException($"Invalid patch: negative x coordinate ({this.positions[^1].X}, delta {deltaX})"); - } - - pos.X = (int)(this.positions[^1].X + deltaX); - + x = this.positions[^1].X + deltaX; long deltaY = JxlPackSigned.UnpackSigned((uint)decoder.ReadHybridUInt((int)JxlPatchContext.PatchOffset, br, contexts, window)); - - if (deltaY < 0 && (int)(-deltaY) > this.positions[^1].Y) - { - throw new InvalidOperationException($"Invalid patch: negative y coordinate ({this.positions[^1].Y}, delta {deltaY})"); - } - - pos.Y = (int)(this.positions[^1].Y + deltaY); + y = this.positions[^1].Y + deltaY; } - if (pos.X + refPos.XSize > (int)xsize) + if (x < 0 || (ulong)x + width > destinationWidth) { - throw new InvalidOperationException($"Invalid patch x: {pos.X} + {refPos.XSize} > {xsize}"); + throw new InvalidOperationException($"Invalid patch x: {x} + {width} > {xsize}"); } - if (pos.Y + refPos.YSize > (int)ysize) + if (y < 0 || (ulong)y + height > destinationHeight) { - throw new InvalidOperationException($"Invalid patch y: {pos.Y} + {refPos.YSize} > {ysize}"); + throw new InvalidOperationException($"Invalid patch y: {y} + {height} > {ysize}"); } + JxlPatchPosition pos = new() + { + X = (int)x, + Y = (int)y, + ReferencePositionIndex = this.referencePositions.Count + }; + for (int j = 0; j < this.blendingsStride; j++) { uint blendMode = (uint)decoder.ReadHybridUInt((int)JxlPatchContext.PatchBlendMode, br, contexts, window); - if ((int)blendMode >= (int)JxlPatchContext.PatchBlendMode) + if (blendMode >= NumPatchBlendModes) { throw new InvalidOperationException($"Invalid patch blend mode: {blendMode}"); } @@ -230,12 +234,14 @@ public void Decode( if (UsesAlpha(info.Mode) && chooseAlpha) { - info.AlphaChannel = decoder.ReadHybridUInt((int)JxlPatchContext.PatchAlphaChannel, br, contexts, window); + uint alphaChannel = (uint)decoder.ReadHybridUInt((int)JxlPatchContext.PatchAlphaChannel, br, contexts, window); - if (info.AlphaChannel >= (int)numExtraChannels) + if (alphaChannel >= numExtraChannels) { - throw new InvalidOperationException($"Invalid alpha channel for blending: {info.AlphaChannel} out of {numExtraChannels}"); + throw new InvalidOperationException($"Invalid alpha channel for blending: {alphaChannel} out of {numExtraChannels}"); } + + info.AlphaChannel = (int)alphaChannel; } else { @@ -260,8 +266,6 @@ public void Decode( this.referencePositions.Add(refPos); } - this.positions.TrimExcess(); - if (!decoder.CheckAnsFinalState()) { throw new InvalidOperationException("ANS checksum failure."); From ec836a10edb37ab2c167e41e2ef2ef9d873bb0fb Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 13:30:23 +1000 Subject: [PATCH 251/328] Fix JPEG XL spline storage ownership and unsigned counts --- .../Processing/Splines/JxlQuantizedSpline.cs | 150 +++++++++++------- .../Jxl/Processing/Splines/JxlSplines.cs | 130 +++++++++------ 2 files changed, 179 insertions(+), 101 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs index def316e5f8..4dff7ca922 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs @@ -12,6 +12,9 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; internal sealed class JxlQuantizedSpline : IDisposable { + /// + /// Owns the reusable control-point allocation. ControlPoints exposes only its current logical range. + /// private IMemoryOwner? memoryOwner; public JxlQuantizedSpline() @@ -26,7 +29,10 @@ public JxlQuantizedSpline() private static ReadOnlySpan ChannelWeight => [0.0042f, 0.075f, 0.07f, .3333f]; - public Memory ControlPoints { get; set; } + /// + /// Gets the double-delta control points after the separately encoded starting point. + /// + public Memory ControlPoints { get; private set; } // NOTE: Do not use Configuration.MemoryAllocator.Allocate2D. This is // a 3x32 array, and renting memory introduces too much overhead for @@ -37,89 +43,127 @@ public JxlQuantizedSpline() public int[] SigmaDct { get; set; } = new int[32]; + /// + /// Releases control-point storage and clears the borrowed view. Repeated disposal has no effect. + /// public void Dispose() { this.memoryOwner?.Dispose(); + this.memoryOwner = null; this.ControlPoints = Memory.Empty; } - public void ReserveControlPoints(Configuration configuration, int n) + /// + /// Selects the control-point count and reuses the allocation when its capacity is sufficient. + /// + /// The configuration whose allocator owns the control points. + /// The validated logical count. The caller writes every selected entry before reading it. + private void ResizeControlPoints(Configuration configuration, int count) { - this.memoryOwner = configuration.MemoryAllocator.Allocate(n); + if (this.memoryOwner is null || this.memoryOwner.Memory.Length < count) + { + // Both callers replace all points, so growth needs no copy. Keep the current owner alive if allocation fails. + IMemoryOwner replacement = configuration.MemoryAllocator.Allocate(count); + this.memoryOwner?.Dispose(); + this.memoryOwner = replacement; + } - this.ControlPoints = this.memoryOwner.Memory; + // Allocators can supply additional capacity. It is not encoded data and must not become a control point. + this.ControlPoints = this.memoryOwner.Memory[..count]; } + /// + /// Quantizes a spline and stores its control-point double deltas in one owned allocation. + /// + /// The configuration whose allocator owns the encoded control points. + /// The spline with at least one point. Its starting point is encoded separately. + /// The spline coefficient quantization adjustment. + /// The Y contribution removed from the X channel. + /// The Y contribution removed from the B channel. + /// The quantized spline, which the caller owns and must dispose. public static JxlQuantizedSpline Create(Configuration configuration, JxlSpline original, int quantizationAdjustment, float yToX, float yToB) { JxlQuantizedSpline spline = new(); - Span controlPoints = original.ControlPoints.Span; - spline.ReserveControlPoints(configuration, controlPoints.Length - 1); + try + { + Span controlPoints = original.ControlPoints.Span; - PointF startingPoint = controlPoints[0]; - int previousX = (int)MathF.Round(startingPoint.X); - int previousY = (int)MathF.Round(startingPoint.Y); - int previousDx = 0; // D stands for delta - int previousDy = 0; // D stands for delta + spline.ResizeControlPoints(configuration, controlPoints.Length - 1); - int length = original.ControlPoints.Length; - IMemoryOwner newControls = configuration.MemoryAllocator.Allocate(length); - Span controlsSpan = newControls.Memory.Span; + PointF startingPoint = controlPoints[0]; - for (int i = 0; i < length; i++) - { - PointF controlPoint = controlPoints[i]; + // The native wire quantization rounds halfway coordinates away from zero. + int previousX = (int)MathF.Round(startingPoint.X, MidpointRounding.AwayFromZero); + int previousY = (int)MathF.Round(startingPoint.Y, MidpointRounding.AwayFromZero); + int previousDx = 0; // D stands for delta + int previousDy = 0; // D stands for delta - int newX = (int)MathF.Round(controlPoint.X); - int newY = (int)MathF.Round(controlPoint.Y); - int newDx = newX - previousX; // D stands for delta - int newDy = newY - previousY; // D stands for delta + // The starting point is stored separately. Encode exactly one double delta for each following point + // into the allocation owned by the returned spline. + Span controlsSpan = spline.ControlPoints.Span; - controlsSpan[i] = new(newDx - previousDx, newDy - previousDy); + for (int i = 0; i < controlsSpan.Length; i++) + { + PointF controlPoint = controlPoints[i + 1]; - previousDx = newDx; - previousDy = newDy; - previousX = newX; - previousY = newY; - } + int newX = (int)MathF.Round(controlPoint.X, MidpointRounding.AwayFromZero); + int newY = (int)MathF.Round(controlPoint.Y, MidpointRounding.AwayFromZero); + int newDx = newX - previousX; // D stands for delta + int newDy = newY - previousY; // D stands for delta - float quant = AdjustedQuant(quantizationAdjustment); - float inverseQuant = InverseAdjustedQuant(quantizationAdjustment); + controlsSpan[i] = new(newDx - previousDx, newDy - previousDy); - for (int j = 0; j < 3; j++) - { - int c = Loops[j]; + previousDx = newDx; + previousDy = newDy; + previousX = newX; + previousY = newY; + } - float factor = (c == 0) ? yToX : (c == 1) ? 0 : yToB; + float quant = AdjustedQuant(quantizationAdjustment); + float inverseQuant = InverseAdjustedQuant(quantizationAdjustment); + + // Quantize Y first. X and B remove the Y contribution restored from those quantized coefficients. + for (int j = 0; j < 3; j++) + { + int c = Loops[j]; + + float factor = (c == 0) ? yToX : (c == 1) ? 0 : yToB; + + // The DC coefficient uses the square-root-of-two scale; the other DCT coefficients use a unit scale. + for (int i = 0; i < 32; i++) + { + float dctFactor = (i == 0) ? JxlDctScales.Sqrt2 : 1.0f; + float inverseDctFactor = (i == 0) ? JxlDctScales.Sqrt05 : 1.0f; + float restoredY = spline.ColorDct[1][i] * inverseDctFactor * ChannelWeight[1] * inverseQuant; + float decorrelated = original.ColorDct[c][i] - (factor * restoredY); + spline.ColorDct[c][i] = ConvertToInteger(decorrelated * dctFactor * quant / ChannelWeight[c]); + } + } - // TODO: lower amount of branches by duplicating code - // for i=0 and adding a separate loop for i=1..31 for (int i = 0; i < 32; i++) { float dctFactor = (i == 0) ? JxlDctScales.Sqrt2 : 1.0f; - float inverseDctFactor = (i == 0) ? JxlDctScales.Sqrt05 : 1.0f; - float restoredY = spline.ColorDct[1][i] * inverseDctFactor * ChannelWeight[1] * inverseQuant; - float decorrelated = spline.ColorDct[c][i] - (factor * restoredY); - spline.ColorDct[c][i] = ConvertToInteger(decorrelated * dctFactor * quant / ChannelWeight[c]); + spline.SigmaDct[i] = ConvertToInteger(original.SigmaDct[i] * dctFactor * quant / ChannelWeight[3]); } - } - for (int i = 0; i < 32; i++) + return spline; + } + catch { - float dctFactor = (i == 0) ? JxlDctScales.Sqrt2 : 1.0f; - spline.SigmaDct[i] = ConvertToInteger(original.SigmaDct[i] * dctFactor * quant / ChannelWeight[1]); + // Creation transfers ownership only when the completed spline is returned. + spline.Dispose(); + throw; } - return spline; - + // Match the native coefficient bound and halfway rounding before storing the integer code. [MethodImpl(MethodImplOptions.AggressiveInlining)] static int ConvertToInteger(float value) { const float max = int.MaxValue - 127f; const float min = -max; - return (int)MathF.Round(Math.Clamp(value, min, max)); + return (int)MathF.Round(Math.Clamp(value, min, max), MidpointRounding.AwayFromZero); } } @@ -261,19 +305,17 @@ public bool TryDecode( int maxControlPoints, ref int totalControlPoints) { - int numControlPoints = decoder.ReadHybridUInt((int)JxlSplineEntropyContext.NumControlPoints, br, contextMap, window); - if (numControlPoints > maxControlPoints) - { - throw new InvalidOperationException("Too many control points"); - } - - totalControlPoints += numControlPoints; + uint encodedControlPoints = (uint)decoder.ReadHybridUInt((int)JxlSplineEntropyContext.NumControlPoints, br, contextMap, window); - if (totalControlPoints >= maxControlPoints) + // The caller already counted each spline's starting point. Check the remaining quota before adding + // this unsigned count or allocating storage. Reaching the quota exactly is valid. + if (encodedControlPoints > (uint)(maxControlPoints - totalControlPoints)) { throw new InvalidOperationException("Too many control points"); } + int numControlPoints = (int)encodedControlPoints; + totalControlPoints += numControlPoints; this.ResizeControlPoints(configuration, numControlPoints); const long deltaLimit = 1L << 30; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplines.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplines.cs index 4783581e52..ea63b5b755 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplines.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplines.cs @@ -12,7 +12,10 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; internal sealed class JxlSplines { - private List splinesStorage = []; + /// + /// Owns decoded splines. A view supplied through SetData remains owned by its caller. + /// + private readonly List splinesStorage = []; private readonly List startingPointsStorage = []; private JxlSplineDataView data = new(); private readonly List segments = []; @@ -27,19 +30,35 @@ internal sealed class JxlSplines public int QuantizationAdjustment { get; private set; } + /// + /// Releases decoded storage and borrows a caller-owned spline view. + /// + /// The spline data, which the caller keeps alive while this object uses it. public void SetData(JxlSplineDataView data) { this.Clear(); this.data = data; } + /// + /// Releases decoded splines and cached segment storage, then removes the active data view. + /// public void Clear() { this.QuantizationAdjustment = 0; + + // Only this list owns its entries. SetData can borrow a different list without transferring ownership. + foreach (JxlQuantizedSpline spline in this.splinesStorage) + { + spline.Dispose(); + } + this.splinesStorage.Clear(); this.startingPointsStorage.Clear(); this.data = new(); this.segments.Clear(); + this.segmentIndices.Dispose(); + this.segmentYStart.Dispose(); this.segmentIndices = EmptyMemoryOwner.Instance; this.segmentYStart = EmptyMemoryOwner.Instance; } @@ -52,67 +71,84 @@ public void Clear() /// The image pixel count used to limit spline data. public void Decode(Configuration configuration, JxlBitReader reader, int numPixels) { - List contextMap = []; + this.Clear(); - JxlAnsCode code = new(); - JxlAnsReader.DecodeHistograms(configuration, reader, (int)JxlSplineEntropyContext.NumSplineContexts, code, ref contextMap, false); + try + { + // Native spline limits bound both control-point storage and the amount of data per image pixel. + const int maximumControlPoints = 1 << 20; + const int maxControlPointsPerPixelRatio = 2; - // Spline fields use direct LZ77 distances; multiplier zero selects that native stream rule. - using JxlAnsSymbolReader decoder = JxlAnsSymbolReader.Create(configuration, code, reader, 0); + List contextMap = []; - // One stream covers starting positions and every spline. Keep its borrowed window and map stable across those loops. - Span window = decoder.Lz77Window; - ReadOnlySpan contexts = CollectionsMarshal.AsSpan(contextMap); - int numSplines = decoder.ReadHybridUInt((int)JxlSplineEntropyContext.NumSplines, reader, contexts, window); - int maxControlPoints = Math.Min(MaxNumControlPoints, numPixels / MaxNumControlPointsPerPixelRatio); + JxlAnsCode code = new(); + JxlAnsReader.DecodeHistograms(configuration, reader, (int)JxlSplineEntropyContext.NumSplineContexts, code, ref contextMap, false); - if (numSplines > maxControlPoints || numSplines + 1 > maxControlPoints) - { - throw new InvalidOperationException("Too many splines: " + numSplines); - } + // Spline fields use direct LZ77 distances; multiplier zero selects that native stream rule. + using JxlAnsSymbolReader decoder = JxlAnsSymbolReader.Create(configuration, code, reader, 0); - numSplines++; + // One stream covers starting positions and every spline. Keep its borrowed window and map stable across those loops. + Span window = decoder.Lz77Window; + ReadOnlySpan contexts = CollectionsMarshal.AsSpan(contextMap); + uint encodedSplines = (uint)decoder.ReadHybridUInt((int)JxlSplineEntropyContext.NumSplines, reader, contexts, window); + int maxControlPoints = Math.Min(maximumControlPoints, numPixels / maxControlPointsPerPixelRatio); - DecodeAllStartingPoints(this.startingPointsStorage, reader, decoder, contextMap, numSplines); + // The wire count excludes the first spline. Reject before adding one or narrowing the UInt32 value. + if (encodedSplines >= (uint)maxControlPoints) + { + throw new InvalidOperationException("Too many splines: " + encodedSplines); + } - int quantizationAdjustment = decoder.ReadHybridUInt((int)JxlSplineEntropyContext.QuantizationAdjustment, reader, contexts, window); - this.QuantizationAdjustment = JxlPackSigned.UnpackSigned((uint)quantizationAdjustment); - this.splinesStorage = new List(numSplines); + int numSplines = (int)encodedSplines + 1; - int numControlPoints = numSplines; + DecodeAllStartingPoints(this.startingPointsStorage, reader, decoder, contextMap, numSplines); - for (int i = 0; i < numSplines; ++i) - { - JxlQuantizedSpline spline = new(); - if (!spline.TryDecode( - configuration, - contexts, - decoder, - reader, - window, - maxControlPoints, - ref numControlPoints)) + int quantizationAdjustment = decoder.ReadHybridUInt((int)JxlSplineEntropyContext.QuantizationAdjustment, reader, contexts, window); + this.QuantizationAdjustment = JxlPackSigned.UnpackSigned((uint)quantizationAdjustment); + _ = this.splinesStorage.EnsureCapacity(numSplines); + + int numControlPoints = numSplines; + + // Attach each spline before decoding. Clear also releases the current allocation if that decode fails. + for (int i = 0; i < numSplines; ++i) { - throw new InvalidOperationException("Could not decode quantized spline. Index of the quantized spline: " + i); + JxlQuantizedSpline spline = new(); + this.splinesStorage.Add(spline); + + if (!spline.TryDecode( + configuration, + contexts, + decoder, + reader, + window, + maxControlPoints, + ref numControlPoints)) + { + throw new InvalidOperationException("Could not decode quantized spline. Index of the quantized spline: " + i); + } } - this.splinesStorage.Add(spline); - } - - if (!decoder.CheckAnsFinalState()) - { - throw new InvalidOperationException("Not ANS final state"); - } + if (!decoder.CheckAnsFinalState()) + { + throw new InvalidOperationException("Not ANS final state"); + } - this.data = new JxlSplineDataView() - { - Splines = this.splinesStorage, - StartingPoints = this.startingPointsStorage - }; + this.data = new JxlSplineDataView() + { + Splines = this.splinesStorage, + StartingPoints = this.startingPointsStorage + }; - if (!this.HasAny) + if (!this.HasAny) + { + throw new InvalidOperationException("Decoded splines but got none"); + } + } + catch { - throw new InvalidOperationException("Decoded splines but got none"); + // No decoded owner escapes a failed stream, including a later spline or the final ANS check. + this.Clear(); + throw; } } From 77b438f84d93b49d8759275a58f044eea3b0beaa Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 13:37:59 +1000 Subject: [PATCH 252/328] Restore measured partial selection for JPEG XL quantization --- .../Common/Extensions/SpanExtensions.cs | 72 +++++++++++++++++++ .../Processing/Quantization/JxlQuantizer.cs | 12 ++-- .../Common/SpanExtensionsTests.cs | 49 +++++++++++++ 3 files changed, 126 insertions(+), 7 deletions(-) create mode 100644 src/ImageSharp/Common/Extensions/SpanExtensions.cs create mode 100644 tests/ImageSharp.Tests/Common/SpanExtensionsTests.cs diff --git a/src/ImageSharp/Common/Extensions/SpanExtensions.cs b/src/ImageSharp/Common/Extensions/SpanExtensions.cs new file mode 100644 index 0000000000..0c25fa646e --- /dev/null +++ b/src/ImageSharp/Common/Extensions/SpanExtensions.cs @@ -0,0 +1,72 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp; + +/// +/// Provides partial selection for mutable spans. +/// +internal static class SpanExtensions +{ + /// + /// Partitions a nonempty span and returns the value at the requested position in sorted order. + /// + /// The comparable sample type. + /// The workspace to partition. Its sample values are retained but their order can change. + /// The zero-based position, within the supplied span. + /// The selected value. Samples before and after it are partitioned rather than fully sorted. + public static T NthElement(this Span span, int n) + where T : IComparable + { + int left = 0; + int right = span.Length - 1; + + while (left < right) + { + // Repeated values share one partition. Partition into values less than, equal to, + // and greater than the pivot. Selecting within the equal range finishes without rescanning each equal sample. + T pivot = span[left + ((right - left) / 2)]; + int lower = left; + int current = left; + int upper = right; + + while (current <= upper) + { + int comparison = span[current].CompareTo(pivot); + if (comparison < 0) + { + (span[lower], span[current]) = (span[current], span[lower]); + lower++; + current++; + } + else if (comparison > 0) + { + // The sample moved from the upper end has not been classified. Examine it before advancing current. + (span[current], span[upper]) = (span[upper], span[current]); + upper--; + } + else + { + current++; + } + } + + // Only the partition containing n can affect the result. The equal partition already occupies + // its final range of sorted positions, even though values on either side are not fully sorted. + if (n < lower) + { + right = lower - 1; + } + else if (n > upper) + { + left = upper + 1; + } + else + { + return span[n]; + } + } + + return span[left]; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs index f06965294c..d334e9921c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs @@ -331,7 +331,7 @@ public bool SetQuantField(Configuration configuration, float dcQuantization, Jxl } // A valid frame supplies one value per DCT block. Copy logical samples only: GetRow also exposes - // the plane's padded stride and following rows. Sorting must not include that storage or mutate the source field. + // the plane's padded stride and following rows. Selection must not include that storage or mutate the source field. int sampleCount = quantizationField.XSize * quantizationField.YSize; using IMemoryOwner samplesOwner = configuration.MemoryAllocator.Allocate(sampleCount); Span samples = samplesOwner.Memory.Span[..sampleCount]; @@ -341,15 +341,13 @@ public bool SetQuantField(Configuration configuration, float dcQuantization, Jxl quantizationField.GetRow(y)[..quantizationField.XSize].CopyTo(samples.Slice(y * quantizationField.XSize, quantizationField.XSize)); } - samples.Sort(); - float medianQuantization = samples[sampleCount / 2]; + float medianQuantization = samples.NthElement(sampleCount / 2); - // Reuse the copied samples for absolute deviations. These have a different order from the original - // values, so sort them before reading their median. Shared tensor operations calculate the deviations. + // Selection only permutes the copied values. Once their median is retained, the same allocation can + // hold absolute deviations. Shared tensor operations provide all SIMD widths and the scalar fallback. TensorPrimitives.Subtract(samples, medianQuantization, samples); TensorPrimitives.Abs(samples, samples); - samples.Sort(); - float medianAbsoluteDeviation = samples[sampleCount / 2]; + float medianAbsoluteDeviation = samples.NthElement(sampleCount / 2); this.ComputeGlobalScaleAndQuant(dcQuantization, medianQuantization, medianAbsoluteDeviation); diff --git a/tests/ImageSharp.Tests/Common/SpanExtensionsTests.cs b/tests/ImageSharp.Tests/Common/SpanExtensionsTests.cs new file mode 100644 index 0000000000..babd7cdd14 --- /dev/null +++ b/tests/ImageSharp.Tests/Common/SpanExtensionsTests.cs @@ -0,0 +1,49 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Tests.Common; + +public class SpanExtensionsTests +{ + /// + /// Checks partial selection against full sorting for fixed numeric inputs. + /// + /// The fixed input values, including repeated and extreme finite values. + [Theory] + [InlineData(new float[] { 7 })] + [InlineData(new float[] { 1, 2, 3, 4, 5 })] + [InlineData(new float[] { 5, 4, 3, 2, 1 })] + [InlineData(new float[] { 2, 2, 2, 2, 2 })] + [InlineData(new float[] { 2, 1, 3, 2, 3, 1 })] + [InlineData(new float[] { -9, 6, 0, float.MinValue, float.MaxValue })] + public void NthElement_SelectsOrderStatisticAndPreservesValues(float[] input) + { + float[] expected = input.ToArray(); + expected.AsSpan().Sort(); + + for (int n = 0; n < input.Length; n++) + { + // Each selection starts with the same fixed input. Full sorting supplies an independent order statistic. + float[] actual = input.ToArray(); + float selected = actual.AsSpan().NthElement(n); + + Assert.Equal(expected[n], selected); + Assert.Equal(selected, actual[n]); + + // Selection partitions around n but does not sort either side. + for (int i = 0; i < n; i++) + { + Assert.True(actual[i] <= selected); + } + + for (int i = n + 1; i < actual.Length; i++) + { + Assert.True(actual[i] >= selected); + } + + // Sorting the partitioned values checks that selection did not lose, duplicate, or change a value. + actual.AsSpan().Sort(); + Assert.Equal(expected, actual); + } + } +} From b4bfad263d1c78bc43bf0cd087c34409e67396d8 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 13:40:26 +1000 Subject: [PATCH 253/328] Correct JPEG XL container header ordering and size bounds --- .../Formats/Jxl/IO/Container/JxlBoxHeader.cs | 69 +++++++++---------- .../Processing/Decoder/JxlDecoderInternal.cs | 20 ++++-- .../Formats/Jxl/IO/FieldsTests.cs | 59 ++++++++++++++++ 3 files changed, 104 insertions(+), 44 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs b/src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs index 8e3600bb5e..055fc706c4 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs @@ -9,13 +9,13 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO.Container; /// -/// Header for JPEG XL container format. +/// Stores a JPEG XL container box's type, total size, and header width. /// [StructLayout(LayoutKind.Sequential, Size = 16)] internal struct JxlBoxHeader { /// - /// Box size in bytes. + /// Total box size in bytes, including its header. Zero extends the box through the end of the input. /// public ulong Size; @@ -25,19 +25,19 @@ internal struct JxlBoxHeader public uint Type; /// - /// True if the size field extends until the end of the file. + /// Whether the box extends through the end of the input. This box cannot be followed by another box. /// public bool SizeExtendsTillEnd; /// - /// True if the size is 64-bit. + /// Whether the header contains an explicit 64-bit size after its type, even when that size is zero. /// public bool ContainsLargeSize; /// /// Initializes a new instance of the struct. /// - /// The size of the box. + /// The total size, including the 8-byte or 16-byte header. Zero denotes an unbounded box. /// The type of the box. /// Does the box size extend till the end of the file? /// Is there a 64-bit size field? @@ -99,60 +99,53 @@ static string Fallback(uint typeCode) } /// - /// Parses the JPEG XL box header. + /// Reads the JPEG XL box's size and type, followed by its extended size when present. /// /// A stream to parse the header from. /// The box header. - /// Thrown when the header is invalid. + /// A nonzero box size is smaller than its header. + /// The input ends before the complete header. public static JxlBoxHeader ReadHeader(Stream stream) { + // Every header begins with size32 and type. The value one selects a following size64; + // it does not move the type behind that field. All integers use big-endian byte order. ulong size = BinaryUtils.ReadUInt32BigEndian(stream); - bool haveSize64 = false; - - if (size == 1) - { - // When the size value is equal to 1, a new 64-bit - // size field follows. - haveSize64 = true; - size = BinaryUtils.ReadUInt64BigEndian(stream); - } - - // Read the 4-byte type field. uint type = BinaryUtils.ReadUInt32BigEndian(stream); + bool haveSize64 = size == 1; if (haveSize64) { - // When the 64-bit largesize was read, - // the size cannot proceed till the end of the file. - if (size is 0 or 1) - { - throw new InvalidOperationException("Large size cannot have another large size or extend till the end of the file"); - } - - return new JxlBoxHeader(size, type, sizeExtendsTillEnd: false, containsLargeSize: true); + size = BinaryUtils.ReadUInt64BigEndian(stream); } - else + + // Match the native container parser: zero remains unbounded with either header width. + // Every bounded size includes at least the complete header, so later payload subtraction cannot underflow. + ulong headerSize = haveSize64 ? 16UL : 8UL; + if (size != 0 && size < headerSize) { - return new JxlBoxHeader(size, type, sizeExtendsTillEnd: size == 0, containsLargeSize: false); + throw new InvalidOperationException("JPEG XL box size is smaller than its header."); } + + return new JxlBoxHeader(size, type, size == 0, haveSize64); } /// - /// Writes the box header to the specified stream. + /// Writes the box header in big-endian order and retains an explicitly selected extended header. /// /// The stream to write the box header to. public readonly void WriteHeader(Stream writer) { - if (this.Size is > uint.MaxValue or 1) - { - BinaryUtils.WriteUInt32BigEndian(writer, 1); // Indicates a large size is present - BinaryUtils.WriteUInt64BigEndian(writer, this.Size); - } - else - { - BinaryUtils.WriteUInt32BigEndian(writer, (uint)this.Size); - } + ulong size = this.SizeExtendsTillEnd ? 0 : this.Size; + bool haveSize64 = this.ContainsLargeSize || size > uint.MaxValue; + // Preserve the header width of parsed boxes, including an extended zero-size final box. + // The type always occupies bytes 4 through 7; an optional extended size occupies bytes 8 through 15. + BinaryUtils.WriteUInt32BigEndian(writer, haveSize64 ? 1U : (uint)size); BinaryUtils.WriteUInt32BigEndian(writer, this.Type); + + if (haveSize64) + { + BinaryUtils.WriteUInt64BigEndian(writer, size); + } } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderInternal.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderInternal.cs index c04c53fb21..974d543c32 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderInternal.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderInternal.cs @@ -2073,20 +2073,28 @@ public void SetJpegBuffer(Memory data) } /// - /// Parses the start of a box. + /// Parses a box header and checks that its total size fits the decoder's stream offsets. /// - /// Input bytes to parse from. - /// Type of the box - /// Output box size. - /// Output header size. + /// The stream positioned at the box header. + /// The box's four-character type code. + /// The total size, or zero for a final box that extends through the input. + /// The 8-byte or 16-byte header size. /// /// Thrown when data is invalid. /// private static void ParseBoxHeader(Stream input, out int type, out long boxSize, out long headerSize) { JxlBoxHeader header = JxlBoxHeader.ReadHeader(input); + + // The wire uses UInt64, but this decoder tracks stream positions with Int64. Reject before narrowing + // so a bounded box cannot become a negative size or be mistaken for an unbounded final box. + if (header.Size > long.MaxValue) + { + throw new InvalidOperationException("JPEG XL box size exceeds the supported stream range."); + } + boxSize = (long)header.Size; - headerSize = (header.ContainsLargeSize ? 12 : 4) + 4; + headerSize = header.ContainsLargeSize ? 16 : 8; type = (int)header.Type; } diff --git a/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs index 37c2494036..d835c560b2 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs @@ -4,6 +4,7 @@ using System.Buffers.Binary; using SixLabors.ImageSharp.Formats.Jxl.Cms; using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.IO.Container; using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Processing; @@ -17,6 +18,64 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jxl.IO; public class FieldsTests { + /// + /// Checks box-header byte order, minimum sizes, and final unbounded boxes against fixed wire bytes. + /// + /// The complete fixed header, with its type before any extended size. + /// The total size stored by the wire header. + /// Whether the header has an explicit 64-bit size. + [Theory] + [InlineData(new byte[] { 0, 0, 0, 8, 0x6A, 0x78, 0x6C, 0x63 }, 8UL, false)] + [InlineData(new byte[] { 0, 0, 0, 0, 0x6A, 0x78, 0x6C, 0x63 }, 0UL, false)] + [InlineData(new byte[] { 0, 0, 0, 1, 0x6A, 0x78, 0x6C, 0x63, 0, 0, 0, 0, 0, 0, 0, 16 }, 16UL, true)] + [InlineData(new byte[] { 0, 0, 0, 1, 0x6A, 0x78, 0x6C, 0x63, 0, 0, 0, 1, 0, 0, 0, 16 }, 0x1_0000_0010UL, true)] + [InlineData(new byte[] { 0, 0, 0, 1, 0x6A, 0x78, 0x6C, 0x63, 0, 0, 0, 0, 0, 0, 0, 0 }, 0UL, true)] + [InlineData(new byte[] { 0, 0, 0, 1, 0x6A, 0x78, 0x6C, 0x63, 255, 255, 255, 255, 255, 255, 255, 255 }, ulong.MaxValue, true)] + public void BoxHeader_ReadAndWriteMatchesFixedWire(byte[] wire, ulong expectedSize, bool extended) + { + // The container stores size32, the four bytes "jxlc", then size64 when size32 is one. + using MemoryStream input = new(wire, false); + JxlBoxHeader header = JxlBoxHeader.ReadHeader(input); + + Assert.Equal(expectedSize, header.Size); + Assert.Equal(0x6A786C63U, header.Type); + Assert.Equal(extended, header.ContainsLargeSize); + Assert.Equal(expectedSize == 0, header.SizeExtendsTillEnd); + + // Compare with the independent wire bytes so paired reader/writer ordering errors cannot hide each other. + using MemoryStream output = new(); + header.WriteHeader(output); + + Assert.Equal(wire, output.ToArray()); + } + + /// + /// Checks that a bounded box cannot declare fewer bytes than its complete header. + /// + /// A fixed 8-byte or 16-byte header with an undersized nonzero length. + [Theory] + [InlineData(new byte[] { 0, 0, 0, 2, 0x6A, 0x78, 0x6C, 0x63 })] + [InlineData(new byte[] { 0, 0, 0, 7, 0x6A, 0x78, 0x6C, 0x63 })] + [InlineData(new byte[] { 0, 0, 0, 1, 0x6A, 0x78, 0x6C, 0x63, 0, 0, 0, 0, 0, 0, 0, 1 })] + [InlineData(new byte[] { 0, 0, 0, 1, 0x6A, 0x78, 0x6C, 0x63, 0, 0, 0, 0, 0, 0, 0, 15 })] + public void BoxHeader_RejectsSizeBelowHeader(byte[] wire) + { + using MemoryStream input = new(wire, false); + + Assert.Throws(() => JxlBoxHeader.ReadHeader(input)); + } + + /// + /// Checks that the complete extended size is required after the box type. + /// + [Fact] + public void BoxHeader_RejectsTruncatedExtendedSize() + { + using MemoryStream input = new([0, 0, 0, 1, 0x6A, 0x78, 0x6C, 0x63, 0, 0, 0, 0, 0, 0, 0]); + + Assert.Throws(() => JxlBoxHeader.ReadHeader(input)); + } + /// /// Checks that the one-bit default shortcut replaces earlier serialized color fields. /// From 129ef2fb3a658e9cf4ac3359e27525d4f2c35ded Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 13:57:43 +1000 Subject: [PATCH 254/328] Reset JPEG XL plane capacity when releasing its allocation --- .../Formats/Jxl/Memory/JxlPlane{T}.cs | 12 ++++++++++- .../Formats/Jxl/IO/ImageBundleTests.cs | 20 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs index dd21ce00ac..1188a79100 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs @@ -236,11 +236,21 @@ public void Swap(JxlPlane other) (this.PixelsPerRow, other.PixelsPerRow) = (other.PixelsPerRow, this.PixelsPerRow); } - /// + /// + /// Releases the allocation and resets the plane to an empty state. + /// public void Dispose() { this.owner?.Dispose(); this.owner = null; this.memory = default; + + // Capacity belongs to the allocation. A released plane cannot satisfy a later ShrinkTo request for sample storage. + this.origin = 0; + this.allocatedWidth = 0; + this.allocatedHeight = 0; + this.XSize = 0; + this.YSize = 0; + this.PixelsPerRow = 0; } } diff --git a/tests/ImageSharp.Tests/Formats/Jxl/IO/ImageBundleTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/IO/ImageBundleTests.cs index d62a339ac7..ddf5377113 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/IO/ImageBundleTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/IO/ImageBundleTests.cs @@ -8,6 +8,7 @@ using SixLabors.ImageSharp.Formats.Jxl.IO; using SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; @@ -18,6 +19,25 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jxl.IO; public class ImageBundleTests { + /// + /// Checks that released plane storage cannot be reused through its previous dimensions. + /// + [Fact] + public void DisposedPlane_CannotReuseReleasedCapacity() + { + using JxlPlane plane = new(TestEnvironment.Configuration, 16, 2); + + // Frame initialization uses ShrinkTo before allocating replacement storage. + // After disposal, that check must reject the dimensions of the released allocation. + plane.Dispose(); + + Assert.False(plane.ShrinkTo(16, 2)); + Assert.Equal(0, plane.XSize); + Assert.Equal(0, plane.YSize); + Assert.Equal(0, plane.PixelsPerRow); + Assert.True(plane.GetRowMemory(0).IsEmpty); + } + /// /// Checks that a sample conversion cannot replace the original ICC bytes used for JPEG reconstruction. /// From f377357841525609cb66a1095e6c830df65060e4 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 14:06:16 +1000 Subject: [PATCH 255/328] Complete JPEG XL spline reconstruction and typed row indexing --- .../Formats/Jxl/Processing/Splines/Dct32.cs | 15 - .../Processing/Splines/JxlQuantizedSpline.cs | 192 +++++++----- .../Jxl/Processing/Splines/JxlSpline.cs | 57 +++- .../Splines/JxlSplineSegmentSpan.cs | 15 +- .../Jxl/Processing/Splines/JxlSplineUtils.cs | 199 ++++++++---- .../Jxl/Processing/Splines/JxlSplines.cs | 296 +++++++++++------- .../Formats/Jxl/Processing/SplineTests.cs | 106 +++++++ 7 files changed, 583 insertions(+), 297 deletions(-) delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Splines/Dct32.cs create mode 100644 tests/ImageSharp.Tests/Formats/Jxl/Processing/SplineTests.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/Dct32.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/Dct32.cs deleted file mode 100644 index e7509e8c05..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Splines/Dct32.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.CompilerServices; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; - -/// -/// Storage for 32 DCT coefficients (floating-point). -/// -[InlineArray(32)] -internal struct Dct32 -{ - private float first; -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs index 4dff7ca922..4241b267fe 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs @@ -167,122 +167,140 @@ static int ConvertToInteger(float value) } } + /// + /// Reconstructs control points and coefficients while limiting the estimated spline rendering work. + /// + /// The configuration that owns the reconstructed control points. + /// The separately decoded first point. + /// The encoded coefficient quantization adjustment. + /// The Y contribution restored to X. + /// The Y contribution restored to B. + /// The frame area in pixels, used only for the rendering-work limit. + /// The accumulated work estimate for all splines in this frame. + /// The caller-owned spline whose existing point storage can be reused. + /// True after reconstruction. Invalid geometry or excessive work throws an image-content exception. public bool Dequantize( Configuration configuration, PointF startingPoint, int quantizationAdjustment, float yToX, float yToB, - long imageSize, - ref long totalEstimatedAreaReached, + ulong imagePixelCount, + ref ulong totalEstimatedAreaReached, JxlSpline result) { - long areaLimit = Math.Min(1024 * imageSize * (1L << 32), 1L << 42); - result.ClearControlPoints(); - result.ReserveControlPoints(configuration, this.ControlPoints.Length + 1); + const ulong initialAreaAllowance = 1UL << 32; + const ulong maximumAreaAllowance = 1UL << 42; + const ulong areaPerImagePixel = 1024; - float px = MathF.Round(startingPoint.X); - float py = MathF.Round(startingPoint.Y); + // Native reconstruction permits 1024 * image pixels + 2^32 units, capped at 2^42. + // Cap the pixel count before multiplication so even a large frame cannot wrap the work limit. + ulong cappedPixelCount = Math.Min(imagePixelCount, (maximumAreaAllowance - initialAreaAllowance) / areaPerImagePixel); + ulong areaLimit = (areaPerImagePixel * cappedPixelCount) + initialAreaAllowance; - if (!this.ValidateSplinePointPos(px, py)) + try { - throw new InvalidOperationException("Spline points out of range"); - } - - int currentX = (int)px; - int currentY = (int)py; - - Span controlPoints = result.ControlPoints.Span; - Span thisControlPoints = this.ControlPoints.Span; - - controlPoints[0] = new(currentX, currentY); + checked + { + // Round only at the spline's integer geometry boundary. Checked conversion rejects nonfinite starting points. + long currentX = (long)MathF.Round(startingPoint.X, MidpointRounding.AwayFromZero); + long currentY = (long)MathF.Round(startingPoint.Y, MidpointRounding.AwayFromZero); + JxlSpline.ValidatePosition(currentX, currentY); - int currentDx = 0; // D stands for delta - int currentDy = 0; // D stands for delta + result.ReserveControlPoints(configuration, this.ControlPoints.Length + 1); + Span controlPoints = result.ControlPoints.Span; + ReadOnlySpan deltas = this.ControlPoints.Span; + controlPoints[0] = new(currentX, currentY); - long manhattanDistance = 0; + long deltaX = 0; + long deltaY = 0; + ulong manhattanDistance = 0; - int length = this.ControlPoints.Length; + for (int i = 0; i < deltas.Length; i++) + { + // The stream stores changes to the previous delta. Accumulate in Int64 before applying the geometry bound. + deltaX += deltas[i].First; + deltaY += deltas[i].Second; + JxlSpline.ValidatePosition(deltaX, deltaY); + manhattanDistance += (ulong)(Math.Abs(deltaX) + Math.Abs(deltaY)); + + if (manhattanDistance > areaLimit) + { + throw new InvalidImageContentException("JPEG XL spline length exceeds the rendering limit."); + } + + currentX += deltaX; + currentY += deltaY; + JxlSpline.ValidatePosition(currentX, currentY); + controlPoints[i + 1] = new(currentX, currentY); + } - for (int i = 0; i < length; i++) - { - JxlControlPoint point = thisControlPoints[i]; - currentDx += point.First; - currentDy += point.Second; - manhattanDistance = Math.Abs(currentDx) + Math.Abs(currentDy); + float inverseQuant = InverseAdjustedQuant(quantizationAdjustment); + Span> colorDct = result.ColorDct; - if (manhattanDistance > areaLimit) - { - throw new InvalidOperationException("Manhattan distance is too large"); - } + for (int channel = 0; channel < 3; channel++) + { + for (int i = 0; i < 32; i++) + { + float inverseDctFactor = i == 0 ? JxlDctScales.Sqrt05 : 1F; + colorDct[channel][i] = this.ColorDct[channel][i] * inverseDctFactor * ChannelWeight[channel] * inverseQuant; + } + } - if (!ValidateSplinePointPos(currentDx, currentDy)) - { - throw new InvalidOperationException("Delta points out of range"); - } + for (int i = 0; i < 32; i++) + { + colorDct[0][i] += yToX * colorDct[1][i]; + colorDct[2][i] += yToB * colorDct[1][i]; + } - currentX += currentDx; - currentY += currentDy; + // These unsigned sums estimate rendering work, not stored color values. Start every channel at zero. + // Checked arithmetic rejects extreme decoded coefficients before a wrapped sum can appear inexpensive. + InlineArray3 colorStorage = default; + Span color = colorStorage; - if (!ValidateSplinePointPos(currentX, currentY)) - { - throw new InvalidOperationException("Current points out of range"); - } + for (int channel = 0; channel < 3; channel++) + { + for (int i = 0; i < 32; i++) + { + color[channel] += (ulong)MathF.Ceiling(inverseQuant * MathF.Abs(this.ColorDct[channel][i])); + } + } - controlPoints[i + 1] = new(currentX, currentY); - } + color[0] += (ulong)MathF.Ceiling(MathF.Abs(yToX)) * color[1]; + color[2] += (ulong)MathF.Ceiling(MathF.Abs(yToB)) * color[1]; - float inverseQuant = InverseAdjustedQuant(quantizationAdjustment); + ulong maximumColor = Math.Max(color[1], Math.Max(color[0], color[2])); + ulong logColor = Math.Max(1UL, JxlMath.CeilLog2Nonzero(1UL + maximumColor)); - for (int c = 0; c < 3; c++) - { - for (int i = 0; i < 32; i++) - { - float inverseDctFactor = (i == 0) ? JxlDctScales.Sqrt05 : 1.0f; - result.ColorDct[c][i] = this.ColorDct[c][i] * inverseDctFactor * ChannelWeight[c] * inverseQuant; - } - } + // Width squared, color magnitude and path length jointly estimate the rendering area. + // Both divisors belong inside the square root, as in the native work estimate. + float weightLimit = MathF.Ceiling(MathF.Sqrt(((float)areaLimit / logColor) / Math.Max(1UL, manhattanDistance))); + Span sigmaDct = result.SigmaDct; + ulong widthEstimate = 0; - for (int i = 0; i < 32; i++) - { - result.ColorDct[0][i] += yToX * result.ColorDct[1][i]; - result.ColorDct[2][i] += yToB * result.ColorDct[1][i]; - } + for (int i = 0; i < 32; i++) + { + float inverseDctFactor = i == 0 ? JxlDctScales.Sqrt05 : 1F; + sigmaDct[i] = this.SigmaDct[i] * inverseDctFactor * ChannelWeight[3] * inverseQuant; + float weightF = MathF.Ceiling(inverseQuant * MathF.Abs(this.SigmaDct[i])); + ulong weight = (ulong)MathF.Min(weightLimit, MathF.Max(1F, weightF)); + widthEstimate += weight * weight * logColor; + } - long widthEstimate = 0; - Span color = stackalloc long[3]; + ulong estimatedArea = widthEstimate * manhattanDistance; + if (estimatedArea > areaLimit - totalEstimatedAreaReached) + { + throw new InvalidImageContentException("JPEG XL splines exceed the rendering-work limit."); + } - for (int c = 0; c < 3; c++) - { - for (int i = 0; i < 32; i++) - { - color[c] += (long)MathF.Ceiling(inverseQuant * MathF.Abs(this.ColorDct[c][i])); + totalEstimatedAreaReached += estimatedArea; + return true; } } - - color[0] += (long)MathF.Ceiling(MathF.Abs(yToX)) * color[1]; - color[2] += (long)MathF.Ceiling(MathF.Abs(yToB)) * color[1]; - - long maxColor = Math.Max(color[1], Math.Max(color[0], color[2])); - long logColor = Math.Max(1L, JxlMath.CeilLog2Nonzero(1L + maxColor)); - float weightLimit = MathF.Ceiling(MathF.Sqrt((float)areaLimit / logColor) / MathF.Max(1, manhattanDistance)); - - for (int i = 0; i < 32; i++) - { - float inverseDctFactor = (i == 0) ? JxlDctScales.Sqrt05 : 1.0f; - result.SigmaDct[i] = this.SigmaDct[i] * inverseDctFactor * ChannelWeight[3] * inverseQuant; - float weightF = MathF.Ceiling(inverseQuant * MathF.Abs(this.SigmaDct[i])); - long weight = (long)Math.Min(weightLimit, Math.Max(1.0f, weightF)); - widthEstimate += weight * weight * logColor; - } - - totalEstimatedAreaReached = widthEstimate * manhattanDistance; - if (totalEstimatedAreaReached > areaLimit) + catch (OverflowException exception) { - throw new InvalidOperationException("Total estimated area is too large"); + throw new InvalidImageContentException("JPEG XL spline work estimate exceeds the supported range.", exception); } - - return true; } /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSpline.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSpline.cs index e3c35d5e09..5e6cb723e8 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSpline.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSpline.cs @@ -7,14 +7,33 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; internal sealed class JxlSpline : IDisposable { + /// + /// The exclusive coordinate limit used by native spline reconstruction to keep geometry arithmetic bounded. + /// + public const int MaximumCoordinate = 1 << 23; + private IMemoryOwner? controlPoints; + private InlineArray3> colorDct; + private InlineArray32 sigmaDct; + /// + /// Gets the logical control points, without unused allocator capacity. + /// public Memory ControlPoints { get; private set; } - public Dct32[] ColorDct { get; set; } = []; + /// + /// Gets the three fixed groups of 32 color coefficients, stored with this spline. + /// + public Span> ColorDct => this.colorDct; - public Dct32 SigmaDct { get; set; } + /// + /// Gets the 32 width coefficients, stored with this spline. + /// + public Span SigmaDct => this.sigmaDct; + /// + /// Releases the control-point allocation and clears its logical view. + /// public void ClearControlPoints() { this.controlPoints?.Dispose(); @@ -23,17 +42,41 @@ public void ClearControlPoints() this.ControlPoints = Memory.Empty; } - public void ReserveControlPoints(Configuration configuration, int n) + /// + /// Selects the control-point count, reusing the current allocation when possible. + /// + /// The configuration whose allocator owns the points. + /// The logical count. The caller writes all selected points before reading them. + public void ReserveControlPoints(Configuration configuration, int count) { - this.ClearControlPoints(); + if (this.controlPoints is null || this.controlPoints.Memory.Length < count) + { + // Reconstruction replaces every point. Growth requires no copy, and allocation failure retains the previous owner. + IMemoryOwner replacement = configuration.MemoryAllocator.Allocate(count); + this.controlPoints?.Dispose(); + this.controlPoints = replacement; + } - this.controlPoints = configuration.MemoryAllocator.Allocate(n); - this.ControlPoints = this.controlPoints.Memory; + this.ControlPoints = this.controlPoints.Memory[..count]; + } + + /// + /// Rejects spline coordinates or reconstructed deltas outside the native geometry limit. + /// + /// The horizontal coordinate or delta, before narrowing. + /// The vertical coordinate or delta, before narrowing. + public static void ValidatePosition(long x, long y) + { + // This is a reconstruction safety bound, not an image-edge clamp. Splines can extend outside the frame. + if (x <= -MaximumCoordinate || x >= MaximumCoordinate || y <= -MaximumCoordinate || y >= MaximumCoordinate) + { + throw new InvalidImageContentException("JPEG XL spline coordinates exceed the supported range."); + } } + /// public void Dispose() { this.ClearControlPoints(); - GC.SuppressFinalize(this); } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineSegmentSpan.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineSegmentSpan.cs index 2c65b1695d..57f608ba12 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineSegmentSpan.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineSegmentSpan.cs @@ -3,9 +3,20 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; -internal struct JxlSplineSegmentSpan(int startInclusive, int endInclusive) +/// +/// Identifies the image rows touched by one cached spline segment. +/// +/// The first row that can contain samples. +/// The row immediately after the affected range. +internal struct JxlSplineSegmentSpan(int startInclusive, int endExclusive) { + /// + /// Gets or sets the first affected row. + /// public int StartInclusive { get; set; } = startInclusive; - public int EndInclusive { get; set; } = endInclusive; + /// + /// Gets or sets the row immediately after the affected range. + /// + public int EndExclusive { get; set; } = endExclusive; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineUtils.cs index 93f240da0d..6b333d39ef 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineUtils.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineUtils.cs @@ -11,6 +11,9 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; internal static class JxlSplineUtils { + /// + /// The spacing, in image pixels along the curve, between reconstructed sample points. + /// public const float DesiredRenderingDistance = 1f; private const float PiBy32 = MathF.PI / 32; @@ -26,11 +29,16 @@ internal static class JxlSplineUtils PiBy32 * 30, PiBy32 * 31, ]; - public static float ContinuousInverseDCT(in Dct32 dct, float t) + /// + /// Evaluates a 32-coefficient inverse DCT at a continuous spline position. + /// + /// The 32 coefficients borrowed from the spline. + /// The position within the 32-sample DCT domain. + /// The reconstructed value at that position. + public static float ContinuousInverseDCT(ReadOnlySpan dct, float t) { ref float multipliers = ref MemoryMarshal.GetReference(ContinuousIDCTMultipliers); - ReadOnlySpan dctData = dct; - ref float dctDataRef = ref MemoryMarshal.GetReference(dctData); + ref float dctDataRef = ref MemoryMarshal.GetReference(dct); if (Vector.Count <= 32 && Vector.IsHardwareAccelerated) { @@ -136,7 +144,8 @@ private static void DrawSegmentPacked(ref JxlSplineSegment segment, bool add, in Vector oneOver2s2 = Vector.Create(0.353553391f); Vector sigmaOver4TimesIntensity = Vector.Create(segment.SigmaOver4TimesIntensity); - Vector dx = JxlSimdUtils.Iota(x + x0).As() - Vector.Create(segment.Center.X); + // Lane i is image column x0 + x + i. Convert the integer columns numerically before measuring distance from the center. + Vector dx = Vector.ConvertToSingle(Vector.CreateSequence(x + x0, 1)) - Vector.Create(segment.Center.X); Vector dy = Vector.Create(y - segment.Center.Y); Vector sqd = (dx * dx) + (dy * dy); @@ -204,9 +213,12 @@ public static void DrawSegment(ref JxlSplineSegment segment, bool add, int y, in int spanX1 = Math.Min(x1, end + 1) - x0; int x = spanX0; - for (; x + Vector.Count <= spanX1; x += Vector.Count) + if (Vector.IsHardwareAccelerated) { - DrawSegmentPacked(ref segment, add, y, x, x0, rows); + for (; x + Vector.Count <= spanX1; x += Vector.Count) + { + DrawSegmentPacked(ref segment, add, y, x, x0, rows); + } } for (; x < spanX1; ++x) @@ -215,15 +227,31 @@ public static void DrawSegment(ref JxlSplineSegment segment, bool add, int y, in } } - public static void ComputeSegments(int imageYSize, PointF center, float intensity, InlineArray3 color, float sigma, List segments, List segmentSpans) + /// + /// Adds a Gaussian spline sample and the image rows that its bounded tail can affect. + /// + /// The frame height in rows. + /// The sample's image coordinates. + /// The arc-length weight of the sample. + /// The sample's three color contributions. + /// The Gaussian width. + /// The caller-owned segment cache. + /// The affected image-row interval for each cached segment. + public static void ComputeSegments( + int imageYSize, + PointF center, + float intensity, + InlineArray3 color, + float sigma, + List segments, + List segmentSpans) { if (!(float.IsFinite(sigma) && sigma != 0.0f && float.IsFinite(1.0f / sigma) && float.IsFinite(intensity))) { return; } - // This is about 30% faster, but for higher precision - // one can change this to 5 instead. + // The native renderer truncates the Gaussian tail at 10^-3, adjusted by the largest channel contribution. const float distanceExp = 3f; float maxColor = MathF.Max(0.01f, MathF.Abs(color[0] * intensity)); @@ -256,7 +284,30 @@ public static void ComputeSegments(int imageYSize, PointF center, float intensit segmentSpans.Add(new JxlSplineSegmentSpan(y0, y1)); } - public static void DrawSegments(Memory rowX, Memory rowY, Memory rowB, int y, int x0, int x1, bool add, Span segments, Span segmentIndices, Span segmentYStart) + /// + /// Applies the indexed spline segments that intersect one color row. + /// + /// The X color samples starting at x0. + /// The Y color samples starting at x0. + /// The B color samples starting at x0. + /// The image row. + /// The first image column, inclusive. + /// The final image column, exclusive. + /// Whether to add or subtract spline samples. + /// The cached spline segments. + /// The indices of the segments assigned to each row. + /// The offsets delimiting each row's range within segmentIndices. + public static void DrawSegments( + Memory rowX, + Memory rowY, + Memory rowB, + int y, + int x0, + int x1, + bool add, + Span segments, + ReadOnlySpan segmentIndices, + ReadOnlySpan segmentYStart) { InlineArray3> rows = default; rows[0] = rowX; @@ -269,7 +320,22 @@ public static void DrawSegments(Memory rowX, Memory rowY, Memory pointsToDraw, float arcLength, List segments, List segmentsSpans) + /// + /// Evaluates color and width along a spline and appends its Gaussian sample segments. + /// + /// The frame height in rows. + /// The reconstructed color and width coefficients. + /// The unit-spaced positions and their distance weights. + /// The complete path length in pixels. + /// The caller-owned segment cache. + /// The image-row interval for each segment. + public static void SegmentsFromPoints( + int imageYSize, + JxlSpline spline, + List<(PointF Point, float Multiplier)> pointsToDraw, + float arcLength, + List segments, + List segmentsSpans) { float inverseArcLength = 1.0f / arcLength; int k = 0; @@ -288,109 +354,108 @@ public static void SegmentsFromPoints(int imageYSize, JxlSpline spline, List<(Po } } - public static void DrawCentripetalCatmullRomSpline(Span points, List result) + /// + /// Appends the centripetal Catmull-Rom curve through the supplied control points. + /// + /// The validated control points, with no equal adjacent points. + /// The caller-owned list that receives 16 samples per interval and the final endpoint. + public static void DrawCentripetalCatmullRomSpline(ReadOnlySpan points, List result) { - if (points.Length == 0) - { - return; - } - if (points.Length == 1) { result.Add(points[0]); return; } - List pointsCopy = []; - for (int i = 0; i < points.Length; i++) - { - pointsCopy.Add(points[i]); - } - - const int numPoints = 16; - pointsCopy.Insert(0, pointsCopy[0] + (pointsCopy[0] - pointsCopy[1])); - pointsCopy.Add(pointsCopy[^1] + (pointsCopy[^1] - pointsCopy[^2])); + const int samplesPerInterval = 16; - for (int start = 0; start < pointsCopy.Count - 3; start++) + // Each interval needs four points. Extrapolate the two endpoint neighbors in a fixed local block, + // rather than copying and extending the complete control-point list. + for (int start = 0; start < points.Length - 1; start++) { - Span p = CollectionsMarshal.AsSpan(pointsCopy)[start..]; + InlineArray4 pointStorage = default; + Span p = pointStorage; + p[0] = start == 0 ? points[0] + (points[0] - points[1]) : points[start - 1]; + p[1] = points[start]; + p[2] = points[start + 1]; + p[3] = start + 2 == points.Length ? points[^1] + (points[^1] - points[^2]) : points[start + 2]; result.Add(p[1]); - InlineArray3 d = default; - InlineArray4 t = default; + InlineArray3 distanceStorage = default; + InlineArray4 parameterStorage = default; + Span distances = distanceStorage; + Span parameters = parameterStorage; - for (int k = 0; k < 3; ++k) + // Centripetal parameter steps are the square roots of Euclidean distances. + // Adjacent control points differ, so every interpolation denominator is positive. + for (int k = 0; k < 3; k++) { - d[k] = MathF.Sqrt(JxlMath.Hypot(p[k + 1].X - p[k].X, p[k + 1].Y - p[k].Y)); - t[k + 1] = t[k] + d[k]; + distances[k] = MathF.Sqrt(float.Hypot(p[k + 1].X - p[k].X, p[k + 1].Y - p[k].Y)); + parameters[k + 1] = parameters[k] + distances[k]; } - for (int i = 1; i < numPoints; ++i) + for (int i = 1; i < samplesPerInterval; i++) { - float tt = d[0] + (((float)i / numPoints) * d[1]); + float position = distances[0] + (((float)i / samplesPerInterval) * distances[1]); InlineArray3 a = default; - for (int k = 0; k < 3; ++k) + for (int k = 0; k < 3; k++) { - a[k] = p[k] + (((tt - t[k]) / d[k]) * (p[k + 1] - p[k])); + a[k] = p[k] + (((position - parameters[k]) / distances[k]) * (p[k + 1] - p[k])); } - InlineArray3 b = default; + InlineArray2 b = default; - for (int k = 0; k < 2; ++k) + for (int k = 0; k < 2; k++) { - b[k] = a[k] + (((tt - t[k]) / (d[k] + d[k + 1])) * (a[k + 1] - a[k])); + b[k] = a[k] + (((position - parameters[k]) / (distances[k] + distances[k + 1])) * (a[k + 1] - a[k])); } - result.Add(b[0] + (((tt - t[1]) / d[1]) * (b[1] - b[0]))); + result.Add(b[0] + (((position - parameters[1]) / distances[1]) * (b[1] - b[0]))); } } - result.Add(pointsCopy[^2]); + result.Add(points[^1]); } - public static void ForEachEquallySpacedPoint(Span points, Action functor) + /// + /// Appends points at unit arc-length steps and the final residual endpoint. + /// + /// The nonempty interpolated path. + /// The caller-owned list of positions and their distance weights. + public static void GetEquallySpacedPoints(ReadOnlySpan points, List<(PointF Point, float Multiplier)> result) { PointF current = points[0]; - functor(current, DesiredRenderingDistance); + result.Add((current, DesiredRenderingDistance)); + int next = 0; - ref PointF next = ref points[0]; - ref PointF end = ref points[^1]; // last - - while (!Unsafe.AreSame(ref next, ref end)) + while (next < points.Length) { - ref PointF previous = ref current; - float arcLengthFromPrevious = 0f; + PointF previous = current; + float distanceFromPrevious = 0F; while (true) { - if (next == end) + if (next == points.Length) { - functor(previous, arcLengthFromPrevious); + // Keep the final weight even when it is zero. The caller uses it to recover the exact path length. + result.Add((previous, distanceFromPrevious)); return; } - float arcLengthToNext = MathF.Sqrt(SquaredNorm(next - previous)); + PointF delta = points[next] - previous; + float distanceToNext = MathF.Sqrt((delta.X * delta.X) + (delta.Y * delta.Y)); - if (arcLengthFromPrevious + arcLengthToNext >= DesiredRenderingDistance) + if (distanceFromPrevious + distanceToNext >= DesiredRenderingDistance) { - current = previous + (((DesiredRenderingDistance - arcLengthFromPrevious) / arcLengthToNext) * (next - previous)); - functor(current, DesiredRenderingDistance); + current = previous + (((DesiredRenderingDistance - distanceFromPrevious) / distanceToNext) * delta); + result.Add((current, DesiredRenderingDistance)); break; } - arcLengthFromPrevious += arcLengthToNext; - previous = ref next; - next = ref Unsafe.Add(ref next, 1); + distanceFromPrevious += distanceToNext; + previous = points[next++]; } } } - - private static float SquaredNorm(PointF pointF) - { - float x = pointF.X; - float y = pointF.Y; - - return (x * x) + (y * y); - } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplines.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplines.cs index ea63b5b755..c36ee30f91 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplines.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplines.cs @@ -7,6 +7,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; @@ -19,8 +20,10 @@ internal sealed class JxlSplines private readonly List startingPointsStorage = []; private JxlSplineDataView data = new(); private readonly List segments = []; - private IMemoryOwner segmentIndices = EmptyMemoryOwner.Instance; - private IMemoryOwner segmentYStart = EmptyMemoryOwner.Instance; + private IMemoryOwner? segmentIndicesOwner; + private IMemoryOwner? segmentYStartOwner; + private Memory segmentIndices; + private Memory segmentYStart; public bool HasAny => this.data.HasAny; @@ -56,11 +59,7 @@ public void Clear() this.splinesStorage.Clear(); this.startingPointsStorage.Clear(); this.data = new(); - this.segments.Clear(); - this.segmentIndices.Dispose(); - this.segmentYStart.Dispose(); - this.segmentIndices = EmptyMemoryOwner.Instance; - this.segmentYStart = EmptyMemoryOwner.Instance; + this.ClearDrawCache(); } /// @@ -101,7 +100,23 @@ public void Decode(Configuration configuration, JxlBitReader reader, int numPixe int numSplines = (int)encodedSplines + 1; - DecodeAllStartingPoints(this.startingPointsStorage, reader, decoder, contextMap, numSplines); + _ = this.startingPointsStorage.EnsureCapacity(numSplines); + long lastX = 0; + long lastY = 0; + + // The first point uses unsigned absolute coordinates. Later points store signed deltas from the previous start. + // Keep integer positions until validation; every accepted coordinate is exactly representable as a float. + for (int i = 0; i < numSplines; i++) + { + uint encodedX = (uint)decoder.ReadHybridUInt((int)JxlSplineEntropyContext.StartingPosition, reader, contexts, window); + uint encodedY = (uint)decoder.ReadHybridUInt((int)JxlSplineEntropyContext.StartingPosition, reader, contexts, window); + long x = i == 0 ? encodedX : lastX + JxlPackSigned.UnpackSigned(encodedX); + long y = i == 0 ? encodedY : lastY + JxlPackSigned.UnpackSigned(encodedY); + JxlSpline.ValidatePosition(x, y); + this.startingPointsStorage.Add(new PointF(x, y)); + lastX = x; + lastY = y; + } int quantizationAdjustment = decoder.ReadHybridUInt((int)JxlSplineEntropyContext.QuantizationAdjustment, reader, contexts, window); this.QuantizationAdjustment = JxlPackSigned.UnpackSigned((uint)quantizationAdjustment); @@ -152,138 +167,181 @@ public void Decode(Configuration configuration, JxlBitReader reader, int numPixe } } + /// + /// Reconstructs the spline segments and builds a contiguous segment-index range for each image row. + /// + /// The configuration whose allocator owns the row index and segment-index buffers. + /// The frame width in pixels. + /// The frame height in pixels. + /// The color correlation restored before spline rendering. public void InitializeDrawCache(Configuration configuration, int imageXSize, int imageYSize, JxlColorCorrelation colorCorrelation) { - this.segments.Clear(); - this.segmentIndices = EmptyMemoryOwner.Instance; - this.segmentYStart = EmptyMemoryOwner.Instance; + this.ClearDrawCache(); - List segmentsSpans = []; - List intermediatePoints = []; - List splines = []; - long totalEstimatedAreaReached = 0; - - for (int i = 0; i < this.data.Splines.Count; i++) + try { - JxlSpline spline = new(); - - if (!this.data.Splines[i].Dequantize( - configuration, - this.data.StartingPoints[i], - this.QuantizationAdjustment, - colorCorrelation.YToXRatio(0), - colorCorrelation.YToBRatio(0), - imageXSize * imageYSize, - ref totalEstimatedAreaReached, - spline)) - { - throw new InvalidOperationException("Could not dequantize a quantized spline"); - } + List segmentSpans = []; + List intermediatePoints = []; + List<(PointF Point, float Multiplier)> pointsToDraw = []; + using JxlSpline spline = new(); + ulong totalEstimatedAreaReached = 0; + ulong imagePixelCount = (ulong)imageXSize * (ulong)imageYSize; - if (AdjacentFind(spline.ControlPoints.Span) != spline.ControlPoints.Length - 1) + // One reconstructed spline and its lists serve the whole frame. Segment construction retains values, + // not references to this workspace, so the next spline can reuse its control-point allocation. + for (int i = 0; i < this.data.Splines.Count; i++) { - throw new InvalidOperationException("Identical successive control points in spline " + i); - } - - splines.Add(spline); - } - -#if JPEG_XL_THROW_ON_LARGE_SPLINE_AREA - if (totalEstimatedAreaReached > Math.Min((8 * imageXSize * imageYSize) + (1 << 25), 1 << 30)) - { - throw new InvalidOperationException("Total spline area is too large"); - } -#endif - - foreach (JxlSpline spline in splines) - { - List<(PointF Point, float Multiplier)> pointsToDraw = []; + this.data.Splines[i].Dequantize( + configuration, + this.data.StartingPoints[i], + this.QuantizationAdjustment, + colorCorrelation.YToXRatio(0), + colorCorrelation.YToBRatio(0), + imagePixelCount, + ref totalEstimatedAreaReached, + spline); + + ReadOnlySpan controlPoints = spline.ControlPoints.Span; + + // Equal neighboring points make the centripetal interpolation denominator zero. + for (int point = 1; point < controlPoints.Length; point++) + { + if (controlPoints[point - 1] == controlPoints[point]) + { + throw new InvalidImageContentException("JPEG XL spline has identical neighboring control points."); + } + } - void AddPoint(PointF point, float multiplier) => pointsToDraw.Add((point, multiplier)); + intermediatePoints.Clear(); + pointsToDraw.Clear(); + JxlSplineUtils.DrawCentripetalCatmullRomSpline(controlPoints, intermediatePoints); + JxlSplineUtils.GetEquallySpacedPoints(CollectionsMarshal.AsSpan(intermediatePoints), pointsToDraw); - intermediatePoints.Clear(); + // The first point carries unit weight. The final point carries the remaining distance, including zero at exact endpoints. + float arcLength = ((pointsToDraw.Count - 2) * JxlSplineUtils.DesiredRenderingDistance) + pointsToDraw[^1].Multiplier; + if (arcLength <= 0F) + { + continue; + } - JxlSplineUtils.DrawCentripetalCatmullRomSpline(spline.ControlPoints.Span, intermediatePoints); - JxlSplineUtils.ForEachEquallySpacedPoint(CollectionsMarshal.AsSpan(intermediatePoints), AddPoint); + JxlSplineUtils.SegmentsFromPoints(imageYSize, spline, pointsToDraw, arcLength, this.segments, segmentSpans); + } - float arcLength = ((pointsToDraw.Count - 2) * JxlSplineUtils.DesiredRenderingDistance) + pointsToDraw[^1].Multiplier; - if (arcLength <= 0f) + // Splines with no length or width contribute no segments, so drawing needs no row index. + if (this.segments.Count == 0) { - // This spline wouldn't have any effect. - continue; + return; } - JxlSplineUtils.SegmentsFromPoints(imageYSize, spline, pointsToDraw, arcLength, this.segments, segmentsSpans); - } - - int segmentYStartNumBytes = (imageYSize + 2) * 4; - this.segmentYStart = configuration.MemoryAllocator.Allocate(segmentYStartNumBytes); + // Signed differences record where segments enter and leave the image rows. Int32 storage is required: + // negative departures and segment offsets cannot be represented by the old byte-sized view. + this.segmentYStartOwner = configuration.MemoryAllocator.Allocate(checked(imageYSize + 2), AllocationOptions.Clean); + this.segmentYStart = this.segmentYStartOwner.Memory[..(imageYSize + 2)]; + Span rowStarts = this.segmentYStart.Span; + Span population = rowStarts[1..]; - Span segmentYStart = this.segmentYStart.Memory.Span; - segmentYStart.Clear(); + foreach (JxlSplineSegmentSpan span in segmentSpans) + { + population[span.StartInclusive]++; + population[span.EndExclusive]--; + } - Span population = segmentYStart[1..]; + int total = 0; + int coverage = 0; - foreach (JxlSplineSegmentSpan segmentSpan in segmentsSpans) - { - population[segmentSpan.StartInclusive]++; - population[segmentSpan.EndInclusive]--; - } + // Convert the difference counts to insertion offsets. The checked sum bounds the allocator size and later list indices. + for (int y = 0; y < imageYSize; y++) + { + coverage = checked(coverage + population[y]); + population[y] = total; + total = checked(total + coverage); + } - int total = 0; - int coverage = 0; + this.segmentIndicesOwner = configuration.MemoryAllocator.Allocate(total); + this.segmentIndices = this.segmentIndicesOwner.Memory[..total]; + Span indices = this.segmentIndices.Span; - for (int y = 0; y < imageYSize; y++) - { - if (population[y] < 0) + // Filling each row advances its insertion offset to the next row's start. Because population begins + // one element after rowStarts, the finished rowStarts[y..y+2] is exactly that row's segment range. + for (int i = 0; i < segmentSpans.Count; i++) { - if (coverage < -population[y]) + JxlSplineSegmentSpan span = segmentSpans[i]; + + for (int y = span.StartInclusive; y < span.EndExclusive; y++) { - throw new InvalidOperationException("Coverage is invalid"); + indices[population[y]++] = i; } } - - coverage += population[y]; - population[y] = (byte)total; - total += coverage; } - - this.segmentIndices = configuration.MemoryAllocator.Allocate(total * 4); - Span segmentIndices = MemoryMarshal.Cast(this.segmentIndices.Memory.Span); - - for (int i = 0; i < this.segments.Count; i++) + catch { - JxlSplineSegmentSpan segmentSpan = segmentsSpans[i]; - - for (int y = segmentSpan.StartInclusive; y < segmentSpan.EndInclusive; y++) - { - segmentIndices[population[y]++] = i; - } + // No partial index is published after reconstruction, geometry, or allocation failure. + this.ClearDrawCache(); + throw; } } - private static int AdjacentFind(Span span) - where T : IEquatable + /// + /// Releases the owned row index and segment-index buffers, retaining reusable segment-list capacity. + /// + private void ClearDrawCache() { - for (int i = 0; i < span.Length - 1; i++) - { - if (span[i].Equals(span[i + 1])) - { - return i; - } - } - - return span.Length; + this.segments.Clear(); + this.segmentIndicesOwner?.Dispose(); + this.segmentYStartOwner?.Dispose(); + this.segmentIndicesOwner = null; + this.segmentYStartOwner = null; + this.segmentIndices = default; + this.segmentYStart = default; } - public void AddTo(JxlImage3 opsin, Rectangle opsinRect) => this.Apply(add: true, opsin, opsinRect); + /// + /// Adds the cached spline samples to an XYB frame region. + /// + /// The frame's XYB planes. + /// The image-coordinate region to modify. + public void AddTo(JxlImage3 opsin, Rectangle opsinRect) => this.Apply(true, opsin, opsinRect); + /// + /// Adds cached spline samples to one borrowed XYB row segment. + /// + /// The X samples beginning at x0. + /// The Y samples beginning at x0. + /// The B samples beginning at x0. + /// The image row index. + /// The first image column, inclusive. + /// The final image column, exclusive. public void AddToRow(Memory rowX, Memory rowY, Memory rowB, int y, int x0, int x1) - => this.ApplyToRow(add: true, rowX, rowY, rowB, y, x0, x1); + => this.ApplyToRow(true, rowX, rowY, rowB, y, x0, x1, this.segmentIndices.Span, this.segmentYStart.Span); - public void SubtractFrom(JxlImage3 opsin) => this.Apply(add: false, opsin, opsin.GetRectangle()); + /// + /// Removes the cached spline samples from a complete XYB frame. + /// + /// The XYB frame to modify. + public void SubtractFrom(JxlImage3 opsin) => this.Apply(false, opsin, opsin.GetRectangle()); - private void ApplyToRow(bool add, Memory rowX, Memory rowY, Memory rowB, int y, int x0, int x1) + /// + /// Applies the cached segment range to one borrowed color row. + /// + /// Whether to add or subtract the spline samples. + /// The X color row beginning at x0. + /// The Y color row beginning at x0. + /// The B color row beginning at x0. + /// The image row index. + /// The first image column, inclusive. + /// The final image column, exclusive. + /// The borrowed segment indices for all rows. + /// The borrowed offsets delimiting each row's index range. + private void ApplyToRow( + bool add, + Memory rowX, + Memory rowY, + Memory rowB, + int y, + int x0, + int x1, + ReadOnlySpan indices, + ReadOnlySpan rowStarts) { if (this.segments.Count == 0) { @@ -299,10 +357,16 @@ private void ApplyToRow(bool add, Memory rowX, Memory rowY, Memory x1, add, CollectionsMarshal.AsSpan(this.segments), - MemoryMarshal.Cast(this.segmentIndices.Memory.Span), - MemoryMarshal.Cast(this.segmentYStart.Memory.Span)); + indices, + rowStarts); } + /// + /// Applies the cached splines row by row while borrowing the index buffers once. + /// + /// Whether to add or subtract the spline samples. + /// The XYB planes to modify. + /// The image-coordinate region to process. private void Apply(bool add, JxlImage3 opsin, Rectangle opsinRect) { if (this.segments.Count == 0) @@ -313,6 +377,8 @@ private void Apply(bool add, JxlImage3 opsin, Rectangle opsinRect) int y0 = RectangleUtils.Y0(in opsinRect); int x0 = RectangleUtils.X0(in opsinRect); int x1 = RectangleUtils.X1(in opsinRect); + ReadOnlySpan indices = this.segmentIndices.Span; + ReadOnlySpan rowStarts = this.segmentYStart.Span; for (int y = 0; y < opsinRect.Height; y++) { @@ -323,18 +389,10 @@ private void Apply(bool add, JxlImage3 opsin, Rectangle opsinRect) opsin.PlaneRowMemory(2, y0 + y)[x0..], y0 + y, x0, - x1); + x1, + indices, + rowStarts); } } - private sealed class EmptyMemoryOwner : IMemoryOwner - { - public static readonly EmptyMemoryOwner Instance = new(); - - public Memory Memory => Memory.Empty; - - public void Dispose() - { - } - } } diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/SplineTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/SplineTests.cs new file mode 100644 index 0000000000..7181810ddf --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/SplineTests.cs @@ -0,0 +1,106 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; + +public class SplineTests +{ + /// + /// Checks control-point double deltas and accumulated reconstruction work through quantization and reconstruction. + /// + [Fact] + public void Reconstruct_PreservesControlPointsAndAccumulatesFrameWork() + { + PointF[] points = [new(0, 0), new(1, 0), new(2, 1)]; + using JxlSpline source = new(); + source.ReserveControlPoints(TestEnvironment.Configuration, points.Length); + points.AsSpan().CopyTo(source.ControlPoints.Span); + + using JxlQuantizedSpline encoded = JxlQuantizedSpline.Create(TestEnvironment.Configuration, source, 0, 0F, 0F); + + // The first point is separate. Deltas (1,0), (1,1) become double deltas (1,0), (0,1). + Assert.Equal(2, encoded.ControlPoints.Length); + Assert.Equal(1, encoded.ControlPoints.Span[0].First); + Assert.Equal(0, encoded.ControlPoints.Span[0].Second); + Assert.Equal(0, encoded.ControlPoints.Span[1].First); + Assert.Equal(1, encoded.ControlPoints.Span[1].Second); + + using JxlSpline reconstructed = new(); + ulong work = 0; + Assert.True(encoded.Dequantize(TestEnvironment.Configuration, points[0], 0, 0F, 0F, 16, ref work, reconstructed)); + Assert.True(points.AsSpan().SequenceEqual(reconstructed.ControlPoints.Span)); + Assert.True(work > 0); + ulong firstSplineWork = work; + + // Reconstructing a second identical spline adds its work to the same frame budget. + Assert.True(encoded.Dequantize(TestEnvironment.Configuration, points[0], 0, 0F, 0F, 16, ref work, reconstructed)); + Assert.Equal(2 * firstSplineWork, work); + Assert.True(points.AsSpan().SequenceEqual(reconstructed.ControlPoints.Span)); + } + + /// + /// Checks the final path position and residual distance, including an exact endpoint with zero residual weight. + /// + /// The length of a horizontal path. + /// The number of unit-spaced and endpoint samples. + /// The distance after the last complete unit step. + [Theory] + [InlineData(0.5F, 2, 0.5F)] + [InlineData(2.5F, 4, 0.5F)] + [InlineData(2F, 4, 0F)] + public void SamplePath_PreservesFinalPositionAndResidual(float length, int expectedCount, float expectedResidual) + { + PointF[] path = [new(0, 0), new(length, 0)]; + List<(PointF Point, float Multiplier)> samples = []; + + JxlSplineUtils.GetEquallySpacedPoints(path, samples); + + Assert.Equal(expectedCount, samples.Count); + Assert.Equal(new PointF(length, 0), samples[^1].Point); + Assert.Equal(expectedResidual, samples[^1].Multiplier); + } + + /// + /// Checks numeric image coordinates in packed spline drawing and its scalar fallback. + /// + [Fact] + public void DrawSegment_UsesNumericImageCoordinates() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + VerifyNumericCoordinates, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + + /// + /// Checks that the Gaussian sample at the spline center contributes to the selected color channel. + /// + private static void VerifyNumericCoordinates() + { + const int count = 33; + float[] red = new float[count]; + float[] green = new float[count]; + float[] blue = new float[count]; + InlineArray3> rows = default; + rows[0] = red; + rows[1] = green; + rows[2] = blue; + + InlineArray3 color = default; + color[0] = 1F; + JxlSplineSegment segment = new() + { + Center = new PointF(16, 0), + MaximumDistance = 32F, + InverseSigma = 1F, + SigmaOver4TimesIntensity = 0.25F, + Color = color + }; + + // The center is inside every packed traversal width. Reinterpreting integer column bits would place it far from the center. + JxlSplineUtils.DrawSegment(ref segment, true, 0, 0, count, rows); + + Assert.True(red[16] > 0F); + Assert.All(green, value => Assert.Equal(0F, value)); + Assert.All(blue, value => Assert.Equal(0F, value)); + } +} From 6cb99383f7c534bd423a665cee19642d3339b8a2 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 14:17:15 +1000 Subject: [PATCH 256/328] Limit spline overflow checks and use shared row ranges --- .../Processing/Splines/JxlQuantizedSpline.cs | 176 +++++++++--------- .../Splines/JxlSplineSegmentSpan.cs | 22 --- .../Jxl/Processing/Splines/JxlSplineUtils.cs | 6 +- .../Jxl/Processing/Splines/JxlSplines.cs | 33 ++-- 4 files changed, 116 insertions(+), 121 deletions(-) delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineSegmentSpan.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs index 4241b267fe..efe035e5fa 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs @@ -198,109 +198,117 @@ public bool Dequantize( ulong cappedPixelCount = Math.Min(imagePixelCount, (maximumAreaAllowance - initialAreaAllowance) / areaPerImagePixel); ulong areaLimit = (areaPerImagePixel * cappedPixelCount) + initialAreaAllowance; - try + // Decoded starts are finite integer positions within +/-2^23. Rounding also matches the encoder's stored starting-point rule. + long currentX = (long)MathF.Round(startingPoint.X, MidpointRounding.AwayFromZero); + long currentY = (long)MathF.Round(startingPoint.Y, MidpointRounding.AwayFromZero); + JxlSpline.ValidatePosition(currentX, currentY); + + // The parser limits the whole frame to 2^20 control points, including each starting point. + result.ReserveControlPoints(configuration, this.ControlPoints.Length + 1); + Span controlPoints = result.ControlPoints.Span; + ReadOnlySpan deltas = this.ControlPoints.Span; + controlPoints[0] = new(currentX, currentY); + + long deltaX = 0; + long deltaY = 0; + ulong manhattanDistance = 0; + + for (int i = 0; i < deltas.Length; i++) { - checked + // Each accepted coordinate and reconstructed delta has magnitude below 2^23. + // Adding one encoded Int32 delta in Int64 cannot overflow before the next geometry check. + deltaX += deltas[i].First; + deltaY += deltas[i].Second; + JxlSpline.ValidatePosition(deltaX, deltaY); + manhattanDistance += (ulong)(Math.Abs(deltaX) + Math.Abs(deltaY)); + + if (manhattanDistance > areaLimit) { - // Round only at the spline's integer geometry boundary. Checked conversion rejects nonfinite starting points. - long currentX = (long)MathF.Round(startingPoint.X, MidpointRounding.AwayFromZero); - long currentY = (long)MathF.Round(startingPoint.Y, MidpointRounding.AwayFromZero); - JxlSpline.ValidatePosition(currentX, currentY); - - result.ReserveControlPoints(configuration, this.ControlPoints.Length + 1); - Span controlPoints = result.ControlPoints.Span; - ReadOnlySpan deltas = this.ControlPoints.Span; - controlPoints[0] = new(currentX, currentY); - - long deltaX = 0; - long deltaY = 0; - ulong manhattanDistance = 0; - - for (int i = 0; i < deltas.Length; i++) - { - // The stream stores changes to the previous delta. Accumulate in Int64 before applying the geometry bound. - deltaX += deltas[i].First; - deltaY += deltas[i].Second; - JxlSpline.ValidatePosition(deltaX, deltaY); - manhattanDistance += (ulong)(Math.Abs(deltaX) + Math.Abs(deltaY)); - - if (manhattanDistance > areaLimit) - { - throw new InvalidImageContentException("JPEG XL spline length exceeds the rendering limit."); - } - - currentX += deltaX; - currentY += deltaY; - JxlSpline.ValidatePosition(currentX, currentY); - controlPoints[i + 1] = new(currentX, currentY); - } - - float inverseQuant = InverseAdjustedQuant(quantizationAdjustment); - Span> colorDct = result.ColorDct; - - for (int channel = 0; channel < 3; channel++) - { - for (int i = 0; i < 32; i++) - { - float inverseDctFactor = i == 0 ? JxlDctScales.Sqrt05 : 1F; - colorDct[channel][i] = this.ColorDct[channel][i] * inverseDctFactor * ChannelWeight[channel] * inverseQuant; - } - } + throw new InvalidImageContentException("JPEG XL spline length exceeds the rendering limit."); + } - for (int i = 0; i < 32; i++) - { - colorDct[0][i] += yToX * colorDct[1][i]; - colorDct[2][i] += yToB * colorDct[1][i]; - } + currentX += deltaX; + currentY += deltaY; + JxlSpline.ValidatePosition(currentX, currentY); + controlPoints[i + 1] = new(currentX, currentY); + } - // These unsigned sums estimate rendering work, not stored color values. Start every channel at zero. - // Checked arithmetic rejects extreme decoded coefficients before a wrapped sum can appear inexpensive. - InlineArray3 colorStorage = default; - Span color = colorStorage; + float inverseQuant = InverseAdjustedQuant(quantizationAdjustment); + Span> colorDct = result.ColorDct; - for (int channel = 0; channel < 3; channel++) - { - for (int i = 0; i < 32; i++) - { - color[channel] += (ulong)MathF.Ceiling(inverseQuant * MathF.Abs(this.ColorDct[channel][i])); - } - } + for (int channel = 0; channel < 3; channel++) + { + for (int i = 0; i < 32; i++) + { + float inverseDctFactor = i == 0 ? JxlDctScales.Sqrt05 : 1F; + colorDct[channel][i] = this.ColorDct[channel][i] * inverseDctFactor * ChannelWeight[channel] * inverseQuant; + } + } - color[0] += (ulong)MathF.Ceiling(MathF.Abs(yToX)) * color[1]; - color[2] += (ulong)MathF.Ceiling(MathF.Abs(yToB)) * color[1]; + for (int i = 0; i < 32; i++) + { + colorDct[0][i] += yToX * colorDct[1][i]; + colorDct[2][i] += yToB * colorDct[1][i]; + } - ulong maximumColor = Math.Max(color[1], Math.Max(color[0], color[2])); - ulong logColor = Math.Max(1UL, JxlMath.CeilLog2Nonzero(1UL + maximumColor)); + ulong logColor; - // Width squared, color magnitude and path length jointly estimate the rendering area. - // Both divisors belong inside the square root, as in the native work estimate. - float weightLimit = MathF.Ceiling(MathF.Sqrt(((float)areaLimit / logColor) / Math.Max(1UL, manhattanDistance))); - Span sigmaDct = result.SigmaDct; - ulong widthEstimate = 0; + try + { + // A coefficient contributes at most 2^31 * 2^28 = 2^59 units. That conversion fits UInt64, + // but summing 32 contributions can reach 2^64. Check only these work-estimate sums and their correlation products. + InlineArray3 colorStorage = default; + Span color = colorStorage; + for (int channel = 0; channel < 3; channel++) + { for (int i = 0; i < 32; i++) { - float inverseDctFactor = i == 0 ? JxlDctScales.Sqrt05 : 1F; - sigmaDct[i] = this.SigmaDct[i] * inverseDctFactor * ChannelWeight[3] * inverseQuant; - float weightF = MathF.Ceiling(inverseQuant * MathF.Abs(this.SigmaDct[i])); - ulong weight = (ulong)MathF.Min(weightLimit, MathF.Max(1F, weightF)); - widthEstimate += weight * weight * logColor; + ulong contribution = (ulong)MathF.Ceiling(inverseQuant * MathF.Abs(this.ColorDct[channel][i])); + color[channel] = checked(color[channel] + contribution); } + } - ulong estimatedArea = widthEstimate * manhattanDistance; - if (estimatedArea > areaLimit - totalEstimatedAreaReached) - { - throw new InvalidImageContentException("JPEG XL splines exceed the rendering-work limit."); - } + // Decoded DC correlation factors have magnitude at most four. Their casts fit; scaling the channel sums can overflow. + ulong xCorrelation = (ulong)MathF.Ceiling(MathF.Abs(yToX)); + ulong bCorrelation = (ulong)MathF.Ceiling(MathF.Abs(yToB)); + color[0] = checked(color[0] + (xCorrelation * color[1])); + color[2] = checked(color[2] + (bCorrelation * color[1])); - totalEstimatedAreaReached += estimatedArea; - return true; - } + ulong maximumColor = Math.Max(color[1], Math.Max(color[0], color[2])); + logColor = Math.Max(1UL, JxlMath.CeilLog2Nonzero(checked(1UL + maximumColor))); } catch (OverflowException exception) { throw new InvalidImageContentException("JPEG XL spline work estimate exceeds the supported range.", exception); } + + // Width squared, color magnitude and path length jointly estimate the rendering area. + // Both divisors belong inside the square root, as in the native work estimate. + float weightLimit = MathF.Ceiling(MathF.Sqrt(((float)areaLimit / logColor) / Math.Max(1UL, manhattanDistance))); + Span sigmaDct = result.SigmaDct; + ulong widthEstimate = 0; + + for (int i = 0; i < 32; i++) + { + float inverseDctFactor = i == 0 ? JxlDctScales.Sqrt05 : 1F; + sigmaDct[i] = this.SigmaDct[i] * inverseDctFactor * ChannelWeight[3] * inverseQuant; + float weightF = MathF.Ceiling(inverseQuant * MathF.Abs(this.SigmaDct[i])); + ulong weight = (ulong)MathF.Min(weightLimit, MathF.Max(1F, weightF)); + widthEstimate += weight * weight * logColor; + } + + // Let A be the area limit, D the path length and L the color logarithm: A <= 2^42, D <= A, L <= 64. + // Including float rounding, W <= 2*sqrt(A/(L*max(1,D)))+1 bounds all 32 width terms and their final area below 2^54. + // These products fit UInt64. A zero-length path contributes no work. + ulong estimatedArea = widthEstimate * manhattanDistance; + if (estimatedArea > areaLimit - totalEstimatedAreaReached) + { + throw new InvalidImageContentException("JPEG XL splines exceed the rendering-work limit."); + } + + totalEstimatedAreaReached += estimatedArea; + return true; } /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineSegmentSpan.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineSegmentSpan.cs deleted file mode 100644 index 57f608ba12..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineSegmentSpan.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; - -/// -/// Identifies the image rows touched by one cached spline segment. -/// -/// The first row that can contain samples. -/// The row immediately after the affected range. -internal struct JxlSplineSegmentSpan(int startInclusive, int endExclusive) -{ - /// - /// Gets or sets the first affected row. - /// - public int StartInclusive { get; set; } = startInclusive; - - /// - /// Gets or sets the row immediately after the affected range. - /// - public int EndExclusive { get; set; } = endExclusive; -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineUtils.cs index 6b333d39ef..494e2de6f8 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineUtils.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineUtils.cs @@ -244,7 +244,7 @@ public static void ComputeSegments( InlineArray3 color, float sigma, List segments, - List segmentSpans) + List segmentSpans) { if (!(float.IsFinite(sigma) && sigma != 0.0f && float.IsFinite(1.0f / sigma) && float.IsFinite(intensity))) { @@ -281,7 +281,7 @@ public static void ComputeSegments( }; segments.Add(segment); - segmentSpans.Add(new JxlSplineSegmentSpan(y0, y1)); + segmentSpans.Add(y0..y1); } /// @@ -335,7 +335,7 @@ public static void SegmentsFromPoints( List<(PointF Point, float Multiplier)> pointsToDraw, float arcLength, List segments, - List segmentsSpans) + List segmentsSpans) { float inverseArcLength = 1.0f / arcLength; int k = 0; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplines.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplines.cs index c36ee30f91..022e1f09c2 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplines.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplines.cs @@ -180,7 +180,7 @@ public void InitializeDrawCache(Configuration configuration, int imageXSize, int try { - List segmentSpans = []; + List segmentSpans = []; List intermediatePoints = []; List<(PointF Point, float Multiplier)> pointsToDraw = []; using JxlSpline spline = new(); @@ -233,28 +233,37 @@ public void InitializeDrawCache(Configuration configuration, int imageXSize, int return; } - // Signed differences record where segments enter and leave the image rows. Int32 storage is required: - // negative departures and segment offsets cannot be represented by the old byte-sized view. - this.segmentYStartOwner = configuration.MemoryAllocator.Allocate(checked(imageYSize + 2), AllocationOptions.Clean); + // Signed differences record where segments enter and leave the image rows. All ranges use forward indices. + // The frame header limits height to 2^30 + 18687, leaving room for two sentinel rows within Int32. + this.segmentYStartOwner = configuration.MemoryAllocator.Allocate(imageYSize + 2, AllocationOptions.Clean); this.segmentYStart = this.segmentYStartOwner.Memory[..(imageYSize + 2)]; Span rowStarts = this.segmentYStart.Span; Span population = rowStarts[1..]; + long indexCount = 0; - foreach (JxlSplineSegmentSpan span in segmentSpans) + foreach (Range span in segmentSpans) { - population[span.StartInclusive]++; - population[span.EndExclusive]--; + population[span.Start.Value]++; + population[span.End.Value]--; + indexCount += span.End.Value - span.Start.Value; + } + + // Each interval length and the number of segments fit Int32, so their total fits Int64. + // Validate the aggregate once before narrowing. Every subsequent row offset is at most this count. + if (indexCount > int.MaxValue) + { + throw new InvalidImageContentException("JPEG XL spline row index exceeds the supported length."); } int total = 0; int coverage = 0; - // Convert the difference counts to insertion offsets. The checked sum bounds the allocator size and later list indices. + // Coverage is an active-segment count, bounded by segmentSpans.Count. The validated total bounds every insertion offset. for (int y = 0; y < imageYSize; y++) { - coverage = checked(coverage + population[y]); + coverage += population[y]; population[y] = total; - total = checked(total + coverage); + total += coverage; } this.segmentIndicesOwner = configuration.MemoryAllocator.Allocate(total); @@ -265,9 +274,9 @@ public void InitializeDrawCache(Configuration configuration, int imageXSize, int // one element after rowStarts, the finished rowStarts[y..y+2] is exactly that row's segment range. for (int i = 0; i < segmentSpans.Count; i++) { - JxlSplineSegmentSpan span = segmentSpans[i]; + Range span = segmentSpans[i]; - for (int y = span.StartInclusive; y < span.EndExclusive; y++) + for (int y = span.Start.Value; y < span.End.Value; y++) { indices[population[y]++] = i; } From b27f8edd938a8c87084e37541b75acb1c3825599 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 14:19:54 +1000 Subject: [PATCH 257/328] Implement JPEG XL size fields and exact aspect-ratio arithmetic --- .../Jxl/IO/Metadata/JxlPreviewHeader.cs | 2 +- .../Formats/Jxl/IO/Metadata/JxlSizeHeader.cs | 82 +++++++++++-- .../Primitives/JxlAspectRatioHelpers.cs | 20 +++- .../Formats/Jxl/IO/FieldsTests.cs | 113 +++++++++++++++++- 4 files changed, 203 insertions(+), 14 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlPreviewHeader.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlPreviewHeader.cs index e46763cfeb..0b6bc1a43d 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlPreviewHeader.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlPreviewHeader.cs @@ -24,7 +24,7 @@ public int XSize { SignedRational signedRational = JxlAspectRatioHelpers.FixedAspectRatios(this.ratio); - return JxlAspectRatioHelpers.MultiplyTruncate(signedRational, this.YSize); + return (int)JxlAspectRatioHelpers.MultiplyTruncate(signedRational, this.YSize); } return this.div8 ? (this.xSizeDiv8 * 8) : this.xSize; diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlSizeHeader.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlSizeHeader.cs index e40720a53c..8fb6fcb3c8 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlSizeHeader.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlSizeHeader.cs @@ -1,13 +1,26 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +/// +/// Stores the JPEG XL image dimensions and their compact wire representation. +/// internal sealed class JxlSizeHeader : IJxlFields { + /// + /// Dimension alternatives store the positive dimension minus one in 9, 13, 18, or 30 bits. + /// + private static readonly JxlU32Enc DimensionEncoding = new( + JxlFieldExpressions.BitsOffset(9, 1), + JxlFieldExpressions.BitsOffset(13, 1), + JxlFieldExpressions.BitsOffset(18, 1), + JxlFieldExpressions.BitsOffset(30, 1)); + private bool isSmall; private int ySizeDiv8Minus1; private int ySize; @@ -15,8 +28,19 @@ internal sealed class JxlSizeHeader : IJxlFields private int xSizeDiv8Minus1; private int xSize; + /// + /// Initializes a size header with the wire defaults of one pixel in each dimension. + /// + public JxlSizeHeader() => JxlBundle.Init(this); + + /// + /// Gets the image height in pixels. + /// public int YSize => this.isSmall ? ((this.ySizeDiv8Minus1 + 1) * 8) : this.ySize; + /// + /// Gets the image width in pixels. A built-in aspect ratio truncates the derived width to a whole pixel. + /// public int XSize { get @@ -25,27 +49,27 @@ public int XSize { SignedRational aspectRatio = JxlAspectRatioHelpers.FixedAspectRatios(this.ratio); - return JxlAspectRatioHelpers.MultiplyTruncate(aspectRatio, this.YSize); + return (int)JxlAspectRatioHelpers.MultiplyTruncate(aspectRatio, this.YSize); } return this.isSmall ? ((this.xSizeDiv8Minus1 + 1) * 8) : this.xSize; } } + /// + /// Selects the compact representation of the supplied positive image dimensions. + /// + /// The image width in pixels. + /// The image height in pixels. public void Set(int x, int y) { - if (x > int.MaxValue || y > int.MaxValue) + if (x <= 0 || y <= 0) { - throw new ArgumentException("Image too large"); - } - - if (x == 0 || y == 0) - { - throw new ArgumentException("Empty image"); + throw new ArgumentException("Image dimensions must be positive."); } this.ratio = JxlAspectRatioHelpers.FindAspectRatio(x, y); - this.isSmall = y < 256 && (y % JxlFrameDimensions.BlockDimensions) == 0 + this.isSmall = y <= 256 && (y % JxlFrameDimensions.BlockDimensions) == 0 && (this.ratio != 0 || (x <= 256 && (x % JxlFrameDimensions.BlockDimensions) == 0)); if (this.isSmall) @@ -70,5 +94,43 @@ public void Set(int x, int y) } } - public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); + /// + public bool Visit(JxlVisitor visitor) + { + if (!visitor.Boolean(false, ref this.isSmall)) + { + return false; + } + + // The small form stores multiples of eight from 8 through 256. Other dimensions use the four U32 alternatives. + // Keep the two conditions separate: the default visitor must initialize both stored representations. + if (visitor.Conditional(this.isSmall) && !visitor.Bits(5, 0, ref Unsafe.As(ref this.ySizeDiv8Minus1))) + { + return false; + } + + if (visitor.Conditional(!this.isSmall) && !visitor.U32(DimensionEncoding, 1, ref Unsafe.As(ref this.ySize))) + { + return false; + } + + // Ratio zero sends the width explicitly. Codes 1 through 7 derive it from the height, with integer truncation. + if (!visitor.Bits(3, 0, ref Unsafe.As(ref this.ratio))) + { + return false; + } + + if (visitor.Conditional(this.ratio == 0 && this.isSmall) && !visitor.Bits(5, 0, ref Unsafe.As(ref this.xSizeDiv8Minus1))) + { + return false; + } + + if (visitor.Conditional(this.ratio == 0 && !this.isSmall) && !visitor.U32(DimensionEncoding, 1, ref Unsafe.As(ref this.xSize))) + { + return false; + } + + // The encoded height fits Int32. A derived width can be twice that height, so validate it before the getter narrows it. + return this.ratio == 0 || JxlAspectRatioHelpers.MultiplyTruncate(JxlAspectRatioHelpers.FixedAspectRatios(this.ratio), this.YSize) <= int.MaxValue; + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlAspectRatioHelpers.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlAspectRatioHelpers.cs index 1380f3d03b..3658a5fdfc 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlAspectRatioHelpers.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlAspectRatioHelpers.cs @@ -31,19 +31,35 @@ internal static class JxlAspectRatioHelpers /// A well-known aspect ratio. public static SignedRational FixedAspectRatios(int ratio) => Ratios[ratio - 1]; + /// + /// Finds the first wire aspect-ratio code that reproduces the width after truncation. + /// + /// The image width in pixels. + /// The image height in pixels. + /// A code from one through seven, or zero when the width must be stored explicitly. public static int FindAspectRatio(int x, int y) { for (int i = 0; i < 7; i++) { if (x == MultiplyTruncate(Ratios[i], y)) { - return i; + return i + 1; } } return 0; } + /// + /// Multiplies a positive dimension by an aspect ratio and truncates to whole pixels. + /// + /// The positive aspect ratio. + /// The image height in pixels. + /// The derived width before validation against the caller's dimension limit. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int MultiplyTruncate(SignedRational rational, int multiplicand) => (multiplicand * rational.Numerator) / rational.Denominator; + public static long MultiplyTruncate(SignedRational rational, int multiplicand) + { + // Widen before multiplying. Valid 30-bit dimensions can overflow Int32 even when the final quotient fits. + return ((long)multiplicand * rational.Numerator) / rational.Denominator; + } } diff --git a/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs index d835c560b2..998b7501c8 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs @@ -18,6 +18,116 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jxl.IO; public class FieldsTests { + /// + /// Checks image dimensions against the size-header fields in libjxl headers.cc, including every built-in ratio. + /// + /// The fixed, least-significant-bit-first header bytes. + /// The decoded image width in pixels. + /// The decoded image height in pixels. + /// The header length before byte padding. + [Theory] + [InlineData(new byte[] { 0x00, 0x10 }, 1, 1, 15)] + [InlineData(new byte[] { 0x41, 0x00 }, 8, 8, 9)] + [InlineData(new byte[] { 0x7F, 0x00 }, 256, 256, 9)] + [InlineData(new byte[] { 0x05, 0x08 }, 40, 24, 14)] + [InlineData(new byte[] { 0x80, 0x00, 0x18, 0x00 }, 13, 17, 26)] + [InlineData(new byte[] { 0x40, 0x10 }, 9, 9, 15)] + [InlineData(new byte[] { 0x40, 0x20 }, 10, 9, 15)] + [InlineData(new byte[] { 0x40, 0x30 }, 12, 9, 15)] + [InlineData(new byte[] { 0x40, 0x40 }, 13, 9, 15)] + [InlineData(new byte[] { 0x40, 0x50 }, 16, 9, 15)] + [InlineData(new byte[] { 0x40, 0x60 }, 11, 9, 15)] + [InlineData(new byte[] { 0x40, 0x70 }, 18, 9, 15)] + [InlineData(new byte[] { 0x02, 0x10, 0x01 }, 513, 513, 19)] + [InlineData(new byte[] { 0x04, 0x00, 0x21 }, 8193, 8193, 24)] + [InlineData(new byte[] { 0x06, 0x00, 0x20, 0x00, 0x02 }, 262145, 262145, 36)] + [InlineData(new byte[] { 0xFE, 0xFF, 0xFF, 0xFF, 0x0B }, 1908874353, 1073741824, 36)] + [InlineData(new byte[] { 0xF6, 0xFF, 0xFF, 0xFF, 0x0F }, 2147483646, 1073741823, 36)] + public void SizeHeader_ReadAndWriteMatchesFixedWire(byte[] wire, int width, int height, int bitCount) + { + // Read independent wire bytes into a reused header so earlier size representations cannot determine the result. + using MemoryStream input = new(wire, false); + JxlBitReader reader = new(input); + JxlSizeHeader size = new(); + size.Set(40, 24); + + Assert.True(JxlBundle.Read(reader, size)); + Assert.Equal((width, height), (size.XSize, size.YSize)); + Assert.Equal(bitCount, reader.TotalBitsConsumed); + + // Select the native compact form from dimensions, then compare its output with the same fixed wire bytes. + size.Set(width, height); + using MemoryStream output = new(); + JxlBitWriter writer = new(output); + + Assert.True(JxlBundle.Write(size, writer, JxlLayerType.Header, null)); + Assert.Equal(bitCount, writer.BitsWritten); + + writer.ZeroPadToByte(); + + Assert.Equal(wire, output.ToArray()); + } + + /// + /// Checks that initialization and explicit reset restore explicit one-by-one size fields. + /// + [Fact] + public void SizeHeader_DefaultsRestoreOnePixelDimensions() + { + JxlSizeHeader size = new(); + + Assert.Equal((1, 1), (size.XSize, size.YSize)); + Assert.True(JxlBundle.AllDefault(size)); + + size.Set(40, 24); + JxlBundle.SetDefault(size); + + Assert.Equal((1, 1), (size.XSize, size.YSize)); + Assert.True(JxlBundle.AllDefault(size)); + + // Defaults use explicit dimensions, not the square ratio selected by Set. Both nine-bit values store dimension minus one. + using MemoryStream output = new(); + JxlBitWriter writer = new(output); + + Assert.True(JxlBundle.Write(size, writer, JxlLayerType.Header, null)); + Assert.Equal(26, writer.BitsWritten); + + writer.ZeroPadToByte(); + + Assert.Equal(new byte[] { 0, 0, 0, 0 }, output.ToArray()); + } + + /// + /// Checks the derived width before it can overflow the Int32 image-dimension contract. + /// + [Fact] + public void ReadSizeHeader_RejectsDerivedWidthOutsideImageRange() + { + // Height is 2^30 and ratio seven is 2:1. The encoded height fits, but its width is 2^31. + using MemoryStream input = new([0xFE, 0xFF, 0xFF, 0xFF, 0x0F]); + JxlBitReader reader = new(input); + JxlSizeHeader size = new(); + + Assert.False(JxlBundle.Read(reader, size)); + } + + /// + /// Checks that the encoder boundary rejects dimensions that cannot describe an image. + /// + /// The supplied width. + /// The supplied height. + [Theory] + [InlineData(0, 1)] + [InlineData(1, 0)] + [InlineData(-1, 1)] + [InlineData(1, -1)] + public void SizeHeader_SetRejectsNonpositiveDimensions(int width, int height) + { + JxlSizeHeader size = new(); + + Assert.Throws(() => size.Set(width, height)); + } + /// /// Checks box-header byte order, minimum sizes, and final unbounded boxes against fixed wire bytes. /// @@ -1123,7 +1233,8 @@ public void TestRoundtripFrame() public void TestOutOfRange() { JxlSizeHeader h = new(); - h.Set(unchecked((int)0xFFFFFFFF), unchecked((int)0xFFFFFFFF)); + // Positive Int32 dimensions are valid inputs to Set, but exceed the largest 30-bit wire representation. + h.Set(int.MaxValue, int.MaxValue); int extensionBits = 999; long totalBits = 999; From f865f912b4d90b0f9c644778e9d18f26e4a4d313 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 14:27:08 +1000 Subject: [PATCH 258/328] Complete JPEG XL bit-depth preview and animation fields --- .../Jxl/IO/Metadata/JxlAnimationHeader.cs | 90 ++++++++- .../Jxl/IO/Metadata/JxlBitDepthMetadata.cs | 33 ++-- .../Jxl/IO/Metadata/JxlPreviewHeader.cs | 81 +++++++- .../Formats/Jxl/IO/FieldsTests.cs | 173 ++++++++++++++++++ 4 files changed, 356 insertions(+), 21 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlAnimationHeader.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlAnimationHeader.cs index 647e8bbc7d..953351ec89 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlAnimationHeader.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlAnimationHeader.cs @@ -5,15 +5,95 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +/// +/// Stores the JPEG XL animation time base, repetition count and timecode flag. +/// internal sealed class JxlAnimationHeader : IJxlFields { - public int TpsNumerator { get; set; } + private uint tpsNumerator; + private uint tpsDenominator; + private uint loopCount; + private bool containsTimecodes; - public int TpsDenominator { get; set; } + /// + /// Initializes an animation header with one tick per second and unlimited repetition. + /// + public JxlAnimationHeader() => JxlBundle.Init(this); - public int LoopCount { get; set; } + /// + /// Gets or sets the numerator of the animation ticks-per-second ratio. + /// + public uint TpsNumerator + { + get => this.tpsNumerator; + set => this.tpsNumerator = value; + } - public bool ContainsTimecodes { get; set; } + /// + /// Gets or sets the denominator of the animation ticks-per-second ratio. + /// + public uint TpsDenominator + { + get => this.tpsDenominator; + set => this.tpsDenominator = value; + } - public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); + /// + /// Gets or sets the number of times to play the animation. Zero means unlimited playback. + /// + public uint LoopCount + { + get => this.loopCount; + set => this.loopCount = value; + } + + /// + /// Gets or sets whether frame headers include timecodes. + /// + public bool ContainsTimecodes + { + get => this.containsTimecodes; + set => this.containsTimecodes = value; + } + + /// + public bool Visit(JxlVisitor visitor) + { + // The positive numerator and denominator form ticks per second. Common rates have direct two-bit alternatives. + if (!visitor.U32( + JxlFieldExpressions.Value(100), + JxlFieldExpressions.Value(1000), + JxlFieldExpressions.BitsOffset(10, 1), + JxlFieldExpressions.BitsOffset(30, 1), + 1, + ref this.tpsNumerator)) + { + return false; + } + + if (!visitor.U32( + JxlFieldExpressions.Value(1), + JxlFieldExpressions.Value(1001), + JxlFieldExpressions.BitsOffset(8, 1), + JxlFieldExpressions.BitsOffset(10, 1), + 1, + ref this.tpsDenominator)) + { + return false; + } + + // Loop count has a full UInt32 alternative. Keep it unsigned instead of narrowing the high-bit values to Int32. + if (!visitor.U32( + JxlFieldExpressions.Value(0), + JxlFieldExpressions.Bits(3), + JxlFieldExpressions.Bits(16), + JxlFieldExpressions.Bits(32), + 0, + ref this.loopCount)) + { + return false; + } + + return visitor.Boolean(false, ref this.containsTimecodes); + } } diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlBitDepthMetadata.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlBitDepthMetadata.cs index a6c9f72482..de8f0ccb3f 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlBitDepthMetadata.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlBitDepthMetadata.cs @@ -10,6 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; /// internal sealed class JxlBitDepthMetadata : IJxlFields { + private bool floatingPointSample; private uint bitsPerSample; private uint exponentBitsPerSample; @@ -19,15 +20,17 @@ internal sealed class JxlBitDepthMetadata : IJxlFields public JxlBitDepthMetadata() => JxlBundle.Init(this); /// - /// Gets or sets a value indicating whether - /// the original (uncompressed) samples are floating point or - /// unsigned integer. + /// Gets or sets whether the original samples use floating-point storage instead of unsigned integer storage. /// - public bool FloatingPointSample { get; set; } + public bool FloatingPointSample + { + get => this.floatingPointSample; + set => this.floatingPointSample = value; + } /// /// Gets or sets the bit depth of the original (uncompressed) image samples. - /// Must be in the range [1, 32]. + /// Integer samples use 1 through 31 bits. Floating-point samples must satisfy the exponent and mantissa limits. /// public uint BitsPerSample { @@ -57,8 +60,15 @@ public uint ExponentBitsPerSample set => this.exponentBitsPerSample = value; } + /// public bool Visit(JxlVisitor visitor) { + // The leading flag selects different U32 alternatives for the same bit-depth field. It is part of every bit-depth header. + if (!visitor.Boolean(false, ref this.floatingPointSample)) + { + return false; + } + if (!this.FloatingPointSample) { bool successful = visitor.U32( @@ -89,22 +99,21 @@ public bool Visit(JxlVisitor visitor) return false; } - this.exponentBitsPerSample--; + // The four-bit wire value is exponent width minus one. Publish the stored width only after the field succeeds. + uint encodedExponent = unchecked(this.exponentBitsPerSample - 1U); - if (!visitor.Bits(4, 7, ref this.exponentBitsPerSample)) + if (!visitor.Bits(4, 7, ref encodedExponent)) { return false; } - this.exponentBitsPerSample++; + this.exponentBitsPerSample = unchecked(encodedExponent + 1U); } if (this.FloatingPointSample) { if (this.exponentBitsPerSample is < 2 or > 8) { - DebugGuard.IsTrue(false, "Invalid exponent_bits_per_sample"); - return false; } @@ -112,15 +121,11 @@ public bool Visit(JxlVisitor visitor) if (mantissaBits is < 2 or > 23) { - DebugGuard.IsTrue(false, "Invalid bits_per_sample"); - return false; } } else if (this.bitsPerSample > 31) { - DebugGuard.IsTrue(false, "Invalid bits_per_sample"); - return false; } diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlPreviewHeader.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlPreviewHeader.cs index 0b6bc1a43d..27f124bad5 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlPreviewHeader.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlPreviewHeader.cs @@ -1,12 +1,35 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +/// +/// Stores the JPEG XL preview dimensions and their compact wire representation. +/// internal sealed class JxlPreviewHeader : IJxlFields { + /// + /// Compact dimensions use units of eight pixels. The four alternatives cover 1 through 544 units. + /// + private static readonly JxlU32Enc DividedDimensionEncoding = new( + JxlFieldExpressions.Value(16), + JxlFieldExpressions.Value(32), + JxlFieldExpressions.BitsOffset(5, 1), + JxlFieldExpressions.BitsOffset(9, 33)); + + /// + /// Explicit pixel dimensions occupy consecutive ranges: 1..64, 65..320, 321..1344, and 1345..5440. + /// + private static readonly JxlU32Enc DimensionEncoding = new( + JxlFieldExpressions.BitsOffset(6, 1), + JxlFieldExpressions.BitsOffset(8, 65), + JxlFieldExpressions.BitsOffset(10, 321), + JxlFieldExpressions.BitsOffset(12, 1345)); + private bool div8; private int ySizeDiv8; private int ySize; @@ -14,8 +37,19 @@ internal sealed class JxlPreviewHeader : IJxlFields private int xSizeDiv8; private int xSize; + /// + /// Initializes a preview header with the wire defaults of one pixel in each dimension. + /// + public JxlPreviewHeader() => JxlBundle.Init(this); + + /// + /// Gets the preview height in pixels. + /// public int YSize => this.div8 ? (this.ySizeDiv8 * 8) : this.ySize; + /// + /// Gets the preview width in pixels. A built-in aspect ratio truncates the derived width to a whole pixel. + /// public int XSize { get @@ -31,9 +65,14 @@ public int XSize } } + /// + /// Selects the native compact representation of the supplied positive preview dimensions. + /// + /// The preview width in pixels. + /// The preview height in pixels. public void Set(int x, int y) { - if (x == 0 || y == 0) + if (x <= 0 || y <= 0) { throw new ArgumentException("Empty preview"); } @@ -64,5 +103,43 @@ public void Set(int x, int y) } } - public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); + /// + public bool Visit(JxlVisitor visitor) + { + if (!visitor.Boolean(false, ref this.div8)) + { + return false; + } + + // Preview dimensions have their own smaller distributions. Do not use the full image-size distribution here. + // Separate conditions also let the default visitor initialize both stored representations. + if (visitor.Conditional(this.div8) && !visitor.U32(DividedDimensionEncoding, 1, ref Unsafe.As(ref this.ySizeDiv8))) + { + return false; + } + + if (visitor.Conditional(!this.div8) && !visitor.U32(DimensionEncoding, 1, ref Unsafe.As(ref this.ySize))) + { + return false; + } + + if (!visitor.Bits(3, 0, ref Unsafe.As(ref this.ratio))) + { + return false; + } + + // A nonzero ratio supplies the width. Otherwise the width uses the same distribution and units as the height. + if (visitor.Conditional(this.ratio == 0 && this.div8) && !visitor.U32(DividedDimensionEncoding, 1, ref Unsafe.As(ref this.xSizeDiv8))) + { + return false; + } + + if (visitor.Conditional(this.ratio == 0 && !this.div8) && !visitor.U32(DimensionEncoding, 1, ref Unsafe.As(ref this.xSize))) + { + return false; + } + + // Wire dimensions are at most 5440 pixels, so even the widest built-in ratio fits the Int32 dimension contract. + return true; + } } diff --git a/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs index 998b7501c8..0e5a8baab8 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs @@ -1189,6 +1189,179 @@ public void TestPreview() } } + /// + /// Checks the sample-type flag, bit-depth alternatives and exponent offset against fixed native header fields. + /// + /// The fixed bit-depth header bytes. + /// Whether samples use floating-point storage. + /// The complete sample width in bits. + /// The exponent width, or zero for integer samples. + /// The header length before byte padding. + [Theory] + [InlineData(new byte[] { 0x00 }, false, 8U, 0U, 3)] + [InlineData(new byte[] { 0x02 }, false, 10U, 0U, 3)] + [InlineData(new byte[] { 0x04 }, false, 12U, 0U, 3)] + [InlineData(new byte[] { 0x06, 0x00 }, false, 1U, 0U, 9)] + [InlineData(new byte[] { 0xF6, 0x00 }, false, 31U, 0U, 9)] + [InlineData(new byte[] { 0x39 }, true, 32U, 8U, 7)] + [InlineData(new byte[] { 0x23 }, true, 16U, 5U, 7)] + [InlineData(new byte[] { 0x35 }, true, 24U, 7U, 7)] + [InlineData(new byte[] { 0x27, 0x02 }, true, 5U, 2U, 13)] + public void BitDepthHeader_ReadAndWriteMatchesFixedWire(byte[] wire, bool floatingPoint, uint sampleBits, uint exponentBits, int bitCount) + { + // The leading Boolean selects the U32 alternatives. Integer input must also clear a previous floating-point exponent width. + JxlBitDepthMetadata depth = new() { FloatingPointSample = true, BitsPerSample = 16, ExponentBitsPerSample = 5 }; + using MemoryStream input = new(wire, false); + JxlBitReader reader = new(input); + + Assert.True(JxlBundle.Read(reader, depth)); + Assert.Equal((floatingPoint, sampleBits, exponentBits), (depth.FloatingPointSample, depth.BitsPerSample, depth.ExponentBitsPerSample)); + Assert.Equal(bitCount, reader.TotalBitsConsumed); + + using MemoryStream output = new(); + JxlBitWriter writer = new(output); + + Assert.True(JxlBundle.Write(depth, writer, JxlLayerType.Header, null)); + Assert.Equal(bitCount, writer.BitsWritten); + + writer.ZeroPadToByte(); + + Assert.Equal(wire, output.ToArray()); + } + + /// + /// Checks invalid integer width, exponent width and mantissa width at the bit-depth parser boundary. + /// + /// The fixed header bytes containing an invalid sample representation. + [Theory] + [InlineData(new byte[] { 0xFE, 0x00 })] + [InlineData(new byte[] { 0x41 })] + [InlineData(new byte[] { 0x1F, 0x02 })] + [InlineData(new byte[] { 0x07, 0x0F })] + public void ReadBitDepthHeader_RejectsInvalidSampleRepresentation(byte[] wire) + { + // These encode integer32, float32/exponent9, float4/exponent2 and float33/exponent8, respectively. + // The last two leave one or 24 mantissa bits; native JPEG XL requires two through 23. + using MemoryStream input = new(wire, false); + JxlBitReader reader = new(input); + + Assert.False(JxlBundle.Read(reader, new JxlBitDepthMetadata())); + } + + /// + /// Checks the preview-specific dimension distributions against fixed native header fields. + /// + /// The fixed preview header bytes. + /// The preview width in pixels. + /// The preview height in pixels. + /// The header length before byte padding. + [Theory] + [InlineData(new byte[] { 0x00, 0x02 }, 1, 1, 12)] + [InlineData(new byte[] { 0x09 }, 128, 128, 6)] + [InlineData(new byte[] { 0x0B }, 256, 256, 6)] + [InlineData(new byte[] { 0x0D, 0x04 }, 24, 16, 11)] + [InlineData(new byte[] { 0x02, 0x08 }, 65, 65, 14)] + [InlineData(new byte[] { 0x04, 0x20 }, 321, 321, 16)] + [InlineData(new byte[] { 0x06, 0x80, 0x00 }, 1345, 1345, 18)] + [InlineData(new byte[] { 0xFF, 0x1F }, 4352, 4352, 15)] + [InlineData(new byte[] { 0xFE, 0x7F, 0xEC, 0xFF }, 5439, 5440, 32)] + public void PreviewHeader_ReadAndWriteMatchesFixedWire(byte[] wire, int width, int height, int bitCount) + { + // Compact previews store units of eight pixels; other previews use their own ranges up to 5440 pixels. + // The largest case has an odd width so Set retains the non-divided form instead of the smaller compact distribution. + JxlPreviewHeader preview = new(); + preview.Set(24, 16); + using MemoryStream input = new(wire, false); + JxlBitReader reader = new(input); + + Assert.True(JxlBundle.Read(reader, preview)); + Assert.Equal((width, height), (preview.XSize, preview.YSize)); + Assert.Equal(bitCount, reader.TotalBitsConsumed); + + preview.Set(width, height); + using MemoryStream output = new(); + JxlBitWriter writer = new(output); + + Assert.True(JxlBundle.Write(preview, writer, JxlLayerType.Header, null)); + Assert.Equal(bitCount, writer.BitsWritten); + + writer.ZeroPadToByte(); + + Assert.Equal(wire, output.ToArray()); + } + + /// + /// Checks animation timing, repetition and timecode fields against fixed native header fields. + /// + /// The fixed animation header bytes. + /// The ticks-per-second numerator. + /// The ticks-per-second denominator. + /// The repetition count, or zero for unlimited repetition. + /// Whether frames include timecodes. + /// The header length before byte padding. + [Theory] + [InlineData(new byte[] { 0x02, 0x00, 0x00 }, 1U, 1U, 0U, false, 17)] + [InlineData(new byte[] { 0x50, 0x00 }, 100U, 1U, 1U, false, 10)] + [InlineData(new byte[] { 0x45 }, 1000U, 1001U, 0U, true, 7)] + [InlineData(new byte[] { 0x5E, 0x80, 0xFF, 0xFF, 0x00 }, 24U, 1U, 65535U, false, 33)] + [InlineData(new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F }, 1073741824U, 1024U, uint.MaxValue, true, 79)] + public void AnimationHeader_ReadAndWriteMatchesFixedWire(byte[] wire, uint numerator, uint denominator, uint loops, bool timecodes, int bitCount) + { + // The final case uses all payload bits of the longest alternatives. In particular, the loop count must remain unsigned. + using MemoryStream input = new(wire, false); + JxlBitReader reader = new(input); + JxlAnimationHeader animation = new(); + + Assert.True(JxlBundle.Read(reader, animation)); + Assert.Equal( + (numerator, denominator, loops, timecodes), + (animation.TpsNumerator, animation.TpsDenominator, animation.LoopCount, animation.ContainsTimecodes)); + + Assert.Equal(bitCount, reader.TotalBitsConsumed); + + using MemoryStream output = new(); + JxlBitWriter writer = new(output); + + Assert.True(JxlBundle.Write(animation, writer, JxlLayerType.Header, null)); + Assert.Equal(bitCount, writer.BitsWritten); + + writer.ZeroPadToByte(); + + Assert.Equal(wire, output.ToArray()); + } + + /// + /// Checks that each header initializes its defaults and restores them after reuse. + /// + [Fact] + public void MetadataHeaders_DefaultsRestoreAfterReuse() + { + JxlBitDepthMetadata depth = new(); + JxlPreviewHeader preview = new(); + JxlAnimationHeader animation = new(); + + Assert.Equal((false, 8U, 0U), (depth.FloatingPointSample, depth.BitsPerSample, depth.ExponentBitsPerSample)); + Assert.Equal((1, 1), (preview.XSize, preview.YSize)); + Assert.Equal((1U, 1U, 0U, false), (animation.TpsNumerator, animation.TpsDenominator, animation.LoopCount, animation.ContainsTimecodes)); + + depth.FloatingPointSample = true; + depth.BitsPerSample = 16; + depth.ExponentBitsPerSample = 5; + preview.Set(256, 256); + animation.TpsNumerator = 1000; + animation.TpsDenominator = 1001; + animation.LoopCount = uint.MaxValue; + animation.ContainsTimecodes = true; + + JxlBundle.SetDefault(depth); + JxlBundle.SetDefault(preview); + JxlBundle.SetDefault(animation); + + Assert.Equal((false, 8U, 0U), (depth.FloatingPointSample, depth.BitsPerSample, depth.ExponentBitsPerSample)); + Assert.Equal((1, 1), (preview.XSize, preview.YSize)); + Assert.Equal((1U, 1U, 0U, false), (animation.TpsNumerator, animation.TpsDenominator, animation.LoopCount, animation.ContainsTimecodes)); + } + [Fact] public void TestRoundtripFrame() { From bfe6615832d635339dc77ca203b69ff0ace14bea Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 14:33:31 +1000 Subject: [PATCH 259/328] Correct JPEG XL progressive pass fields and resolution targets --- .../Formats/Jxl/IO/FrameHeader/JxlPasses.cs | 66 +++++++++-------- .../Formats/Jxl/IO/FieldsTests.cs | 70 +++++++++++++++++++ 2 files changed, 108 insertions(+), 28 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlPasses.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlPasses.cs index 87277414ac..4b5d1934f1 100644 --- a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlPasses.cs +++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlPasses.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; /// -/// Used for decoding to lower resolutions. +/// Stores progressive-pass coefficient shifts and resolution transitions. /// internal sealed class JxlPasses : IJxlFields { @@ -18,6 +18,11 @@ internal sealed class JxlPasses : IJxlFields private uint numPasses; private uint numDownsample; + /// + /// Initializes one full-resolution pass with no explicit downsampling transitions. + /// + public JxlPasses() => JxlBundle.Init(this); + /// /// Gets or sets the number of passes. /// @@ -28,7 +33,7 @@ public uint NumberOfPasses } /// - /// Gets or sets the number of downsamples. + /// Gets or sets the number of declared resolution transitions. /// public uint NumberOfDownsamples { @@ -37,20 +42,26 @@ public uint NumberOfDownsamples } /// - /// Gets the downsample values. + /// Gets the spatial reduction factor reached at each declared transition. /// public uint[] Downsample { get; } = new uint[MaxPasses]; /// - /// Gets the last pass values. + /// Gets the zero-based final pass index for each resolution transition. /// public uint[] LastPass { get; } = new uint[MaxPasses]; /// - /// Gets the shift values. + /// Gets the coefficient bit shift for each pass. The final pass has an implicit zero shift. /// public uint[] Shift { get; } = new uint[MaxPasses]; + /// + /// Gets the spatial refinement shifts covered by one valid pass index. + /// + /// The zero-based pass index, below NumberOfPasses. + /// The smallest shift reconstructed by this pass. + /// The largest shift reconstructed by this pass. public void GetDownsamplingBracket(int pass, out int minShift, out int maxShift) { maxShift = 2; @@ -100,18 +111,24 @@ public void GetDownsamplingBracket(int pass, out int minShift, out int maxShift) } } - public uint GetDownsamplingTargetForCompletedPasses(int num) + /// + /// Gets the downsampling factor after the specified number of completed passes. + /// + /// The number of passes already decoded. + /// The available factor: eight before refinement, then the declared factors, and one after all passes. + public uint GetDownsamplingTargetForCompletedPasses(int completedPassCount) { - if (num >= this.numPasses) + if (completedPassCount >= this.numPasses) { return 1; } - uint result = 0; + // Before any declared transition, JPEG XL exposes the one-eighth-resolution DC image. + uint result = 8; for (int i = 0; i < this.numDownsample; i++) { - if (num > this.LastPass[i]) + if (completedPassCount > this.LastPass[i]) { result = Math.Min(result, this.Downsample[i]); } @@ -120,26 +137,24 @@ public uint GetDownsamplingTargetForCompletedPasses(int num) return result; } + /// public bool Visit(JxlVisitor visitor) { - if (visitor.U32( + // This distribution bounds the count to 1..11, which also bounds the fixed pass arrays. + if (!visitor.U32( JxlFieldExpressions.Value(1), JxlFieldExpressions.Value(2), - JxlFieldExpressions.Value(2), - JxlFieldExpressions.BitsOffset(1, 3), - 0, + JxlFieldExpressions.Value(3), + JxlFieldExpressions.BitsOffset(3, 4), + 1, ref this.numPasses)) { return false; } - if (this.numPasses > MaxPasses) - { - return false; - } - if (visitor.Conditional(this.numPasses != 1)) { + // There are at most four transitions because their factors must strictly decrease through 8,4,2,1. if (!visitor.U32( JxlFieldExpressions.Value(0), JxlFieldExpressions.Value(1), @@ -151,14 +166,9 @@ public bool Visit(JxlVisitor visitor) return false; } - if (this.numDownsample > 4) - { - return false; - } - if (this.numDownsample > this.numPasses) { - throw new InvalidOperationException("Number of downsaples is greater than number of passes"); + return false; } for (int i = 0; i < this.numPasses - 1; i++) @@ -186,7 +196,7 @@ public bool Visit(JxlVisitor visitor) if (i > 0 && this.Downsample[i] >= this.Downsample[i - 1]) { - throw new InvalidOperationException("Downsample sequence should decrease"); + return false; } } @@ -196,7 +206,7 @@ public bool Visit(JxlVisitor visitor) JxlFieldExpressions.Value(0), JxlFieldExpressions.Value(1), JxlFieldExpressions.Value(2), - JxlFieldExpressions.Value(3), + JxlFieldExpressions.Bits(3), 0, ref this.LastPass[i])) { @@ -205,12 +215,12 @@ public bool Visit(JxlVisitor visitor) if (i > 0 && this.LastPass[i] <= this.LastPass[i - 1]) { - throw new InvalidOperationException("Last pass sequence should increase"); + return false; } if (this.LastPass[i] >= this.numPasses) { - throw new InvalidOperationException("Last pass is greater than number of passes"); + return false; } } } diff --git a/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs index 0e5a8baab8..42a267dd3b 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs @@ -18,6 +18,76 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jxl.IO; public class FieldsTests { + /// + /// Checks progressive pass counts, shifts and transition indices against fixed native field encodings. + /// + /// The fixed least-significant-bit-first pass header. + /// The number of progressive passes. + /// The number of explicit resolution transitions. + /// The header length before byte padding. + [Theory] + [InlineData(new byte[] { 0x00 }, 1U, 0U, 2)] + [InlineData(new byte[] { 0x02 }, 3U, 0U, 8)] + [InlineData(new byte[] { 0x03, 0x00 }, 4U, 0U, 13)] + [InlineData(new byte[] { 0x1F, 0x00, 0x00, 0x00 }, 11U, 0U, 27)] + [InlineData(new byte[] { 0x55, 0x00 }, 2U, 1U, 10)] + [InlineData(new byte[] { 0x33, 0x00, 0xA0, 0x0B }, 8U, 1U, 28)] + public void PassesHeader_ReadAndWriteMatchesFixedWire(byte[] wire, uint count, uint transitions, int bitCount) + { + using MemoryStream input = new(wire, false); + JxlBitReader reader = new(input); + JxlPasses passes = new(); + + Assert.True(JxlBundle.Read(reader, passes)); + Assert.Equal(count, passes.NumberOfPasses); + Assert.Equal(transitions, passes.NumberOfDownsamples); + Assert.Equal(0U, passes.Shift[(int)count - 1]); + Assert.Equal(bitCount, reader.TotalBitsConsumed); + + using MemoryStream output = new(); + JxlBitWriter writer = new(output); + + Assert.True(JxlBundle.Write(passes, writer, JxlLayerType.Header, null)); + Assert.Equal(bitCount, writer.BitsWritten); + + writer.ZeroPadToByte(); + + Assert.Equal(wire, output.ToArray()); + } + + /// + /// Checks that a declared transition takes effect only after its last pass completes. + /// + [Fact] + public void CompletedPasses_SelectDeclaredResolutionTargets() + { + // Eight passes declare factor two after pass index five. Before that, the implicit DC target is factor eight. + using MemoryStream input = new([0x33, 0x00, 0xA0, 0x0B]); + JxlBitReader reader = new(input); + JxlPasses passes = new(); + + Assert.True(JxlBundle.Read(reader, passes)); + Assert.Equal(5U, passes.LastPass[0]); + Assert.Equal(2U, passes.Downsample[0]); + Assert.Equal(8U, passes.GetDownsamplingTargetForCompletedPasses(0)); + Assert.Equal(8U, passes.GetDownsamplingTargetForCompletedPasses(5)); + Assert.Equal(2U, passes.GetDownsamplingTargetForCompletedPasses(6)); + Assert.Equal(1U, passes.GetDownsamplingTargetForCompletedPasses(8)); + } + + /// + /// Checks the cross-field limit on progressive resolution transitions. + /// + [Fact] + public void ReadPassesHeader_RejectsMoreTransitionsThanPasses() + { + // The first two fields declare two passes and three transitions. No shift or transition payload follows. + using MemoryStream input = new([0x0D]); + JxlBitReader reader = new(input); + + Assert.False(JxlBundle.Read(reader, new JxlPasses())); + } + /// /// Checks image dimensions against the size-header fields in libjxl headers.cc, including every built-in ratio. /// From c16e66b2f3ba4437c8d3bc358c9fcbefcd4b1b0b Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 19:09:57 +1000 Subject: [PATCH 260/328] Correct JPEG XL bundle ownership and conversion lifetimes --- .../Encoder/JxlImageBundleEncoder.cs | 93 +++++++--- .../Jxl/Processing/Image/JxlImageBundle.cs | 172 ++++++++++++------ .../RenderPipeline/LowMemoryRenderPipeline.cs | 9 - .../RenderPipeline/WriteToImageBundleStage.cs | 23 +-- 4 files changed, 196 insertions(+), 101 deletions(-) delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/LowMemoryRenderPipeline.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlImageBundleEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlImageBundleEncoder.cs index 42e9c26f89..74583a7cba 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlImageBundleEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlImageBundleEncoder.cs @@ -121,32 +121,52 @@ public static bool TryApplyColorTransform( return false; } - // Copies ib:rect, converts, and copies into out. + /// + /// Copies a region to planar storage using the requested color encoding. + /// + /// The configuration that owns conversion storage. + /// The image metadata, including target intensity. + /// The source image planes and color encoding. + /// The source region in pixels. + /// The required output color encoding. + /// The color transform provider. + /// The destination color planes. + /// Whether the conversion succeeded. public static bool CopyToT( Configuration configuration, JxlImageMetadata metadata, - JxlImageBundle ib, + JxlImageBundle imageBundle, Rectangle rectangle, JxlColorEncoding desiredEncoding, JxlCmsInterface cms, JxlImage3 output) => TryApplyColorTransform( configuration, - ib.CurrentColorEncoding, + imageBundle.CurrentColorEncoding, metadata.IntensityTarget, - ib.Color!, - ib.Black!, + imageBundle.Color!, + imageBundle.Black!, rectangle, desiredEncoding, cms, output); + /// + /// Selects the input bundle or converts it to the requested color encoding. + /// + /// The configuration that owns conversion storage. + /// The source bundle. + /// The required output color encoding. + /// The color transform provider. + /// A distinct caller-owned bundle that receives converted samples when needed. + /// The borrowed input or converted bundle. Its existing owner keeps it alive. + /// Whether the required output encoding is available. public static bool TransformIfNeeded( Configuration configuration, JxlImageBundle input, JxlColorEncoding desiredEncoding, JxlCmsInterface cms, - JxlImageBundle store, + JxlImageBundle convertedImageStorage, ref JxlImageBundle output) { if (input.CurrentColorEncoding.IsSameColorEncoding(desiredEncoding) && !input.ContainsBlack) @@ -155,47 +175,60 @@ public static bool TransformIfNeeded( return true; } - using JxlImage3 color = new(configuration, input.Color!.XSize, input.Color.YSize); + JxlImage3? color = new(configuration, input.Color!.XSize, input.Color.YSize); + List>? extraChannels = null; - if (!JxlImageOperations.CopyImage(input.Color!, color)) + try { - return false; - } - - if (!store.SetFromImage(color, input.CurrentColorEncoding)) - { - return false; - } + JxlImageOperations.ForceCopyImage(input.Color!, color); - if (input.ContainsExtraChannels()) - { - List> extraChannels = []; + // The destination owns its previous planes. Clear its old channels before replacing a differently sized frame. + // The input remains borrowed throughout this operation, including when the output selects it without conversion. + convertedImageStorage.ClearExtraChannels(); + if (!convertedImageStorage.SetFromImage(color, input.CurrentColorEncoding)) + { + return false; + } - foreach (JxlPlane extraChannel in input.EnumerateExtraChannels()) + // SetFromImage took ownership. Do not dispose its image through the local cleanup path. + color = null; + if (input.ContainsExtraChannels()) { - JxlPlane ec = new(configuration, extraChannel.XSize, extraChannel.YSize); + // Reserve every list slot before allocating its plane. Ownership cannot be stranded by a later list-growth failure. + extraChannels = new(input.ExtraChannelCount); + foreach (JxlPlane extraChannel in input.EnumerateExtraChannels()) + { + JxlPlane copy = new(configuration, extraChannel.XSize, extraChannel.YSize); + extraChannels.Add(copy); + JxlImageOperations.ForceCopyImage(extraChannel, copy); + } - if (!JxlImageOperations.CopyImage(extraChannel, ec)) + if (!convertedImageStorage.TrySetExtraChannels(extraChannels)) { return false; } - extraChannels.Add(ec); - extraChannel.Dispose(); + extraChannels = null; } - if (!store.TrySetExtraChannels(extraChannels)) + if (!convertedImageStorage.TransformTo(configuration, desiredEncoding, cms)) { return false; } - } - if (!store.TransformTo(configuration, desiredEncoding, cms)) + output = convertedImageStorage; + return true; + } + finally { - return false; + color?.Dispose(); + if (extraChannels is not null) + { + foreach (JxlPlane copy in extraChannels) + { + copy.Dispose(); + } + } } - - output = store; - return true; } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs index 2fc5555f0b..45d5d00db2 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs @@ -13,7 +13,11 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Image; /// /// Stores frame sample planes, the original image metadata, and the current sample encoding. /// -internal sealed class JxlImageBundle +/// +/// The bundle owns assigned color and extra-channel planes. Metadata and row views are borrowed. +/// Successful setters transfer plane ownership; failed setters leave ownership with the caller. +/// +internal sealed class JxlImageBundle : IDisposable { /// /// Image data for additional channels. @@ -137,10 +141,15 @@ public JxlImageBundle(JxlImageMetadata metadata) /// public JxlImage3? Color { get; private set; } + /// + /// Gets the number of owned extra-channel planes. + /// + public int ExtraChannelCount => this.extraChannels.Count; + /// /// Gets a value indicating whether the color data is present and usable. /// - public bool HasColor => this.Color?.XSize != 0; + public bool HasColor => this.Color is not null && this.Color.XSize != 0; /// /// Gets the width. @@ -154,12 +163,12 @@ public int XSize return this.JpegData!.Width; } - if (this.Color?.XSize != 0) + if (this.HasColor) { return this.Color!.XSize; } - return this.extraChannels?.Count > 0 ? 0 : this.extraChannels![0].XSize; + return this.extraChannels.Count == 0 ? 0 : this.extraChannels[0].XSize; } } @@ -175,12 +184,12 @@ public int YSize return this.JpegData!.Height; } - if (this.Color?.YSize != 0) + if (this.HasColor) { return this.Color!.YSize; } - return this.extraChannels?.Count > 0 ? 0 : this.extraChannels![0].YSize; + return this.extraChannels.Count == 0 ? 0 : this.extraChannels[0].YSize; } } @@ -275,11 +284,10 @@ public bool VerifySizes() public void OverrideProfile(JxlColorEncoding encoding) => this.CurrentColorEncoding = encoding; /// - /// If the color data is present, assigns it to and returns true, - /// otherwise returns false and assigns null. + /// Borrows the color image without transferring its ownership. /// - /// Output color data. - /// True if color isn't null. + /// The borrowed color image, or null when none is present. + /// Whether the bundle contains a color image. public bool TryGetColor(out JxlImage3? color) { color = null; @@ -292,14 +300,26 @@ public bool TryGetColor(out JxlImage3? color) } /// - /// Removes the color data, replacing it with a new Image3F with 0 as width and height. + /// Releases the owned color planes and removes the color image. /// - public void RemoveColor() => this.Color = new JxlImage3(); + public void RemoveColor() + { + this.Color?.Dispose(); + this.Color = null; + } /// - /// Removes all extra channels, if any. + /// Releases and removes every owned extra-channel plane. /// - public void ClearExtraChannels() => this.extraChannels.Clear(); + public void ClearExtraChannels() + { + foreach (JxlPlane plane in this.extraChannels) + { + plane.Dispose(); + } + + this.extraChannels.Clear(); + } /// /// Returns true if there is at least 1 extra channel. @@ -308,47 +328,65 @@ public bool TryGetColor(out JxlImage3? color) public bool ContainsExtraChannels() => this.extraChannels.Count > 0; /// - /// Returns an enumerable for extra channels. + /// Borrows the owned extra-channel planes in metadata order. /// - /// Extra channels enumerable. + /// The borrowed planes. The caller must not dispose them. public IEnumerable> EnumerateExtraChannels() => this.extraChannels; /// - /// Adds a specified extra channel. + /// Takes ownership of an extra-channel plane and appends it in metadata order. /// - /// The extra channel to be added to the image bundle. - public void AddExtraChannel(JxlPlane channel) => this.extraChannels.Add(channel); + /// The caller-owned plane, which the caller must not dispose after this call. + public void AddExtraChannel(JxlPlane channel) + { + try + { + this.extraChannels.Add(channel); + } + catch + { + // This unconditional transfer also covers list-allocation failure. The caller no longer owns this plane after handing it over. + channel.Dispose(); + throw; + } + } + /// + /// Borrows one owned extra-channel plane. + /// + /// The metadata-ordered channel index. + /// The borrowed plane. public JxlPlane GetExtraChannel(int i) => this.extraChannels[i]; /// - /// Sets the extra channels. + /// Replaces the extra channels and takes ownership when their dimensions are valid. /// - /// The extra channels. - /// - /// True if each plane had width and height greater than 0 and sizes are correct - /// after changing the extra channels; false otherwise. - /// + /// The caller-owned planes in metadata order. On success, the caller must not modify the list or dispose its planes. + /// Whether ownership transferred. On failure, this bundle and the supplied planes remain unchanged. public bool TrySetExtraChannels(List> extraChannels) { + int width = this.HasColor || this.IsJpeg ? this.XSize : extraChannels.Count == 0 ? 0 : extraChannels[0].XSize; + int height = this.HasColor || this.IsJpeg ? this.YSize : extraChannels.Count == 0 ? 0 : extraChannels[0].YSize; + foreach (JxlPlane plane in extraChannels) { - if (plane.XSize == 0 || plane.YSize == 0) + if (plane.XSize == 0 || plane.YSize == 0 || plane.XSize != width || plane.YSize != height) { return false; } } + // Validate before releasing the previous channels. Ownership transfers only after every supplied plane has passed. + this.ClearExtraChannels(); this.extraChannels = extraChannels; - - return this.VerifySizes(); + return true; } /// /// Attempts to set the alpha channel. /// - /// The alpha channel to set. - /// True if it was set successfully; false otherwise. + /// The caller-owned alpha plane. + /// Whether ownership transferred. On success, the caller must not dispose the plane. /// Thrown if the corresponding extra channel has incorrect info. public bool TrySetAlpha(JxlPlane alpha) { @@ -364,6 +402,11 @@ public bool TrySetAlpha(JxlPlane alpha) return false; } + if ((this.HasColor || this.ContainsExtraChannels() || this.IsJpeg) && (alpha.XSize != this.XSize || alpha.YSize != this.YSize)) + { + return false; + } + int eciIndex = this.Metadata.ExtraChannels.IndexOf(eci); if (eciIndex != this.extraChannels.Count) @@ -373,7 +416,7 @@ public bool TrySetAlpha(JxlPlane alpha) this.extraChannels.Add(alpha); - return this.VerifySizes(); + return true; } /// @@ -419,11 +462,11 @@ public bool VerifyMetadata() } /// - /// Updates the bundle from the specified image. + /// Replaces the color image and takes ownership when its encoding and dimensions are valid. /// - /// Color data. + /// The caller-owned color image. On success, the caller must not dispose it or assign it to another bundle. /// Current color encoding. - /// True if setting the image succeeded; otherwise false. + /// Whether ownership transferred. On failure, this bundle and the supplied image remain unchanged. public bool SetFromImage(JxlImage3 color, JxlColorEncoding current) { if (color.XSize == 0 || color.YSize == 0) @@ -437,10 +480,20 @@ public bool SetFromImage(JxlImage3 color, JxlColorEncoding current) return false; } + foreach (JxlPlane plane in this.extraChannels) + { + if (plane.XSize != color.XSize || plane.YSize != color.YSize) + { + return false; + } + } + + // Every production caller supplies newly owned storage. Existing row views remain borrows and cannot become a second image owner. + this.RemoveColor(); this.Color = color; this.CurrentColorEncoding = current; - return this.VerifySizes(); + return true; } /// @@ -492,29 +545,37 @@ public bool ShrinkTo(int width, int height) public JxlImageBundle Copy(Configuration configuration) { JxlImageBundle copy = new(this.Metadata); - - if (this.Color is not null) + try { - copy.Color = new JxlImage3(configuration, this.Color.XSize, this.Color.YSize); - } + // Reserve list slots before allocating planes, so adding an owned plane cannot fail while growing the list. + copy.extraChannels.EnsureCapacity(this.extraChannels.Count); + if (this.Color is not null) + { + copy.Color = new JxlImage3(configuration, this.Color.XSize, this.Color.YSize); + JxlImageOperations.ForceCopyImage(this.Color, copy.Color); + } - copy.CurrentColorEncoding = this.CurrentColorEncoding; - copy.JpegData = this.JpegData; - copy.ColorTransform = this.ColorTransform; - copy.ChromaSubsampling = this.ChromaSubsampling; + // Metadata and JPEG coefficient data remain shared managed objects. Only the sample planes transfer or copy ownership. + copy.CurrentColorEncoding = this.CurrentColorEncoding; + copy.JpegData = this.JpegData; + copy.ColorTransform = this.ColorTransform; + copy.ChromaSubsampling = this.ChromaSubsampling; - foreach (JxlPlane plane in this.extraChannels) - { - JxlPlane ec = new(configuration, plane.XSize, plane.YSize); - if (!JxlImageOperations.CopyImage(plane, ec)) + foreach (JxlPlane plane in this.extraChannels) { - throw new InvalidOperationException("Cannot copy extra channel"); + JxlPlane ec = new(configuration, plane.XSize, plane.YSize); + copy.extraChannels.Add(ec); + JxlImageOperations.ForceCopyImage(plane, ec); } - copy.extraChannels.Add(ec); + return copy; + } + catch + { + // The partial copy already owns every assigned plane. Disposing it leaves all source planes untouched. + copy.Dispose(); + throw; } - - return copy; } public bool CopyTo( @@ -545,4 +606,13 @@ public bool TransformTo( this.CurrentColorEncoding = desiredEncoding; return true; } + + /// + /// Releases owned sample planes without disposing shared metadata or borrowed row views. + /// + public void Dispose() + { + this.RemoveColor(); + this.ClearExtraChannels(); + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/LowMemoryRenderPipeline.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/LowMemoryRenderPipeline.cs deleted file mode 100644 index 17704e6ccc..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/LowMemoryRenderPipeline.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; - -// TODO: render_pipeline/low_memory_render_pipeline.cc -internal class LowMemoryRenderPipeline -{ -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToImageBundleStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToImageBundleStage.cs index 6b9c9337f4..0eccbc2e8e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToImageBundleStage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToImageBundleStage.cs @@ -31,21 +31,15 @@ public override void SetInputSizes(Span inputSizes) JxlImage3 tmp = new(this.GetConfiguration(), inputSizes[0].Width, inputSizes[0].Height); + // The bundle releases its previous channels. New planes transfer to it as each setter succeeds. + bundle.ClearExtraChannels(); + if (!bundle.SetFromImage(tmp, colorEncoding)) { + tmp.Dispose(); throw new InvalidOperationException("Could not set the image of an image bundle"); } - foreach (JxlPlane ec in bundle.EnumerateExtraChannels()) - { - // We don't want a memory leak here, as we're - // removing old extra channels and inserting - // new ones - ec.Dispose(); - } - - bundle.ClearExtraChannels(); - for (int c = 3; c < inputSizes.Length; c++) { JxlPlane ch = new(this.GetConfiguration(), inputSizes[c].Width, inputSizes[c].Height); @@ -53,7 +47,14 @@ public override void SetInputSizes(Span inputSizes) } } - public override void ProcessRow(Buffer2D> inputRows, Buffer2D> outputRows, int xExtraLeft, int xExtraRight, int width, int xPos, int yPos) + public override void ProcessRow( + Buffer2D> inputRows, + Buffer2D> outputRows, + int xExtraLeft, + int xExtraRight, + int width, + int xPos, + int yPos) { if (xExtraLeft != 0 || xExtraRight != 0) { From bad4a9f65e1e2cc21aafc7bcc2fc6fc1415d1121 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 19:13:30 +1000 Subject: [PATCH 261/328] Reuse sequential JPEG XL search workspace and correct tile offsets --- .../AcStrategy/JxlAcStrategyEncoder.cs | 52 +++++-- .../JxlChromaFromLumaEncoder.cs | 22 ++- .../Heuristics/JxlAcStrategyHeuristics.cs | 139 ++++++++++++------ .../Encoder/Heuristics/JxlCflHeuristics.cs | 111 +++++++++----- 4 files changed, 229 insertions(+), 95 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyEncoder.cs index 2afa749350..c0d4fe023f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyEncoder.cs @@ -656,6 +656,24 @@ public static bool TryMergeAcStrategy( return true; } + /// + /// Compares a square transform with its horizontal and vertical divisions and updates the selected strategies. + /// + /// The square's side length in eight-pixel blocks. + /// Whether the full square transform is a candidate. + /// The search rectangle's first block column. + /// The search rectangle's first block row. + /// The square's block column within the search rectangle. + /// The square's block row within the search rectangle. + /// The borrowed source views, matrices and search weights. + /// The three channel correlation factors. + /// The selected strategies to update. + /// The existing entropy multiplier for divided candidates. + /// The existing entropy multiplier for the square candidate. + /// The rectangle's reusable per-block entropy estimates. + /// The borrowed candidate coefficient storage. + /// The borrowed reconstruction-error and transform workspace. + /// Whether every required entropy estimate and strategy update succeeded. public static bool FindBestFirstLevelDivisionForSquare( int blocks, bool allowSquareTransform, @@ -670,8 +688,7 @@ public static bool FindBestFirstLevelDivisionForSquare( float entropyMulJXJ, Span entropyEstimate, Span block, - Span scratchSpace, - ref int quantized) + Span scratchSpace) { int blocksHalf = blocks / 2; JxlAcStrategyType acsRawJXK = AcsVerticalSplit(blocks); @@ -864,16 +881,28 @@ public static bool FindBestFirstLevelDivisionForSquare( return true; } + /// + /// Selects transform strategies for one rectangle using floating-point entropy and reconstruction estimates. + /// + /// The fixed compression settings. + /// The borrowed source views, matrices and search weights. + /// The rectangle in block coordinates, at most eight by eight blocks. + /// The frame's color-correlation maps. + /// The borrowed float allocation: three coefficient regions followed by error and transform scratch. + /// The destination strategy image. + /// Whether all strategy updates succeeded. public static bool ProcessAcStrategyRectangle( JxlCompressionParameters cparams, JxlAcStrategyConfiguration config, Rectangle rect, JxlColorCorrelationMap cmap, Span block, - ref int quantized, JxlAcStrategyImage acStrategy) { float butteraugliTarget = cparams.ButteraugliDistance; + + // EstimateEntropy places rounded coefficients in float vectors. It needs no separate integer coefficient buffer. + // The first three maximum-area regions hold channel coefficients; the remaining regions hold error and transform scratch. Span scratchSpace = block[(3 * JxlAcStrategy.MaximumCoefficientArea)..]; int bx = rect.X0(); int by = rect.Y0(); @@ -915,10 +944,8 @@ public static bool ProcessAcStrategyRectangle( butteraugliTarget, config, cmapFactors, - acStrategy, block, scratchSpace, - quantized, out float entropy, out JxlAcStrategyType bestOf8x8s); @@ -988,8 +1015,7 @@ public static bool ProcessAcStrategyRectangle( entropy_mul64X64, entropyEstimate, block, - scratchSpace, - ref quantized)) + scratchSpace)) { return false; } @@ -1028,8 +1054,7 @@ public static bool ProcessAcStrategyRectangle( entropy_mul32X32, entropyEstimate, block, - scratchSpace, - ref quantized)) + scratchSpace)) { return false; } @@ -1069,8 +1094,7 @@ public static bool ProcessAcStrategyRectangle( entropy_mul16X16, entropyEstimate, block, - scratchSpace, - ref quantized)) + scratchSpace)) { return false; } @@ -1137,8 +1161,7 @@ public static bool ProcessAcStrategyRectangle( entropy_mul16X16, entropyEstimate, block, - scratchSpace, - ref quantized)) + scratchSpace)) { return false; } @@ -1171,8 +1194,7 @@ public static bool ProcessAcStrategyRectangle( entropy_mul32X32, entropyEstimate, block, - scratchSpace, - ref quantized)) + scratchSpace)) { return false; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlChromaFromLumaEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlChromaFromLumaEncoder.cs index 8ea341aa8a..b2fb99d575 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlChromaFromLumaEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlChromaFromLumaEncoder.cs @@ -126,6 +126,22 @@ public static int FindBestMultiplier(Span valuesM, Span valuesS, i return (int)Math.Clamp(MathF.Round(x), -128.0f, 127.0f); } + /// + /// Computes the X and B correlation multipliers for one tile and records its frame-wide DC pairs. + /// + /// The borrowed XYB source planes. + /// The block-aligned source region in pixels. + /// The prepared dequantization matrices. + /// The selected strategies, unused when is true. + /// The quantization field, or null to use a unit field. + /// The frame's quantization controls. + /// The tile in block coordinates relative to , at most eight by eight blocks. + /// Whether to use the existing fast correlation estimate. + /// Whether to use DCT8 rather than reading selected strategies. + /// The destination X-correlation map. + /// The destination B-correlation map. + /// Four owned-by-caller DC rows, with vector-rounded frame block count as their logical width. + /// The borrowed float workspace, partitioned into transform blocks, tile coefficients, DC values and transform scratch. public static void ComputeTile( JxlImage3 opsin, Rectangle opsinRect, @@ -161,6 +177,8 @@ public static void ComputeTile( Span dcValuesYB = dcValues.GetRow(2); Span dcValuesB = dcValues.GetRow(3); + // Maximum transform blocks hold 256 by 256 samples, including candidates too large for this color tile. + // The kernel records their DC values but uses only transforms that fit the tile for its AC correlation estimate. Span blockY = mem; Span blockX = blockY[JxlAcStrategy.MaximumCoefficientArea..]; Span blockB = blockX[JxlAcStrategy.MaximumCoefficientArea..]; @@ -172,7 +190,9 @@ public static void ComputeTile( Span coeffsYB = coeffsX[(JxlChromaFromLuma.ColorTileDimension * JxlChromaFromLuma.ColorTileDimension)..]; Span coeffsB = coeffsYB[(JxlChromaFromLuma.ColorTileDimension * JxlChromaFromLuma.ColorTileDimension)..]; - mem = mem[(4 * JxlAcStrategy.MaximumCoefficientArea)..]; + // The four coefficient arrays hold one color tile each, not one maximum-size transform each. + // Advance by exactly those tile arrays so the following DC arrays and transform scratch match the owner's allocation. + mem = mem[(4 * JxlChromaFromLuma.ColorTileDimension * JxlChromaFromLuma.ColorTileDimension)..]; const int dcSize = JxlAcStrategy.MaximumCoefficientBlocks * JxlAcStrategy.MaximumCoefficientBlocks; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlAcStrategyHeuristics.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlAcStrategyHeuristics.cs index 3c9741a1a8..d2bf6a4042 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlAcStrategyHeuristics.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlAcStrategyHeuristics.cs @@ -11,42 +11,98 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Heuristics; -internal class JxlAcStrategyHeuristics(Configuration configuration, JxlCompressionParameters cparams) : IDisposable +/// +/// Selects transform strategies with one reusable workspace for sequential rectangle processing. +/// +/// +/// Source images, masks and matrices remain owned by the frame encoder. They must outlive this helper's processing calls. +/// Each search rectangle covers at most eight by eight blocks. Scratch also accommodates the largest supported transform candidate. +/// +internal sealed class JxlAcStrategyHeuristics : IDisposable { - private IMemoryOwner? memoryOwner; - private IMemoryOwner? qMemoryOwner; - - public Configuration Configuration { get; set; } = configuration; - - public JxlCompressionParameters CompressionParameters { get; set; } = cparams; - - public JxlAcStrategyConfiguration Config { get; set; } = new(new()); + /// + /// Owns floating-point search storage, absent when the configured speed uses only DCT8. + /// + private readonly IMemoryOwner? memoryOwner; + + /// + /// Borrows the floating-point workspace for every processed rectangle. + /// + private readonly Memory scratch; + + /// + /// Whether the configured speed selects DCT8 without searching other strategies. + /// + private readonly bool useDct8Only; + + /// + /// Initializes strategy selection with the frame's real matrices and one sequential workspace. + /// + /// The configuration whose allocator owns scratch storage. + /// The frame encoder's compression settings, which remain fixed while this helper is used. + /// The frame-owned dequantization matrices borrowed by the search. + public JxlAcStrategyHeuristics(Configuration configuration, JxlCompressionParameters parameters, JxlDequantMatrices matrices) + { + this.Configuration = configuration; + this.CompressionParameters = parameters; + this.Config = new(matrices); + this.useDct8Only = parameters.SpeedTier >= JxlSpeedTier.Cheetah; - public int MemoryPerThread { get; set; } + if (!this.useDct8Only) + { + // Counts are float elements, not bytes. Three maximum-area regions hold the candidate's channel coefficients. + // A fourth holds reconstruction error. The remaining two regions and three SIMD-width strips serve the forward/inverse transforms. + // Each maximum-area region accommodates a 256 by 256 transform. Rounded coefficients remain float vectors in the entropy calculation. + int dctScratchLength = 3 * (JxlSimdUtils.MaxVectorSize / sizeof(float)) * JxlAcStrategy.MaximumBlockDimension; + int scratchLength = (6 * JxlAcStrategy.MaximumCoefficientArea) + dctScratchLength; + IMemoryOwner memory = configuration.MemoryAllocator.Allocate(scratchLength); + this.memoryOwner = memory; + this.scratch = memory.Memory; + } + } - public Memory Memory { get; set; } + /// + /// Gets the configuration that owns scratch allocations. + /// + public Configuration Configuration { get; } - public int QMemoryPerThread { get; set; } + /// + /// Gets the frame encoder's fixed compression settings. + /// + public JxlCompressionParameters CompressionParameters { get; } - public Memory QMemory { get; set; } + /// + /// Gets the matrix references and borrowed source views used by the strategy kernel. + /// + public JxlAcStrategyConfiguration Config { get; } + /// + /// Releases owned scratch without disposing the borrowed source, masks or matrices. + /// public void Dispose() { this.memoryOwner?.Dispose(); - this.qMemoryOwner?.Dispose(); } + /// + /// Binds source and mask rows and prepares the matrices needed by the configured strategy search. + /// + /// The frame-owned source planes. + /// The block-aligned source region corresponding to the search coordinates. + /// The frame-owned quantization field in block coordinates. + /// The optional masking plane, or an empty plane when masking is absent. + /// The optional per-block masking plane, or an empty plane when absent. + /// Whether the required matrices were computed. Processing starts only after this returns true. public bool Initialize( JxlImage3 src, Rectangle rectIn, JxlPlane quantField, JxlPlane mask, - JxlPlane mask1x1, - JxlDequantMatrices matrices) + JxlPlane mask1x1) { - this.Config.Dequant = matrices; + JxlDequantMatrices matrices = this.Config.Dequant; - if (this.CompressionParameters.SpeedTier < JxlSpeedTier.Cheetah) + if (!this.useDct8Only) { uint acsMask = 0; @@ -71,6 +127,10 @@ public bool Initialize( this.Config.QuantFieldRow = quantField.GetRowMemory(0); this.Config.QuantFieldStride = quantField.PixelsPerRow; + // Rebinding must also remove optional views from an earlier source. Empty masks never borrow a row. + this.Config.MaskingFieldRow = default; + this.Config.MaskingFieldStride = 0; + if (mask.XSize > 0 && mask.YSize > 0) { this.Config.MaskingFieldRow = mask.GetRowMemory(0); @@ -78,6 +138,8 @@ public bool Initialize( } this.Config.Mask1x1Width = mask1x1.XSize; + this.Config.Masking1x1FieldRow = default; + this.Config.Masking1x1FieldStride = 0; if (mask1x1.XSize > 0 && mask1x1.YSize > 0) { @@ -108,43 +170,38 @@ public bool Initialize( return true; } - public void PrepareForThreads(int numThreads) + /// + /// Selects strategies for one rectangle using the same scratch as all preceding rectangles. + /// + /// The search rectangle in block coordinates, at most eight by eight blocks. + /// The frame-owned color-correlation maps. + /// The destination strategy image. + /// Whether the rectangle's strategies were selected successfully. + public bool ProcessRectangle(Rectangle rect, JxlColorCorrelationMap cmap, JxlAcStrategyImage acStrategy) { - int dctScratchSize = 3 * (JxlSimdUtils.MaxVectorSize / sizeof(float)) * JxlAcStrategy.MaximumBlockDimension; - - this.MemoryPerThread = (6 * JxlAcStrategy.MaximumCoefficientArea) + dctScratchSize; - int memBytes = numThreads * this.MemoryPerThread; - this.memoryOwner = this.Configuration.MemoryAllocator.Allocate(memBytes); - this.Memory = this.memoryOwner.Memory; - - this.QMemoryPerThread = JxlAcStrategy.MaximumCoefficientArea; - int qmemBytes = numThreads * this.QMemoryPerThread; - this.qMemoryOwner = this.Configuration.MemoryAllocator.Allocate(qmemBytes); - this.QMemory = this.qMemoryOwner.Memory; - } - - public bool ProcessRectangle(Rectangle rect, JxlColorCorrelationMap cmap, JxlAcStrategyImage acStrategy, int thread) - { - if (this.CompressionParameters.SpeedTier >= JxlSpeedTier.Cheetah) + if (this.useDct8Only) { acStrategy.FillDct8(in rect); return true; } - ref int qmem = ref this.QMemory.Span[thread * this.QMemoryPerThread]; + // Borrow the span once for this rectangle. The kernel reuses its regions for every candidate; none escape this call. + Span scratch = this.scratch.Span; - bool status = JxlAcStrategyEncoder.ProcessAcStrategyRectangle( + return JxlAcStrategyEncoder.ProcessAcStrategyRectangle( this.CompressionParameters, this.Config, rect, cmap, - this.Memory[(thread * this.MemoryPerThread)..], - ref qmem, + scratch, acStrategy); - - return status; } + /// + /// Records the selected strategy counts when the encoder requests statistics. + /// + /// The completed strategy image. + /// The optional encoder statistics. public void Finalize(JxlAcStrategyImage acStrategy, JxlAuxiliaryOutput? auxOut) { if (auxOut is not null) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlCflHeuristics.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlCflHeuristics.cs index f40545ffdc..4dd0a93980 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlCflHeuristics.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlCflHeuristics.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Buffers; -using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.ChromaFromLuma; @@ -12,45 +11,82 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Heuristics; /// -/// Chroma from luma heuristics +/// Computes color-correlation tiles with one reusable sequential workspace and frame-wide DC storage. /// -internal sealed class JxlCflHeuristics(Configuration configuration) : IDisposable +/// Source planes, quantization controls and destination maps remain owned by the frame encoder. +internal sealed class JxlCflHeuristics : IDisposable { - // TODO - private Memory scratchSpace; - private JxlPlane? dcValues; - private IMemoryOwner? scratchSpaceMemoryOwner; + /// + /// Owns floating-point coefficient and transform scratch reused by every tile. + /// + private readonly IMemoryOwner scratchSpaceMemoryOwner; - private static int ItemsPerThread - { - get - { - int dctScratchSize = 3 * (JxlSimdUtils.MaxVectorSize / sizeof(float)) * JxlAcStrategy.MaximumBlockDimension; - int dcScratchSize = 3 * JxlAcStrategy.MaximumCoefficientBlocks * JxlAcStrategy.MaximumCoefficientBlocks; + /// + /// Borrows the owned scratch allocation without a byte-to-float memory cast. + /// + private readonly Memory scratchSpace; - return (JxlAcStrategy.MaximumCoefficientArea * 3) - + (JxlChromaFromLuma.ColorTileDimension * JxlChromaFromLuma.ColorTileDimension * 4) - + (JxlAcStrategy.MaximumCoefficientArea * 2) - + dctScratchSize + dcScratchSize; - } - } + /// + /// Owns four rows of frame-wide DC pairs: Y for X, X, Y for B, and B. + /// + private readonly JxlPlane dcValues; - public void PrepareForThreads(int numThreads) + /// + /// Initializes storage for the frame's block-aligned source region. + /// + /// The configuration whose allocator owns sample storage. + /// The positive frame region in pixels. Its width and height are multiples of the eight-pixel block dimension. + public JxlCflHeuristics(Configuration configuration, Rectangle rect) { - int memBytes = numThreads * ItemsPerThread * sizeof(float); + int xSizeBlocks = rect.Width / JxlFrameDimensions.BlockDimensions; + int ySizeBlocks = rect.Height / JxlFrameDimensions.BlockDimensions; + this.dcValues = JxlChromaFromLumaEncoder.InitializeDcStorage(configuration, checked(xSizeBlocks * ySizeBlocks)); + + try + { + this.scratchSpaceMemoryOwner = configuration.MemoryAllocator.Allocate(ScratchLength); + } + catch + { + this.dcValues.Dispose(); + throw; + } - this.scratchSpaceMemoryOwner = configuration.MemoryAllocator.Allocate(memBytes); this.scratchSpace = this.scratchSpaceMemoryOwner.Memory; } - public void Initialize(Rectangle rect) + /// + /// Gets the workspace length in float elements for the existing tile kernel. + /// + private static int ScratchLength { - int xSizeBlocks = rect.Width / JxlFrameDimensions.BlockDimensions; - int ySizeBlocks = rect.Height / JxlFrameDimensions.BlockDimensions; + get + { + // Transform blocks can cover up to 256 by 256 pixels. A color tile covers 64 by 64 pixels, independently of that maximum. + // Regions follow ComputeTile's order: three transform blocks; four tile coefficient arrays; three DC arrays; transform scratch. + int dctScratchLength = 3 * (JxlSimdUtils.MaxVectorSize / sizeof(float)) * JxlAcStrategy.MaximumBlockDimension; + int dcScratchLength = 3 * JxlAcStrategy.MaximumCoefficientBlocks * JxlAcStrategy.MaximumCoefficientBlocks; - this.dcValues = JxlChromaFromLumaEncoder.InitializeDcStorage(configuration, xSizeBlocks * ySizeBlocks); + return (JxlAcStrategy.MaximumCoefficientArea * 3) + + (JxlChromaFromLuma.ColorTileDimension * JxlChromaFromLuma.ColorTileDimension * 4) + + dcScratchLength + + (JxlAcStrategy.MaximumCoefficientArea * 2) + + dctScratchLength; + } } + /// + /// Computes one color-correlation tile using the same scratch as all preceding tiles. + /// + /// The tile in block coordinates, at most eight by eight blocks. + /// The frame-owned XYB sample planes. + /// The pixel region corresponding to the frame-wide DC storage. + /// The frame-owned dequantization matrices. + /// The selected strategies, or null to use DCT8 for every block. + /// The frame-owned quantization field. + /// The frame-owned quantization controls. + /// Whether to use the existing fast correlation estimate. + /// The destination color-correlation maps. public void ComputeTile( Rectangle rectangle, JxlImage3 opsin, @@ -60,18 +96,10 @@ public void ComputeTile( JxlPlane rawQuantField, JxlQuantizer quantizer, bool fast, - int thread, JxlColorCorrelationMap cmap) { - if (this.dcValues is null) - { - throw new InvalidOperationException("CfL heuristics not initialized; call .Initialize(Rectangle) first"); - } - - bool useDct8 = acStrategy is null; - int itemsPerThread = ItemsPerThread; - - Span scratch = MemoryMarshal.Cast(this.scratchSpace.Span).Slice(thread * itemsPerThread, itemsPerThread); + // Borrow once before entering the coefficient loops. The helper owns both buffers; the kernel only uses their views. + Span scratch = this.scratchSpace.Span; JxlChromaFromLumaEncoder.ComputeTile( opsin, opsinRect, @@ -81,12 +109,19 @@ public void ComputeTile( quantizer, rectangle, fast, - useDct8, + acStrategy is null, cmap.YToXMap!, cmap.YToBMap!, this.dcValues, scratch); } - public void Dispose() => this.scratchSpaceMemoryOwner?.Dispose(); + /// + /// Releases the owned scratch and DC storage without disposing borrowed frame inputs. + /// + public void Dispose() + { + this.scratchSpaceMemoryOwner.Dispose(); + this.dcValues.Dispose(); + } } From 3b284b9c20d67bd9f8dc0e5e1fc8f8f390e56c46 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 19:32:17 +1000 Subject: [PATCH 262/328] Consolidate JPEG XL noise fitting without owning array copies --- .../Encoder/Noise/JxlLossFunction.cs | 70 ----- .../Encoder/Noise/JxlNoiseEncoder.cs | 253 +++++++++++++++--- .../Optimize/JxlOptimizeArrayFactory{T}.cs | 12 - .../Encoder/Optimize/JxlOptimizeArray{T}.cs | 213 --------------- .../Optimize/JxlOptimizeFunction{T}.cs | 9 - .../Encoder/Noise/NoiseEncoderTests.cs | 42 +++ 6 files changed, 257 insertions(+), 342 deletions(-) delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlLossFunction.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Optimize/JxlOptimizeArrayFactory{T}.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Optimize/JxlOptimizeArray{T}.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Optimize/JxlOptimizeFunction{T}.cs create mode 100644 tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/Noise/NoiseEncoderTests.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlLossFunction.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlLossFunction.cs deleted file mode 100644 index a08891d0e4..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlLossFunction.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.InteropServices; -using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Optimize; -using SixLabors.ImageSharp.Formats.Jxl.Processing.Noise; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Noise; - -internal sealed class JxlLossFunction(List noiseLevels) -{ - public double Compute(Span w, Span df, bool skipRegularization = false) - { - const double reg = 0.005; - const double asym = 1.1; - - double lossFunction = 0; - - w.Clear(); - - ReadOnlySpan levels = CollectionsMarshal.AsSpan(noiseLevels); - - for (int i = 0; i < levels.Length; i++) - { - JxlNoiseLevel nl = levels[i]; - - JxlNoiseIndexAndFraction pos = JxlNoiseHelper.IndexAndFraction(nl.Intensity); - - double low = w[pos.Index]; - double hi = w[pos.Index + 1]; - double val = (low * (1.0f - pos.Fraction)) + (hi * pos.Fraction); - double dist = val - nl.NoiseLevel; - - if (dist > 0) - { - lossFunction += asym * dist * dist; - df[pos.Index] -= asym * (1.0f - pos.Fraction) * dist; - df[pos.Index + 1] -= asym * pos.Fraction * dist; - } - else - { - lossFunction += dist * dist; - df[pos.Index] -= (1.0f - pos.Fraction) * dist; - df[pos.Index + 1] -= pos.Fraction * dist; - } - } - - if (skipRegularization) - { - return lossFunction; - } - - int levelsSize = levels.Length; - - for (int i = 0; i + 1 < w.Length; i++) - { - double diff = w[i] - w[i + 1]; - lossFunction += reg * levelsSize * diff * diff; - df[i] -= reg * diff * levelsSize; - df[i + 1] += reg * diff * levelsSize; - } - - return lossFunction; - } - - public double OptimizeFunction(JxlOptimizeArray a, ref JxlOptimizeArray b) => - this.Compute( - a.Span, - b.Span); -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseEncoder.cs index 2b9367768d..2add2f6794 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseEncoder.cs @@ -6,7 +6,6 @@ using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; -using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Optimize; using SixLabors.ImageSharp.Formats.Jxl.Processing.Noise; using SixLabors.ImageSharp.Memory; @@ -100,20 +99,18 @@ public static IMemoryOwner GetSadScoresForPatches( return sadScores; } - public static void OptimizeNoiseParameters( - Configuration configuration, - List noiseLevel, - JxlNoiseParameters noiseParameters, - float multiplier) + /// + /// Fits the eight noise lookup values to a nonempty set of patch measurements. + /// + /// The measurements selected by the patch threshold. + /// The lookup values to replace, or clear when the fit error is too large. + /// The strength applied to the fitted lookup values. + public static void OptimizeNoiseParameters(List noiseLevel, JxlNoiseParameters noiseParameters, float multiplier) { const double maxError = 1e-3; - const double precision = 1e-8; - const int maxIterations = 40; - - Span noiseLevelSpan = CollectionsMarshal.AsSpan(noiseLevel); + ReadOnlySpan noiseLevelSpan = CollectionsMarshal.AsSpan(noiseLevel); - // TODO: this is equivalent to .Average(x => x.NoiseLevel). - // Unfortunately we can't use LINQ on a Span or Memory though. + // Start with the measured mean at every lookup point. Patch selection guarantees at least one measurement. float avg = 0; for (int i = 0; i < noiseLevelSpan.Length; i++) @@ -123,25 +120,20 @@ public static void OptimizeNoiseParameters( avg /= noiseLevelSpan.Length; - JxlLossFunction lossFunction = new(noiseLevel); - JxlOptimizeArray parameterVector = new(configuration, OptimizeArrayWidth, avg); + // The curve always has eight points. Its numeric workspace stays on the stack and owns no pooled memory. + InlineArray8 parameterStorage = default; + Span parameters = parameterStorage; + parameters.Fill(avg); + OptimizeNoiseCurve(noiseLevelSpan, parameters); - parameterVector = JxlOptimizeArray.OptimizeWithScaledConjugateGradientMethod( - new Factory(configuration), - lossFunction.OptimizeFunction, - parameterVector, - precision, - maxIterations); - - // Clamp to account codestream limits - // TODO: TensorPrimitives - for (int i = 0; i < parameterVector.Span.Length; i++) + // The lookup is stored as floats. Apply that rounding before measuring the final fit against the codestream limits. + for (int i = 0; i < parameters.Length; i++) { - parameterVector[i] = Math.Clamp(parameterVector[i] * multiplier, 0.0f, JxlNoiseConstants.NoiseLutMax); + parameters[i] = Math.Clamp((float)(parameters[i] * multiplier), 0F, JxlNoiseConstants.NoiseLutMax); } - Span unused = stackalloc double[parameterVector.Span.Length]; - double loss = lossFunction.Compute(parameterVector.Span, unused, skipRegularization: true) / noiseLevel.Count; + InlineArray8 unusedGradient = default; + float loss = (float)(ComputeNoiseLoss(noiseLevelSpan, parameters, unusedGradient, false) / noiseLevelSpan.Length); if (loss > maxError) { @@ -149,10 +141,187 @@ public static void OptimizeNoiseParameters( return; } - for (int i = 0; i < parameterVector.Span.Length; i++) + for (int i = 0; i < parameters.Length; i++) + { + noiseParameters.Lookup[i] = (float)parameters[i]; + } + } + + /// + /// Fits a noise curve with scaled conjugate gradients and independent accepted and trial state. + /// + /// The nonempty patch measurements. + /// The eight initial values, replaced with the last accepted curve. + private static void OptimizeNoiseCurve(ReadOnlySpan levels, Span parameters) + { + const double gradientNormThreshold = 1e-8; + const double squaredGradientThreshold = gradientNormThreshold * gradientNormThreshold; + const double probeScale = 0.0001; + const double minimumDamping = 1e-15; + const double maximumDamping = 1e15; + const int maximumIterations = 40; + + // Each span has its own eight doubles. Accepting a trial must copy values, not alias the next trial's storage. + // TensorPrimitives selects the available SIMD width and handles the scalar remainder of these small spans. + InlineArray8 trialStorage = default; + InlineArray8 gradientStorage = default; + InlineArray8 trialGradientStorage = default; + InlineArray8 previousGradientStorage = default; + InlineArray8 directionStorage = default; + InlineArray8 differenceStorage = default; + Span trial = trialStorage; + Span gradient = gradientStorage; + Span trialGradient = trialGradientStorage; + Span previousGradient = previousGradientStorage; + Span direction = directionStorage; + Span difference = differenceStorage; + + double loss = ComputeNoiseLoss(levels, parameters, gradient, true); + gradient.CopyTo(direction); + double squaredGradient = TensorPrimitives.Dot(gradient, gradient); + double damping = 1; + double slope = 0; + double squaredDirection = 0; + double curvature = 0; + bool accepted = true; + int acceptedCount = 0; + + // Constant measurements can already match the initial flat curve. Stop before dividing by its zero direction norm. + for (int iteration = 0; iteration < maximumIterations && squaredGradient > squaredGradientThreshold; iteration++) + { + if (accepted) + { + // ComputeNoiseLoss returns the negative gradient. A negative slope therefore denotes a descent direction. + slope = -TensorPrimitives.Dot(direction, gradient); + if (slope >= 0) + { + gradient.CopyTo(direction); + slope = -TensorPrimitives.Dot(direction, gradient); + } + + // Probe the gradient a short distance along the direction to estimate its curvature. + squaredDirection = TensorPrimitives.Dot(direction, direction); + double probeStep = probeScale / Math.Sqrt(squaredDirection); + TensorPrimitives.Multiply(direction, probeStep, trial); + TensorPrimitives.Add(parameters, trial, trial); + _ = ComputeNoiseLoss(levels, trial, trialGradient, true); + TensorPrimitives.Subtract(gradient, trialGradient, difference); + curvature = TensorPrimitives.Dot(direction, difference) / probeStep; + } + + // Positive damping keeps the trial step usable when the measured curvature is not positive. + double dampedCurvature = curvature + (damping * squaredDirection); + if (dampedCurvature <= 0) + { + dampedCurvature = damping * squaredDirection; + damping -= curvature / squaredDirection; + } + + double step = -slope / dampedCurvature; + TensorPrimitives.Multiply(direction, step, trial); + TensorPrimitives.Add(parameters, trial, trial); + double trialLoss = ComputeNoiseLoss(levels, trial, trialGradient, true); + double agreement = 2 * (trialLoss - loss) / (step * slope); + accepted = agreement >= 0; + + if (accepted) + { + acceptedCount++; + trial.CopyTo(parameters); + gradient.CopyTo(previousGradient); + trialGradient.CopyTo(gradient); + squaredGradient = TensorPrimitives.Dot(gradient, gradient); + loss = trialLoss; + + if (squaredGradient <= squaredGradientThreshold) + { + break; + } + } + + // Rejecting a step retains the accepted curve and gradient. Retry with the same curvature and stronger damping. + if (agreement < 0.25) + { + damping = Math.Min(4 * damping, maximumDamping); + } + else if (agreement > 0.75) + { + damping = Math.Max(0.25 * damping, minimumDamping); + } + + // Restart after eight accepted steps. Otherwise combine the new gradient with the previous search direction. + if (acceptedCount % JxlNoiseParameters.NoisePoints == 0) + { + gradient.CopyTo(direction); + damping = 1; + } + else if (accepted) + { + TensorPrimitives.Subtract(previousGradient, gradient, difference); + double conjugateWeight = TensorPrimitives.Dot(difference, gradient) / slope; + TensorPrimitives.Multiply(direction, conjugateWeight, direction); + TensorPrimitives.Add(direction, gradient, direction); + } + } + } + + /// + /// Measures the noise-curve error and writes its negative gradient without changing the curve. + /// + /// The patch measurements. + /// The eight lookup values. + /// Eight independent output values for the negative gradient. + /// Whether to penalize differences between adjacent lookup values. + /// The summed fitting error, including the selected regularization. + private static double ComputeNoiseLoss( + ReadOnlySpan levels, + ReadOnlySpan parameters, + Span negativeGradient, + bool includeRegularization) + { + const double regularization = 0.005; + const double overestimateWeight = 1.1; + negativeGradient.Clear(); + double loss = 0; + + // Each measurement interpolates two adjacent lookup points. Overestimates receive a slightly larger penalty. + // The gradient sign and scale match the scaled conjugate-gradient calculation above. + for (int i = 0; i < levels.Length; i++) { - noiseParameters.Lookup[i] = (float)parameterVector.Span[i]; + JxlNoiseLevel level = levels[i]; + JxlNoiseIndexAndFraction position = JxlNoiseHelper.IndexAndFraction(level.Intensity); + double low = parameters[position.Index]; + double high = parameters[position.Index + 1]; + double value = (low * (1F - position.Fraction)) + (high * position.Fraction); + double distance = value - level.NoiseLevel; + + if (distance > 0) + { + loss += overestimateWeight * distance * distance; + negativeGradient[position.Index] -= overestimateWeight * (1F - position.Fraction) * distance; + negativeGradient[position.Index + 1] -= overestimateWeight * position.Fraction * distance; + } + else + { + loss += distance * distance; + negativeGradient[position.Index] -= (1F - position.Fraction) * distance; + negativeGradient[position.Index + 1] -= position.Fraction * distance; + } } + + if (includeRegularization) + { + // Scale smoothing by the measurement count so its strength does not depend on the number of selected patches. + for (int i = 0; i + 1 < parameters.Length; i++) + { + double difference = parameters[i] - parameters[i + 1]; + loss += regularization * levels.Length * difference * difference; + negativeGradient[i] -= regularization * difference * levels.Length; + negativeGradient[i + 1] += regularization * difference * levels.Length; + } + } + + return loss; } public static List GetNoiseLevel( @@ -282,6 +451,14 @@ public static void EncodeFloatParameter(float value, float precision, JxlBitWrit writer.Write(10, absValQuant); } + /// + /// Estimates a noise lookup from selected patches and clears it when no usable fit is available. + /// + /// The allocator configuration for patch scores. + /// The borrowed image planes used to measure patch noise. + /// The lookup values to replace or clear. + /// The strength used to scale the fitted noise. + /// Whether the fitted lookup contains a nonzero noise level. public static bool GetNoiseParameter(Configuration configuration, JxlImage3 opsin, JxlNoiseParameters noiseParams, float qualityCoefficient) { const int blockSize = 8; @@ -298,7 +475,15 @@ public static bool GetNoiseParameter(Configuration configuration, JxlImage3 nl = GetNoiseLevel(opsin, sadScores.GetSpan(), sadThreshold, blockSize); - OptimizeNoiseParameters(configuration, nl, noiseParams, qualityCoefficient * 1.4f); + + // The histogram threshold can exclude every patch. There is then no measured curve to average or fit. + if (nl.Count == 0) + { + noiseParams.Clear(); + return false; + } + + OptimizeNoiseParameters(nl, noiseParams, qualityCoefficient * 1.4f); return noiseParams.ContainsAny; } @@ -325,12 +510,4 @@ public static bool EncodeNoise(JxlNoiseParameters noiseParams, JxlBitWriter writ return true; }); } - - private sealed class Factory : JxlOptimizeArrayFactory - { - public Factory(Configuration configuration) - : base(configuration, OptimizeArrayWidth) - { - } - } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Optimize/JxlOptimizeArrayFactory{T}.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Optimize/JxlOptimizeArrayFactory{T}.cs deleted file mode 100644 index 8b14fad0d4..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Optimize/JxlOptimizeArrayFactory{T}.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Optimize; - -internal class JxlOptimizeArrayFactory(Configuration configuration, int length) - where T : unmanaged, INumber -{ - public JxlOptimizeArray CreateArray(T defaultValue = default) => new(configuration, length, defaultValue); -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Optimize/JxlOptimizeArray{T}.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Optimize/JxlOptimizeArray{T}.cs deleted file mode 100644 index bd172350b3..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Optimize/JxlOptimizeArray{T}.cs +++ /dev/null @@ -1,213 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; -using System.Numerics; -using System.Numerics.Tensors; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Optimize; - -internal struct JxlOptimizeArray : IDisposable - where T : unmanaged, INumber -{ - private readonly IMemoryOwner memoryOwner; - private readonly int length; - private readonly Configuration configuration; - private bool isDisposed; - - public JxlOptimizeArray(Configuration configuration, int length, T value) - { - this.memoryOwner = configuration.MemoryAllocator.Allocate(length); - this.memoryOwner.Memory.Span.Fill(value); - this.length = length; - this.configuration = configuration; - } - - public readonly Span Span => this.memoryOwner.Memory.Span; - - public readonly ref T this[int index] - { - get - { - DebugGuard.MustBeLessThan(index, this.length, nameof(index)); - return ref this.memoryOwner.Memory.Span[index]; - } - } - - public static JxlOptimizeArray operator +(JxlOptimizeArray a, JxlOptimizeArray b) - { - JxlOptimizeArray z = new(a.configuration, a.length, default); - TensorPrimitives.Add(a.Span, b.Span, z.Span); - return z; - } - - public static JxlOptimizeArray operator -(JxlOptimizeArray a, JxlOptimizeArray b) - { - JxlOptimizeArray z = new(a.configuration, a.length, default); - TensorPrimitives.Subtract(a.Span, b.Span, z.Span); - return z; - } - - public static JxlOptimizeArray operator *(T a, JxlOptimizeArray b) - { - JxlOptimizeArray z = new(b.configuration, b.length, default); - TensorPrimitives.Multiply(b.Span, a, z.Span); - return z; - } - - public readonly T DotProduct(JxlOptimizeArray y) => TensorPrimitives.Dot(this.Span, y.Span); - - /// - /// Finds objective-function parameters with the scaled conjugate gradient method. - /// - /// The floating-point type used by the objective and the search. - /// The factory for search arrays. - /// The objective function. - /// The initial parameter values. - /// The gradient norm at which the search stops. - /// The maximum number of search iterations. - /// The parameter values at the end of the search. - public static JxlOptimizeArray OptimizeWithScaledConjugateGradientMethod( - JxlOptimizeArrayFactory factory, - JxlOptimizeFunction function, - JxlOptimizeArray w0, - TFloat gradientNormalThreshold, - int maxIterations) - where TFloat : unmanaged, INumber, IBinaryFloatingPointIeee754 - { - // These variable names look cryptic, but they're part of - // the Scaled Conjugate Gradient method. See the reference - // implementation: - // https://github.com/libjxl/libjxl/blob/main/lib/jxl/enc_optimize.h#L81-L188 - int n = w0.length; - TFloat rsqThreshold = gradientNormalThreshold * gradientNormalThreshold; - - TFloat sigma0 = TFloat.CreateSaturating(0.0001); - TFloat lMin = TFloat.CreateSaturating(1.0e-15); - TFloat lMax = TFloat.CreateSaturating(1.0e15); - - JxlOptimizeArray w = w0; - - JxlOptimizeArray wp = factory.CreateArray(); - JxlOptimizeArray r = factory.CreateArray(); - JxlOptimizeArray rt = factory.CreateArray(); - JxlOptimizeArray e = factory.CreateArray(); - JxlOptimizeArray p = factory.CreateArray(); - - TFloat psq = default; - TFloat fp; - TFloat D = default; - TFloat d; - TFloat m = default; - TFloat a; - TFloat b; - TFloat s; - TFloat t = default; - - TFloat fw = function(w, ref r); - TFloat rsq = r.DotProduct(r); - e = r; - p = r; - - TFloat l = TFloat.CreateSaturating(1.0); - bool success = true; - long nSuccess = 0; - long k = 0; - - // Hot loop - while (k++ < maxIterations) - { - if (success) - { - m = -p.DotProduct(r); - - if (m >= TFloat.Zero) - { - p = r; - m = -p.DotProduct(r); - } - - psq = p.DotProduct(p); - s = sigma0 / TFloat.Sqrt(psq); - _ = function(w + (s * p), ref rt); - t = p.DotProduct(r - rt) / s; - } - - d = t + (l * psq); - if (d <= TFloat.Zero) - { - d = l * psq; - l -= t / psq; - } - - a = -m / d; - wp = w + (a * p); - fp = function(wp, ref rt); - - D = TFloat.CreateSaturating(2.0) * (fp - fw) / (a * m); - if (D >= TFloat.Zero) - { - success = true; - nSuccess++; - w = wp; - } - else - { - success = false; - } - - if (success) - { - e = r; - r = rt; - rsq = r.DotProduct(r); - fw = fp; - - if (rsq <= rsqThreshold) - { - break; - } - } - - if (D < TFloat.CreateSaturating(0.25)) - { - l = TFloat.Min(TFloat.CreateSaturating(4) * l, lMax); - } - else if (D > TFloat.CreateSaturating(0.75)) - { - l = TFloat.Max(TFloat.CreateSaturating(0.25) * l, lMax); - } - - if ((nSuccess % n) == 0) - { - p = r; - l = TFloat.CreateSaturating(1.0); - } - else if (success) - { - b = (e - r).DotProduct(r) / m; - p = (b * p) + r; - } - } - - // clean up - wp.Dispose(); - r.Dispose(); - rt.Dispose(); - e.Dispose(); - p.Dispose(); - - return w; - } - - public void Dispose() - { - if (this.isDisposed) - { - return; - } - - this.isDisposed = true; - this.memoryOwner.Dispose(); - } -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Optimize/JxlOptimizeFunction{T}.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Optimize/JxlOptimizeFunction{T}.cs deleted file mode 100644 index be9533b148..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Optimize/JxlOptimizeFunction{T}.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Optimize; - -internal delegate T JxlOptimizeFunction(JxlOptimizeArray a, ref JxlOptimizeArray b) - where T : unmanaged, INumber; diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/Noise/NoiseEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/Noise/NoiseEncoderTests.cs new file mode 100644 index 0000000000..6cb53362d8 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/Noise/NoiseEncoderTests.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Noise; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Noise; + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing.Encoder.Noise; + +/// +/// Tests noise-curve fitting for the JPEG XL encoder. +/// +public class NoiseEncoderTests +{ + /// + /// Checks that an already optimal constant curve retains every lookup value. + /// + /// The constant measured noise level. + [Theory] + [InlineData(0F)] + [InlineData(0.125F)] + public void OptimizeNoiseParameters_ConstantMeasurements_PreservesFlatCurve(float noiseLevel) + { + // These intensities produce exact lookup positions or half-way fractions. A flat curve has zero fitting and smoothing error. + // Its gradient is zero before the first search step, so the optimizer must not divide by the direction's zero length. + List measurements = + [ + new(noiseLevel, 0F), + new(noiseLevel, 0.25F), + new(noiseLevel, 0.5F), + new(noiseLevel, 0.75F), + new(noiseLevel, 1F), + ]; + + JxlNoiseParameters parameters = new(); + JxlNoiseEncoder.OptimizeNoiseParameters(measurements, parameters, 1F); + + foreach (float value in parameters.Lookup) + { + Assert.Equal(noiseLevel, value); + } + } +} From 6a22f21543086120fb10fa7cee7a01e2a1d11fd4 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 19:45:59 +1000 Subject: [PATCH 263/328] Consolidate JPEG XL comparison ownership and result transfers --- .../Jxl/Processing/Butteraugli/Butteraugli.cs | 436 ++++++++++++------ .../Butteraugli/ButteraugliBlurTemp.cs | 22 +- .../Butteraugli/ButteraugliComparator.cs | 188 +++++--- .../Butteraugli/ButteraugliPsychoImage.cs | 53 ++- .../Comparator/ButteraugliJxlComparator.cs | 83 ---- .../Encoder/Comparator/JxlComparator.cs | 125 ++++- .../JxlComparatorScoreComputation.cs | 68 ++- .../Encoder/Comparator/NullComparator.cs | 23 - .../Encoder/JxlAdaptiveQuantization.cs | 2 +- 9 files changed, 625 insertions(+), 375 deletions(-) delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/ButteraugliJxlComparator.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/NullComparator.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs index ddd93445d4..2608c820f7 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs @@ -553,10 +553,18 @@ public static bool SeparateLFAndMF( return true; } + /// + /// Splits the middle-frequency XYB image in place and writes the high-frequency X and Y planes. + /// + /// The configuration used if an output plane needs more capacity. + /// The caller-owned middle-frequency image, updated in place. + /// Two borrowed, caller-owned planes in X, Y order. Their identities remain unchanged. + /// The borrowed blur workspace. + /// Whether all blur and frequency-separation stages succeeded. public static bool SeparateMfAndHf( Configuration configuration, JxlImage3 mf, - ref InlineArray2> hf, + Span> hf, ButteraugliBlurTemp blurTemp) { const float sigmaHf = 3.22489901262f; @@ -564,8 +572,14 @@ public static bool SeparateMfAndHf( int xSize = mf.XSize; int ySize = mf.YSize; - hf[0] = JxlPlane.Create(configuration, xSize, ySize); - hf[1] = JxlPlane.Create(configuration, xSize, ySize); + for (int i = 0; i < 2; i++) + { + if (!hf[i].ShrinkTo(xSize, ySize)) + { + using JxlPlane replacement = new(configuration, xSize, ySize); + hf[i].Swap(replacement); + } + } int lanes = Vector.Count; @@ -645,10 +659,18 @@ public static bool SeparateMfAndHf( return SuppressXByY(hf[1], hf[0]); } + /// + /// Splits the high-frequency X and Y planes in place and writes their ultra-high-frequency planes. + /// + /// The configuration used if an output plane needs more capacity. + /// Two borrowed high-frequency planes in X, Y order, updated in place. + /// Two borrowed, caller-owned output planes in X, Y order. Their identities remain unchanged. + /// The borrowed blur workspace. + /// Whether both frequency separations succeeded. public static bool SeparateHFAndUHF( Configuration configuration, - InlineArray2> hf, - InlineArray2> uhf, + Span> hf, + Span> uhf, ButteraugliBlurTemp blurTemp) { const float sigmaUhf = 1.56416327805f; @@ -656,8 +678,15 @@ public static bool SeparateHFAndUHF( int xSize = hf[0].XSize; int ySize = hf[0].YSize; - uhf[0] = JxlPlane.Create(configuration, xSize, ySize); - uhf[1] = JxlPlane.Create(configuration, xSize, ySize); + // Borrow the owner's actual plane entries. Assigning to an InlineArray passed by value would lose the new allocations. + for (int i = 0; i < 2; i++) + { + if (!uhf[i].ShrinkTo(xSize, ySize)) + { + using JxlPlane replacement = new(configuration, xSize, ySize); + uhf[i].Swap(replacement); + } + } int lanes = Vector.Count; @@ -742,58 +771,57 @@ public static bool SeparateHFAndUHF( return true; } - public static void DeallocateHFAndUHF(InlineArray2> hf, InlineArray2> uhf) + /// + /// Releases high- and ultra-high-frequency sample storage after masking has consumed it. + /// + /// The two borrowed high-frequency planes. + /// The two borrowed ultra-high-frequency planes. + public static void DeallocateHFAndUHF(Span> hf, Span> uhf) { for (int i = 0; i < 2; i++) { hf[i].Dispose(); uhf[i].Dispose(); - - hf[i] = new JxlPlane(); - uhf[i] = new JxlPlane(); } } + /// + /// Writes all frequency bands into the supplied owner, reusing its existing sample capacity when possible. + /// + /// The configuration used for frequency-band sample allocations. + /// The borrowed blur workspace. + /// The borrowed transformed input image. + /// The owner of every output plane, including partially completed outputs on failure. + /// Whether all frequency bands were produced. public static bool SeparateFrequencies( Configuration configuration, ButteraugliBlurTemp blurTemp, JxlImage3 xyb, ButteraugliPsychoImage ps) { - ps.Lf = new JxlImage3( - configuration, - xyb.XSize, - xyb.YSize); - - ps.Mf = new JxlImage3( - configuration, - xyb.XSize, - xyb.YSize); - - if (!SeparateLFAndMF( - configuration, - xyb, - ps.Lf, - ps.Mf, - blurTemp)) + if (!ps.Lf.ShrinkTo(xyb.XSize, xyb.YSize)) + { + using JxlImage3 replacement = new(configuration, xyb.XSize, xyb.YSize); + ps.Lf.Swap(replacement); + } + + if (!ps.Mf.ShrinkTo(xyb.XSize, xyb.YSize)) + { + using JxlImage3 replacement = new(configuration, xyb.XSize, xyb.YSize); + ps.Mf.Swap(replacement); + } + + if (!SeparateLFAndMF(configuration, xyb, ps.Lf, ps.Mf, blurTemp)) { return false; } - if (!SeparateMfAndHf( - configuration, - ps.Mf, - ref ps.Hf, - blurTemp)) + if (!SeparateMfAndHf(configuration, ps.Mf, ps.Hf, blurTemp)) { return false; } - if (!SeparateHFAndUHF( - configuration, - ps.Hf, - ps.Uhf, - blurTemp)) + if (!SeparateHFAndUHF(configuration, ps.Hf, ps.Uhf, blurTemp)) { return false; } @@ -1653,6 +1681,16 @@ public static void FuzzyErosion(JxlPlane from, JxlPlane to) } } + /// + /// Builds a perceptual mask and optionally accumulates its AC correction into caller-owned storage. + /// + /// The configuration used for owned sample allocations. + /// The borrowed reference masking samples. + /// The borrowed distorted masking samples. + /// The borrowed blur workspace. + /// The existing AC accumulation plane, or null when no AC correction is needed. + /// The newly allocated output, transferred to the caller on success and released on failure. + /// Whether mask construction succeeded. public static bool Mask( Configuration configuration, JxlPlane mask0, @@ -1666,55 +1704,80 @@ public static bool Mask( mask = new(configuration, width, height); - const float mul = 6.19424080439f; - const float bias = 12.61050594197f; - const float radius = 2.7f; - - JxlPlane diff0 = new(configuration, width, height); - JxlPlane diff1 = new(configuration, width, height); - JxlPlane blurred0 = new(configuration, width, height); - JxlPlane blurred1 = new(configuration, width, height); + bool completed = false; - DiffPrecompute(mask0, mul, bias, diff0); - DiffPrecompute(mask1, mul, bias, diff1); - - if (!Blur(configuration, diff0, radius, blurTemp, blurred0)) + try { - return false; - } + const float mul = 6.19424080439f; + const float bias = 12.61050594197f; + const float radius = 2.7f; - FuzzyErosion(blurred0, diff0); + using JxlPlane diff0 = new(configuration, width, height); + using JxlPlane diff1 = new(configuration, width, height); + using JxlPlane blurred0 = new(configuration, width, height); + using JxlPlane blurred1 = new(configuration, width, height); - if (!Blur(configuration, diff1, radius, blurTemp, blurred1)) - { - return false; - } + DiffPrecompute(mask0, mul, bias, diff0); + DiffPrecompute(mask1, mul, bias, diff1); - for (int y = 0; y < height; y++) - { - Span maskRow = mask.GetRow(y); - Span diffRow = diffAc is not null ? diffAc.GetRow(y) : []; + if (!Blur(configuration, diff0, radius, blurTemp, blurred0)) + { + return false; + } - ReadOnlySpan diff0Row = diff0.GetRow(y); - ReadOnlySpan blur0Row = blurred0.GetRow(y); - ReadOnlySpan blur1Row = blurred1.GetRow(y); + FuzzyErosion(blurred0, diff0); - for (int x = 0; x < width; x++) + if (!Blur(configuration, diff1, radius, blurTemp, blurred1)) { - maskRow[x] = diff0Row[x]; + return false; + } + + for (int y = 0; y < height; y++) + { + Span maskRow = mask.GetRow(y); + Span diffRow = diffAc is not null ? diffAc.GetRow(y) : []; + + ReadOnlySpan diff0Row = diff0.GetRow(y); + ReadOnlySpan blur0Row = blurred0.GetRow(y); + ReadOnlySpan blur1Row = blurred1.GetRow(y); - if (diffRow.Length > 0) + for (int x = 0; x < width; x++) { - const float maskToErrorMul = 10.0f; - float diff = blur0Row[x] - blur1Row[x]; - diffRow[x] += maskToErrorMul * diff * diff; + maskRow[x] = diff0Row[x]; + + if (diffRow.Length > 0) + { + const float maskToErrorMul = 10.0f; + float diff = blur0Row[x] - blur1Row[x]; + diffRow[x] += maskToErrorMul * diff * diff; + } } } - } - return true; + completed = true; + return true; + } + finally + { + if (!completed) + { + mask.Dispose(); + } + } } + /// + /// Writes a perceptual mask from borrowed frequency bands and optionally updates an existing AC plane. + /// + /// The configuration used for temporary sample allocations. + /// The borrowed reference frequency bands. + /// The borrowed distorted frequency bands. + /// The comparison width. + /// The comparison height. + /// The borrowed blur workspace. + /// The caller-owned result plane, which receives the completed mask allocation. + /// The existing AC accumulation plane, or null when only the mask is needed. + /// Whether mask construction succeeded. public static bool MaskButteraugliPsychoImage( Configuration configuration, ButteraugliPsychoImage pi0, @@ -1723,28 +1786,27 @@ public static bool MaskButteraugliPsychoImage( int height, ButteraugliBlurTemp blurTemp, JxlPlane mask, - out JxlPlane? diffAc) + JxlPlane? diffAc) { - JxlPlane mask0 = new(configuration, width, height); - JxlPlane mask1 = new(configuration, width, height); - - CombineChannelsForMasking( - pi0.Hf, - pi0.Uhf, - mask0); - - CombineChannelsForMasking( - pi1.Hf, - pi1.Uhf, - mask1); - - return Mask( - configuration, - mask0, - mask1, - blurTemp, - mask, - out diffAc); + using JxlPlane mask0 = new(configuration, width, height); + using JxlPlane mask1 = new(configuration, width, height); + + CombineChannelsForMasking(pi0.Hf, pi0.Uhf, mask0); + CombineChannelsForMasking(pi1.Hf, pi1.Uhf, mask1); + + bool completed = Mask(configuration, mask0, mask1, blurTemp, diffAc, out JxlPlane allocatedMask); + + using (allocatedMask) + { + if (!completed) + { + return false; + } + + // Transfer only the completed output. The temporary then releases any previous caller allocation. + mask.Swap(allocatedMask); + return true; + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -2160,12 +2222,23 @@ public static bool ButteraugliDiffmapInPlace( InlineArray2> hf0 = default; InlineArray2> hf1 = default; - if (!SeparateMfAndHf(configuration, image0, ref hf0, blurTemp)) + // The inline arrays borrow these owners. Using declarations release every partially built band on early return. + using JxlPlane hf0X = new(); + using JxlPlane hf0Y = new(); + using JxlPlane hf1X = new(); + using JxlPlane hf1Y = new(); + + hf0[0] = hf0X; + hf0[1] = hf0Y; + hf1[0] = hf1X; + hf1[1] = hf1Y; + + if (!SeparateMfAndHf(configuration, image0, hf0, blurTemp)) { return false; } - if (!SeparateMfAndHf(configuration, image1, ref hf1, blurTemp)) + if (!SeparateMfAndHf(configuration, image1, hf1, blurTemp)) { return false; } @@ -2218,6 +2291,16 @@ public static bool ButteraugliDiffmapInPlace( InlineArray2> uhf0 = default; InlineArray2> uhf1 = default; + using JxlPlane uhf0X = new(); + using JxlPlane uhf0Y = new(); + using JxlPlane uhf1X = new(); + using JxlPlane uhf1Y = new(); + + uhf0[0] = uhf0X; + uhf0[1] = uhf0Y; + uhf1[0] = uhf1X; + uhf1[1] = uhf1Y; + if (!SeparateHFAndUHF(configuration, hf0, uhf0, blurTemp)) { return false; @@ -2301,22 +2384,25 @@ public static bool ButteraugliDiffmapInPlace( return false; } - for (int y = 0; y < ySize; y++) + using (newBlockDiffAc) { - ReadOnlySpan dc = newBlockDiffAc.GetRow(y); - ReadOnlySpan ac = newBlockDiffAc.GetRow(y); - Span output = diffmap.GetRow(y); - ReadOnlySpan maskRow = mask.GetRow(y); - - for (int x = 0; x < xSize; x++) + for (int y = 0; y < ySize; y++) { - float m = maskRow[x]; + ReadOnlySpan dc = newBlockDiffAc.GetRow(y); + ReadOnlySpan ac = newBlockDiffAc.GetRow(y); + Span output = diffmap.GetRow(y); + ReadOnlySpan maskRow = mask.GetRow(y); - output[x] = MathF.Sqrt((dc[x] * (float)MaskDcY(m)) + (ac[x] * (float)MaskY(m))); + for (int x = 0; x < xSize; x++) + { + float m = maskRow[x]; + + output[x] = MathF.Sqrt((dc[x] * (float)MaskDcY(m)) + (ac[x] * (float)MaskY(m))); + } } - } - return true; + return true; + } } // Calculate a 2x2 subsampled image for purposes of recursive butteraugli at @@ -2493,6 +2579,16 @@ public static float ButteraugliFuzzyClass(float score) return val; } + /// + /// Computes the perceptual difference map and score, using input storage for full-size comparisons. + /// + /// The configuration used for sample allocations. + /// The reference pixels, which full-size comparison can modify. + /// The distorted pixels, which full-size comparison can modify. + /// The perceptual comparison parameters. + /// The caller-owned output plane. Its identity does not change. + /// The comparison score, or zero when comparison fails. + /// Whether every comparison stage succeeded. public static bool ButteraugliInterfaceInPlace( Configuration configuration, JxlImage3 rgb0, @@ -2506,7 +2602,7 @@ public static bool ButteraugliInterfaceInPlace( int xsize = rgb0.XSize; int ysize = rgb0.YSize; - if ((xsize & ysize) == 0) // equivalent to xsize == 0 || ysize == 0, minus one branch + if (xsize == 0 || ysize == 0) { throw new InvalidOperationException("Zero-sized image"); } @@ -2520,9 +2616,21 @@ public static bool ButteraugliInterfaceInPlace( if (xsize < max || ysize < max) { - bool ok = ButteraugliDiffmapSmall(configuration, max, rgb0, rgb1, parameters, out diffmap); - diffvalue = ButteraugliScoreFromDiffmap(diffmap, parameters); - return ok; + bool completed = ButteraugliDiffmapSmall(configuration, max, rgb0, rgb1, parameters, out JxlPlane smallDiffmap); + + using (smallDiffmap) + { + if (!completed) + { + return false; + } + + diffvalue = ButteraugliScoreFromDiffmap(smallDiffmap, parameters); + + // Transfer the completed allocation into the caller's plane. The temporary releases the caller's previous allocation. + diffmap.Swap(smallDiffmap); + return true; + } } JxlPlane subDiffmap = new(); @@ -2552,6 +2660,16 @@ public static bool ButteraugliInterfaceInPlace( return true; } + /// + /// Computes the perceptual difference map and score without changing either input image. + /// + /// The configuration used for sample allocations. + /// The borrowed reference pixels. + /// The borrowed distorted pixels of the same size. + /// The perceptual comparison parameters. + /// The caller-owned plane that receives the completed map. Its identity does not change. + /// The comparison score, or zero when comparison fails. + /// Whether every comparison stage succeeded. public static bool ButteraugliInterface( Configuration configuration, JxlImage3 rgb0, @@ -2562,13 +2680,21 @@ public static bool ButteraugliInterface( { diffValue = 0; - if (!ButteraugliDiffmap(configuration, rgb0, rgb1, parameters, out diffmap)) + bool completed = ButteraugliDiffmap(configuration, rgb0, rgb1, parameters, out JxlPlane result); + + using (result) { - return false; - } + if (!completed) + { + return false; + } - diffValue = ButteraugliScoreFromDiffmap(diffmap, parameters); - return true; + diffValue = ButteraugliScoreFromDiffmap(result, parameters); + + // Keep the caller's plane object and dispose its previous allocation after the storage exchange. + diffmap.Swap(result); + return true; + } } public static bool ButteraugliInterface( @@ -2589,6 +2715,15 @@ public static bool ButteraugliInterface( return ButteraugliInterface(configuration, rgb0, rgb1, parameters, diffmap, out diffValue); } + /// + /// Allocates the perceptual difference map for two borrowed RGB images. + /// + /// The configuration used for sample allocations. + /// The borrowed reference pixels. + /// The borrowed distorted pixels of the same size. + /// The perceptual comparison parameters. + /// The output owner transferred on success, or an empty plane on failure. + /// Whether every comparison stage succeeded. public static bool ButteraugliDiffmap( Configuration configuration, JxlImage3 rgb0, @@ -2596,12 +2731,10 @@ public static bool ButteraugliDiffmap( ButteraugliParameters parameters, out JxlPlane diffmap) { - diffmap = new(); - int xsize = rgb0.XSize; int ysize = rgb0.YSize; - if ((xsize & ysize) == 0) // equivalent to xsize == 0 || ysize == 0, minus one branch + if (xsize == 0 || ysize == 0) { throw new InvalidOperationException("Zero-sized image"); } @@ -2615,17 +2748,38 @@ public static bool ButteraugliDiffmap( if (xsize < max || ysize < max) { - if (!ButteraugliDiffmapSmall(configuration, max, rgb0, rgb1, parameters, out diffmap)) - { - return false; - } + // The padded comparison already produced the result. Do not replace it with the small comparator's empty decomposition. + return ButteraugliDiffmapSmall(configuration, max, rgb0, rgb1, parameters, out diffmap); } - ButteraugliComparator butteraugli = ButteraugliComparator.Make(configuration, rgb0, parameters); + using ButteraugliComparator butteraugli = ButteraugliComparator.Make(configuration, rgb0, parameters); + diffmap = new(); + bool completed = false; - return butteraugli.Diffmap(configuration, rgb1, diffmap); + try + { + completed = butteraugli.Diffmap(configuration, rgb1, diffmap); + return completed; + } + finally + { + if (!completed) + { + diffmap.Dispose(); + } + } } + /// + /// Compares padded small images and transfers only the original-size region of the resulting difference map. + /// + /// The configuration used for sample allocations. + /// The minimum comparison extent in each dimension. + /// The borrowed reference pixels. + /// The borrowed distorted pixels of the same size. + /// The perceptual comparison parameters. + /// The original-size output owner transferred on success, or an empty plane on failure. + /// Whether the padded comparison succeeded. public static bool ButteraugliDiffmapSmall( Configuration configuration, int max, @@ -2660,23 +2814,37 @@ public static bool ButteraugliDiffmapSmall( } } - JxlPlane diffmapScaled = new(); - bool ok = ButteraugliDiffmap(configuration, scaled0, scaled1, parameters, out diffmapScaled); - - diffmap = new(configuration, xsize, ysize); + bool completed = ButteraugliDiffmap(configuration, scaled0, scaled1, parameters, out JxlPlane diffmapScaled); - for (int y = 0; y < ysize; y++) + using (diffmapScaled) { - Span diffmapRow = diffmap.GetRow(y); - Span diffmapScaledRow = diffmapScaled.GetRow(y + yborder); + if (!completed) + { + diffmap = new(); + return false; + } + + diffmap = new(configuration, xsize, ysize); + bool copied = false; - for (int x = 0; x < xsize; x++) + try { - diffmapRow[x] = diffmapScaledRow[x + xborder]; + for (int y = 0; y < ysize; y++) + { + diffmapScaled.GetRow(y + yborder).Slice(xborder, xsize).CopyTo(diffmap.GetRow(y)); + } + + copied = true; + return true; + } + finally + { + if (!copied) + { + diffmap.Dispose(); + } } } - - return ok; } public static float ButteraugliScoreFromDiffmap(JxlPlane diffmap, ButteraugliParameters parameters) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliBlurTemp.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliBlurTemp.cs index 26aa8372ce..6722e55e14 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliBlurTemp.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliBlurTemp.cs @@ -5,20 +5,34 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; +/// +/// Owns the reusable transposed plane used by Butteraugli's two-pass blur. +/// internal sealed class ButteraugliBlurTemp : IDisposable { - public JxlPlane TransposedTemp { get; set; } = new(); + /// + /// Gets the owned transposed scratch plane. Callers borrow it until the next blur or disposal. + /// + public JxlPlane TransposedTemp { get; } = new(); + /// + /// Reuses or replaces the scratch allocation for the transposed input dimensions. + /// + /// The allocator configuration used if the existing capacity is too small. + /// The input plane whose dimensions are transposed. + /// The borrowed scratch plane, with width equal to the input height and height equal to its width. public JxlPlane GetTransposed(Configuration configuration, JxlPlane input) { - if (this.TransposedTemp.XSize == 0) + if (!this.TransposedTemp.ShrinkTo(input.YSize, input.XSize)) { - // Yes, YSize and XSize are swapped - this.TransposedTemp = JxlPlane.Create(configuration, input.YSize, input.XSize); + // Allocate before exchanging ownership. Disposal then releases the old allocation, including on a later blur failure. + using JxlPlane replacement = new(configuration, input.YSize, input.XSize); + this.TransposedTemp.Swap(replacement); } return this.TransposedTemp; } + /// public void Dispose() => this.TransposedTemp.Dispose(); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs index 3d07cb1d4a..09c95fccea 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs @@ -6,87 +6,110 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; +/// +/// Owns the reference frequency bands, lower-resolution comparators and reusable blur storage for Butteraugli. +/// +/// Comparisons reuse mutable scratch and run sequentially for each comparator. internal class ButteraugliComparator : IDisposable { private readonly int xSize; private readonly int ySize; - private ButteraugliParameters parameters; + private readonly ButteraugliParameters parameters; private readonly ButteraugliPsychoImage pi0 = new(); - private JxlImage3? temp; + private readonly JxlImage3 temp; private readonly ButteraugliBlurTemp blurTemp = new(); private ButteraugliComparator? sub; - public ButteraugliComparator(int xsize, int ysize, ButteraugliParameters parameters) + /// + /// Initializes a comparator and its full-resolution scratch image without taking ownership of input pixels. + /// + /// The configuration used for owned sample allocations. + /// The comparison width. + /// The comparison height. + /// The perceptual comparison parameters. + public ButteraugliComparator(Configuration configuration, int xsize, int ysize, ButteraugliParameters parameters) { this.xSize = xsize; this.ySize = ysize; this.parameters = parameters; + this.temp = new(configuration, xsize, ysize); } - public JxlImage3 Temp => this.temp ??= new(); + /// + /// Gets the borrowed blur scratch image. The comparator retains this allocation between comparisons. + /// + public JxlImage3 Temp => this.temp; + /// + /// Builds the owned reference decomposition and its lower-resolution comparisons from borrowed RGB samples. + /// + /// The configuration used for owned sample allocations. + /// The borrowed reference pixels. + /// The perceptual comparison parameters. + /// The comparator that owns the completed reference decomposition. public static ButteraugliComparator Make(Configuration configuration, JxlImage3 rgb0, ButteraugliParameters parameters) { int xSize = rgb0.XSize; int ySize = rgb0.YSize; - ButteraugliComparator result = new(xSize, ySize, parameters) - { - temp = new JxlImage3(configuration, xSize, ySize) - }; + ButteraugliComparator result = new(configuration, xSize, ySize, parameters); - if (xSize < 8 || ySize < 8) + try { - return result; - } + if (xSize < 8 || ySize < 8) + { + return result; + } - JxlImage3 xyb0 = new(configuration, xSize, ySize); + using JxlImage3 xyb0 = new(configuration, xSize, ySize); - if (!Butteraugli.OpsinDynamicsImage(configuration, rgb0, parameters, result.Temp, result.blurTemp, xyb0)) - { - throw new InvalidOperationException("OpsinDynamicsImage failed"); - } + if (!Butteraugli.OpsinDynamicsImage(configuration, rgb0, parameters, result.Temp, result.blurTemp, xyb0)) + { + throw new InvalidOperationException("OpsinDynamicsImage failed"); + } + + if (!Butteraugli.SeparateFrequencies(configuration, result.blurTemp, xyb0, result.pi0)) + { + throw new InvalidOperationException("Could not separate frequencies"); + } - result.ReleaseTemp(); + using JxlImage3 subsampledRgb0 = Butteraugli.SubSample2x(configuration, rgb0); + result.sub = Make(configuration, subsampledRgb0, parameters); - if (!Butteraugli.SeparateFrequencies(configuration, result.blurTemp, xyb0, result.pi0)) + return result; + } + catch { - throw new InvalidOperationException("Could not separate frequencies"); + // Construction publishes ownership only after every required frequency band and lower-resolution comparator succeeds. + result.Dispose(); + throw; } - - JxlImage3 subsampledRgb0 = Butteraugli.SubSample2x(configuration, rgb0); - result.sub = Make(configuration, subsampledRgb0, parameters); - - return result; - } - - public void ReleaseTemp() - { - this.temp?.Dispose(); - this.temp = null; } /// /// Computes the butteraugli map between the original image given in the constructor and the distorted image given here. /// + /// The configuration used for temporary sample allocations. + /// The borrowed distorted pixels. + /// The caller-owned result plane. Its storage is resized or reused to match the reference. + /// Whether all comparison stages succeeded. public virtual bool Diffmap(Configuration configuration, JxlImage3 rgb1, JxlPlane result) { if (this.xSize < 8 || this.ySize < 8) { + this.PrepareResult(configuration, result); result.Clear(); return true; } - JxlImage3 xyb1 = new(configuration, this.xSize, this.ySize); + using JxlImage3 xyb1 = new(configuration, this.xSize, this.ySize); if (!Butteraugli.OpsinDynamicsImage(configuration, rgb1, this.parameters, this.Temp, this.blurTemp, xyb1)) { return false; } - this.ReleaseTemp(); - - if (!this.DiffmapOpsinDynamicsImage(configuration, xyb1, out result)) + if (!this.DiffmapOpsinDynamicsImage(configuration, xyb1, result)) { return false; } @@ -98,17 +121,18 @@ public virtual bool Diffmap(Configuration configuration, JxlImage3 rgb1, return true; } - JxlImage3 subXyb = new(configuration, this.sub.xSize, this.sub.ySize); - JxlImage3 subsampledRgb1 = Butteraugli.SubSample2x(configuration, rgb1); + using JxlImage3 subXyb = new(configuration, this.sub.xSize, this.sub.ySize); + using JxlImage3 subsampledRgb1 = Butteraugli.SubSample2x(configuration, rgb1); if (!Butteraugli.OpsinDynamicsImage(configuration, subsampledRgb1, this.parameters, this.sub.Temp, this.sub.blurTemp, subXyb)) { return false; } - this.sub.ReleaseTemp(); + using JxlPlane subResult = new(); - if (!this.DiffmapOpsinDynamicsImage(configuration, subXyb, out JxlPlane subResult)) + // Use the lower-resolution reference decomposition with its matching lower-resolution distorted image. + if (!this.sub.DiffmapOpsinDynamicsImage(configuration, subXyb, subResult)) { return false; } @@ -120,34 +144,42 @@ public virtual bool Diffmap(Configuration configuration, JxlImage3 rgb1, } /// - /// Same as Diffmap but OpsinDynamicsImage() was already applied. + /// Computes the comparison from borrowed samples that already passed through the opsin dynamics transform. /// - public bool DiffmapOpsinDynamicsImage(Configuration configuration, JxlImage3 xyb1, out JxlPlane result) + /// The configuration used for temporary sample allocations. + /// The borrowed transformed samples. + /// The caller-owned result plane. + /// Whether frequency separation and comparison succeeded. + public bool DiffmapOpsinDynamicsImage(Configuration configuration, JxlImage3 xyb1, JxlPlane result) { - result = new(); - if (this.xSize < 8 || this.ySize < 8) { + this.PrepareResult(configuration, result); result.Clear(); return true; } - ButteraugliPsychoImage pi1 = new(); + using ButteraugliPsychoImage pi1 = new(); if (!Butteraugli.SeparateFrequencies(configuration, this.blurTemp, xyb1, pi1)) { return false; } - result = new(configuration, this.xSize, this.ySize); return this.DiffmapPsychoImage(configuration, pi1, result); } /// - /// Same as above but the frequency decomposition was already applied. + /// Compares a borrowed frequency decomposition against the owned reference decomposition. /// + /// The configuration used for temporary sample allocations. + /// The borrowed distorted frequency bands. + /// The caller-owned result plane. + /// Whether every comparison and mask stage succeeded. public bool DiffmapPsychoImage(Configuration configuration, ButteraugliPsychoImage pi1, JxlPlane diffmap) { + this.PrepareResult(configuration, diffmap); + if (this.xSize < 8 || this.ySize < 8) { diffmap.Clear(); @@ -157,8 +189,8 @@ public bool DiffmapPsychoImage(Configuration configuration, ButteraugliPsychoIma float hfAsymmetry = this.parameters.HfAsymmetry; float xmul = this.parameters.XMultiplier; - JxlPlane diffs = new(configuration, this.xSize, this.ySize); - JxlImage3 blockDiffAc = new(configuration, this.xSize, this.ySize); + using JxlPlane diffs = new(configuration, this.xSize, this.ySize); + using JxlImage3 blockDiffAc = new(configuration, this.xSize, this.ySize); JxlImageOperations.ZeroFillImage(blockDiffAc); @@ -215,8 +247,8 @@ public bool DiffmapPsychoImage(Configuration configuration, ButteraugliPsychoIma } if (!Butteraugli.MaltaDiffMapLf( - this.pi0.Mf!.Plane(1), - pi1.Mf!.Plane(1), + this.pi0.Mf.Plane(1), + pi1.Mf.Plane(1), Butteraugli.WHfMalta, Butteraugli.WMfMalta, Butteraugli.Norm1Mf, @@ -228,8 +260,8 @@ public bool DiffmapPsychoImage(Configuration configuration, ButteraugliPsychoIma } if (!Butteraugli.MaltaDiffMapLf( - this.pi0.Mf!.Plane(0), - pi1.Mf!.Plane(0), + this.pi0.Mf.Plane(0), + pi1.Mf.Plane(0), Butteraugli.WHfMaltaX, Butteraugli.WMfMaltaX, Butteraugli.Norm1MfX, @@ -240,7 +272,7 @@ public bool DiffmapPsychoImage(Configuration configuration, ButteraugliPsychoIma return false; } - JxlImage3 blockDiffDc = new(configuration, this.xSize, this.ySize); + using JxlImage3 blockDiffDc = new(configuration, this.xSize, this.ySize); for (int c = 0; c < 3; c++) { @@ -261,42 +293,52 @@ public bool DiffmapPsychoImage(Configuration configuration, ButteraugliPsychoIma blockDiffAc.Plane(c)); Butteraugli.SetL2Diff( - this.pi0.Lf!.Plane(c), - pi1.Lf!.Plane(c), + this.pi0.Lf.Plane(c), + pi1.Lf.Plane(c), Butteraugli.Wmul[6 + c], blockDiffDc.Plane(c)); } - JxlPlane mask = new(); - - if (!Butteraugli.MaskButteraugliPsychoImage( - configuration, - this.pi0, - pi1, - this.xSize, - this.ySize, - this.blurTemp, - mask, - out JxlPlane? diffAc)) - { - return false; - } + using JxlPlane mask = new(); - if (diffAc is not null) + if (!Butteraugli.MaskButteraugliPsychoImage(configuration, this.pi0, pi1, this.xSize, this.ySize, this.blurTemp, mask, blockDiffAc.Plane(1))) { - JxlImageOperations.ForceCopyImage(diffAc, blockDiffAc.Plane(1)); + return false; } return Butteraugli.CombineChannelsToDiffmap(mask, blockDiffDc, blockDiffAc, xmul, diffmap); } + /// + /// Writes the reference image's perceptual mask into caller-owned storage. + /// + /// The configuration used for temporary sample allocations. + /// The caller-owned output plane. + /// Whether mask construction succeeded. public virtual bool Mask(Configuration configuration, JxlPlane mask) - => Butteraugli.MaskButteraugliPsychoImage(configuration, this.pi0, this.pi0, this.xSize, this.ySize, this.blurTemp, mask, out _); + => Butteraugli.MaskButteraugliPsychoImage(configuration, this.pi0, this.pi0, this.xSize, this.ySize, this.blurTemp, mask, null); + /// public void Dispose() { - this.temp?.Dispose(); - this.temp = null; + this.temp.Dispose(); + this.pi0.Dispose(); + this.sub?.Dispose(); + this.sub = null; this.blurTemp.Dispose(); } + + /// + /// Reuses the caller's result capacity or exchanges it for an allocation matching the reference dimensions. + /// + /// The configuration used if a larger allocation is required. + /// The result plane whose identity and ownership remain with the caller. + private void PrepareResult(Configuration configuration, JxlPlane result) + { + if (!result.ShrinkTo(this.xSize, this.ySize)) + { + using JxlPlane replacement = new(configuration, this.xSize, this.ySize); + result.Swap(replacement); + } + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliPsychoImage.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliPsychoImage.cs index d5840af082..2b0ed73937 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliPsychoImage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliPsychoImage.cs @@ -1,22 +1,59 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Memory; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; -// Public fields because InlineArray2 values -// cannot be mutated if it's a property #pragma warning disable SA1401 // Fields should be private -internal sealed class ButteraugliPsychoImage +/// +/// Owns one image's low, middle, high and ultra-high frequency bands for perceptual comparison. +/// +internal sealed class ButteraugliPsychoImage : IDisposable { - public InlineArray2> Uhf; // XY + /// + /// Owned ultra-high-frequency X and Y planes. Their entries remain stable while their sample allocations can be replaced. + /// + public InlineArray2> Uhf; - public InlineArray2> Hf; // XY + /// + /// Owned high-frequency X and Y planes. Frequency separation borrows the entries rather than replacing a copied array. + /// + public InlineArray2> Hf; - public JxlImage3? Mf { get; set; } // XYB + /// + /// Initializes empty owned planes without allocating sample storage. + /// + public ButteraugliPsychoImage() + { + for (int i = 0; i < 2; i++) + { + this.Hf[i] = new(); + this.Uhf[i] = new(); + } + } - public JxlImage3? Lf { get; set; } // XYB + /// + /// Gets the owned middle-frequency XYB image. + /// + public JxlImage3 Mf { get; } = new(); + + /// + /// Gets the owned low-frequency XYB image. + /// + public JxlImage3 Lf { get; } = new(); + + /// + public void Dispose() + { + for (int i = 0; i < 2; i++) + { + this.Hf[i].Dispose(); + this.Uhf[i].Dispose(); + } + + this.Mf.Dispose(); + this.Lf.Dispose(); + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/ButteraugliJxlComparator.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/ButteraugliJxlComparator.cs deleted file mode 100644 index f63d569395..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/ButteraugliJxlComparator.cs +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics.Tensors; -using SixLabors.ImageSharp.Formats.Jxl.Cms; -using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; -using SixLabors.ImageSharp.Formats.Jxl.Memory; -using SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; -using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Comparator; - -/// -/// Compares images using Google Butteraugli. -/// -internal sealed class ButteraugliJxlComparator(ButteraugliParameters parameters, JxlCmsInterface cms) - : JxlComparator -{ - private int width; - private int height; - private float intensityTarget; - private ButteraugliComparator? comparator; - - public override float GoodQualityScore => Butteraugli.Butteraugli.ButteraugliFuzzyInverse(1.5f); - - public override float BadQualityScore => Butteraugli.Butteraugli.ButteraugliFuzzyInverse(0.5f); - - public override void CompareWith(Configuration configuration, JxlImageBundle actual, JxlPlane? diffMap, out float score) - { - DebugGuard.NotNull(this.comparator, nameof(this.comparator)); - - if (this.width != actual.XSize || this.height != actual.YSize) - { - throw new InvalidOperationException("Image must have same size"); - } - - JxlImageMetadata metadata = actual.Metadata; - JxlImageBundle store = new(metadata); - - JxlTransformsEncoder.TransformIfNeeded(configuration, actual, JxlColorEncoding.LinearSRgb(actual.IsGray), cms, store, out JxlImageBundle actualLinearSRgb); - - JxlPlane tempDiffmap = new(configuration, this.width, this.height); - JxlImage3 scaledActualLinearSRgb = actualLinearSRgb.Color!; - - if (this.intensityTarget != 0 && actual.Metadata.IntensityTarget != this.intensityTarget) - { - JxlImage3 scaledActualLinearSRgbStore = new(configuration, this.width, this.height); - scaledActualLinearSRgb = scaledActualLinearSRgbStore; - - float scale = actual.Metadata.IntensityTarget / this.intensityTarget; - for (int c = 0; c < 3; ++c) - { - for (int y = 0; y < this.height; ++y) - { - Span souceRow = actualLinearSRgb.Color!.PlaneRow(c, y); - Span scaledRow = scaledActualLinearSRgb.PlaneRow(c, y); - TensorPrimitives.Multiply(souceRow, scale, scaledRow); - } - } - } - - if (!this.comparator.Diffmap(configuration, scaledActualLinearSRgb, tempDiffmap)) - { - throw new InvalidOperationException("Diffmap failed"); - } - - score = Butteraugli.Butteraugli.ButteraugliScoreFromDiffmap(tempDiffmap, parameters); - diffMap?.Swap(tempDiffmap); - } - - public override void SetReferenceImage(Configuration configuration, JxlImageBundle reference) - { - JxlImageMetadata metadata = reference.Metadata; - JxlImageBundle store = new(metadata); - - JxlTransformsEncoder.TransformIfNeeded(configuration, reference, JxlColorEncoding.LinearSRgb(reference.IsGray), cms, store, out JxlImageBundle refLinearSRgb); - this.comparator = ButteraugliComparator.Make(configuration, refLinearSRgb.Color!, parameters); - - this.width = reference.XSize; - this.height = reference.YSize; - this.intensityTarget = reference.Metadata.IntensityTarget; - } -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparator.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparator.cs index 8a8be68646..72f4c26b34 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparator.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparator.cs @@ -1,27 +1,128 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics.Tensors; +using SixLabors.ImageSharp.Formats.Jxl.Cms; +using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Memory; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Comparator; /// -/// Compares images. +/// Compares encoder samples against a retained reference using Butteraugli. /// -internal abstract class JxlComparator +/// The perceptual comparison parameters. +/// The color conversion provider for the source samples. +internal sealed class JxlComparator(ButteraugliParameters parameters, JxlCmsInterface cms) : IDisposable { - public abstract float GoodQualityScore { get; } + private int width; + private int height; + private float intensityTarget; + private ButteraugliComparator? comparator; - public abstract float BadQualityScore { get; } + /// + /// Gets the score used for the encoder's good-quality target. + /// + public float GoodQualityScore => Butteraugli.Butteraugli.ButteraugliFuzzyInverse(1.5f); - // Sets the reference image, the first to compare. - public abstract void SetReferenceImage(Configuration configuration, JxlImageBundle reference); + /// + /// Gets the score used for the encoder's bad-quality target. + /// + public float BadQualityScore => Butteraugli.Butteraugli.ButteraugliFuzzyInverse(0.5f); - // Sets the actual image, the second to compare. - public abstract void CompareWith( - Configuration configuration, - JxlImageBundle actual, - JxlPlane? diffMap, - out float score); + /// + /// Compares borrowed samples with the current reference and writes an optional difference map. + /// + /// The configuration that owns temporary sample storage. + /// The borrowed image to compare without changing its samples. + /// The caller-owned difference map, or null when only a score is required. + /// The computed perceptual difference. + public void CompareWith(Configuration configuration, JxlImageBundle actual, JxlPlane? diffMap, out float score) + { + DebugGuard.NotNull(this.comparator, nameof(this.comparator)); + + if (this.width != actual.XSize || this.height != actual.YSize) + { + throw new InvalidOperationException("Image must have same size"); + } + + JxlImageMetadata metadata = actual.Metadata; + using JxlImageBundle store = new(metadata); + + // The selected source can alias the caller's bundle. Only store owns temporary converted planes. + JxlImageBundle actualLinearSRgb = actual; + if (!JxlImageBundleEncoder.TransformIfNeeded(configuration, actual, JxlColorEncoding.LinearSrgb(actual.IsGray), cms, store, ref actualLinearSRgb)) + { + throw new InvalidOperationException("Could not convert the comparison image to linear sRGB."); + } + + using JxlPlane tempDiffmap = new(configuration, this.width, this.height); + bool scaleIntensity = this.intensityTarget != 0 && actual.Metadata.IntensityTarget != this.intensityTarget; + using JxlImage3? scaledStorage = scaleIntensity && ReferenceEquals(actualLinearSRgb, actual) + ? new(configuration, this.width, this.height) + : null; + + JxlImage3 scaledActualLinearSRgb = scaledStorage ?? actualLinearSRgb.Color!; + + if (scaleIntensity) + { + // Reuse converted planes when store owns them. A borrowed caller image needs separate storage before scaling. + float scale = actual.Metadata.IntensityTarget / this.intensityTarget; + for (int c = 0; c < 3; ++c) + { + for (int y = 0; y < this.height; ++y) + { + // PlaneRow includes subsequent rows. Restrict the operation to this logical row, excluding padding. + ReadOnlySpan sourceRow = actualLinearSRgb.Color!.PlaneRow(c, y)[..this.width]; + Span scaledRow = scaledActualLinearSRgb.PlaneRow(c, y)[..this.width]; + TensorPrimitives.Multiply(sourceRow, scale, scaledRow); + } + } + } + + if (!this.comparator.Diffmap(configuration, scaledActualLinearSRgb, tempDiffmap)) + { + throw new InvalidOperationException("Diffmap failed"); + } + + score = Butteraugli.Butteraugli.ButteraugliScoreFromDiffmap(tempDiffmap, parameters); + diffMap?.Swap(tempDiffmap); + } + + /// + /// Replaces the owned reference decomposition using borrowed source samples. + /// + /// The configuration that owns reference storage. + /// The borrowed reference samples. + public void SetReferenceImage(Configuration configuration, JxlImageBundle reference) + { + JxlImageMetadata metadata = reference.Metadata; + using JxlImageBundle store = new(metadata); + + JxlImageBundle refLinearSRgb = reference; + if (!JxlImageBundleEncoder.TransformIfNeeded(configuration, reference, JxlColorEncoding.LinearSrgb(reference.IsGray), cms, store, ref refLinearSRgb)) + { + throw new InvalidOperationException("Could not convert the reference image to linear sRGB."); + } + + // Build the replacement before releasing the previous reference. A failed conversion or allocation leaves it usable. + ButteraugliComparator replacement = ButteraugliComparator.Make(configuration, refLinearSRgb.Color!, parameters); + this.comparator?.Dispose(); + this.comparator = replacement; + + this.width = reference.XSize; + this.height = reference.YSize; + this.intensityTarget = reference.Metadata.IntensityTarget; + } + + /// + /// Releases the reference decomposition and its reusable scratch without disposing borrowed source images. + /// + public void Dispose() + { + this.comparator?.Dispose(); + this.comparator = null; + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparatorScoreComputation.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparatorScoreComputation.cs index b0530ee720..e90748564f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparatorScoreComputation.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparatorScoreComputation.cs @@ -3,7 +3,6 @@ using System.Numerics.Tensors; using SixLabors.ImageSharp.Formats.Jxl.Cms; -using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; @@ -67,7 +66,7 @@ private static void ComputeScoreImpl( JxlPlane? diffMap, out float score) { - comparator.SetReferenceImage(rgb0); + comparator.SetReferenceImage(configuration, rgb0); comparator.CompareWith(configuration, rgb1, diffMap, out score); } @@ -81,29 +80,20 @@ public static void ComputeScore( JxlPlane? diffMap = null, bool ignoreAlpha = false) { - JxlImageMetadata metadata0 = rgb0.Metadata.Copy(); - - JxlImageBundle store0 = new(metadata0); - - JxlTransformsEncoder.TransformIfNeeded( - rgb0, - JxlColorEncoding.LinearSRgb(rgb0.IsGray), - cms, - pool, - store0, - out JxlImageBundle linearSrgb0); - - JxlImageMetadata metadata1 = rgb1.Metadata.Copy(); - - JxlImageBundle store1 = new(metadata1); + // Original metadata stays borrowed. Conversion changes the bundle's current encoding, not the shared metadata. + using JxlImageBundle store0 = new(rgb0.Metadata); + JxlImageBundle linearSrgb0 = rgb0; + if (!JxlImageBundleEncoder.TransformIfNeeded(configuration, rgb0, JxlColorEncoding.LinearSrgb(rgb0.IsGray), cms, store0, ref linearSrgb0)) + { + throw new InvalidOperationException("Could not convert the first comparison image to linear sRGB."); + } - JxlTransformsEncoder.TransformIfNeeded( - rgb1, - JxlColorEncoding.LinearSRgb(rgb1.IsGray), - cms, - pool, - store1, - out JxlImageBundle linearSrgb1); + using JxlImageBundle store1 = new(rgb1.Metadata); + JxlImageBundle linearSrgb1 = rgb1; + if (!JxlImageBundleEncoder.TransformIfNeeded(configuration, rgb1, JxlColorEncoding.LinearSrgb(rgb1.IsGray), cms, store1, ref linearSrgb1)) + { + throw new InvalidOperationException("Could not convert the second comparison image to linear sRGB."); + } if (ignoreAlpha || (!rgb0.ContainsAlpha && !rgb1.ContainsAlpha)) { @@ -114,15 +104,18 @@ public static void ComputeScore( comparator, diffMap, out score); + + // No background comparison is required when opacity is absent or deliberately ignored. + return; } - JxlPlane diffMapBlack = new(); + using JxlPlane diffMapBlack = new(); float distBlack; { const float black = 0.0f; - JxlImageBundle blendedBlack0 = linearSrgb0.Copy(configuration); - JxlImageBundle blendedBlack1 = linearSrgb1.Copy(configuration); + using JxlImageBundle blendedBlack0 = linearSrgb0.Copy(configuration); + using JxlImageBundle blendedBlack1 = linearSrgb1.Copy(configuration); AlphaBlend(black, blendedBlack0); AlphaBlend(black, blendedBlack1); @@ -136,13 +129,13 @@ public static void ComputeScore( out distBlack); } - JxlPlane diffMapWhite = new(); + using JxlPlane diffMapWhite = new(); float distWhite; { const float white = 1.0f; - JxlImageBundle blendedWhite0 = linearSrgb0.Copy(configuration); - JxlImageBundle blendedWhite1 = linearSrgb1.Copy(configuration); + using JxlImageBundle blendedWhite0 = linearSrgb0.Copy(configuration); + using JxlImageBundle blendedWhite1 = linearSrgb1.Copy(configuration); AlphaBlend(white, blendedWhite0); AlphaBlend(white, blendedWhite1); @@ -161,19 +154,20 @@ public static void ComputeScore( int xSize = rgb0.XSize; int ySize = rgb0.YSize; - JxlPlane outputDiffMap = new( - configuration, - xSize, - ySize); + using JxlPlane outputDiffMap = new(configuration, xSize, ySize); for (int y = 0; y < ySize; y++) { - ReadOnlySpan rowBlack = diffMapBlack.GetRow(y); - ReadOnlySpan rowWhite = diffMapWhite.GetRow(y); - Span rowOut = diffMap.GetRow(y); + // GetRow also exposes following rows. Combine only the current logical row. + ReadOnlySpan rowBlack = diffMapBlack.GetRow(y)[..xSize]; + ReadOnlySpan rowWhite = diffMapWhite.GetRow(y)[..xSize]; + Span rowOut = outputDiffMap.GetRow(y)[..xSize]; TensorPrimitives.Max(rowBlack, rowWhite, rowOut); } + + // The caller receives the computed plane. The temporary takes and releases the caller's previous storage. + diffMap.Swap(outputDiffMap); } score = MathF.Max(distBlack, distWhite); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/NullComparator.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/NullComparator.cs deleted file mode 100644 index dc255e91b2..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/NullComparator.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Formats.Jxl.Memory; -using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Comparator; - -/// -/// Performs no image comparison at all. -/// -internal sealed class NullComparator : JxlComparator -{ - public override float GoodQualityScore => 0; - - public override float BadQualityScore => 0; - - public override void CompareWith(Configuration configuration, JxlImageBundle actual, JxlPlane? diffMap, out float score) => score = 0; - - public override void SetReferenceImage(Configuration configuration, JxlImageBundle reference) - { - } -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs index 6ff47945e3..7fc738c9c1 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs @@ -929,7 +929,7 @@ public static bool FindBestQuantization( parameters.IntensityTarget = tf.IsPq || tf.IsHlg ? frameHeader.Metadata.ImageMetadata.IntensityTarget : 0; - ButteraugliJxlComparator comparator = new(parameters, cms); + using JxlComparator comparator = new(parameters, cms); comparator.SetReferenceImage(configuration, linear); bool lowerIsBetter = comparator.GoodQualityScore < comparator.BadQualityScore; From 694d558b855c7cefe3846d83c916a2343c5e10f2 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 20:42:29 +1000 Subject: [PATCH 264/328] Reuse JPEG XL comparison workspace and shared SIMD traversal --- .../Helpers/TensorPrimitivesUtilities.cs | 252 ++- .../AcStrategy/JxlAcStrategyImage.cs | 6 + .../Jxl/Processing/Butteraugli/Butteraugli.cs | 1587 ++++++++++------- .../Butteraugli/ButteraugliBlurTemp.cs | 25 +- .../Butteraugli/ButteraugliComparator.cs | 198 +- .../Butteraugli/ButteraugliPsychoImage.cs | 25 + .../Encoder/Comparator/JxlComparator.cs | 24 +- .../JxlComparatorScoreComputation.cs | 92 +- .../DotDetection/JxlDotDetectionUtils.cs | 6 +- .../Encoder/JxlAdaptiveQuantization.cs | 113 +- .../Encoder/JxlConvolveSeparable5.cs | 276 ++- .../Processing/Image/JxlImageOperations.cs | 52 +- .../Formats/Jxl/Processing/JxlConvolve.cs | 43 + .../Processing/Quantization/JxlQuantizer.cs | 19 +- .../Jxl/Processing/ButteraugliTests.cs | 156 ++ 15 files changed, 2032 insertions(+), 842 deletions(-) diff --git a/src/ImageSharp/Common/Helpers/TensorPrimitivesUtilities.cs b/src/ImageSharp/Common/Helpers/TensorPrimitivesUtilities.cs index f390e501b8..641348cec0 100644 --- a/src/ImageSharp/Common/Helpers/TensorPrimitivesUtilities.cs +++ b/src/ImageSharp/Common/Helpers/TensorPrimitivesUtilities.cs @@ -52,62 +52,270 @@ public interface IUnaryOperator } /// - /// Transforms a component span in place with the widest supported SIMD width. + /// Defines scalar and SIMD forms of an operation on two independent component inputs. + /// + public interface IBinaryOperator + { + /// + /// Transforms two scalar components. + /// + /// The first input. + /// The second input. + /// The transformed component. + public float Invoke(float first, float second); + + /// + /// Transforms corresponding components in two SIMD registers. + /// + /// The first inputs. + /// The second inputs. + /// The transformed components in their original lane order. + public Vector128 Invoke(Vector128 first, Vector128 second); + + /// + public Vector256 Invoke(Vector256 first, Vector256 second); + + /// + public Vector512 Invoke(Vector512 first, Vector512 second); + } + + /// + /// Defines scalar and SIMD forms of an operation on three independent component inputs. + /// + public interface ITernaryOperator + { + /// + /// Transforms three scalar components. + /// + /// The first input. + /// The second input. + /// The third input. + /// The transformed component. + public float Invoke(float first, float second, float third); + + /// + /// Transforms corresponding components in three SIMD registers. + /// + /// The first inputs. + /// The second inputs. + /// The third inputs. + /// The transformed components in their original lane order. + public Vector128 Invoke(Vector128 first, Vector128 second, Vector128 third); + + /// + public Vector256 Invoke(Vector256 first, Vector256 second, Vector256 third); + + /// + public Vector512 Invoke(Vector512 first, Vector512 second, Vector512 third); + } + + /// + /// Transforms every component in place with the widest supported SIMD width. /// - /// - /// Every supplied component is transformed, regardless of its position in the span. - /// /// The operation configured before traversal. /// The independent components to transform. /// The scalar and SIMD forms of the operation. public static void Invoke(Span values, TOperator operation) where TOperator : struct, IUnaryOperator + => Invoke(values, values, operation); + + /// + /// Transforms independent components into equally sized output with the widest supported SIMD width. + /// + /// The operation configured before traversal. + /// The independent input components. + /// The output, with capacity for every input component. + /// The scalar and SIMD forms of the operation. + /// Input and output are disjoint or begin at the same address. Each complete register is loaded before its output is stored. + public static void Invoke(ReadOnlySpan source, Span destination, TOperator operation) + where TOperator : struct, IUnaryOperator + { + ref float input = ref MemoryMarshal.GetReference(source); + ref float output = ref MemoryMarshal.GetReference(destination); + int i = 0; + + if (Vector512.IsHardwareAccelerated) + { + nuint count = (source.Length - i).Vector512Count(); + + // Each lane owns one component. Smaller registers continue from the first unprocessed component. + for (nuint block = 0; block < count; block++, i += Vector512.Count) + { + Vector512 value = Vector512.LoadUnsafe(ref input, (uint)i); + operation.Invoke(value).StoreUnsafe(ref output, (uint)i); + } + } + + if (Vector256.IsHardwareAccelerated) + { + nuint count = (source.Length - i).Vector256Count(); + + // Each lane owns one component. Smaller registers continue from the first unprocessed component. + for (nuint block = 0; block < count; block++, i += Vector256.Count) + { + Vector256 value = Vector256.LoadUnsafe(ref input, (uint)i); + operation.Invoke(value).StoreUnsafe(ref output, (uint)i); + } + } + + if (Vector128.IsHardwareAccelerated) + { + nuint count = (source.Length - i).Vector128Count(); + + // Each lane owns one component. Smaller registers continue from the first unprocessed component. + for (nuint block = 0; block < count; block++, i += Vector128.Count) + { + Vector128 value = Vector128.LoadUnsafe(ref input, (uint)i); + operation.Invoke(value).StoreUnsafe(ref output, (uint)i); + } + } + + // The scalar remainder applies the same equation without reading or writing padding. + for (; i < source.Length; i++) + { + Unsafe.Add(ref output, (uint)i) = operation.Invoke(Unsafe.Add(ref input, (uint)i)); + } + } + + /// + /// Transforms corresponding components from two equally sized spans into caller-owned output. + /// + /// The operation configured before traversal. + /// The first inputs. + /// The second inputs, with the same length as first. + /// The output, with capacity for every input component. + /// The scalar and SIMD forms of the operation. + /// + /// Inputs and output are disjoint or begin at the same address. Both inputs are loaded before each store, + /// so an output span can also supply either input. The operation does not interpret pixel layout or alpha. + /// + public static void Invoke(ReadOnlySpan first, ReadOnlySpan second, Span destination, TOperator operation) + where TOperator : struct, IBinaryOperator + { + ref float input0 = ref MemoryMarshal.GetReference(first); + ref float input1 = ref MemoryMarshal.GetReference(second); + ref float output = ref MemoryMarshal.GetReference(destination); + int i = 0; + + if (Vector512.IsHardwareAccelerated) + { + nuint count = (first.Length - i).Vector512Count(); + + for (nuint block = 0; block < count; block++, i += Vector512.Count) + { + Vector512 value0 = Vector512.LoadUnsafe(ref input0, (uint)i); + Vector512 value1 = Vector512.LoadUnsafe(ref input1, (uint)i); + operation.Invoke(value0, value1).StoreUnsafe(ref output, (uint)i); + } + } + + if (Vector256.IsHardwareAccelerated) + { + nuint count = (first.Length - i).Vector256Count(); + + for (nuint block = 0; block < count; block++, i += Vector256.Count) + { + Vector256 value0 = Vector256.LoadUnsafe(ref input0, (uint)i); + Vector256 value1 = Vector256.LoadUnsafe(ref input1, (uint)i); + operation.Invoke(value0, value1).StoreUnsafe(ref output, (uint)i); + } + } + + if (Vector128.IsHardwareAccelerated) + { + nuint count = (first.Length - i).Vector128Count(); + + for (nuint block = 0; block < count; block++, i += Vector128.Count) + { + Vector128 value0 = Vector128.LoadUnsafe(ref input0, (uint)i); + Vector128 value1 = Vector128.LoadUnsafe(ref input1, (uint)i); + operation.Invoke(value0, value1).StoreUnsafe(ref output, (uint)i); + } + } + + for (; i < first.Length; i++) + { + float value0 = Unsafe.Add(ref input0, (uint)i); + float value1 = Unsafe.Add(ref input1, (uint)i); + Unsafe.Add(ref output, (uint)i) = operation.Invoke(value0, value1); + } + } + + /// + /// Transforms corresponding components from three equally sized spans into caller-owned output. + /// + /// The operation configured before traversal. + /// The first inputs. + /// The second inputs, with the same length as first. + /// The third inputs, with the same length as first. + /// The output, with capacity for every input component. + /// The scalar and SIMD forms of the operation. + /// + /// Inputs and output are disjoint or begin at the same address. All three inputs are loaded before each store, + /// so an output span can also supply any one of the inputs. The operation does not interpret pixel layout or alpha. + /// + public static void Invoke( + ReadOnlySpan first, + ReadOnlySpan second, + ReadOnlySpan third, + Span destination, + TOperator operation) + where TOperator : struct, ITernaryOperator { - ref float first = ref MemoryMarshal.GetReference(values); + ref float input0 = ref MemoryMarshal.GetReference(first); + ref float input1 = ref MemoryMarshal.GetReference(second); + ref float input2 = ref MemoryMarshal.GetReference(third); + ref float output = ref MemoryMarshal.GetReference(destination); int i = 0; if (Vector512.IsHardwareAccelerated) { - nuint count = (values.Length - i).Vector512Count(); + nuint count = (first.Length - i).Vector512Count(); - // Each lane holds one independent component. Complete registers do not overlap. for (nuint block = 0; block < count; block++, i += Vector512.Count) { - Vector512 input = Vector512.LoadUnsafe(ref first, (uint)i); - Vector512 output = operation.Invoke(input); - output.StoreUnsafe(ref first, (uint)i); + // Load all corresponding inputs before the store. Same-start in-place calls cannot overwrite a later input block. + Vector512 value0 = Vector512.LoadUnsafe(ref input0, (uint)i); + Vector512 value1 = Vector512.LoadUnsafe(ref input1, (uint)i); + Vector512 value2 = Vector512.LoadUnsafe(ref input2, (uint)i); + operation.Invoke(value0, value1, value2).StoreUnsafe(ref output, (uint)i); } } if (Vector256.IsHardwareAccelerated) { - nuint count = (values.Length - i).Vector256Count(); + nuint count = (first.Length - i).Vector256Count(); - // Continue with complete smaller registers, then process the remaining components below. for (nuint block = 0; block < count; block++, i += Vector256.Count) { - Vector256 input = Vector256.LoadUnsafe(ref first, (uint)i); - Vector256 output = operation.Invoke(input); - output.StoreUnsafe(ref first, (uint)i); + // Load all corresponding inputs before the store. Same-start in-place calls cannot overwrite a later input block. + Vector256 value0 = Vector256.LoadUnsafe(ref input0, (uint)i); + Vector256 value1 = Vector256.LoadUnsafe(ref input1, (uint)i); + Vector256 value2 = Vector256.LoadUnsafe(ref input2, (uint)i); + operation.Invoke(value0, value1, value2).StoreUnsafe(ref output, (uint)i); } } if (Vector128.IsHardwareAccelerated) { - nuint count = (values.Length - i).Vector128Count(); + nuint count = (first.Length - i).Vector128Count(); for (nuint block = 0; block < count; block++, i += Vector128.Count) { - Vector128 input = Vector128.LoadUnsafe(ref first, (uint)i); - Vector128 output = operation.Invoke(input); - output.StoreUnsafe(ref first, (uint)i); + // Load all corresponding inputs before the store. Same-start in-place calls cannot overwrite a later input block. + Vector128 value0 = Vector128.LoadUnsafe(ref input0, (uint)i); + Vector128 value1 = Vector128.LoadUnsafe(ref input1, (uint)i); + Vector128 value2 = Vector128.LoadUnsafe(ref input2, (uint)i); + operation.Invoke(value0, value1, value2).StoreUnsafe(ref output, (uint)i); } } - // The scalar remainder applies the same operation to each component. - for (; i < values.Length; i++) + for (; i < first.Length; i++) { - Unsafe.Add(ref first, (uint)i) = operation.Invoke(Unsafe.Add(ref first, (uint)i)); + float value0 = Unsafe.Add(ref input0, (uint)i); + float value1 = Unsafe.Add(ref input1, (uint)i); + float value2 = Unsafe.Add(ref input2, (uint)i); + Unsafe.Add(ref output, (uint)i) = operation.Invoke(value0, value1, value2); } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyImage.cs b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyImage.cs index 82e85faca5..b178693106 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyImage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyImage.cs @@ -59,6 +59,12 @@ private JxlAcStrategyImage(Configuration configuration, int xSize, int ySize) /// public int PixelsPerRow => this.stride; + /// + /// Borrows all transform selections so a bulk traversal can acquire its span once. + /// + /// The first row, its padding and subsequent rows, addressed with . + public ReadOnlySpan GetRows() => this.row.Span; + /// /// Borrows a row of transform selections. /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs index 2608c820f7..7825a176cd 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs @@ -2,8 +2,11 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using System.Numerics.Tensors; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; @@ -68,17 +71,87 @@ internal static class Butteraugli 29.2353797994f, 0.844626970982f, 0.703646627719f, ]; - public static ReadOnlySpan ComputeKernel(float sigma) + /// + /// The offset in the high-frequency masking denominator. + /// + private const float MaskYOffset = 0.829591754942F; + + /// + /// The scale in the high-frequency masking denominator. + /// + private const float MaskYScaler = 0.451936922203F; + + /// + /// The numerator in the high-frequency masking response. + /// + private const float MaskYMultiplier = 2.5485944793F; + + /// + /// The offset in the low-frequency masking denominator. + /// + private const float MaskDcYOffset = 0.20025578522F; + + /// + /// The scale in the low-frequency masking denominator. + /// + private const float MaskDcYScaler = 3.87449418804F; + + /// + /// The numerator in the low-frequency masking response. + /// + private const float MaskDcYMultiplier = 0.505054525019F; + + // These five Gaussian kernels are fixed by the comparison equations. Build them once, outside repeated comparisons. + private static readonly float[] OpsinDynamicsKernel = ComputeKernel(1.2F); + private static readonly float[] LowFrequencyKernel = ComputeKernel(7.15593339443F); + private static readonly float[] HighFrequencyKernel = ComputeKernel(3.22489901262F); + private static readonly float[] UltraHighFrequencyKernel = ComputeKernel(1.56416327805F); + private static readonly float[] MaskingKernel = ComputeKernel(2.7F); + + /// + /// The normalized five-tap weights used by the mirrored single-scan Opsin blur. + /// + private static readonly JxlWeightsSeparable5 OpsinBlurWeights; + + /// + /// Prepares the five-tap weights once using the native Gaussian normalization and replicated coefficient layout. + /// + static Butteraugli() + { + float weightSum = 0F; + foreach (float weight in OpsinDynamicsKernel) + { + weightSum += weight; + } + + float scale = 1F / weightSum; + float center = OpsinDynamicsKernel[2] * scale; + float near = OpsinDynamicsKernel[1] * scale; + float far = OpsinDynamicsKernel[0] * scale; + JxlWeightsSeparable5 weights = default; + + // The shared convolution stores each coefficient four times for native SIMD broadcasts. + for (int i = 0; i < 4; i++) + { + weights.Horizontal[i] = weights.Vertical[i] = center; + weights.Horizontal[4 + i] = weights.Vertical[4 + i] = near; + weights.Horizontal[8 + i] = weights.Vertical[8 + i] = far; + } + + OpsinBlurWeights = weights; + } + + /// + /// Builds one fixed Gaussian kernel without normalizing its border weights. + /// + /// The standard deviation fixed by the comparison stage. + /// The symmetric samples from minus to plus two-and-one-quarter standard deviations. + private static float[] ComputeKernel(float sigma) { const float m = 2.25f; // Accuracy increases when m is increased float scaler = -1.0f / (2.0f * sigma * sigma); int diff = Math.Max(1, (int)(m * MathF.Abs(sigma))); - // If sigma is very large we should not return a 'new float[]' allocation. - // This guard is temporary so we can verify the range of the number of elements. - // TODO: remove guard if the value doesn't exceed the limit for many JXL files - DebugGuard.MustBeLessThanOrEqualTo(sigma, 32f, nameof(sigma)); - float[] kernel = new float[(2 * diff) + 1]; for (int i = -diff; i <= diff; i++) @@ -89,16 +162,22 @@ public static ReadOnlySpan ComputeKernel(float sigma) return kernel; } - public static void ConvolveBorderColumn( - JxlPlane input, - ReadOnlySpan kernel, - int x, - Span rowOut) + /// + /// Convolves one border column, normalizing only the Gaussian weights whose source samples exist. + /// + /// The complete borrowed source storage. + /// The logical source width. + /// The logical source height. + /// The physical source row stride. + /// The fixed symmetric Gaussian coefficients. + /// The source column to convolve. + /// The destination row in the transposed output. + public static void ConvolveBorderColumn(ReadOnlySpan input, int width, int height, int stride, ReadOnlySpan kernel, int x, Span rowOut) { int offset = kernel.Length / 2; int minX = x < offset ? 0 : x - offset; - int maxX = Math.Min(input.XSize - 1, x + offset); + int maxX = Math.Min(width - 1, x + offset); float weight = 0.0f; @@ -109,9 +188,9 @@ public static void ConvolveBorderColumn( float scale = 1.0f / weight; - for (int y = 0; y < input.YSize; y++) + for (int y = 0; y < height; y++) { - Span rowIn = input.GetRow(y); + ReadOnlySpan rowIn = input[(y * stride)..]; float sum = 0.0f; @@ -124,10 +203,14 @@ public static void ConvolveBorderColumn( } } - public static bool ConvolutionWithTranspose( - JxlPlane input, - ReadOnlySpan kernel, - JxlPlane output) + /// + /// Applies the native truncated Gaussian FIR and transposes its output into prepared storage. + /// + /// The borrowed source plane. + /// The fixed symmetric Gaussian coefficients. + /// The distinct preallocated output with width and height exchanged. + /// Whether the output has the required transposed dimensions. + public static bool ConvolutionWithTranspose(JxlPlane input, ReadOnlySpan kernel, JxlPlane output) { if (output.XSize != input.YSize) { @@ -161,6 +244,9 @@ public static bool ConvolutionWithTranspose( scaledKernel[i] = kernel[i] * scaleNoBorder; } + ReadOnlySpan inputSamples = input.GetRow(0); + Span outputSamples = output.GetRow(0); + // Middle switch (len) { @@ -173,7 +259,7 @@ public static bool ConvolutionWithTranspose( for (int y = 0; y < input.YSize; y++) { - Span rowIn = input.GetRow(y); + ReadOnlySpan rowIn = inputSamples[(y * input.PixelsPerRow)..]; for (int x = border1; x < border2; x++) { @@ -184,7 +270,7 @@ public static bool ConvolutionWithTranspose( float sum2 = (rowIn[i + 2] + rowIn[i + 4]) * sk2; float sum = (rowIn[i + 3] * sk3) + sum0 + sum1 + sum2; - output.GetRow(x)[y] = sum; + outputSamples[(x * output.PixelsPerRow) + y] = sum; } } @@ -195,7 +281,7 @@ public static bool ConvolutionWithTranspose( { for (int y = 0; y < input.YSize; y++) { - Span rowIn = input.GetRow(y); + ReadOnlySpan rowIn = inputSamples[(y * input.PixelsPerRow)..]; for (int x = border1; x < border2; x++) { @@ -211,7 +297,7 @@ public static bool ConvolutionWithTranspose( float sum = rowIn[i + 6] * scaledKernel[6]; - output.GetRow(x)[y] = sum + sum0 + sum1 + sum2 + sum3; + outputSamples[(x * output.PixelsPerRow) + y] = sum + sum0 + sum1 + sum2 + sum3; } } @@ -222,7 +308,7 @@ public static bool ConvolutionWithTranspose( { for (int y = 0; y < input.YSize; y++) { - Span rowIn = input.GetRow(y); + ReadOnlySpan rowIn = inputSamples[(y * input.PixelsPerRow)..]; for (int x = border1; x < border2; x++) { @@ -239,7 +325,7 @@ public static bool ConvolutionWithTranspose( float sum = rowIn[i + 7] * scaledKernel[7]; - output.GetRow(x)[y] = sum + sum0 + sum1 + sum2 + sum3; + outputSamples[(x * output.PixelsPerRow) + y] = sum + sum0 + sum1 + sum2 + sum3; } } @@ -250,7 +336,7 @@ public static bool ConvolutionWithTranspose( { for (int y = 0; y < input.YSize; y++) { - Span rowIn = input.GetRow(y); + ReadOnlySpan rowIn = inputSamples[(y * input.PixelsPerRow)..]; for (int x = border1; x < border2; x++) { @@ -278,7 +364,7 @@ public static bool ConvolutionWithTranspose( float sum = rowIn[i + 16] * scaledKernel[16]; - output.GetRow(x)[y] = sum + sum0 + sum1 + sum2 + sum3; + outputSamples[(x * output.PixelsPerRow) + y] = sum + sum0 + sum1 + sum2 + sum3; } } @@ -292,82 +378,39 @@ public static bool ConvolutionWithTranspose( // Left border for (int x = 0; x < border1; x++) { - ConvolveBorderColumn(input, kernel, x, output.GetRow(x)); + ConvolveBorderColumn(inputSamples, input.XSize, input.YSize, input.PixelsPerRow, kernel, x, outputSamples[(x * output.PixelsPerRow)..]); } // Right border for (int x = border2; x < input.XSize; x++) { - ConvolveBorderColumn(input, kernel, x, output.GetRow(x)); - } - - return true; - } - - private static bool Blur( - Configuration configuration, - JxlPlane input, - float sigma, - ButteraugliBlurTemp temp, - JxlPlane output) - { - ReadOnlySpan kernel = ComputeKernel(sigma); - - // Separable5 does an in-place convolution, so this fast path is not safe - // if input aliases output. - if (kernel.Length == 5 && !ReferenceEquals(input, output)) - { - float sumWeights = 0.0f; - - foreach (float w in kernel) - { - sumWeights += w; - } - - float scale = 1.0f / sumWeights; - - float w0 = kernel[2] * scale; - float w1 = kernel[1] * scale; - float w2 = kernel[0] * scale; - - JxlWeightsSeparable5 weights = default; - FillRep4(ref weights.Horizontal, w0, w1, w2); - FillRep4(ref weights.Vertical, w0, w1, w2); - - if (!Separable5(input, input.GetRectangle(), weights, null, output)) - { - return false; - } - - return true; - } - - JxlPlane tempT = temp.GetTransposed(configuration, input); - - if (!ConvolutionWithTranspose(input, kernel, tempT)) - { - return false; - } - - if (!ConvolutionWithTranspose(tempT, kernel, output)) - { - return false; + ConvolveBorderColumn(inputSamples, input.XSize, input.YSize, input.PixelsPerRow, kernel, x, outputSamples[(x * output.PixelsPerRow)..]); } return true; } /// - /// Equivalent to HWY_REP4. + /// Applies a separable Gaussian blur using prepared storage and the native border rule for its fixed kernel. /// - private static void FillRep4(ref InlineArray12 values, float a, float b, float c) + /// The borrowed source samples. + /// The fixed, symmetric kernel for this stage. + /// The prepared transposed scratch plane, distinct from input and output. + /// The preallocated result plane, distinct from input for the five-tap Opsin kernel. + /// Whether both passes completed with matching dimensions. + /// Other kernels can update input in place because their first pass writes only transposed scratch. + private static bool Blur(JxlPlane input, ReadOnlySpan kernel, ButteraugliBlurTemp temp, JxlPlane output) { - for (int i = 0; i < 4; i++) + if (kernel.Length == 5) { - values[i] = a; - values[4 + i] = b; - values[8 + i] = c; + // The native five-tap path mirrors edges and combines both dimensions in one scan. + // Other kernels use the truncated, renormalized FIR below; these border rules are intentionally different. + return JxlConvolve.Separable5(input, input.GetRectangle(), OpsinBlurWeights, output); } + + // The first pass writes only transposed scratch. The second pass can therefore overwrite the original input safely. + JxlPlane transposed = temp.TransposedTemp; + return ConvolutionWithTranspose(input, kernel, transposed) && ConvolutionWithTranspose(transposed, kernel, output); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -432,15 +475,23 @@ public static void XybLowFrequencyToValues( valY = y * yMul; } + /// + /// Converts low-frequency XYB planes to the weighted values used by the comparison equations. + /// + /// The preallocated low-frequency image, updated in place. public static void XybLowFrequencyToValues(JxlImage3 xybLf) { int lanes = Vector.Count; + Span samples0 = xybLf.Plane(0).GetRow(0); + Span samples1 = xybLf.Plane(1).GetRow(0); + Span samples2 = xybLf.Plane(2).GetRow(0); + for (int y = 0; y < xybLf.YSize; y++) { - Span rowX = xybLf.PlaneRow(0, y); - Span rowY = xybLf.PlaneRow(1, y); - Span rowB = xybLf.PlaneRow(2, y); + Span rowX = samples0[(y * xybLf.Plane(0).PixelsPerRow)..]; + Span rowY = samples1[(y * xybLf.Plane(1).PixelsPerRow)..]; + Span rowB = samples2[(y * xybLf.Plane(2).PixelsPerRow)..]; for (int x = 0; x < xybLf.XSize; x += lanes) { @@ -463,6 +514,12 @@ public static void XybLowFrequencyToValues(JxlImage3 xybLf) } } + /// + /// Reduces high-frequency X according to the corresponding Y magnitude. + /// + /// The borrowed Y frequency samples. + /// The equally sized X frequency samples, updated in place. + /// Whether both plane dimensions match. public static bool SuppressXByY(JxlPlane inY, JxlPlane inOutX) { if (!JxlImageOperations.SameSize(inOutX, inY)) @@ -481,10 +538,13 @@ public static bool SuppressXByY(JxlPlane inY, JxlPlane inOutX) Vector oneMinusS = new(1.0f - s); Vector ywv = new(suppress); + Span samplesY = inY.GetRow(0); + Span samplesX = inOutX.GetRow(0); + for (int y = 0; y < ySize; y++) { - ReadOnlySpan rowY = inY.GetRow(y); - Span rowX = inOutX.GetRow(y); + ReadOnlySpan rowY = samplesY[(y * inY.PixelsPerRow)..]; + Span rowX = samplesX[(y * inOutX.PixelsPerRow)..]; for (int x = 0; x < xSize; x += lanes) { @@ -501,26 +561,36 @@ public static bool SuppressXByY(JxlPlane inY, JxlPlane inOutX) return true; } + /// + /// Subtracts equally sized sample planes into preallocated output, which may alias either input. + /// + /// The minuend plane. + /// The subtrahend plane. + /// The preallocated result plane. public static void Subtract(JxlPlane a, JxlPlane b, JxlPlane c) { - int lanes = Vector.Count; + ReadOnlySpan samplesA = a.GetRow(0); + ReadOnlySpan samplesB = b.GetRow(0); + Span samplesC = c.GetRow(0); for (int y = 0; y < a.YSize; y++) { - ReadOnlySpan rowA = a.GetRow(y); - ReadOnlySpan rowB = b.GetRow(y); - Span rowC = c.GetRow(y); - - for (int x = 0; x < a.XSize; x += lanes) - { - Vector va = new(rowA.Slice(x, lanes)); - Vector vb = new(rowB.Slice(x, lanes)); - - (va - vb).CopyTo(rowC.Slice(x, lanes)); - } + ReadOnlySpan rowA = samplesA.Slice(y * a.PixelsPerRow, a.XSize); + ReadOnlySpan rowB = samplesB.Slice(y * b.PixelsPerRow, a.XSize); + Span rowC = samplesC.Slice(y * c.PixelsPerRow, a.XSize); + TensorPrimitives.Subtract(rowA, rowB, rowC); } } + /// + /// Writes low-frequency samples and the remaining middle-frequency samples into prepared images. + /// + /// The comparison configuration. + /// The borrowed transformed input. + /// The preallocated low-frequency output. + /// The preallocated middle-frequency output. + /// The prepared transposed blur workspace. + /// Whether the frequency blur completed. public static bool SeparateLFAndMF( Configuration configuration, JxlImage3 xyb, @@ -528,16 +598,9 @@ public static bool SeparateLFAndMF( JxlImage3 mf, ButteraugliBlurTemp blurTemp) { - const float sigmaLf = 7.15593339443f; - for (int i = 0; i < 3; i++) { - if (!Blur( - configuration, - xyb.Plane(i), - sigmaLf, - blurTemp, - lf.Plane(i))) + if (!Blur(xyb.Plane(i), LowFrequencyKernel, blurTemp, lf.Plane(i))) { return false; } @@ -556,38 +619,23 @@ public static bool SeparateLFAndMF( /// /// Splits the middle-frequency XYB image in place and writes the high-frequency X and Y planes. /// - /// The configuration used if an output plane needs more capacity. + /// The comparison configuration. /// The caller-owned middle-frequency image, updated in place. - /// Two borrowed, caller-owned planes in X, Y order. Their identities remain unchanged. + /// Two preallocated, caller-owned planes in X, Y order. Their identities remain unchanged. /// The borrowed blur workspace. /// Whether all blur and frequency-separation stages succeeded. - public static bool SeparateMfAndHf( - Configuration configuration, - JxlImage3 mf, - Span> hf, - ButteraugliBlurTemp blurTemp) + public static bool SeparateMfAndHf(Configuration configuration, JxlImage3 mf, Span> hf, ButteraugliBlurTemp blurTemp) { - const float sigmaHf = 3.22489901262f; - int xSize = mf.XSize; int ySize = mf.YSize; - for (int i = 0; i < 2; i++) - { - if (!hf[i].ShrinkTo(xSize, ySize)) - { - using JxlPlane replacement = new(configuration, xSize, ySize); - hf[i].Swap(replacement); - } - } - int lanes = Vector.Count; for (int i = 0; i < 3; i++) { if (i == 2) { - if (!Blur(configuration, mf.Plane(i), sigmaHf, blurTemp, mf.Plane(i))) + if (!Blur(mf.Plane(i), HighFrequencyKernel, blurTemp, mf.Plane(i))) { return false; } @@ -595,10 +643,13 @@ public static bool SeparateMfAndHf( break; } + Span firstSamples = mf.Plane(i).GetRow(0); + Span secondSamples = hf[i].GetRow(0); + for (int y = 0; y < ySize; y++) { - Span rowMf = mf.PlaneRow(i, y); - Span rowHf = hf[i].GetRow(y); + Span rowMf = firstSamples[(y * mf.Plane(i).PixelsPerRow)..]; + Span rowHf = secondSamples[(y * hf[i].PixelsPerRow)..]; for (int x = 0; x < xSize; x += lanes) { @@ -607,7 +658,7 @@ public static bool SeparateMfAndHf( } } - if (!Blur(configuration, mf.Plane(i), sigmaHf, blurTemp, mf.Plane(i))) + if (!Blur(mf.Plane(i), HighFrequencyKernel, blurTemp, mf.Plane(i))) { return false; } @@ -618,8 +669,8 @@ public static bool SeparateMfAndHf( for (int y = 0; y < ySize; y++) { - Span rowMf = mf.PlaneRow(0, y); - Span rowHf = hf[0].GetRow(y); + Span rowMf = firstSamples[(y * mf.Plane(0).PixelsPerRow)..]; + Span rowHf = secondSamples[(y * hf[0].PixelsPerRow)..]; for (int x = 0; x < xSize; x += lanes) { @@ -639,8 +690,8 @@ public static bool SeparateMfAndHf( for (int y = 0; y < ySize; y++) { - Span rowMf = mf.PlaneRow(1, y); - Span rowHf = hf[1].GetRow(y); + Span rowMf = firstSamples[(y * mf.Plane(1).PixelsPerRow)..]; + Span rowHf = secondSamples[(y * hf[1].PixelsPerRow)..]; for (int x = 0; x < xSize; x += lanes) { @@ -662,40 +713,27 @@ public static bool SeparateMfAndHf( /// /// Splits the high-frequency X and Y planes in place and writes their ultra-high-frequency planes. /// - /// The configuration used if an output plane needs more capacity. + /// The comparison configuration. /// Two borrowed high-frequency planes in X, Y order, updated in place. - /// Two borrowed, caller-owned output planes in X, Y order. Their identities remain unchanged. + /// Two preallocated, caller-owned output planes in X, Y order. Their identities remain unchanged. /// The borrowed blur workspace. /// Whether both frequency separations succeeded. - public static bool SeparateHFAndUHF( - Configuration configuration, - Span> hf, - Span> uhf, - ButteraugliBlurTemp blurTemp) + public static bool SeparateHFAndUHF(Configuration configuration, Span> hf, Span> uhf, ButteraugliBlurTemp blurTemp) { - const float sigmaUhf = 1.56416327805f; - int xSize = hf[0].XSize; int ySize = hf[0].YSize; - // Borrow the owner's actual plane entries. Assigning to an InlineArray passed by value would lose the new allocations. - for (int i = 0; i < 2; i++) - { - if (!uhf[i].ShrinkTo(xSize, ySize)) - { - using JxlPlane replacement = new(configuration, xSize, ySize); - uhf[i].Swap(replacement); - } - } - int lanes = Vector.Count; for (int i = 0; i < 2; i++) { + Span firstSamples = hf[i].GetRow(0); + Span secondSamples = uhf[i].GetRow(0); + for (int y = 0; y < ySize; y++) { - Span rowUhf = uhf[i].GetRow(y); - Span rowHf = hf[i].GetRow(y); + Span rowUhf = secondSamples[(y * uhf[i].PixelsPerRow)..]; + Span rowHf = firstSamples[(y * hf[i].PixelsPerRow)..]; for (int x = 0; x < xSize; x += lanes) { @@ -703,7 +741,7 @@ public static bool SeparateHFAndUHF( } } - if (!Blur(configuration, hf[i], sigmaUhf, blurTemp, hf[i])) + if (!Blur(hf[i], UltraHighFrequencyKernel, blurTemp, hf[i])) { return false; } @@ -715,8 +753,8 @@ public static bool SeparateHFAndUHF( for (int y = 0; y < ySize; y++) { - Span rowUhf = uhf[0].GetRow(y); - Span rowHf = hf[0].GetRow(y); + Span rowUhf = secondSamples[(y * uhf[0].PixelsPerRow)..]; + Span rowHf = firstSamples[(y * hf[0].PixelsPerRow)..]; for (int x = 0; x < xSize; x += lanes) { @@ -744,8 +782,8 @@ public static bool SeparateHFAndUHF( for (int y = 0; y < ySize; y++) { - Span rowUhf = uhf[1].GetRow(y); - Span rowHf = hf[1].GetRow(y); + Span rowUhf = secondSamples[(y * uhf[1].PixelsPerRow)..]; + Span rowHf = firstSamples[(y * hf[1].PixelsPerRow)..]; for (int x = 0; x < xSize; x += lanes) { @@ -786,31 +824,15 @@ public static void DeallocateHFAndUHF(Span> hf, Span - /// Writes all frequency bands into the supplied owner, reusing its existing sample capacity when possible. + /// Writes all frequency bands into the supplied preallocated owner. /// - /// The configuration used for frequency-band sample allocations. + /// The comparison configuration. /// The borrowed blur workspace. /// The borrowed transformed input image. - /// The owner of every output plane, including partially completed outputs on failure. + /// The owner of every preallocated output plane. Frequency separation does not resize or replace its storage. /// Whether all frequency bands were produced. - public static bool SeparateFrequencies( - Configuration configuration, - ButteraugliBlurTemp blurTemp, - JxlImage3 xyb, - ButteraugliPsychoImage ps) + public static bool SeparateFrequencies(Configuration configuration, ButteraugliBlurTemp blurTemp, JxlImage3 xyb, ButteraugliPsychoImage ps) { - if (!ps.Lf.ShrinkTo(xyb.XSize, xyb.YSize)) - { - using JxlImage3 replacement = new(configuration, xyb.XSize, xyb.YSize); - ps.Lf.Swap(replacement); - } - - if (!ps.Mf.ShrinkTo(xyb.XSize, xyb.YSize)) - { - using JxlImage3 replacement = new(configuration, xyb.XSize, xyb.YSize); - ps.Mf.Swap(replacement); - } - if (!SeparateLFAndMF(configuration, xyb, ps.Lf, ps.Mf, blurTemp)) { return false; @@ -1220,65 +1242,55 @@ static Vector Load(ReadOnlySpan row, int index) return retval; } + /// + /// Evaluates one Malta result using a zero-padded neighborhood when a complete SIMD load would cross an image edge. + /// + /// The complete borrowed difference plane, starting at its first logical sample. + /// The logical plane width. + /// The logical plane height. + /// The physical row stride. + /// The sample column. + /// The sample row. + /// Whether to use the low-frequency Malta equation. + /// The first lane of the selected Malta equation. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static float PaddedMaltaUnit( - JxlPlane diffs, - int x0, - int y0, - bool isLF) + private static float PaddedMaltaUnit(ReadOnlySpan diffs, int width, int height, int stride, int x0, int y0, bool isLf) { - if (x0 >= 4 && - y0 >= 4 && - x0 < diffs.XSize - 4 && - y0 < diffs.YSize - 4) + int index = (y0 * stride) + x0; + if (x0 >= 4 && y0 >= 4 && x0 + Vector.Count + 4 <= width && y0 < height - 4) { - return isLF - ? MaltaUnitLF( - diffs.GetRow(y0), - x0, - diffs.PixelsPerRow)[0] - : MaltaUnit( - diffs.GetRow(y0), - x0, - diffs.PixelsPerRow)[0]; + return isLf ? MaltaUnitLF(diffs, index, stride)[0] : MaltaUnit(diffs, index, stride)[0]; } - Span borderImage = stackalloc float[12 * 9]; + // The equation reaches four rows and columns from the center. Extra columns keep every SIMD load + // inside this local buffer even though only its first result lane is used at image borders. + int paddedStride = 8 + Vector.Count; + Span borderImage = stackalloc float[9 * paddedStride]; + borderImage.Clear(); for (int dy = 0; dy < 9; dy++) { int y = y0 + dy - 4; - - if (y < 0 || y >= diffs.YSize) + if ((uint)y >= (uint)height) { - borderImage.Slice(dy * 12, 12).Clear(); continue; } - ReadOnlySpan rowDiffs = diffs.GetRow(y); + ReadOnlySpan rowDiffs = diffs[(y * stride)..]; + Span paddedRow = borderImage[(dy * paddedStride)..]; for (int dx = 0; dx < 9; dx++) { int x = x0 + dx - 4; - - borderImage[(dy * 12) + dx] = - x < 0 || x >= diffs.XSize - ? 0.0f - : rowDiffs[x]; + if ((uint)x < (uint)width) + { + paddedRow[dx] = rowDiffs[x]; + } } - - borderImage.Slice((dy * 12) + 9, 3).Clear(); } - return isLF - ? MaltaUnitLF( - diffs.GetRow(y0), - x0, - diffs.PixelsPerRow)[0] - : MaltaUnit( - borderImage, - (4 * 12) + 4, - 12)[0]; + int center = (4 * paddedStride) + 4; + return isLf ? MaltaUnitLF(borderImage, center, paddedStride)[0] : MaltaUnit(borderImage, center, paddedStride)[0]; } public static bool MaltaDiffMap( @@ -1336,11 +1348,18 @@ public static bool MaltaDiffMap( float norm2_0Lt1 = (float)(mulli * MathF.Sqrt(weight1 * w0Lt1) / ((len * 2) + 1) * norm1); + // Malta neighborhoods reach four rows and four columns from each center. Borrow complete planes once, + // then retain global indices so negative relative offsets never refer to a sliced-away preceding row. + ReadOnlySpan samples0 = lum0.GetRow(0); + ReadOnlySpan samples1 = lum1.GetRow(0); + Span differenceSamples = diffs.GetRow(0); + Span outputSamples = blockDiffAc.GetRow(0); + for (int y = 0; y < height; y++) { - ReadOnlySpan row0 = lum0.GetRow(y); - ReadOnlySpan row1 = lum1.GetRow(y); - Span rowDiffs = diffs.GetRow(y); + ReadOnlySpan row0 = samples0[(y * lum0.PixelsPerRow)..]; + ReadOnlySpan row1 = samples1[(y * lum1.PixelsPerRow)..]; + Span rowDiffs = differenceSamples[(y * diffs.PixelsPerRow)..]; for (int x = 0; x < width; x++) { @@ -1389,11 +1408,11 @@ public static bool MaltaDiffMap( for (; y0 < 4; y0++) { - Span row = blockDiffAc.GetRow(y0); + Span row = outputSamples[(y0 * blockDiffAc.PixelsPerRow)..]; for (int x = 0; x < width; x++) { - row[x] += PaddedMaltaUnit(diffs, x, y0, isLf); + row[x] += PaddedMaltaUnit(differenceSamples, width, height, diffs.PixelsPerRow, x, y0, isLf); } } @@ -1404,41 +1423,41 @@ public static bool MaltaDiffMap( for (; y0 < height - 4; y0++) { - ReadOnlySpan input = diffs.GetRow(y0); - Span output = blockDiffAc.GetRow(y0); + Span output = outputSamples[(y0 * blockDiffAc.PixelsPerRow)..]; ref float outputReference = ref MemoryMarshal.GetReference(output); int x = 0; - for (; x < alignedX; x++) + for (; x < Math.Min(alignedX, width); x++) { - output[x] += PaddedMaltaUnit(diffs, x, y0, isLf); + output[x] += PaddedMaltaUnit(differenceSamples, width, height, diffs.PixelsPerRow, x, y0, isLf); } + // Every lane and its four neighboring columns remain inside the logical width. Border calls use the zero-padded scalar-lane path. for (; x + lanes + 4 <= width; x += lanes) { Vector value = Vector.LoadUnsafe(ref Unsafe.Add(ref outputReference, x)); Vector malta = isLf - ? MaltaUnitLF(input, x, stride) - : MaltaUnit(input, x, stride); + ? MaltaUnitLF(differenceSamples, (y0 * stride) + x, stride) + : MaltaUnit(differenceSamples, (y0 * stride) + x, stride); (value + malta).CopyTo(output[x..]); } for (; x < width; x++) { - output[x] += PaddedMaltaUnit(diffs, x, y0, isLf); + output[x] += PaddedMaltaUnit(differenceSamples, width, height, diffs.PixelsPerRow, x, y0, isLf); } } for (; y0 < height; y0++) { - Span row = blockDiffAc.GetRow(y0); + Span row = outputSamples[(y0 * blockDiffAc.PixelsPerRow)..]; for (int x = 0; x < width; x++) { - row[x] += PaddedMaltaUnit(diffs, x, y0, isLf); + row[x] += PaddedMaltaUnit(differenceSamples, width, height, diffs.PixelsPerRow, x, y0, isLf); } } @@ -1495,10 +1514,13 @@ public static bool MaltaDiffMapLf( blockDiffAc); } - public static void CombineChannelsForMasking( - InlineArray2> hf, - InlineArray2> uhf, - JxlPlane output) + /// + /// Combines X and Y high-frequency bands into the perceptual masking response. + /// + /// The borrowed high-frequency X and Y planes. + /// The borrowed ultra-high-frequency X and Y planes. + /// The distinct preallocated masking output. + public static void CombineChannelsForMasking(InlineArray2> hf, InlineArray2> uhf, JxlPlane output) { // Only X and Y components are involved in masking. ReadOnlySpan muls = @@ -1511,16 +1533,32 @@ public static void CombineChannelsForMasking( int width = hf[0].XSize; int height = hf[0].YSize; + Span hfSamples0 = hf[0].GetRow(0); + Span hfSamples1 = hf[1].GetRow(0); + Span uhfSamples0 = uhf[0].GetRow(0); + Span uhfSamples1 = uhf[1].GetRow(0); + Span outputSamples = output.GetRow(0); + for (int y = 0; y < height; y++) { - ReadOnlySpan rowYHf = hf[1].GetRow(y); - ReadOnlySpan rowYUhf = uhf[1].GetRow(y); - ReadOnlySpan rowXHf = hf[0].GetRow(y); - ReadOnlySpan rowXUhf = uhf[0].GetRow(y); + ReadOnlySpan rowYHf = hfSamples1[(y * hf[1].PixelsPerRow)..]; + ReadOnlySpan rowYUhf = uhfSamples1[(y * uhf[1].PixelsPerRow)..]; + ReadOnlySpan rowXHf = hfSamples0[(y * hf[0].PixelsPerRow)..]; + ReadOnlySpan rowXUhf = uhfSamples0[(y * uhf[0].PixelsPerRow)..]; - Span row = output.GetRow(y); + Span row = outputSamples[(y * output.PixelsPerRow)..]; - for (int x = 0; x < width; x++) + int x = 0; + + // Each lane processes one logical sample. The scalar remainder retains the same arithmetic order. + for (; x <= width - Vector.Count; x += Vector.Count) + { + Vector xDiff = (new Vector(rowXUhf[x..]) + new Vector(rowXHf[x..])) * Vector.Create(muls[0]); + Vector yDiff = (new Vector(rowYUhf[x..]) * Vector.Create(muls[1])) + (new Vector(rowYHf[x..]) * Vector.Create(muls[2])); + Vector.SquareRoot((xDiff * xDiff) + (yDiff * yDiff)).CopyTo(row[x..]); + } + + for (; x < width; x++) { float xDiff = (rowXUhf[x] + rowXHf[x]) * muls[0]; float yDiff = (rowYUhf[x] * muls[1]) + (rowYHf[x] * muls[2]); @@ -1530,11 +1568,14 @@ public static void CombineChannelsForMasking( } } - public static void DiffPrecompute( - JxlPlane xyb, - float mul, - float biasArg, - JxlPlane output) + /// + /// Writes the biased square-root response used before masking blur and erosion. + /// + /// The borrowed input samples. + /// The fixed scale applied to the absolute samples. + /// The bias before multiplication by the scale. + /// The preallocated output plane. + public static void DiffPrecompute(JxlPlane xyb, float mul, float biasArg, JxlPlane output) { int width = xyb.XSize; int height = xyb.YSize; @@ -1542,18 +1583,17 @@ public static void DiffPrecompute( float bias = mul * biasArg; float sqrtBias = MathF.Sqrt(bias); + Span inputSamples = xyb.GetRow(0); + Span outputSamples = output.GetRow(0); + + MaskPrecomputeOperator operation = new(mul, bias, sqrtBias); + for (int y = 0; y < height; y++) { - ReadOnlySpan rowIn = xyb.GetRow(y); - Span rowOut = output.GetRow(y); + ReadOnlySpan rowIn = inputSamples[(y * xyb.PixelsPerRow)..]; + Span rowOut = outputSamples[(y * output.PixelsPerRow)..]; - for (int x = 0; x < width; x++) - { - rowOut[x] = - MathF.Sqrt( - (mul * MathF.Abs(rowIn[x])) + bias) - - sqrtBias; - } + TensorPrimitives_.Invoke(rowIn[..width], rowOut[..width], operation); } } @@ -1584,112 +1624,181 @@ private static void StoreMin3( } } + /// + /// Writes the weighted three-smallest neighborhood response into distinct, preallocated storage. + /// + /// The borrowed input plane, retained unchanged during neighboring reads. + /// The distinct preallocated output plane. public static void FuzzyErosion(JxlPlane from, JxlPlane to) { int width = from.XSize; int height = from.YSize; - const int step = 3; + ReadOnlySpan inputSamples = from.GetRow(0); + Span outputSamples = to.GetRow(0); for (int y = 0; y < height; y++) { - Span output = to.GetRow(y); + Span output = outputSamples[(y * to.PixelsPerRow)..]; + int x = 0; + int firstVector = y >= step && y < height - step ? Math.Min(step, width) : width; - for (int x = 0; x < width; x++) + for (; x < firstVector; x++) + { + output[x] = FuzzyErosionAt(inputSamples, from.PixelsPerRow, width, height, x, y); + } + + // Each lane selects the same three minima from its nine neighbors. The border path omits unavailable neighbors. + for (; x <= width - step - Vector.Count; x += Vector.Count) { - float min0 = from.GetRow(y)[x]; - float min1 = 2 * min0; - float min2 = min1; + int center = (y * from.PixelsPerRow) + x; + Vector min0 = new(inputSamples[center..]); + Vector min1 = Vector.Create(2F) * min0; + Vector min2 = min1; + int above = center - (step * from.PixelsPerRow); + int below = center + (step * from.PixelsPerRow); - if (x >= step) - { - StoreMin3( - from.GetRow(y)[x - step], - ref min0, - ref min1, - ref min2); + StoreMin3(new Vector(inputSamples[(center - step)..]), ref min0, ref min1, ref min2); + StoreMin3(new Vector(inputSamples[(above - step)..]), ref min0, ref min1, ref min2); + StoreMin3(new Vector(inputSamples[(below - step)..]), ref min0, ref min1, ref min2); + StoreMin3(new Vector(inputSamples[(center + step)..]), ref min0, ref min1, ref min2); + StoreMin3(new Vector(inputSamples[(above + step)..]), ref min0, ref min1, ref min2); + StoreMin3(new Vector(inputSamples[(below + step)..]), ref min0, ref min1, ref min2); + StoreMin3(new Vector(inputSamples[above..]), ref min0, ref min1, ref min2); + StoreMin3(new Vector(inputSamples[below..]), ref min0, ref min1, ref min2); + ((Vector.Create(0.45F) * min0) + (Vector.Create(0.3F) * min1) + (Vector.Create(0.25F) * min2)).CopyTo(output[x..]); + } - if (y >= step) - { - StoreMin3( - from.GetRow(y - step)[x - step], - ref min0, - ref min1, - ref min2); - } + for (; x < width; x++) + { + output[x] = FuzzyErosionAt(inputSamples, from.PixelsPerRow, width, height, x, y); + } + } + } - if (y < height - step) - { - StoreMin3( - from.GetRow(y + step)[x - step], - ref min0, - ref min1, - ref min2); - } - } + /// + /// Inserts one sample per SIMD lane into the three retained minima, using the scalar comparison order. + /// + /// The samples to insert. + /// The first minimum in each lane. + /// The second minimum in each lane. + /// The third minimum in each lane. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void StoreMin3(Vector value, ref Vector min0, ref Vector min1, ref Vector min2) + { + Vector less2 = Vector.LessThan(value, min2); + Vector less1 = less2 & Vector.LessThan(value, min1); + Vector less0 = less1 & Vector.LessThan(value, min0); + min2 = Vector.ConditionalSelect(less2, Vector.ConditionalSelect(less1, min1, value), min2); + min1 = Vector.ConditionalSelect(less1, Vector.ConditionalSelect(less0, min0, value), min1); + min0 = Vector.ConditionalSelect(less0, value, min0); + } - if (x < width - step) - { - StoreMin3( - from.GetRow(y)[x + step], - ref min0, - ref min1, - ref min2); + /// + /// Evaluates the erosion equation at a border sample, ignoring neighbors outside the image. + /// + /// The complete borrowed input storage. + /// The physical row stride. + /// The logical width. + /// The logical height. + /// The sample column. + /// The sample row. + /// The same weighted minima used by the SIMD interior. + private static float FuzzyErosionAt(ReadOnlySpan inputSamples, int stride, int width, int height, int x, int y) + { + const int step = 3; + float min0 = inputSamples[(y * stride) + x]; + float min1 = 2 * min0; + float min2 = min1; - if (y >= step) - { - StoreMin3( - from.GetRow(y - step)[x + step], - ref min0, - ref min1, - ref min2); - } + if (x >= step) + { + StoreMin3( + inputSamples[(y * stride) + x - step], + ref min0, + ref min1, + ref min2); - if (y < height - step) - { - StoreMin3( - from.GetRow(y + step)[x + step], - ref min0, - ref min1, - ref min2); - } - } + if (y >= step) + { + StoreMin3( + inputSamples[((y - step) * stride) + x - step], + ref min0, + ref min1, + ref min2); + } - if (y >= step) - { - StoreMin3( - from.GetRow(y - step)[x], - ref min0, - ref min1, - ref min2); - } + if (y < height - step) + { + StoreMin3( + inputSamples[((y + step) * stride) + x - step], + ref min0, + ref min1, + ref min2); + } + } - if (y < height - step) - { - StoreMin3( - from.GetRow(y + step)[x], - ref min0, - ref min1, - ref min2); - } + if (x < width - step) + { + StoreMin3( + inputSamples[(y * stride) + x + step], + ref min0, + ref min1, + ref min2); + + if (y >= step) + { + StoreMin3( + inputSamples[((y - step) * stride) + x + step], + ref min0, + ref min1, + ref min2); + } - output[x] = - (0.45f * min0) + - (0.3f * min1) + - (0.25f * min2); + if (y < height - step) + { + StoreMin3( + inputSamples[((y + step) * stride) + x + step], + ref min0, + ref min1, + ref min2); } } + + if (y >= step) + { + StoreMin3( + inputSamples[((y - step) * stride) + x], + ref min0, + ref min1, + ref min2); + } + + if (y < height - step) + { + StoreMin3( + inputSamples[((y + step) * stride) + x], + ref min0, + ref min1, + ref min2); + } + + return (0.45F * min0) + (0.3F * min1) + (0.25F * min2); } /// - /// Builds a perceptual mask and optionally accumulates its AC correction into caller-owned storage. + /// Builds a perceptual mask in borrowed storage and optionally updates an existing AC accumulation plane. /// - /// The configuration used for owned sample allocations. + /// The comparison configuration. /// The borrowed reference masking samples. /// The borrowed distorted masking samples. - /// The borrowed blur workspace. + /// The prepared transposed blur workspace. /// The existing AC accumulation plane, or null when no AC correction is needed. - /// The newly allocated output, transferred to the caller on success and released on failure. + /// The preallocated output plane. + /// The preallocated reference difference and erosion workspace. + /// The preallocated distorted difference workspace. + /// The preallocated reference blur workspace, distinct from diff0 during erosion. + /// The preallocated distorted blur workspace. /// Whether mask construction succeeded. public static bool Mask( Configuration configuration, @@ -1697,87 +1806,79 @@ public static bool Mask( JxlPlane mask1, ButteraugliBlurTemp blurTemp, JxlPlane? diffAc, - out JxlPlane mask) + JxlPlane mask, + JxlPlane diff0, + JxlPlane diff1, + JxlPlane blurred0, + JxlPlane blurred1) { + const float mul = 6.19424080439f; + const float bias = 12.61050594197f; int width = mask0.XSize; int height = mask0.YSize; - mask = new(configuration, width, height); - - bool completed = false; + DiffPrecompute(mask0, mul, bias, diff0); + DiffPrecompute(mask1, mul, bias, diff1); - try + if (!Blur(diff0, MaskingKernel, blurTemp, blurred0)) { - const float mul = 6.19424080439f; - const float bias = 12.61050594197f; - const float radius = 2.7f; - - using JxlPlane diff0 = new(configuration, width, height); - using JxlPlane diff1 = new(configuration, width, height); - using JxlPlane blurred0 = new(configuration, width, height); - using JxlPlane blurred1 = new(configuration, width, height); - - DiffPrecompute(mask0, mul, bias, diff0); - DiffPrecompute(mask1, mul, bias, diff1); - - if (!Blur(configuration, diff0, radius, blurTemp, blurred0)) - { - return false; - } - - FuzzyErosion(blurred0, diff0); - - if (!Blur(configuration, diff1, radius, blurTemp, blurred1)) - { - return false; - } + return false; + } - for (int y = 0; y < height; y++) - { - Span maskRow = mask.GetRow(y); - Span diffRow = diffAc is not null ? diffAc.GetRow(y) : []; + // Erosion reads neighboring samples. Its source must remain separate from its output until every row is complete. + FuzzyErosion(blurred0, diff0); - ReadOnlySpan diff0Row = diff0.GetRow(y); - ReadOnlySpan blur0Row = blurred0.GetRow(y); - ReadOnlySpan blur1Row = blurred1.GetRow(y); + if (!Blur(diff1, MaskingKernel, blurTemp, blurred1)) + { + return false; + } - for (int x = 0; x < width; x++) - { - maskRow[x] = diff0Row[x]; + Span maskSamples = mask.GetRow(0); + ReadOnlySpan diff0Samples = diff0.GetRow(0); + ReadOnlySpan blur0Samples = blurred0.GetRow(0); + ReadOnlySpan blur1Samples = blurred1.GetRow(0); + Span diffSamples = diffAc is not null ? diffAc.GetRow(0) : default; - if (diffRow.Length > 0) - { - const float maskToErrorMul = 10.0f; - float diff = blur0Row[x] - blur1Row[x]; - diffRow[x] += maskToErrorMul * diff * diff; - } - } - } - - completed = true; - return true; - } - finally + for (int y = 0; y < height; y++) { - if (!completed) + Span maskRow = maskSamples.Slice(y * mask.PixelsPerRow, width); + ReadOnlySpan diff0Row = diff0Samples.Slice(y * diff0.PixelsPerRow, width); + diff0Row.CopyTo(maskRow); + + if (diffAc is not null) { - mask.Dispose(); + Span diffRow = diffSamples.Slice(y * diffAc.PixelsPerRow, width); + ReadOnlySpan blur0Row = blur0Samples.Slice(y * blurred0.PixelsPerRow, width); + ReadOnlySpan blur1Row = blur1Samples.Slice(y * blurred1.PixelsPerRow, width); + TensorPrimitives_.Invoke(blur0Row, blur1Row, diffRow, diffRow, default(MaskCorrectionOperator)); } } + + return true; } /// - /// Writes a perceptual mask from borrowed frequency bands and optionally updates an existing AC plane. + /// Builds a perceptual mask from borrowed frequency bands using six preallocated scratch planes. /// - /// The configuration used for temporary sample allocations. - /// The borrowed reference frequency bands. - /// The borrowed distorted frequency bands. + /// The comparison configuration. + /// The unchanged reference frequency bands. + /// The distorted frequency bands. Their high-frequency planes remain readable during this operation. /// The comparison width. /// The comparison height. - /// The borrowed blur workspace. - /// The caller-owned result plane, which receives the completed mask allocation. + /// The prepared transposed blur workspace. + /// The preallocated result plane. /// The existing AC accumulation plane, or null when only the mask is needed. + /// The reference masking workspace. + /// The distorted masking workspace. + /// The reference difference and erosion workspace. + /// The distorted difference workspace. + /// The reference blur workspace. + /// The distorted blur workspace. /// Whether mask construction succeeded. + /// + /// All six scratch planes are distinct and match the comparison dimensions. After frequency comparison, + /// distorted middle- and low-frequency bands can provide this storage. Reference bands cannot provide scratch storage. + /// public static bool MaskButteraugliPsychoImage( Configuration configuration, ButteraugliPsychoImage pi0, @@ -1786,37 +1887,22 @@ public static bool MaskButteraugliPsychoImage( int height, ButteraugliBlurTemp blurTemp, JxlPlane mask, - JxlPlane? diffAc) + JxlPlane? diffAc, + JxlPlane mask0, + JxlPlane mask1, + JxlPlane diff0, + JxlPlane diff1, + JxlPlane blurred0, + JxlPlane blurred1) { - using JxlPlane mask0 = new(configuration, width, height); - using JxlPlane mask1 = new(configuration, width, height); - CombineChannelsForMasking(pi0.Hf, pi0.Uhf, mask0); CombineChannelsForMasking(pi1.Hf, pi1.Uhf, mask1); - - bool completed = Mask(configuration, mask0, mask1, blurTemp, diffAc, out JxlPlane allocatedMask); - - using (allocatedMask) - { - if (!completed) - { - return false; - } - - // Transfer only the completed output. The temporary then releases any previous caller allocation. - mask.Swap(allocatedMask); - return true; - } + return Mask(configuration, mask0, mask1, blurTemp, diffAc, mask, diff0, diff1, blurred0, blurred1); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static float MaskY(float delta) { - const float offset = 0.829591754942f; - const float scaler = 0.451936922203f; - const float mul = 2.5485944793f; - - float c = mul / ((scaler * delta) + offset); + float c = MaskYMultiplier / ((MaskYScaler * delta) + MaskYOffset); float result = GlobalScale * (1.0f + c); return result * result; @@ -1825,11 +1911,7 @@ public static float MaskY(float delta) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static float MaskDcY(float delta) { - const float offset = 0.20025578522f; - const float scaler = 3.87449418804f; - const float mul = 0.505054525019f; - - float c = mul / ((scaler * delta) + offset); + float c = MaskDcYMultiplier / ((MaskDcYScaler * delta) + MaskDcYOffset); float result = GlobalScale * (1.0f + c); return result * result; @@ -1841,6 +1923,15 @@ public static float MaskColor( float mask) => (color[0] * mask) + (color[1] * mask) + (color[2] * mask); + /// + /// Combines the masked low- and high-frequency channel errors into the caller's prepared difference plane. + /// + /// The borrowed perceptual mask. + /// The borrowed low-frequency channel errors. + /// The borrowed high-frequency channel errors. + /// The additional weight for the X channel. + /// The preallocated output plane. + /// Whether the result dimensions match the mask. public static bool CombineChannelsToDiffmap( JxlPlane mask, JxlImage3 blockDiffDc, @@ -1853,159 +1944,389 @@ public static bool CombineChannelsToDiffmap( return false; } - Span diffDc = stackalloc float[3]; - Span diffAc = stackalloc float[3]; - - int xsize = mask.XSize; - int ysize = mask.YSize; - - for (int y = 0; y < ysize; ++y) - { - Span rowOut = result.GetRow(y); + ReadOnlySpan maskSamples = mask.GetRow(0); + ReadOnlySpan dc0 = blockDiffDc.Plane(0).GetRow(0); + ReadOnlySpan dc1 = blockDiffDc.Plane(1).GetRow(0); + ReadOnlySpan dc2 = blockDiffDc.Plane(2).GetRow(0); + ReadOnlySpan ac0 = blockDiffAc.Plane(0).GetRow(0); + ReadOnlySpan ac1 = blockDiffAc.Plane(1).GetRow(0); + ReadOnlySpan ac2 = blockDiffAc.Plane(2).GetRow(0); + Span outputSamples = result.GetRow(0); + Vector xWeight = Vector.Create(xmul); + Vector globalScale = Vector.Create(GlobalScale); + int width = mask.XSize; + + for (int y = 0; y < mask.YSize; y++) + { + ReadOnlySpan maskRow = maskSamples[(y * mask.PixelsPerRow)..]; + ReadOnlySpan dcRow0 = dc0[(y * blockDiffDc.Plane(0).PixelsPerRow)..]; + ReadOnlySpan dcRow1 = dc1[(y * blockDiffDc.Plane(1).PixelsPerRow)..]; + ReadOnlySpan dcRow2 = dc2[(y * blockDiffDc.Plane(2).PixelsPerRow)..]; + ReadOnlySpan acRow0 = ac0[(y * blockDiffAc.Plane(0).PixelsPerRow)..]; + ReadOnlySpan acRow1 = ac1[(y * blockDiffAc.Plane(1).PixelsPerRow)..]; + ReadOnlySpan acRow2 = ac2[(y * blockDiffAc.Plane(2).PixelsPerRow)..]; + Span outputRow = outputSamples[(y * result.PixelsPerRow)..]; + int x = 0; - for (int x = 0; x < xsize; ++x) + // Lanes hold consecutive pixels. Keep channel weighting and the left-to-right sums identical to MaskColor. + for (; x <= width - Vector.Count; x += Vector.Count) { - float val = mask.GetRow(y)[x]; - - float maskVal = MaskY(val); - float dcMaskVal = MaskDcY(val); - - for (int i = 0; i < 3; ++i) - { - diffDc[i] = blockDiffDc.PlaneRow(i, y)[x]; - diffAc[i] = blockDiffAc.PlaneRow(i, y)[x]; - } + Vector value = new(maskRow[x..]); + Vector acMask = Vector.Create(MaskYMultiplier) / ((Vector.Create(MaskYScaler) * value) + Vector.Create(MaskYOffset)); + Vector dcMask = Vector.Create(MaskDcYMultiplier) / ((Vector.Create(MaskDcYScaler) * value) + Vector.Create(MaskDcYOffset)); + acMask = globalScale * (Vector.One + acMask); + dcMask = globalScale * (Vector.One + dcMask); + acMask *= acMask; + dcMask *= dcMask; - diffAc[0] *= xmul; - diffDc[0] *= xmul; + Vector dc = (((new Vector(dcRow0[x..]) * xWeight) * dcMask) + (new Vector(dcRow1[x..]) * dcMask)) + + (new Vector(dcRow2[x..]) * dcMask); + Vector ac = (((new Vector(acRow0[x..]) * xWeight) * acMask) + (new Vector(acRow1[x..]) * acMask)) + + (new Vector(acRow2[x..]) * acMask); + Vector.SquareRoot(dc + ac).CopyTo(outputRow[x..]); + } - rowOut[x] = MathF.Sqrt( - MaskColor(diffDc, dcMaskVal) + - MaskColor(diffAc, maskVal)); + for (; x < width; x++) + { + float acMask = MaskY(maskRow[x]); + float dcMask = MaskDcY(maskRow[x]); + float dc = (((dcRow0[x] * xmul) * dcMask) + (dcRow1[x] * dcMask)) + (dcRow2[x] * dcMask); + float ac = (((acRow0[x] * xmul) * acMask) + (acRow1[x] * acMask)) + (acRow2[x] * acMask); + outputRow[x] = MathF.Sqrt(dc + ac); } } return true; } - public static void L2Diff( - JxlPlane i0, - JxlPlane i1, - float w, - JxlPlane diffmap) + /// + /// Adds weighted squared component differences to an existing error plane. + /// + /// The borrowed reference samples. + /// The equally sized distorted samples. + /// The squared-error weight. Zero leaves the output unchanged. + /// The preallocated accumulation plane. + public static void L2Diff(JxlPlane i0, JxlPlane i1, float w, JxlPlane diffmap) { if (w == 0) { return; } + Span samples0 = i0.GetRow(0); + Span samples1 = i1.GetRow(0); + Span differenceSamples = diffmap.GetRow(0); + + AccumulateSquaredDifferenceOperator operation = new(w); + for (int y = 0; y < i0.YSize; ++y) { - ReadOnlySpan row0 = i0.GetRow(y); - ReadOnlySpan row1 = i1.GetRow(y); - Span rowDiff = diffmap.GetRow(y); + ReadOnlySpan row0 = samples0[(y * i0.PixelsPerRow)..]; + ReadOnlySpan row1 = samples1[(y * i1.PixelsPerRow)..]; + Span rowDiff = differenceSamples[(y * diffmap.PixelsPerRow)..]; - for (int x = 0; x < i0.XSize; ++x) - { - float diff = row0[x] - row1[x]; - rowDiff[x] += diff * diff * w; - } + TensorPrimitives_.Invoke(row0[..i0.XSize], row1[..i0.XSize], rowDiff[..i0.XSize], rowDiff[..i0.XSize], operation); } } - public static void SetL2Diff( - JxlPlane i0, - JxlPlane i1, - float w, - JxlPlane diffmap) + /// + /// Writes weighted squared component differences into a prepared error plane. + /// + /// The borrowed reference samples. + /// The equally sized distorted samples. + /// The squared-error weight. Zero leaves the output unchanged. + /// The preallocated output plane. + public static void SetL2Diff(JxlPlane i0, JxlPlane i1, float w, JxlPlane diffmap) { if (w == 0) { return; } + Span samples0 = i0.GetRow(0); + Span samples1 = i1.GetRow(0); + Span differenceSamples = diffmap.GetRow(0); + + SetSquaredDifferenceOperator operation = new(w); + for (int y = 0; y < i0.YSize; ++y) { - ReadOnlySpan row0 = i0.GetRow(y); - ReadOnlySpan row1 = i1.GetRow(y); - Span rowDiff = diffmap.GetRow(y); + ReadOnlySpan row0 = samples0[(y * i0.PixelsPerRow)..]; + ReadOnlySpan row1 = samples1[(y * i1.PixelsPerRow)..]; + Span rowDiff = differenceSamples[(y * diffmap.PixelsPerRow)..]; - for (int x = 0; x < i0.XSize; ++x) - { - float diff = row0[x] - row1[x]; - rowDiff[x] = diff * diff * w; - } + TensorPrimitives_.Invoke(row0[..i0.XSize], row1[..i0.XSize], rowDiff[..i0.XSize], operation); } } - public static void L2DiffAsymmetric( - JxlPlane i0, - JxlPlane i1, - float w0gt1, - float w0lt1, - JxlPlane diffmap) + /// + /// Accumulates asymmetric high-frequency error with identical scalar and SIMD equations. + /// + /// The borrowed reference samples. + /// The equally sized distorted samples. + /// The primary symmetric error weight. + /// The additional asymmetric error weight. + /// The preallocated accumulation plane. + public static void L2DiffAsymmetric(JxlPlane i0, JxlPlane i1, float w0gt1, float w0lt1, JxlPlane diffmap) { if (w0gt1 == 0 && w0lt1 == 0) { return; } - Vector vw0gt1 = new(w0gt1 * 0.8f); - Vector vw0lt1 = new(w0lt1 * 0.8f); + AsymmetricDifferenceOperator operation = new(w0gt1, w0lt1); - int lanes = Vector.Count; + Span samples0 = i0.GetRow(0); + Span samples1 = i1.GetRow(0); + Span differenceSamples = diffmap.GetRow(0); for (int y = 0; y < i0.YSize; ++y) { - ReadOnlySpan row0 = i0.GetRow(y); - ReadOnlySpan row1 = i1.GetRow(y); - Span rowDiff = diffmap.GetRow(y); + ReadOnlySpan row0 = samples0[(y * i0.PixelsPerRow)..]; + ReadOnlySpan row1 = samples1[(y * i1.PixelsPerRow)..]; + Span rowDiff = differenceSamples[(y * diffmap.PixelsPerRow)..]; - for (int x = 0; x < i0.XSize; x += lanes) - { - Vector val0 = new(row0[x..]); - Vector val1 = new(row1[x..]); + TensorPrimitives_.Invoke(row0[..i0.XSize], row1[..i0.XSize], rowDiff[..i0.XSize], rowDiff[..i0.XSize], operation); + } + } + + /// + /// Accumulates a weighted squared difference without changing the subtraction or multiplication order. + /// + /// The fixed squared-error weight. + private readonly struct AccumulateSquaredDifferenceOperator(float weight) : TensorPrimitives_.ITernaryOperator + { + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float Invoke(float first, float second, float third) + { + float difference = first - second; + return third + (difference * difference * weight); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector128 Invoke(Vector128 first, Vector128 second, Vector128 third) + { + Vector128 difference = first - second; + return third + (difference * difference * Vector128.Create(weight)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector256 Invoke(Vector256 first, Vector256 second, Vector256 third) + { + Vector256 difference = first - second; + return third + (difference * difference * Vector256.Create(weight)); + } - // Primary symmetric quadratic objective. - Vector diff = val0 - val1; + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector512 Invoke(Vector512 first, Vector512 second, Vector512 third) + { + Vector512 difference = first - second; + return third + (difference * difference * Vector512.Create(weight)); + } + } - Vector total = - (diff * diff * vw0gt1) + new Vector(rowDiff[x..]); + /// + /// Sets a weighted squared difference from its two source components. + /// + /// The fixed squared-error weight. + private readonly struct SetSquaredDifferenceOperator(float weight) : TensorPrimitives_.IBinaryOperator + { + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float Invoke(float first, float second) + { + float difference = first - second; + return difference * difference * weight; + } - Vector fabs0 = Vector.Abs(val0); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector128 Invoke(Vector128 first, Vector128 second) + { + Vector128 difference = first - second; + return difference * difference * Vector128.Create(weight); + } - Vector tooSmall = fabs0 * new Vector(0.4f); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector256 Invoke(Vector256 first, Vector256 second) + { + Vector256 difference = first - second; + return difference * difference * Vector256.Create(weight); + } - Vector tooBig = fabs0; + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector512 Invoke(Vector512 first, Vector512 second) + { + Vector512 difference = first - second; + return difference * difference * Vector512.Create(weight); + } + } - Vector ifNeg = - Vector.ConditionalSelect( - Vector.GreaterThan(val1, -tooSmall), - val1 + tooSmall, - Vector.ConditionalSelect( - Vector.LessThan(val1, -tooBig), - -val1 - tooBig, - Vector.Zero)); + /// + /// Computes the masking response from one sample without widening its arithmetic. + /// + /// The fixed scale applied before the square root. + /// The fixed offset inside the square root. + /// The square root of bias, subtracted after the square root. + private readonly struct MaskPrecomputeOperator(float multiplier, float bias, float squareRootBias) : TensorPrimitives_.IUnaryOperator + { + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float Invoke(float value) + => MathF.Sqrt((multiplier * MathF.Abs(value)) + bias) - squareRootBias; - Vector ifPos = - Vector.ConditionalSelect( - Vector.LessThan(val1, tooSmall), - tooSmall - val1, - Vector.ConditionalSelect( - Vector.GreaterThan(val1, tooBig), - val1 - tooBig, - Vector.Zero)); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector128 Invoke(Vector128 value) + => Vector128.Sqrt((Vector128.Create(multiplier) * Vector128.Abs(value)) + Vector128.Create(bias)) - Vector128.Create(squareRootBias); - Vector v = - Vector.ConditionalSelect( - Vector.LessThan(val0, Vector.Zero), - ifNeg, - ifPos); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector256 Invoke(Vector256 value) + => Vector256.Sqrt((Vector256.Create(multiplier) * Vector256.Abs(value)) + Vector256.Create(bias)) - Vector256.Create(squareRootBias); - total += vw0lt1 * v * v; + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector512 Invoke(Vector512 value) + => Vector512.Sqrt((Vector512.Create(multiplier) * Vector512.Abs(value)) + Vector512.Create(bias)) - Vector512.Create(squareRootBias); - total.CopyTo(rowDiff[x..]); - } + } + + /// + /// Adds the masking correction to an existing AC error sample. + /// + private readonly struct MaskCorrectionOperator : TensorPrimitives_.ITernaryOperator + { + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float Invoke(float first, float second, float third) + { + float difference = first - second; + return third + (10F * difference * difference); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector128 Invoke(Vector128 first, Vector128 second, Vector128 third) + { + Vector128 difference = first - second; + return third + (Vector128.Create(10F) * difference * difference); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector256 Invoke(Vector256 first, Vector256 second, Vector256 third) + { + Vector256 difference = first - second; + return third + (Vector256.Create(10F) * difference * difference); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector512 Invoke(Vector512 first, Vector512 second, Vector512 third) + { + Vector512 difference = first - second; + return third + (Vector512.Create(10F) * difference * difference); + } + } + + /// + /// Accumulates the asymmetric high-frequency error with the same branch priorities in every SIMD lane. + /// + private readonly struct AsymmetricDifferenceOperator : TensorPrimitives_.ITernaryOperator + { + private readonly float largerWeight; + private readonly float smallerWeight; + + /// + /// Prepares the native weights once before component traversal. + /// + /// The primary symmetric weight. + /// The asymmetric residual weight. + public AsymmetricDifferenceOperator(float largerWeight, float smallerWeight) + { + this.largerWeight = largerWeight * 0.8F; + this.smallerWeight = smallerWeight * 0.8F; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float Invoke(float first, float second, float third) + { + float difference = first - second; + float total = (difference * difference * this.largerWeight) + third; + float magnitude = MathF.Abs(first); + float tooSmall = magnitude * 0.4F; + float residual = first < 0F + ? second > -tooSmall ? second + tooSmall : second < -magnitude ? -second - magnitude : 0F + : second < tooSmall ? tooSmall - second : second > magnitude ? second - magnitude : 0F; + return total + (this.smallerWeight * residual * residual); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector128 Invoke(Vector128 first, Vector128 second, Vector128 third) + { + Vector128 difference = first - second; + Vector128 total = (difference * difference * Vector128.Create(this.largerWeight)) + third; + Vector128 magnitude = Vector128.Abs(first); + Vector128 tooSmall = magnitude * Vector128.Create(0.4F); + Vector128 negative = Vector128.ConditionalSelect( + Vector128.GreaterThan(second, -tooSmall), + second + tooSmall, + Vector128.ConditionalSelect(Vector128.LessThan(second, -magnitude), -second - magnitude, Vector128.Zero)); + Vector128 positive = Vector128.ConditionalSelect( + Vector128.LessThan(second, tooSmall), + tooSmall - second, + Vector128.ConditionalSelect(Vector128.GreaterThan(second, magnitude), second - magnitude, Vector128.Zero)); + Vector128 residual = Vector128.ConditionalSelect(Vector128.LessThan(first, Vector128.Zero), negative, positive); + return total + (Vector128.Create(this.smallerWeight) * residual * residual); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector256 Invoke(Vector256 first, Vector256 second, Vector256 third) + { + Vector256 difference = first - second; + Vector256 total = (difference * difference * Vector256.Create(this.largerWeight)) + third; + Vector256 magnitude = Vector256.Abs(first); + Vector256 tooSmall = magnitude * Vector256.Create(0.4F); + Vector256 negative = Vector256.ConditionalSelect( + Vector256.GreaterThan(second, -tooSmall), + second + tooSmall, + Vector256.ConditionalSelect(Vector256.LessThan(second, -magnitude), -second - magnitude, Vector256.Zero)); + Vector256 positive = Vector256.ConditionalSelect( + Vector256.LessThan(second, tooSmall), + tooSmall - second, + Vector256.ConditionalSelect(Vector256.GreaterThan(second, magnitude), second - magnitude, Vector256.Zero)); + Vector256 residual = Vector256.ConditionalSelect(Vector256.LessThan(first, Vector256.Zero), negative, positive); + return total + (Vector256.Create(this.smallerWeight) * residual * residual); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector512 Invoke(Vector512 first, Vector512 second, Vector512 third) + { + Vector512 difference = first - second; + Vector512 total = (difference * difference * Vector512.Create(this.largerWeight)) + third; + Vector512 magnitude = Vector512.Abs(first); + Vector512 tooSmall = magnitude * Vector512.Create(0.4F); + Vector512 negative = Vector512.ConditionalSelect( + Vector512.GreaterThan(second, -tooSmall), + second + tooSmall, + Vector512.ConditionalSelect(Vector512.LessThan(second, -magnitude), -second - magnitude, Vector512.Zero)); + Vector512 positive = Vector512.ConditionalSelect( + Vector512.LessThan(second, tooSmall), + tooSmall - second, + Vector512.ConditionalSelect(Vector512.GreaterThan(second, magnitude), second - magnitude, Vector512.Zero)); + Vector512 residual = Vector512.ConditionalSelect(Vector512.LessThan(first, Vector512.Zero), negative, positive); + return total + (Vector512.Create(this.smallerWeight) * residual * residual); } } @@ -2060,6 +2381,16 @@ private static Vector Gamma(Vector v) return (kRetMul * log) + kRetAdd; } + /// + /// Transforms borrowed RGB samples into the perceptual XYB representation using prepared blur and output storage. + /// + /// The comparison configuration. + /// The borrowed RGB image. It may also supply the output storage. + /// The perceptual comparison parameters. + /// The distinct preallocated three-plane blur workspace. + /// The prepared transposed blur workspace. + /// The preallocated transformed output. + /// Whether all blur stages completed. public static bool OpsinDynamicsImage( Configuration configuration, JxlImage3 rgb, @@ -2073,19 +2404,18 @@ public static bool OpsinDynamicsImage( return false; } - const float sigma = 1.2f; - if (!Blur(configuration, rgb.Plane(0), sigma, blurTemp, blurred.Plane(0))) + if (!Blur(rgb.Plane(0), OpsinDynamicsKernel, blurTemp, blurred.Plane(0))) { return false; } - if (!Blur(configuration, rgb.Plane(1), sigma, blurTemp, blurred.Plane(1))) + if (!Blur(rgb.Plane(1), OpsinDynamicsKernel, blurTemp, blurred.Plane(1))) { return false; } - if (!Blur(configuration, rgb.Plane(2), sigma, blurTemp, blurred.Plane(2))) + if (!Blur(rgb.Plane(2), OpsinDynamicsKernel, blurTemp, blurred.Plane(2))) { return false; } @@ -2096,19 +2426,29 @@ public static bool OpsinDynamicsImage( Vector minValue = new(1e-4f); + Span rgbSamples0 = rgb.Plane(0).GetRow(0); + Span rgbSamples1 = rgb.Plane(1).GetRow(0); + Span rgbSamples2 = rgb.Plane(2).GetRow(0); + Span blurredSamples0 = blurred.Plane(0).GetRow(0); + Span blurredSamples1 = blurred.Plane(1).GetRow(0); + Span blurredSamples2 = blurred.Plane(2).GetRow(0); + Span xybSamples0 = xyb.Plane(0).GetRow(0); + Span xybSamples1 = xyb.Plane(1).GetRow(0); + Span xybSamples2 = xyb.Plane(2).GetRow(0); + for (int y = 0; y < rgb.YSize; ++y) { - ReadOnlySpan rowR = rgb.PlaneRow(0, y); - ReadOnlySpan rowG = rgb.PlaneRow(1, y); - ReadOnlySpan rowB = rgb.PlaneRow(2, y); + ReadOnlySpan rowR = rgbSamples0[(y * rgb.Plane(0).PixelsPerRow)..]; + ReadOnlySpan rowG = rgbSamples1[(y * rgb.Plane(1).PixelsPerRow)..]; + ReadOnlySpan rowB = rgbSamples2[(y * rgb.Plane(2).PixelsPerRow)..]; - ReadOnlySpan blurredR = blurred.PlaneRow(0, y); - ReadOnlySpan blurredG = blurred.PlaneRow(1, y); - ReadOnlySpan blurredB = blurred.PlaneRow(2, y); + ReadOnlySpan blurredR = blurredSamples0[(y * blurred.Plane(0).PixelsPerRow)..]; + ReadOnlySpan blurredG = blurredSamples1[(y * blurred.Plane(1).PixelsPerRow)..]; + ReadOnlySpan blurredB = blurredSamples2[(y * blurred.Plane(2).PixelsPerRow)..]; - Span outX = xyb.PlaneRow(0, y); - Span outY = xyb.PlaneRow(1, y); - Span outB = xyb.PlaneRow(2, y); + Span outX = xybSamples0[(y * xyb.Plane(0).PixelsPerRow)..]; + Span outY = xybSamples1[(y * xyb.Plane(1).PixelsPerRow)..]; + Span outB = xybSamples2[(y * xyb.Plane(2).PixelsPerRow)..]; for (int x = 0; x < rgb.XSize; x += lanes) { @@ -2177,7 +2517,7 @@ public static bool ButteraugliDiffmapInPlace( int xSize = image0.XSize; int ySize = image0.YSize; - using ButteraugliBlurTemp blurTemp = new(); + using ButteraugliBlurTemp blurTemp = new(configuration, xSize, ySize); using (JxlImage3 temp = new(configuration, xSize, ySize)) { @@ -2223,10 +2563,10 @@ public static bool ButteraugliDiffmapInPlace( InlineArray2> hf1 = default; // The inline arrays borrow these owners. Using declarations release every partially built band on early return. - using JxlPlane hf0X = new(); - using JxlPlane hf0Y = new(); - using JxlPlane hf1X = new(); - using JxlPlane hf1Y = new(); + using JxlPlane hf0X = new(configuration, xSize, ySize); + using JxlPlane hf0Y = new(configuration, xSize, ySize); + using JxlPlane hf1X = new(configuration, xSize, ySize); + using JxlPlane hf1Y = new(configuration, xSize, ySize); hf0[0] = hf0X; hf0[1] = hf0Y; @@ -2284,17 +2624,15 @@ public static bool ButteraugliDiffmapInPlace( blockDiffAc); } - // Free MF images - image0.Dispose(); - image1.Dispose(); + // The input owners remain alive. Masking reuses their middle-frequency samples after the error stages consume them. InlineArray2> uhf0 = default; InlineArray2> uhf1 = default; - using JxlPlane uhf0X = new(); - using JxlPlane uhf0Y = new(); - using JxlPlane uhf1X = new(); - using JxlPlane uhf1Y = new(); + using JxlPlane uhf0X = new(configuration, xSize, ySize); + using JxlPlane uhf0Y = new(configuration, xSize, ySize); + using JxlPlane uhf1X = new(configuration, xSize, ySize); + using JxlPlane uhf1Y = new(configuration, xSize, ySize); uhf0[0] = uhf0X; uhf0[1] = uhf0Y; @@ -2379,19 +2717,23 @@ public static bool ButteraugliDiffmapInPlace( DeallocateHFAndUHF(hf0, uhf0); DeallocateHFAndUHF(hf1, uhf1); - if (!Mask(configuration, mask0, mask1, blurTemp, mask, out JxlPlane newBlockDiffAc)) + using JxlPlane newBlockDiffAc = new(configuration, xSize, ySize); + if (!Mask(configuration, mask0, mask1, blurTemp, mask, newBlockDiffAc, image0.Plane(0), image0.Plane(1), image0.Plane(2), image1.Plane(0))) { return false; } - using (newBlockDiffAc) { + ReadOnlySpan combinedSamples = newBlockDiffAc.GetRow(0); + Span outputSamples = diffmap.GetRow(0); + ReadOnlySpan maskSamples = mask.GetRow(0); + for (int y = 0; y < ySize; y++) { - ReadOnlySpan dc = newBlockDiffAc.GetRow(y); - ReadOnlySpan ac = newBlockDiffAc.GetRow(y); - Span output = diffmap.GetRow(y); - ReadOnlySpan maskRow = mask.GetRow(y); + ReadOnlySpan dc = combinedSamples[(y * newBlockDiffAc.PixelsPerRow)..]; + ReadOnlySpan ac = dc; + Span output = outputSamples[(y * diffmap.PixelsPerRow)..]; + ReadOnlySpan maskRow = maskSamples[(y * mask.PixelsPerRow)..]; for (int x = 0; x < xSize; x++) { @@ -2405,72 +2747,95 @@ public static bool ButteraugliDiffmapInPlace( } } - // Calculate a 2x2 subsampled image for purposes of recursive butteraugli at - // multiresolution. + /// + /// Allocates a half-resolution image for reference setup, averaging each available two-by-two source group. + /// + /// The allocation configuration. + /// The borrowed full-resolution image. + /// The caller-owned half-resolution image. public static JxlImage3 SubSample2x(Configuration configuration, JxlImage3 input) { - int xs = (input.XSize + 1) / 2; - int ys = (input.YSize + 1) / 2; - - JxlImage3 retval = new(configuration, xs, ys); + JxlImage3 output = new(configuration, (input.XSize + 1) / 2, (input.YSize + 1) / 2); + try + { + SubSample2x(input, output); + return output; + } + catch + { + output.Dispose(); + throw; + } + } - for (int c = 0; c < 3; ++c) + /// + /// Writes half-resolution samples into prepared storage without allocating or changing either image's dimensions. + /// + /// The borrowed full-resolution image. + /// The distinct preallocated image with each dimension rounded up after division by two. + public static void SubSample2x(JxlImage3 input, JxlImage3 output) + { + for (int c = 0; c < 3; c++) { - for (int y = 0; y < ys; ++y) + JxlPlane inputPlane = input.Plane(c); + JxlPlane outputPlane = output.Plane(c); + ReadOnlySpan sourceSamples = inputPlane.GetRow(0); + Span outputSamples = outputPlane.GetRow(0); + + for (int y = 0; y < output.YSize; y++) { - for (int x = 0; x < xs; ++x) - { - retval.PlaneRow(c, y)[x] = 0.0f; - } + outputSamples.Slice(y * outputPlane.PixelsPerRow, output.XSize).Clear(); } - } - for (int c = 0; c < 3; ++c) - { - for (int y = 0; y < input.YSize; ++y) + for (int y = 0; y < input.YSize; y++) { - ReadOnlySpan srcRow = input.PlaneRow(c, y); + ReadOnlySpan sourceRow = sourceSamples[(y * inputPlane.PixelsPerRow)..]; + Span outputRow = outputSamples[((y / 2) * outputPlane.PixelsPerRow)..]; - for (int x = 0; x < input.XSize; ++x) + for (int x = 0; x < input.XSize; x++) { - retval.PlaneRow(c, y / 2)[x / 2] += 0.25f * srcRow[x]; + outputRow[x / 2] += 0.25F * sourceRow[x]; } } + // A final unmatched row or column supplies two samples rather than four. Preserve the native edge normalization. if ((input.XSize & 1) != 0) { - for (int y = 0; y < retval.YSize; ++y) + for (int y = 0; y < output.YSize; y++) { - int lastColumn = retval.XSize - 1; - retval.PlaneRow(c, y)[lastColumn] *= 2.0f; + outputSamples[(y * outputPlane.PixelsPerRow) + output.XSize - 1] *= 2F; } } if ((input.YSize & 1) != 0) { - for (int x = 0; x < retval.XSize; ++x) - { - int lastRow = retval.YSize - 1; - retval.PlaneRow(c, lastRow)[x] *= 2.0f; - } + Span lastRow = outputSamples.Slice((output.YSize - 1) * outputPlane.PixelsPerRow, output.XSize); + TensorPrimitives.Multiply(lastRow, 2F, lastRow); } } - - return retval; } + /// + /// Mixes a borrowed lower-resolution difference map into the full-resolution map using nearest-neighbor expansion. + /// + /// The half-resolution difference map. + /// The lower-resolution contribution weight. + /// The full-resolution map, updated in place. public static void AddSupersampled2x(JxlPlane src, float w, JxlPlane dest) { - const float heuristicMixingValue = 0.3f; + const float heuristicMixingValue = 0.3F; + ReadOnlySpan sourceSamples = src.GetRow(0); + Span destinationSamples = dest.GetRow(0); - for (int y = 0; y < dest.YSize; ++y) + for (int y = 0; y < dest.YSize; y++) { - Span destRow = dest.GetRow(y); + ReadOnlySpan sourceRow = sourceSamples[((y / 2) * src.PixelsPerRow)..]; + Span destinationRow = destinationSamples[(y * dest.PixelsPerRow)..]; - for (int x = 0; x < dest.XSize; ++x) + for (int x = 0; x < dest.XSize; x++) { - destRow[x] *= 1.0f - (heuristicMixingValue * w); - destRow[x] += w * src.GetRow(y / 2)[x / 2]; + destinationRow[x] *= 1F - (heuristicMixingValue * w); + destinationRow[x] += w * sourceRow[x / 2]; } } } @@ -2510,12 +2875,17 @@ public static JxlImage3 CreateHeatMapImage(Configuration configuration, J // or keep it) JxlImage3 heatmap = new(configuration, distmap.XSize, distmap.YSize); + ReadOnlySpan differenceSamples = distmap.GetRow(0); + Span heatmap0 = heatmap.Plane(0).GetRow(0); + Span heatmap1 = heatmap.Plane(1).GetRow(0); + Span heatmap2 = heatmap.Plane(2).GetRow(0); + for (int y = 0; y < distmap.YSize; y++) { - Span row_distmap = distmap.GetRow(y); - Span row_h0 = heatmap.PlaneRow(0, y); - Span row_h1 = heatmap.PlaneRow(1, y); - Span row_h2 = heatmap.PlaneRow(2, y); + ReadOnlySpan row_distmap = differenceSamples[(y * distmap.PixelsPerRow)..]; + Span row_h0 = heatmap0[(y * heatmap.Plane(0).PixelsPerRow)..]; + Span row_h1 = heatmap1[(y * heatmap.Plane(1).PixelsPerRow)..]; + Span row_h2 = heatmap2[(y * heatmap.Plane(2).PixelsPerRow)..]; for (int x = 0; x < distmap.XSize; ++x) { @@ -2753,7 +3123,7 @@ public static bool ButteraugliDiffmap( } using ButteraugliComparator butteraugli = ButteraugliComparator.Make(configuration, rgb0, parameters); - diffmap = new(); + diffmap = new(configuration, xsize, ysize); bool completed = false; try @@ -2801,6 +3171,11 @@ public static bool ButteraugliDiffmapSmall( for (int i = 0; i < 3; ++i) { + ReadOnlySpan source0 = rgb0.Plane(i).GetRow(0); + ReadOnlySpan source1 = rgb1.Plane(i).GetRow(0); + Span destination0 = scaled0.Plane(i).GetRow(0); + Span destination1 = scaled1.Plane(i).GetRow(0); + for (int y = 0; y < yscaled; y++) { for (int x = 0; x < xscaled; x++) @@ -2808,8 +3183,8 @@ public static bool ButteraugliDiffmapSmall( int x2 = Math.Min(xsize - 1, x > xborder ? x - xborder : 0); int y2 = Math.Min(ysize - 1, y > yborder ? y - yborder : 0); - scaled0.PlaneRow(i, y)[x] = rgb0.PlaneRow(i, y2)[x2]; - scaled1.PlaneRow(i, y)[x] = rgb1.PlaneRow(i, y2)[x2]; + destination0[(y * scaled0.Plane(i).PixelsPerRow) + x] = source0[(y2 * rgb0.Plane(i).PixelsPerRow) + x2]; + destination1[(y * scaled1.Plane(i).PixelsPerRow) + x] = source1[(y2 * rgb1.Plane(i).PixelsPerRow) + x2]; } } } @@ -2829,9 +3204,13 @@ public static bool ButteraugliDiffmapSmall( try { + ReadOnlySpan scaledSamples = diffmapScaled.GetRow(0); + Span outputSamples = diffmap.GetRow(0); + for (int y = 0; y < ysize; y++) { - diffmapScaled.GetRow(y + yborder).Slice(xborder, xsize).CopyTo(diffmap.GetRow(y)); + scaledSamples.Slice(((y + yborder) * diffmapScaled.PixelsPerRow) + xborder, xsize) + .CopyTo(outputSamples.Slice(y * diffmap.PixelsPerRow, xsize)); } copied = true; @@ -2847,13 +3226,21 @@ public static bool ButteraugliDiffmapSmall( } } + /// + /// Returns the largest logical difference-map sample. + /// + /// The borrowed difference map; row padding is excluded. + /// The comparison parameters retained by the score API. + /// The maximum difference-map sample, with a lower bound of zero. public static float ButteraugliScoreFromDiffmap(JxlPlane diffmap, ButteraugliParameters parameters) { float retval = 0.0f; + ReadOnlySpan samples = diffmap.GetRow(0); + for (int y = 0; y < diffmap.YSize; ++y) { - Span row = diffmap.GetRow(y); + ReadOnlySpan row = samples[(y * diffmap.PixelsPerRow)..]; for (int x = 0; x < diffmap.XSize; ++x) { retval = Math.Max(retval, row[x]); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliBlurTemp.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliBlurTemp.cs index 6722e55e14..3062cc26e5 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliBlurTemp.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliBlurTemp.cs @@ -11,27 +11,18 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; internal sealed class ButteraugliBlurTemp : IDisposable { /// - /// Gets the owned transposed scratch plane. Callers borrow it until the next blur or disposal. + /// Allocates transposed scratch storage for every blur in a fixed-size comparison. /// - public JxlPlane TransposedTemp { get; } = new(); + /// The configuration that owns the sample allocation. + /// The comparison width. + /// The comparison height. + public ButteraugliBlurTemp(Configuration configuration, int width, int height) + => this.TransposedTemp = new(configuration, height, width); /// - /// Reuses or replaces the scratch allocation for the transposed input dimensions. + /// Gets the owned transposed scratch plane. Callers borrow it until the next blur or disposal. /// - /// The allocator configuration used if the existing capacity is too small. - /// The input plane whose dimensions are transposed. - /// The borrowed scratch plane, with width equal to the input height and height equal to its width. - public JxlPlane GetTransposed(Configuration configuration, JxlPlane input) - { - if (!this.TransposedTemp.ShrinkTo(input.YSize, input.XSize)) - { - // Allocate before exchanging ownership. Disposal then releases the old allocation, including on a later blur failure. - using JxlPlane replacement = new(configuration, input.YSize, input.XSize); - this.TransposedTemp.Swap(replacement); - } - - return this.TransposedTemp; - } + public JxlPlane TransposedTemp { get; } /// public void Dispose() => this.TransposedTemp.Dispose(); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs index 09c95fccea..181f864a09 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs @@ -10,18 +10,46 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; /// Owns the reference frequency bands, lower-resolution comparators and reusable blur storage for Butteraugli. /// /// Comparisons reuse mutable scratch and run sequentially for each comparator. -internal class ButteraugliComparator : IDisposable +internal sealed class ButteraugliComparator : IDisposable { private readonly int xSize; private readonly int ySize; private readonly ButteraugliParameters parameters; + /// + /// The reference decomposition, retained unchanged across comparisons. + /// private readonly ButteraugliPsychoImage pi0 = new(); - private readonly JxlImage3 temp; - private readonly ButteraugliBlurTemp blurTemp = new(); + + /// + /// The distorted decomposition. Its MF and LF planes become six mask scratch planes after their last comparison. + /// + private readonly ButteraugliPsychoImage distorted = new(); + + /// + /// The three blurred RGB planes, reused for accumulated AC differences after the opsin transform. + /// + private readonly JxlImage3 temp = new(); + + /// + /// The transformed actual image, reused for DC differences after frequency separation. + /// + private readonly JxlImage3 transformedActual = new(); + + /// + /// Malta difference scratch, reused for the output mask after the last Malta comparison. + /// + private readonly JxlPlane differences = new(); + + /// + /// The lower-resolution comparison result. It remains distinct from this comparator's result and mask storage. + /// + private readonly JxlPlane subsampledDifference = new(); + + private readonly ButteraugliBlurTemp blurTemp; private ButteraugliComparator? sub; /// - /// Initializes a comparator and its full-resolution scratch image without taking ownership of input pixels. + /// Allocates the comparison workspace without taking ownership of input pixels. /// /// The configuration used for owned sample allocations. /// The comparison width. @@ -32,11 +60,32 @@ public ButteraugliComparator(Configuration configuration, int xsize, int ysize, this.xSize = xsize; this.ySize = ysize; this.parameters = parameters; - this.temp = new(configuration, xsize, ysize); + this.blurTemp = new(configuration, xsize, ysize); + + try + { + // All repeated-comparison storage belongs to this owner. Swapping preserves the empty wrappers used for failure cleanup. + using JxlImage3 blurStorage = new(configuration, xsize, ysize); + this.temp.Swap(blurStorage); + + using JxlImage3 transformedStorage = new(configuration, xsize, ysize); + this.transformedActual.Swap(transformedStorage); + + this.pi0.Allocate(configuration, xsize, ysize); + this.distorted.Allocate(configuration, xsize, ysize); + + using JxlPlane differenceStorage = new(configuration, xsize, ysize); + this.differences.Swap(differenceStorage); + } + catch + { + this.Dispose(); + throw; + } } /// - /// Gets the borrowed blur scratch image. The comparator retains this allocation between comparisons. + /// Gets the borrowed three-plane workspace. Opsin uses it for blurred RGB; the next phase replaces those samples with AC differences. /// public JxlImage3 Temp => this.temp; @@ -61,20 +110,25 @@ public static ButteraugliComparator Make(Configuration configuration, JxlImage3< return result; } - using JxlImage3 xyb0 = new(configuration, xSize, ySize); - - if (!Butteraugli.OpsinDynamicsImage(configuration, rgb0, parameters, result.Temp, result.blurTemp, xyb0)) + if (!Butteraugli.OpsinDynamicsImage(configuration, rgb0, parameters, result.Temp, result.blurTemp, result.transformedActual)) { throw new InvalidOperationException("OpsinDynamicsImage failed"); } - if (!Butteraugli.SeparateFrequencies(configuration, result.blurTemp, xyb0, result.pi0)) + if (!Butteraugli.SeparateFrequencies(configuration, result.blurTemp, result.transformedActual, result.pi0)) { throw new InvalidOperationException("Could not separate frequencies"); } - using JxlImage3 subsampledRgb0 = Butteraugli.SubSample2x(configuration, rgb0); - result.sub = Make(configuration, subsampledRgb0, parameters); + // Smaller comparisons return only zero. Do not construct their frequency bands or blur workspace. + if ((xSize + 1) / 2 >= 8 && (ySize + 1) / 2 >= 8) + { + using JxlImage3 subsampledRgb0 = Butteraugli.SubSample2x(configuration, rgb0); + result.sub = Make(configuration, subsampledRgb0, parameters); + + using JxlPlane subResult = new(configuration, result.sub.xSize, result.sub.ySize); + result.subsampledDifference.Swap(subResult); + } return result; } @@ -87,57 +141,48 @@ public static ButteraugliComparator Make(Configuration configuration, JxlImage3< } /// - /// Computes the butteraugli map between the original image given in the constructor and the distorted image given here. + /// Compares borrowed distorted RGB against the retained reference and writes the perceptual difference map. /// - /// The configuration used for temporary sample allocations. + /// The configuration used by the comparison stages. /// The borrowed distorted pixels. - /// The caller-owned result plane. Its storage is resized or reused to match the reference. + /// The caller-owned result plane, already allocated at the reference dimensions. /// Whether all comparison stages succeeded. - public virtual bool Diffmap(Configuration configuration, JxlImage3 rgb1, JxlPlane result) + public bool Diffmap(Configuration configuration, JxlImage3 rgb1, JxlPlane result) { if (this.xSize < 8 || this.ySize < 8) { - this.PrepareResult(configuration, result); result.Clear(); return true; } - using JxlImage3 xyb1 = new(configuration, this.xSize, this.ySize); - - if (!Butteraugli.OpsinDynamicsImage(configuration, rgb1, this.parameters, this.Temp, this.blurTemp, xyb1)) + if (!Butteraugli.OpsinDynamicsImage(configuration, rgb1, this.parameters, this.Temp, this.blurTemp, this.transformedActual)) { return false; } - if (!this.DiffmapOpsinDynamicsImage(configuration, xyb1, result)) + if (!this.DiffmapOpsinDynamicsImage(configuration, this.transformedActual, result)) { return false; } if (this.sub is not null) { - if (this.sub.xSize < 8 || this.sub.ySize < 8) - { - return true; - } + // Subsampling fills the child's reusable transformed image. OpsinDynamicsImage supports identical input and output. + Butteraugli.SubSample2x(rgb1, this.sub.transformedActual); - using JxlImage3 subXyb = new(configuration, this.sub.xSize, this.sub.ySize); - using JxlImage3 subsampledRgb1 = Butteraugli.SubSample2x(configuration, rgb1); - - if (!Butteraugli.OpsinDynamicsImage(configuration, subsampledRgb1, this.parameters, this.sub.Temp, this.sub.blurTemp, subXyb)) + if (!Butteraugli.OpsinDynamicsImage( + configuration, this.sub.transformedActual, this.parameters, this.sub.Temp, this.sub.blurTemp, this.sub.transformedActual)) { return false; } - using JxlPlane subResult = new(); - // Use the lower-resolution reference decomposition with its matching lower-resolution distorted image. - if (!this.sub.DiffmapOpsinDynamicsImage(configuration, subXyb, subResult)) + if (!this.sub.DiffmapOpsinDynamicsImage(configuration, this.sub.transformedActual, this.subsampledDifference)) { return false; } - Butteraugli.AddSupersampled2x(subResult, 0.5f, result); + Butteraugli.AddSupersampled2x(this.subsampledDifference, 0.5f, result); } return true; @@ -146,40 +191,35 @@ public virtual bool Diffmap(Configuration configuration, JxlImage3 rgb1, /// /// Computes the comparison from borrowed samples that already passed through the opsin dynamics transform. /// - /// The configuration used for temporary sample allocations. + /// The configuration used by the comparison stages. /// The borrowed transformed samples. - /// The caller-owned result plane. + /// The caller-owned result plane, already allocated at the reference dimensions. /// Whether frequency separation and comparison succeeded. public bool DiffmapOpsinDynamicsImage(Configuration configuration, JxlImage3 xyb1, JxlPlane result) { if (this.xSize < 8 || this.ySize < 8) { - this.PrepareResult(configuration, result); result.Clear(); return true; } - using ButteraugliPsychoImage pi1 = new(); - - if (!Butteraugli.SeparateFrequencies(configuration, this.blurTemp, xyb1, pi1)) + if (!Butteraugli.SeparateFrequencies(configuration, this.blurTemp, xyb1, this.distorted)) { return false; } - return this.DiffmapPsychoImage(configuration, pi1, result); + return this.DiffmapPsychoImage(configuration, this.distorted, result); } /// /// Compares a borrowed frequency decomposition against the owned reference decomposition. /// - /// The configuration used for temporary sample allocations. + /// The configuration used by the comparison stages. /// The borrowed distorted frequency bands. - /// The caller-owned result plane. + /// The caller-owned result plane, already allocated at the reference dimensions. /// Whether every comparison and mask stage succeeded. public bool DiffmapPsychoImage(Configuration configuration, ButteraugliPsychoImage pi1, JxlPlane diffmap) { - this.PrepareResult(configuration, diffmap); - if (this.xSize < 8 || this.ySize < 8) { diffmap.Clear(); @@ -189,8 +229,10 @@ public bool DiffmapPsychoImage(Configuration configuration, ButteraugliPsychoIma float hfAsymmetry = this.parameters.HfAsymmetry; float xmul = this.parameters.XMultiplier; - using JxlPlane diffs = new(configuration, this.xSize, this.ySize); - using JxlImage3 blockDiffAc = new(configuration, this.xSize, this.ySize); + // Opsin no longer needs blurred RGB, and frequency separation has consumed transformedActual. + // Reuse those allocations for AC accumulation and fully written DC differences. Only AC requires clearing. + JxlPlane diffs = this.differences; + JxlImage3 blockDiffAc = this.temp; JxlImageOperations.ZeroFillImage(blockDiffAc); @@ -272,7 +314,7 @@ public bool DiffmapPsychoImage(Configuration configuration, ButteraugliPsychoIma return false; } - using JxlImage3 blockDiffDc = new(configuration, this.xSize, this.ySize); + JxlImage3 blockDiffDc = this.transformedActual; for (int c = 0; c < 3; c++) { @@ -299,9 +341,25 @@ public bool DiffmapPsychoImage(Configuration configuration, ButteraugliPsychoIma blockDiffDc.Plane(c)); } - using JxlPlane mask = new(); - - if (!Butteraugli.MaskButteraugliPsychoImage(configuration, this.pi0, pi1, this.xSize, this.ySize, this.blurTemp, mask, blockDiffAc.Plane(1))) + // All MF and LF samples have now contributed to their differences. Reuse their six planes as mask scratch. + // Hf and Uhf remain intact until masking reads them; Malta scratch can now hold the completed mask. + JxlPlane mask = this.differences; + + if (!Butteraugli.MaskButteraugliPsychoImage( + configuration, + this.pi0, + pi1, + this.xSize, + this.ySize, + this.blurTemp, + mask, + blockDiffAc.Plane(1), + this.distorted.Mf.Plane(0), + this.distorted.Mf.Plane(1), + this.distorted.Mf.Plane(2), + this.distorted.Lf.Plane(0), + this.distorted.Lf.Plane(1), + this.distorted.Lf.Plane(2))) { return false; } @@ -312,33 +370,37 @@ public bool DiffmapPsychoImage(Configuration configuration, ButteraugliPsychoIma /// /// Writes the reference image's perceptual mask into caller-owned storage. /// - /// The configuration used for temporary sample allocations. - /// The caller-owned output plane. + /// The configuration used by the mask stages. + /// The caller-owned output plane, already allocated at the reference dimensions. /// Whether mask construction succeeded. - public virtual bool Mask(Configuration configuration, JxlPlane mask) - => Butteraugli.MaskButteraugliPsychoImage(configuration, this.pi0, this.pi0, this.xSize, this.ySize, this.blurTemp, mask, null); + public bool Mask(Configuration configuration, JxlPlane mask) + => Butteraugli.MaskButteraugliPsychoImage( + configuration, + this.pi0, + this.pi0, + this.xSize, + this.ySize, + this.blurTemp, + mask, + null, + this.distorted.Mf.Plane(0), + this.distorted.Mf.Plane(1), + this.distorted.Mf.Plane(2), + this.distorted.Lf.Plane(0), + this.distorted.Lf.Plane(1), + this.distorted.Lf.Plane(2)); /// public void Dispose() { this.temp.Dispose(); + this.transformedActual.Dispose(); + this.distorted.Dispose(); + this.differences.Dispose(); + this.subsampledDifference.Dispose(); this.pi0.Dispose(); this.sub?.Dispose(); this.sub = null; this.blurTemp.Dispose(); } - - /// - /// Reuses the caller's result capacity or exchanges it for an allocation matching the reference dimensions. - /// - /// The configuration used if a larger allocation is required. - /// The result plane whose identity and ownership remain with the caller. - private void PrepareResult(Configuration configuration, JxlPlane result) - { - if (!result.ShrinkTo(this.xSize, this.ySize)) - { - using JxlPlane replacement = new(configuration, this.xSize, this.ySize); - result.Swap(replacement); - } - } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliPsychoImage.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliPsychoImage.cs index 2b0ed73937..bfdd6f3109 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliPsychoImage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliPsychoImage.cs @@ -44,6 +44,31 @@ public ButteraugliPsychoImage() /// public JxlImage3 Lf { get; } = new(); + /// + /// Allocates every frequency band before comparisons begin, without replacing the owned plane wrappers. + /// + /// The configuration used for sample allocations. + /// The comparison width in samples. + /// The comparison height in rows. + /// The containing comparator releases partially allocated bands if setup fails. + public void Allocate(Configuration configuration, int width, int height) + { + using JxlImage3 middle = new(configuration, width, height); + this.Mf.Swap(middle); + + using JxlImage3 low = new(configuration, width, height); + this.Lf.Swap(low); + + for (int c = 0; c < 2; c++) + { + using JxlPlane high = new(configuration, width, height); + this.Hf[c].Swap(high); + + using JxlPlane ultraHigh = new(configuration, width, height); + this.Uhf[c].Swap(ultraHigh); + } + } + /// public void Dispose() { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparator.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparator.cs index 72f4c26b34..0fa78e60c3 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparator.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparator.cs @@ -33,13 +33,13 @@ internal sealed class JxlComparator(ButteraugliParameters parameters, JxlCmsInte public float BadQualityScore => Butteraugli.Butteraugli.ButteraugliFuzzyInverse(0.5f); /// - /// Compares borrowed samples with the current reference and writes an optional difference map. + /// Compares borrowed samples with the current reference and writes the difference map into reusable storage. /// /// The configuration that owns temporary sample storage. /// The borrowed image to compare without changing its samples. - /// The caller-owned difference map, or null when only a score is required. + /// The caller-owned map allocated at the reference dimensions. /// The computed perceptual difference. - public void CompareWith(Configuration configuration, JxlImageBundle actual, JxlPlane? diffMap, out float score) + public void CompareWith(Configuration configuration, JxlImageBundle actual, JxlPlane diffMap, out float score) { DebugGuard.NotNull(this.comparator, nameof(this.comparator)); @@ -58,7 +58,7 @@ public void CompareWith(Configuration configuration, JxlImageBundle actual, JxlP throw new InvalidOperationException("Could not convert the comparison image to linear sRGB."); } - using JxlPlane tempDiffmap = new(configuration, this.width, this.height); + // The caller allocates its map before a search. Every comparison overwrites the logical samples without exchanging storage. bool scaleIntensity = this.intensityTarget != 0 && actual.Metadata.IntensityTarget != this.intensityTarget; using JxlImage3? scaledStorage = scaleIntensity && ReferenceEquals(actualLinearSRgb, actual) ? new(configuration, this.width, this.height) @@ -72,23 +72,27 @@ public void CompareWith(Configuration configuration, JxlImageBundle actual, JxlP float scale = actual.Metadata.IntensityTarget / this.intensityTarget; for (int c = 0; c < 3; ++c) { + JxlPlane source = actualLinearSRgb.Color!.Plane(c); + JxlPlane destination = scaledActualLinearSRgb.Plane(c); + ReadOnlySpan sourceSamples = source.GetRow(0); + Span destinationSamples = destination.GetRow(0); + for (int y = 0; y < this.height; ++y) { - // PlaneRow includes subsequent rows. Restrict the operation to this logical row, excluding padding. - ReadOnlySpan sourceRow = actualLinearSRgb.Color!.PlaneRow(c, y)[..this.width]; - Span scaledRow = scaledActualLinearSRgb.PlaneRow(c, y)[..this.width]; + // Strides include padding. Select only the logical samples without acquiring Memory.Span for each row. + ReadOnlySpan sourceRow = sourceSamples.Slice(y * source.PixelsPerRow, this.width); + Span scaledRow = destinationSamples.Slice(y * destination.PixelsPerRow, this.width); TensorPrimitives.Multiply(sourceRow, scale, scaledRow); } } } - if (!this.comparator.Diffmap(configuration, scaledActualLinearSRgb, tempDiffmap)) + if (!this.comparator.Diffmap(configuration, scaledActualLinearSRgb, diffMap)) { throw new InvalidOperationException("Diffmap failed"); } - score = Butteraugli.Butteraugli.ButteraugliScoreFromDiffmap(tempDiffmap, parameters); - diffMap?.Swap(tempDiffmap); + score = Butteraugli.Butteraugli.ButteraugliScoreFromDiffmap(diffMap, parameters); } /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparatorScoreComputation.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparatorScoreComputation.cs index e90748564f..b5abad27f0 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparatorScoreComputation.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparatorScoreComputation.cs @@ -8,17 +8,33 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Comparator; +/// +/// Scores borrowed images, including their appearance on black and white backgrounds when alpha is present. +/// internal static class JxlComparatorScoreComputation { + /// + /// Blends one linear-light plane over a constant background in the gamma-compressed comparison space. + /// + /// The borrowed linear-light color planes. + /// The selected color plane. + /// The constant linear-light background component. + /// The borrowed opacity plane. + /// The result planes. The selected plane can also supply the input. private static void AlphaBlend(JxlImage3 input, int c, float backgroundLinear, JxlPlane alpha, JxlImage3 output) { float background = (float)JxlGammaCorrect.LinearToSRgb8Direct(backgroundLinear); + ReadOnlySpan alphaSamples = alpha.GetRow(0); + JxlPlane source = input.Plane(c); + JxlPlane destination = output.Plane(c); + ReadOnlySpan inputSamples = source.GetRow(0); + Span outputSamples = destination.GetRow(0); for (int y = 0; y < output.YSize; y++) { - ReadOnlySpan rowA = alpha.GetRow(y); - ReadOnlySpan rowI = input.PlaneRow(c, y); - Span rowO = output.PlaneRow(c, y); + ReadOnlySpan rowA = alphaSamples[(y * alpha.PixelsPerRow)..]; + ReadOnlySpan rowI = inputSamples[(y * source.PixelsPerRow)..]; + Span rowO = outputSamples[(y * destination.PixelsPerRow)..]; for (int x = 0; x < output.XSize; x++) { @@ -45,6 +61,11 @@ private static void AlphaBlend(JxlImage3 input, int c, float backgroundLi } } + /// + /// Flattens an owned comparison copy over a constant background without changing its source bundle. + /// + /// The constant linear-light background component. + /// The mutable comparison copy. private static void AlphaBlend(float backgroundLinear, JxlImageBundle image) { if (!image.ContainsAlpha) @@ -58,18 +79,38 @@ private static void AlphaBlend(float backgroundLinear, JxlImageBundle image) } } + /// + /// Prepares a reference decomposition and writes the comparison into already allocated result storage. + /// + /// The configuration that owns comparison storage. + /// The borrowed reference samples. + /// The borrowed distorted samples. + /// The owner that retains the reference decomposition. + /// The result plane, allocated at the reference dimensions. + /// The computed perceptual score. private static void ComputeScoreImpl( Configuration configuration, JxlImageBundle rgb0, JxlImageBundle rgb1, JxlComparator comparator, - JxlPlane? diffMap, + JxlPlane diffMap, out float score) { comparator.SetReferenceImage(configuration, rgb0); comparator.CompareWith(configuration, rgb1, diffMap, out score); } + /// + /// Computes a perceptual score, retaining the larger black- or white-background error when alpha is included. + /// + /// The configuration that owns temporary comparison copies and maps. + /// The borrowed reference bundle, which remains unchanged. + /// The borrowed distorted bundle, which remains unchanged. + /// The reusable comparison owner. This operation replaces its reference. + /// The color conversion provider for the source encodings. + /// The computed perceptual score. + /// The owner that receives the result plane, or null when only the score is needed. + /// Whether to compare color samples without background blending. public static void ComputeScore( Configuration configuration, JxlImageBundle rgb0, @@ -97,19 +138,17 @@ public static void ComputeScore( if (ignoreAlpha || (!rgb0.ContainsAlpha && !rgb1.ContainsAlpha)) { - ComputeScoreImpl( - configuration, - linearSrgb0, - linearSrgb1, - comparator, - diffMap, - out score); + // This standalone operation owns its result. Search callers instead retain their result map across comparisons. + using JxlPlane result = new(configuration, rgb0.XSize, rgb0.YSize); + ComputeScoreImpl(configuration, linearSrgb0, linearSrgb1, comparator, result, out score); + + diffMap?.Swap(result); // No background comparison is required when opacity is absent or deliberately ignored. return; } - using JxlPlane diffMapBlack = new(); + using JxlPlane diffMapBlack = new(configuration, rgb0.XSize, rgb0.YSize); float distBlack; { const float black = 0.0f; @@ -120,16 +159,10 @@ public static void ComputeScore( AlphaBlend(black, blendedBlack0); AlphaBlend(black, blendedBlack1); - ComputeScoreImpl( - configuration, - blendedBlack0, - blendedBlack1, - comparator, - diffMapBlack, - out distBlack); + ComputeScoreImpl(configuration, blendedBlack0, blendedBlack1, comparator, diffMapBlack, out distBlack); } - using JxlPlane diffMapWhite = new(); + using JxlPlane diffMapWhite = new(configuration, rgb0.XSize, rgb0.YSize); float distWhite; { const float white = 1.0f; @@ -140,13 +173,7 @@ public static void ComputeScore( AlphaBlend(white, blendedWhite0); AlphaBlend(white, blendedWhite1); - ComputeScoreImpl( - configuration, - blendedWhite0, - blendedWhite1, - comparator, - diffMapWhite, - out distWhite); + ComputeScoreImpl(configuration, blendedWhite0, blendedWhite1, comparator, diffMapWhite, out distWhite); } if (diffMap is not null) @@ -155,13 +182,16 @@ public static void ComputeScore( int ySize = rgb0.YSize; using JxlPlane outputDiffMap = new(configuration, xSize, ySize); + ReadOnlySpan blackSamples = diffMapBlack.GetRow(0); + ReadOnlySpan whiteSamples = diffMapWhite.GetRow(0); + Span outputSamples = outputDiffMap.GetRow(0); for (int y = 0; y < ySize; y++) { - // GetRow also exposes following rows. Combine only the current logical row. - ReadOnlySpan rowBlack = diffMapBlack.GetRow(y)[..xSize]; - ReadOnlySpan rowWhite = diffMapWhite.GetRow(y)[..xSize]; - Span rowOut = outputDiffMap.GetRow(y)[..xSize]; + // Select logical rows from the spans borrowed before traversal. Each plane can have a different padded stride. + ReadOnlySpan rowBlack = blackSamples.Slice(y * diffMapBlack.PixelsPerRow, xSize); + ReadOnlySpan rowWhite = whiteSamples.Slice(y * diffMapWhite.PixelsPerRow, xSize); + Span rowOut = outputSamples.Slice(y * outputDiffMap.PixelsPerRow, xSize); TensorPrimitives.Max(rowBlack, rowWhite, rowOut); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlDotDetectionUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlDotDetectionUtils.cs index 43783c7e91..22cfe72e60 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlDotDetectionUtils.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/DotDetection/JxlDotDetectionUtils.cs @@ -539,17 +539,17 @@ public static bool ExtractComponent(Rectangle rect, JxlPlane img, List AdaptiveQuantizationMap( } } - private static JxlPlane TileDistMap(Configuration configuration, JxlPlane distMap, int tileSize, int margin, JxlAcStrategyImage acStrategy) + /// + /// Writes one perceptual error per transform block into reusable search storage. + /// + /// The full-resolution perceptual differences. + /// The pixel width and height of one quantization block. + /// The additional sampled pixels around each transform. + /// The selected transforms and their covered blocks. + /// The preallocated per-block result. + private static void TileDistMap(JxlPlane distMap, int tileSize, int margin, JxlAcStrategyImage acStrategy, JxlPlane tileDistMap) { - int tileXSize = (distMap.XSize + tileSize - 1) / tileSize; - int tileYSize = (distMap.YSize + tileSize - 1) / tileSize; - - JxlPlane tileDistMap = new(configuration, tileXSize, tileYSize); + int tileXSize = tileDistMap.XSize; + int tileYSize = tileDistMap.YSize; + ReadOnlySpan differences = distMap.GetRow(0); + Span tileDifferences = tileDistMap.GetRow(0); + ReadOnlySpan strategies = acStrategy.GetRows(); const float borderMul = 0.98f; const float cornerMul = 0.7f; @@ -633,8 +643,7 @@ private static JxlPlane TileDistMap(Configuration configuration, JxlPlane for (int tileY = 0; tileY < tileYSize; tileY++) { - JxlAcStrategyRow acStrategyRow = acStrategy.GetRow(tileY); - Span distRow = tileDistMap.GetRow(tileY); + JxlAcStrategyRow acStrategyRow = new(strategies.Slice(tileY * acStrategy.PixelsPerRow, acStrategy.XSize)); for (int tileX = 0; tileX < tileXSize; tileX++) { @@ -666,7 +675,7 @@ private static JxlPlane TileDistMap(Configuration configuration, JxlPlane yMul = borderMul; } - ReadOnlySpan row = distMap.GetRow(y); + ReadOnlySpan row = differences[(y * distMap.PixelsPerRow)..]; for (int x = xBegin; x < xEnd; x++) { @@ -703,19 +712,13 @@ private static JxlPlane TileDistMap(Configuration configuration, JxlPlane float tileDist = tileNorm * MathF.Pow((float)(distNorm / pixels), 1.0f / 16.0f); - distRow[tileX] = tileDist; - + // All blocks of a multi-block transform share its error. Slice the existing result span rather than reacquiring row memory. for (int iy = 0; iy < acs.CoveredBlocksY; iy++) { - for (int ix = 0; ix < acs.CoveredBlocksX; ix++) - { - tileDistMap.GetRow(tileY + iy)[tileX + ix] = tileDist; - } + tileDifferences.Slice(((tileY + iy) * tileDistMap.PixelsPerRow) + tileX, acs.CoveredBlocksX).Fill(tileDist); } } } - - return tileDistMap; } private static JxlImageBundle RoundtripImage( @@ -830,6 +833,8 @@ public static float InitialQuantDc(float butteraugliTarget) public static bool AdjustQuantField(JxlAcStrategyImage acStrategy, Rectangle rect, float butteraugliTarget, JxlPlane quantField) { int stride = quantField.PixelsPerRow; + ReadOnlySpan strategies = acStrategy.GetRows(); + Span quantizationSamples = quantField.GetRow(0); float meanMaxMixer = 1.0f; { const float limit = 1.54138f; @@ -849,8 +854,8 @@ public static bool AdjustQuantField(JxlAcStrategyImage acStrategy, Rectangle rec for (int y = 0; y < rect.Height; y++) { - JxlAcStrategyRow acStrategyRow = acStrategy.GetRow(in rect, y); - Span quantRow = quantField.GetRow(rect, y); + JxlAcStrategyRow acStrategyRow = new(strategies.Slice(((rect.Top + y) * acStrategy.PixelsPerRow) + rect.Left, rect.Width)); + Span quantRow = quantizationSamples[(((rect.Top + y) * stride) + rect.Left)..]; for (int x = 0; x < rect.Width; x++) { @@ -869,9 +874,10 @@ public static bool AdjustQuantField(JxlAcStrategyImage acStrategy, Rectangle rec float max = quantRow[x]; float mean = 0f; - for (int iy = 0; iy < acs.CoveredBlocksY; y++) + // Each transform owns a rectangular set of quantization blocks. Visit that set without advancing the outer block coordinates. + for (int iy = 0; iy < acs.CoveredBlocksY; iy++) { - for (int ix = 0; ix < acs.CoveredBlocksX; x++) + for (int ix = 0; ix < acs.CoveredBlocksX; ix++) { mean += quantRow[x + ix + (iy * stride)]; max = MathF.Max(quantRow[x + ix + (iy * stride)], max); @@ -888,10 +894,7 @@ public static bool AdjustQuantField(JxlAcStrategyImage acStrategy, Rectangle rec for (int iy = 0; iy < acs.CoveredBlocksY; iy++) { - for (int ix = 0; ix < acs.CoveredBlocksX; ix++) - { - quantRow[x + ix + (iy * stride)] = max; - } + quantRow.Slice(x + (iy * stride), acs.CoveredBlocksX).Fill(max); } } } @@ -974,18 +977,25 @@ public static bool FindBestQuantization( iters = MaxButteraugliIterations; } - JxlPlane tileDistmap = new(); + // The search keeps these allocations through every attempt. Each attempt overwrites the result maps and reuses the median scratch. + int tileSize = JxlFrameDimensions.BlockDimensions * cparams.Resampling; + using JxlPlane tileDistmap = new(configuration, (linear.XSize + tileSize - 1) / tileSize, (linear.YSize + tileSize - 1) / tileSize); + using JxlPlane diffmap = new(configuration, linear.XSize, linear.YSize); + using IMemoryOwner quantizationScratch = configuration.MemoryAllocator.Allocate(quantField.XSize * quantField.YSize); + Span scratch = quantizationScratch.Memory.Span; + Span quantizationSamples = quantField.GetRow(0); + ReadOnlySpan initialSamples = initialQuantField.GetRow(0); + ReadOnlySpan tileSamples = tileDistmap.GetRow(0); for (int i = 0; i < iters + 1; i++) { - if (!quantizer.SetQuantField(configuration, initialQuantDc, quantField, rawQuantField)) + if (!quantizer.SetQuantField(initialQuantDc, quantField, rawQuantField, scratch)) { return false; } - JxlImageBundle decLinear = RoundtripImage(configuration, frameHeader, opsin, encState, cms); + using JxlImageBundle decLinear = RoundtripImage(configuration, frameHeader, opsin, encState, cms); - JxlPlane diffmap = new(); comparator.CompareWith(configuration, decLinear, diffmap, out float score); if (!lowerIsBetter) @@ -994,7 +1004,7 @@ public static bool FindBestQuantization( ScoreImage(-1.0f, diffmap); } - tileDistmap = TileDistMap(configuration, diffmap, 8 * cparams.Resampling, 0, encState.Shared.AcStrategy); + TileDistMap(diffmap, tileSize, 0, encState.Shared.AcStrategy, tileDistmap); auxOut.NumberOfButteraugliIterations++; if (i == iters) @@ -1012,8 +1022,8 @@ public static bool FindBestQuantization( for (int y = 0; y < quantField.YSize; y++) { - Span rowQ = quantField.GetRow(y); - Span rowInit = initialQuantField.GetRow(y); + Span rowQ = quantizationSamples[(y * quantField.PixelsPerRow)..]; + ReadOnlySpan rowInit = initialSamples[(y * initialQuantField.PixelsPerRow)..]; for (int x = 0; x < quantField.XSize; x++) { @@ -1053,8 +1063,8 @@ public static bool FindBestQuantization( { for (int y = 0; y < quantField.YSize; y++) { - Span rowDist = tileDistmap.GetRow(y); - Span rowQ = quantField.GetRow(y); + ReadOnlySpan rowDist = tileSamples[(y * tileDistmap.PixelsPerRow)..]; + Span rowQ = quantizationSamples[(y * quantField.PixelsPerRow)..]; for (int x = 0; x < quantField.XSize; x++) { @@ -1088,8 +1098,8 @@ public static bool FindBestQuantization( { for (int y = 0; y < quantField.YSize; y++) { - Span rowDist = tileDistmap.GetRow(y); - Span rowQ = quantField.GetRow(y); + ReadOnlySpan rowDist = tileSamples[(y * tileDistmap.PixelsPerRow)..]; + Span rowQ = quantizationSamples[(y * quantField.PixelsPerRow)..]; for (int x = 0; x < quantField.XSize; x++) { @@ -1127,7 +1137,7 @@ public static bool FindBestQuantization( } } - if (!quantizer.SetQuantField(configuration, initialQuantDc, quantField, rawQuantField)) + if (!quantizer.SetQuantField(initialQuantDc, quantField, rawQuantField, scratch)) { return false; } @@ -1160,18 +1170,31 @@ public static bool FindBestQuantizationMaxError( inverseMaxError[1] = 1.0f / cparams.MaxError[1]; inverseMaxError[2] = 1.0f / cparams.MaxError[2]; + // Median selection mutates only this search-owned copy. Reuse its allocation for every attempt and the final field update. + using IMemoryOwner quantizationScratch = configuration.MemoryAllocator.Allocate(quantField.XSize * quantField.YSize); + Span scratch = quantizationScratch.Memory.Span; + Span quantizationSamples = quantField.GetRow(0); + ReadOnlySpan strategies = encState.Shared.AcStrategy.GetRows(); + ReadOnlySpan source0 = opsin.Plane(0).GetRow(0); + ReadOnlySpan source1 = opsin.Plane(1).GetRow(0); + ReadOnlySpan source2 = opsin.Plane(2).GetRow(0); + for (int i = 0; i < MaxButteraugliIterations + 1; i++) { - if (!quantizer.SetQuantField(configuration, initialQuantDc, quantField, rawQuantField)) + if (!quantizer.SetQuantField(initialQuantDc, quantField, rawQuantField, scratch)) { return false; } - JxlImageBundle decoded = RoundtripImage(configuration, frameHeader, opsin, encState, cms); + using JxlImageBundle decoded = RoundtripImage(configuration, frameHeader, opsin, encState, cms); + JxlImage3 reconstructed = decoded.Color!; + ReadOnlySpan decoded0 = reconstructed.Plane(0).GetRow(0); + ReadOnlySpan decoded1 = reconstructed.Plane(1).GetRow(0); + ReadOnlySpan decoded2 = reconstructed.Plane(2).GetRow(0); for (int by = 0; by < encState.Shared.FrameDimensions.YSizeBlocks; by++) { - JxlAcStrategyRow acStrategyRow = encState.Shared.AcStrategy.GetRow(by); + JxlAcStrategyRow acStrategyRow = new(strategies.Slice(by * encState.Shared.AcStrategy.PixelsPerRow, encState.Shared.AcStrategy.XSize)); for (int bx = 0; bx < encState.Shared.FrameDimensions.XSizeBlocks; bx++) { @@ -1186,6 +1209,10 @@ public static bool FindBestQuantizationMaxError( for (int c = 0; c < 3; c++) { + // Borrow all planes once per image. Channel selection changes spans, not memory ownership or row acquisition. + ReadOnlySpan sourceSamples = c == 0 ? source0 : c == 1 ? source1 : source2; + ReadOnlySpan decodedSamples = c == 0 ? decoded0 : c == 1 ? decoded1 : decoded2; + for (int y = by * JxlFrameDimensions.BlockDimensions; y < (by + acs.CoveredBlocksY) * JxlFrameDimensions.BlockDimensions; y++) { if (y >= decoded.YSize) @@ -1193,8 +1220,8 @@ public static bool FindBestQuantizationMaxError( continue; } - Span inRow = opsin.PlaneRow(c, y); - Span decRow = decoded.Color!.PlaneRow(c, y); + ReadOnlySpan inRow = sourceSamples[(y * opsin.PixelsPerRow)..]; + ReadOnlySpan decRow = decodedSamples[(y * reconstructed.PixelsPerRow)..]; for (int x = bx * JxlFrameDimensions.BlockDimensions; x < (bx + acs.CoveredBlocksX) * JxlFrameDimensions.BlockDimensions; x++) { @@ -1222,7 +1249,7 @@ public static bool FindBestQuantizationMaxError( for (int qy = by; qy < by + acs.CoveredBlocksY; qy++) { - Span qfRow = quantField.GetRow(qy); + Span qfRow = quantizationSamples[(qy * quantField.PixelsPerRow)..]; for (int qx = bx; qx < bx + acs.CoveredBlocksX; qx++) { @@ -1233,7 +1260,7 @@ public static bool FindBestQuantizationMaxError( } } - if (!quantizer.SetQuantField(configuration, initialQuantDc, quantField, rawQuantField)) + if (!quantizer.SetQuantField(initialQuantDc, quantField, rawQuantField, scratch)) { return false; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlConvolveSeparable5.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlConvolveSeparable5.cs index 82e2912932..fdbc962dd0 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlConvolveSeparable5.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlConvolveSeparable5.cs @@ -1,28 +1,278 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; +/// +/// Applies a separable five-tap kernel during one traversal of an output row. +/// internal static class JxlConvolveSeparable5 { - public static Vector HorizontalConvolve(ref float pos, Vector wh0, Vector wh1, Vector wh2) + /// + /// Convolves five borrowed source rows without allocating intermediate rows. + /// + /// The vertically mirrored row two positions above the output. + /// The vertically mirrored row one position above the output. + /// The source row at the output position. + /// The vertically mirrored row one position below the output. + /// The vertically mirrored row two positions below the output. + /// The horizontal and vertical weights in four-value groups. + /// The distinct output row. All rows have the rectangle's logical width. + public static void ConvolveRow( + ReadOnlySpan top2, + ReadOnlySpan top1, + ReadOnlySpan middle, + ReadOnlySpan bottom1, + ReadOnlySpan bottom2, + in JxlWeightsSeparable5 weights, + Span output) { - Vector c = Vector.LoadUnsafe(ref pos); - Vector mul0 = c * wh0; + float horizontal0 = weights.Horizontal[0]; + float horizontal1 = weights.Horizontal[4]; + float horizontal2 = weights.Horizontal[8]; + float vertical0 = weights.Vertical[0]; + float vertical1 = weights.Vertical[4]; + float vertical2 = weights.Vertical[8]; + int width = output.Length; + int x = 0; - Vector l1 = Vector.LoadUnsafe(ref Unsafe.Subtract(ref pos, 1)); - Vector r1 = Vector.LoadUnsafe(ref Unsafe.Add(ref pos, 1)); - Vector l2 = Vector.LoadUnsafe(ref Unsafe.Subtract(ref pos, 2)); - Vector r2 = Vector.LoadUnsafe(ref Unsafe.Add(ref pos, 2)); + // The first two samples need reflected horizontal neighbors. This also handles rows narrower than the kernel. + for (; x < Math.Min(2, width); x++) + { + output[x] = ConvolvePixel(top2, top1, middle, bottom1, bottom2, x, horizontal0, horizontal1, horizontal2, vertical0, vertical1, vertical2); + } - Vector sum1 = l1 + r1; - Vector mul1 = Vector.FusedMultiplyAdd(sum1, wh1, mul0); - Vector sum2 = l2 + r2; - Vector mul2 = Vector.FusedMultiplyAdd(sum2, wh2, mul1); + ref float top2Ref = ref MemoryMarshal.GetReference(top2); + ref float top1Ref = ref MemoryMarshal.GetReference(top1); + ref float middleRef = ref MemoryMarshal.GetReference(middle); + ref float bottom1Ref = ref MemoryMarshal.GetReference(bottom1); + ref float bottom2Ref = ref MemoryMarshal.GetReference(bottom2); + ref float outputRef = ref MemoryMarshal.GetReference(output); - return mul2; + // Each lane produces one adjacent output sample. Complete blocks need two real samples on either side. + // Neighbor loads are unaligned. Their bounds exclude row padding, so the final block never reads past logical samples. + if (Vector512.IsHardwareAccelerated) + { + Vector512 horizontal0Vector = Vector512.Create(horizontal0); + Vector512 horizontal1Vector = Vector512.Create(horizontal1); + Vector512 horizontal2Vector = Vector512.Create(horizontal2); + Vector512 vertical0Vector = Vector512.Create(vertical0); + Vector512 vertical1Vector = Vector512.Create(vertical1); + Vector512 vertical2Vector = Vector512.Create(vertical2); + + for (; x + Vector512.Count + 2 <= width; x += Vector512.Count) + { + Vector512 center = HorizontalConvolve(ref Unsafe.Add(ref middleRef, x), horizontal0Vector, horizontal1Vector, horizontal2Vector); + Vector512 top = HorizontalConvolve(ref Unsafe.Add(ref top1Ref, x), horizontal0Vector, horizontal1Vector, horizontal2Vector); + Vector512 bottom = HorizontalConvolve(ref Unsafe.Add(ref bottom1Ref, x), horizontal0Vector, horizontal1Vector, horizontal2Vector); + Vector512 result = Vector512.MultiplyAddEstimate(top + bottom, vertical1Vector, center * vertical0Vector); + top = HorizontalConvolve(ref Unsafe.Add(ref top2Ref, x), horizontal0Vector, horizontal1Vector, horizontal2Vector); + bottom = HorizontalConvolve(ref Unsafe.Add(ref bottom2Ref, x), horizontal0Vector, horizontal1Vector, horizontal2Vector); + result = Vector512.MultiplyAddEstimate(top + bottom, vertical2Vector, result); + result.StoreUnsafe(ref outputRef, (nuint)x); + } + } + + if (Vector256.IsHardwareAccelerated) + { + Vector256 horizontal0Vector = Vector256.Create(horizontal0); + Vector256 horizontal1Vector = Vector256.Create(horizontal1); + Vector256 horizontal2Vector = Vector256.Create(horizontal2); + Vector256 vertical0Vector = Vector256.Create(vertical0); + Vector256 vertical1Vector = Vector256.Create(vertical1); + Vector256 vertical2Vector = Vector256.Create(vertical2); + + for (; x + Vector256.Count + 2 <= width; x += Vector256.Count) + { + Vector256 center = HorizontalConvolve(ref Unsafe.Add(ref middleRef, x), horizontal0Vector, horizontal1Vector, horizontal2Vector); + Vector256 top = HorizontalConvolve(ref Unsafe.Add(ref top1Ref, x), horizontal0Vector, horizontal1Vector, horizontal2Vector); + Vector256 bottom = HorizontalConvolve(ref Unsafe.Add(ref bottom1Ref, x), horizontal0Vector, horizontal1Vector, horizontal2Vector); + Vector256 result = Vector256.MultiplyAddEstimate(top + bottom, vertical1Vector, center * vertical0Vector); + top = HorizontalConvolve(ref Unsafe.Add(ref top2Ref, x), horizontal0Vector, horizontal1Vector, horizontal2Vector); + bottom = HorizontalConvolve(ref Unsafe.Add(ref bottom2Ref, x), horizontal0Vector, horizontal1Vector, horizontal2Vector); + result = Vector256.MultiplyAddEstimate(top + bottom, vertical2Vector, result); + result.StoreUnsafe(ref outputRef, (nuint)x); + } + } + + if (Vector128.IsHardwareAccelerated) + { + Vector128 horizontal0Vector = Vector128.Create(horizontal0); + Vector128 horizontal1Vector = Vector128.Create(horizontal1); + Vector128 horizontal2Vector = Vector128.Create(horizontal2); + Vector128 vertical0Vector = Vector128.Create(vertical0); + Vector128 vertical1Vector = Vector128.Create(vertical1); + Vector128 vertical2Vector = Vector128.Create(vertical2); + + for (; x + Vector128.Count + 2 <= width; x += Vector128.Count) + { + Vector128 center = HorizontalConvolve(ref Unsafe.Add(ref middleRef, x), horizontal0Vector, horizontal1Vector, horizontal2Vector); + Vector128 top = HorizontalConvolve(ref Unsafe.Add(ref top1Ref, x), horizontal0Vector, horizontal1Vector, horizontal2Vector); + Vector128 bottom = HorizontalConvolve(ref Unsafe.Add(ref bottom1Ref, x), horizontal0Vector, horizontal1Vector, horizontal2Vector); + Vector128 result = Vector128.MultiplyAddEstimate(top + bottom, vertical1Vector, center * vertical0Vector); + top = HorizontalConvolve(ref Unsafe.Add(ref top2Ref, x), horizontal0Vector, horizontal1Vector, horizontal2Vector); + bottom = HorizontalConvolve(ref Unsafe.Add(ref bottom2Ref, x), horizontal0Vector, horizontal1Vector, horizontal2Vector); + result = Vector128.MultiplyAddEstimate(top + bottom, vertical2Vector, result); + result.StoreUnsafe(ref outputRef, (nuint)x); + } + } + + // Narrower SIMD blocks finish complete interior groups. Reflect the final scalar samples and right border. + for (; x < width; x++) + { + output[x] = ConvolvePixel(top2, top1, middle, bottom1, bottom2, x, horizontal0, horizontal1, horizontal2, vertical0, vertical1, vertical2); + } + } + + /// + /// Convolves one output sample with reflected neighbors and the same row grouping as the SIMD path. + /// + /// The vertically mirrored row two positions above the output. + /// The vertically mirrored row one position above the output. + /// The row at the output position. + /// The vertically mirrored row one position below the output. + /// The vertically mirrored row two positions below the output. + /// The horizontal output position. + /// The horizontal center weight. + /// The horizontal distance-one weight. + /// The horizontal distance-two weight. + /// The vertical center weight. + /// The vertical distance-one weight. + /// The vertical distance-two weight. + /// The weighted output sample. + private static float ConvolvePixel( + ReadOnlySpan top2, + ReadOnlySpan top1, + ReadOnlySpan middle, + ReadOnlySpan bottom1, + ReadOnlySpan bottom2, + int x, + float horizontal0, + float horizontal1, + float horizontal2, + float vertical0, + float vertical1, + float vertical2) + { + // Reflect at the edge sample: -1 maps to 0, -2 maps to 1. Mirror also handles repeated reflections at width one. + // All five rows use the same neighbor indices, so compute them once per border sample. + int left1 = JxlImageOperations.Mirror(x - 1, middle.Length); + int left2 = JxlImageOperations.Mirror(x - 2, middle.Length); + int right1 = JxlImageOperations.Mirror((long)x + 1, middle.Length); + int right2 = JxlImageOperations.Mirror((long)x + 2, middle.Length); + float center = HorizontalConvolve(middle, x, left1, left2, right1, right2, horizontal0, horizontal1, horizontal2); + float top = HorizontalConvolve(top1, x, left1, left2, right1, right2, horizontal0, horizontal1, horizontal2); + float bottom = HorizontalConvolve(bottom1, x, left1, left2, right1, right2, horizontal0, horizontal1, horizontal2); + float result = float.MultiplyAddEstimate(top + bottom, vertical1, center * vertical0); + top = HorizontalConvolve(top2, x, left1, left2, right1, right2, horizontal0, horizontal1, horizontal2); + bottom = HorizontalConvolve(bottom2, x, left1, left2, right1, right2, horizontal0, horizontal1, horizontal2); + return float.MultiplyAddEstimate(top + bottom, vertical2, result); + } + + /// + /// Convolves one horizontal sample using reflected neighbor indices. + /// + /// The borrowed logical row. + /// The center index. + /// The reflected distance-one index to the left. + /// The reflected distance-two index to the left. + /// The reflected distance-one index to the right. + /// The reflected distance-two index to the right. + /// The center weight. + /// The distance-one weight. + /// The distance-two weight. + /// The horizontal weighted sum. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float HorizontalConvolve( + ReadOnlySpan row, + int x, + int left1, + int left2, + int right1, + int right2, + float weight0, + float weight1, + float weight2) + { + float result = float.MultiplyAddEstimate(row[left1] + row[right1], weight1, row[x] * weight0); + return float.MultiplyAddEstimate(row[left2] + row[right2], weight2, result); + } + + /// + /// Convolves 4 adjacent horizontal samples with two valid neighbors on each side. + /// + /// The first center sample. Unaligned loads can read two samples before this reference. + /// The broadcast center weight. + /// The broadcast distance-one weight. + /// The broadcast distance-two weight. + /// One horizontal weighted sum per lane. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 HorizontalConvolve( + ref float position, + Vector128 weight0, + Vector128 weight1, + Vector128 weight2) + { + Vector128 center = Vector128.LoadUnsafe(ref position); + Vector128 left1 = Vector128.LoadUnsafe(ref Unsafe.Subtract(ref position, 1)); + Vector128 right1 = Vector128.LoadUnsafe(ref Unsafe.Add(ref position, 1)); + Vector128 result = Vector128.MultiplyAddEstimate(left1 + right1, weight1, center * weight0); + Vector128 left2 = Vector128.LoadUnsafe(ref Unsafe.Subtract(ref position, 2)); + Vector128 right2 = Vector128.LoadUnsafe(ref Unsafe.Add(ref position, 2)); + return Vector128.MultiplyAddEstimate(left2 + right2, weight2, result); + } + + /// + /// Convolves 8 adjacent horizontal samples with two valid neighbors on each side. + /// + /// The first center sample. Unaligned loads can read two samples before this reference. + /// The broadcast center weight. + /// The broadcast distance-one weight. + /// The broadcast distance-two weight. + /// One horizontal weighted sum per lane. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 HorizontalConvolve( + ref float position, + Vector256 weight0, + Vector256 weight1, + Vector256 weight2) + { + Vector256 center = Vector256.LoadUnsafe(ref position); + Vector256 left1 = Vector256.LoadUnsafe(ref Unsafe.Subtract(ref position, 1)); + Vector256 right1 = Vector256.LoadUnsafe(ref Unsafe.Add(ref position, 1)); + Vector256 result = Vector256.MultiplyAddEstimate(left1 + right1, weight1, center * weight0); + Vector256 left2 = Vector256.LoadUnsafe(ref Unsafe.Subtract(ref position, 2)); + Vector256 right2 = Vector256.LoadUnsafe(ref Unsafe.Add(ref position, 2)); + return Vector256.MultiplyAddEstimate(left2 + right2, weight2, result); + } + + /// + /// Convolves 16 adjacent horizontal samples with two valid neighbors on each side. + /// + /// The first center sample. Unaligned loads can read two samples before this reference. + /// The broadcast center weight. + /// The broadcast distance-one weight. + /// The broadcast distance-two weight. + /// One horizontal weighted sum per lane. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 HorizontalConvolve( + ref float position, + Vector512 weight0, + Vector512 weight1, + Vector512 weight2) + { + Vector512 center = Vector512.LoadUnsafe(ref position); + Vector512 left1 = Vector512.LoadUnsafe(ref Unsafe.Subtract(ref position, 1)); + Vector512 right1 = Vector512.LoadUnsafe(ref Unsafe.Add(ref position, 1)); + Vector512 result = Vector512.MultiplyAddEstimate(left1 + right1, weight1, center * weight0); + Vector512 left2 = Vector512.LoadUnsafe(ref Unsafe.Subtract(ref position, 2)); + Vector512 right2 = Vector512.LoadUnsafe(ref Unsafe.Add(ref position, 2)); + return Vector512.MultiplyAddEstimate(left2 + right2, weight2, result); } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs index 8114bd6bde..95209cdde3 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs @@ -36,7 +36,7 @@ public static bool SameSize(JxlImage3 a, JxlImage3 b) where T : unmanaged => a.XSize == b.XSize && a.YSize == b.YSize; /// - /// Copies everything from one plane to another. + /// Copies logical samples between equally sized planes without changing row padding or a storage prefix. /// /// The type of planes to copy. /// Source plane (read-only) @@ -57,11 +57,13 @@ public static bool CopyImage(JxlPlane from, JxlPlane to) return true; } + // Borrow each allocation once at its logical origin. Source and destination can retain different physical row strides. + ReadOnlySpan source = from.GetRow(0); + Span destination = to.GetRow(0); + for (int y = 0; y < from.YSize; y++) { - Span rowFrom = from.GetRow(y); - Span rowTo = to.GetRow(y); - rowFrom[..from.XSize].CopyTo(rowTo); + source.Slice(y * from.PixelsPerRow, from.XSize).CopyTo(destination.Slice(y * to.PixelsPerRow, to.XSize)); } return true; @@ -84,7 +86,7 @@ public static void ForceCopyImage(JxlPlane from, JxlPlane to) } /// - /// Copies everything from one image to another. + /// Copies logical samples in all three planes without changing row padding or a storage prefix. /// /// The type of image to copy. /// Source image (read-only) @@ -105,17 +107,9 @@ public static bool CopyImage(JxlImage3 from, JxlImage3 to) return true; } - for (int c = 0; c < 3; c++) - { - for (int y = 0; y < from.YSize; y++) - { - Span rowFrom = from.PlaneRow(c, y); - Span rowTo = to.PlaneRow(c, y); - rowFrom[..from.XSize].CopyTo(rowTo); - } - } - - return true; + return CopyImage(from.Plane(0), to.Plane(0)) + && CopyImage(from.Plane(1), to.Plane(1)) + && CopyImage(from.Plane(2), to.Plane(2)); } /// @@ -384,7 +378,7 @@ public static void ScaleImage(T lambda, JxlImage3 image) } /// - /// Fills every value in the plane with . + /// Fills logical samples with without changing row padding or a storage prefix. /// /// The type of the plane. /// The value to fill everything with. @@ -392,15 +386,17 @@ public static void ScaleImage(T lambda, JxlImage3 image) public static void FillImage(T value, JxlPlane image) where T : unmanaged { + // GetRow(0) excludes any prefix. Slice the retained stride so padding remains unchanged after a logical shrink. + Span storage = image.GetRow(0); + for (int y = 0; y < image.YSize; y++) { - Span row = image.GetRow(y); - row[..image.XSize].Fill(value); + storage.Slice(y * image.PixelsPerRow, image.XSize).Fill(value); } } /// - /// Fills every value in every plane of the image with . + /// Fills logical samples in all three planes with , leaving padding unchanged. /// /// The type of the image. /// The value to fill every plane with. @@ -414,7 +410,7 @@ public static void FillImage(T value, JxlImage3 image) } /// - /// Sets every value in the plane to 0. See also . + /// Clears logical samples without changing row padding or a storage prefix. /// /// The type of the plane. /// The image to clear. @@ -426,9 +422,12 @@ public static void ZeroFillImage(JxlPlane image) return; } + Span storage = image.GetRow(0); + + // Only the logical width belongs to this clear. Retained capacity can include padding and unused rows. for (int y = 0; y < image.YSize; y++) { - image.GetRow(y)[..image.XSize].Clear(); + storage.Slice(y * image.PixelsPerRow, image.XSize).Clear(); } } @@ -503,17 +502,16 @@ public static void FillPlane(T value, JxlPlane image, Rectangle rect) } /// - /// Clears every plane within the image. + /// Clears logical samples in all three planes, leaving padding unchanged. /// /// The type of the image /// The image whose all planes will be cleared. public static void ZeroFillImage(JxlImage3 image) where T : unmanaged { - for (int plane = 0; plane < 3; plane++) - { - ZeroFillImage(image.Plane(plane)); - } + ZeroFillImage(image.Plane(0)); + ZeroFillImage(image.Plane(1)); + ZeroFillImage(image.Plane(2)); } private static bool DownsampleImageCore(JxlPlane input, int factor, JxlPlane output) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs index 99307cbc8c..5bc0fb04d3 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs @@ -6,6 +6,8 @@ using System.Runtime.InteropServices; using System.Runtime.Intrinsics; using SixLabors.ImageSharp.Formats.Jxl.Memory; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; @@ -15,6 +17,47 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing; /// internal static class JxlConvolve { + /// + /// Applies a symmetric separable five-by-five kernel in one traversal without intermediate sample allocations. + /// + /// The borrowed source plane. + /// The source rectangle. Its horizontal edges define the reflected row bounds. + /// The symmetric horizontal and vertical weights in four-value groups. + /// The distinct output plane, with logical dimensions equal to the source rectangle. + /// Whether the output dimensions match the source rectangle. + /// Vertical neighbors reflect at the full input image edges. The caller supplies a rectangle inside the input plane. + public static bool Separable5(JxlPlane input, in Rectangle rectangle, in JxlWeightsSeparable5 weights, JxlPlane output) + { + if (rectangle.Width != output.XSize || rectangle.Height != output.YSize) + { + return false; + } + + // Borrow once at each logical origin. Storage prefixes and different physical row strides remain outside the operation. + ReadOnlySpan source = input.GetRow(0); + Span destination = output.GetRow(0); + int inputStride = input.PixelsPerRow; + int outputStride = output.PixelsPerRow; + + for (int y = 0; y < rectangle.Height; y++) + { + int sourceY = rectangle.Y + y; + + // Reflect at image edges. Mirror also handles repeated reflections for images only one to four rows high. + ReadOnlySpan top2 = source.Slice((JxlImageOperations.Mirror(sourceY - 2, input.YSize) * inputStride) + rectangle.X, rectangle.Width); + ReadOnlySpan top1 = source.Slice((JxlImageOperations.Mirror(sourceY - 1, input.YSize) * inputStride) + rectangle.X, rectangle.Width); + ReadOnlySpan middle = source.Slice((sourceY * inputStride) + rectangle.X, rectangle.Width); + ReadOnlySpan bottom1 = source.Slice((JxlImageOperations.Mirror(sourceY + 1L, input.YSize) * inputStride) + rectangle.X, rectangle.Width); + ReadOnlySpan bottom2 = source.Slice((JxlImageOperations.Mirror(sourceY + 2L, input.YSize) * inputStride) + rectangle.X, rectangle.Width); + Span rowOutput = destination.Slice(y * outputStride, rectangle.Width); + + // Horizontal results stay in registers. Combine the five rows directly into the output without a row cache or another image pass. + JxlConvolveSeparable5.ConvolveRow(top2, top1, middle, bottom1, bottom2, weights, rowOutput); + } + + return true; + } + /// /// Weighted sum of 1x5 pixels around ix, iy with [wx2, wx1, wx0, wx1, wx2]. /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs index d334e9921c..184120f022 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Buffers; using System.Numerics.Tensors; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Fields; @@ -301,10 +300,13 @@ public void ComputeGlobalScaleAndQuant(float dcQuantization, float medianQuantiz /// The destination integer quantization values. public void SetQuantFieldRect(JxlPlane quantizationField, in Rectangle blockBounds, JxlPlane quantizedField) { + ReadOnlySpan source = quantizationField.GetRow(0); + Span destination = quantizedField.GetRow(0); + for (int y = 0; y < blockBounds.Height; y++) { - ReadOnlySpan quantizationRow = quantizationField.GetRow(blockBounds, y); - Span quantizedRow = quantizedField.GetRow(blockBounds, y); + ReadOnlySpan quantizationRow = source[(((blockBounds.Top + y) * quantizationField.PixelsPerRow) + blockBounds.Left)..]; + Span quantizedRow = destination[(((blockBounds.Top + y) * quantizedField.PixelsPerRow) + blockBounds.Left)..]; for (int x = 0; x < blockBounds.Width; x++) { @@ -318,12 +320,12 @@ public void SetQuantFieldRect(JxlPlane quantizationField, in Rectangle bl /// /// Sets the global quantization scale and optionally writes the integer quantization field. /// - /// The configuration that owns temporary sample storage. /// The requested DC quantization scale. /// The source per-block quantization values. /// The destination integer field, or null to update only the global scale. + /// Reusable storage with capacity for every logical sample. It must not overlap either field. /// Whether the destination field has matching dimensions. - public bool SetQuantField(Configuration configuration, float dcQuantization, JxlPlane quantizationField, JxlPlane? quantizedField) + public bool SetQuantField(float dcQuantization, JxlPlane quantizationField, JxlPlane? quantizedField, Span scratch) { if (quantizedField is not null && quantizedField.GetRectangle() != quantizationField.GetRectangle()) { @@ -333,12 +335,13 @@ public bool SetQuantField(Configuration configuration, float dcQuantization, Jxl // A valid frame supplies one value per DCT block. Copy logical samples only: GetRow also exposes // the plane's padded stride and following rows. Selection must not include that storage or mutate the source field. int sampleCount = quantizationField.XSize * quantizationField.YSize; - using IMemoryOwner samplesOwner = configuration.MemoryAllocator.Allocate(sampleCount); - Span samples = samplesOwner.Memory.Span[..sampleCount]; + Span samples = scratch[..sampleCount]; + ReadOnlySpan source = quantizationField.GetRow(0); for (int y = 0; y < quantizationField.YSize; y++) { - quantizationField.GetRow(y)[..quantizationField.XSize].CopyTo(samples.Slice(y * quantizationField.XSize, quantizationField.XSize)); + ReadOnlySpan row = source.Slice(y * quantizationField.PixelsPerRow, quantizationField.XSize); + row.CopyTo(samples.Slice(y * quantizationField.XSize, quantizationField.XSize)); } float medianQuantization = samples.NthElement(sampleCount / 2); diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs index bdd85769ee..60c27d4f55 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs @@ -3,8 +3,11 @@ using Microsoft.Diagnostics.Runtime.Interop; using SixLabors.ImageSharp.Formats.Jxl.Memory; +using SixLabors.ImageSharp.Formats.Jxl.Processing; using SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using SixLabors.ImageSharp.Tests.TestUtilities; namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; @@ -20,6 +23,159 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; /// public class ButteraugliTests { + /// + /// Checks weighted squared differences, accumulation and reused output across all SIMD widths and scalar tails. + /// + /// The logical width, independent of the padded physical stride. + [Theory] + [InlineData(1)] + [InlineData(3)] + [InlineData(17)] + [InlineData(33)] + public void L2Differences_PreserveWeightsAndAccumulation(int width) + { + // The feature runner invokes this same numerical contract in a process with each selected SIMD capability. + static void RunTest(string serialized) + { + int sampleWidth = FeatureTestRunner.Deserialize(serialized); + const int height = 3; + const float padding = -123F; + using JxlPlane reference = new(TestEnvironment.Configuration, sampleWidth, height); + using JxlPlane distorted = new(TestEnvironment.Configuration, sampleWidth, height); + using JxlPlane difference = new(TestEnvironment.Configuration, sampleWidth, height); + Span referenceSamples = reference.GetRow(0); + Span distortedSamples = distorted.GetRow(0); + Span differenceSamples = difference.GetRow(0); + differenceSamples.Fill(padding); + + for (int y = 0; y < height; y++) + { + for (int x = 0; x < sampleWidth; x++) + { + float value = (x % 5) - 2; + float delta = ((x + (2 * y)) % 7) - 3; + referenceSamples[(y * reference.PixelsPerRow) + x] = value; + distortedSamples[(y * distorted.PixelsPerRow) + x] = value - delta; + } + } + + for (int stage = 0; stage < 3; stage++) + { + // Quarter and half weights keep every expected result exact. The last call must overwrite the accumulated result. + if (stage == 0) + { + Butteraugli.SetL2Diff(reference, distorted, 0.25F, difference); + } + else if (stage == 1) + { + Butteraugli.L2Diff(reference, distorted, 0.5F, difference); + } + else + { + Butteraugli.SetL2Diff(reference, distorted, 0.5F, difference); + } + + float expectedWeight = stage == 0 ? 0.25F : stage == 1 ? 0.75F : 0.5F; + for (int y = 0; y < height; y++) + { + ReadOnlySpan row = differenceSamples.Slice(y * difference.PixelsPerRow, difference.PixelsPerRow); + + for (int x = 0; x < sampleWidth; x++) + { + float delta = ((x + (2 * y)) % 7) - 3; + Assert.Equal(delta * delta * expectedWeight, row[x]); + } + + for (int x = sampleWidth; x < row.Length; x++) + { + Assert.Equal(padding, row[x]); + } + } + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + width, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } + + /// + /// Checks the separable five-tap path's reflected edges, exact sample values and untouched padding. + /// + /// The logical width, including the single-sample reflection case. + [Theory] + [InlineData(1)] + [InlineData(3)] + [InlineData(17)] + [InlineData(33)] + public void Separable5_ReflectsImpulseAtImageEdges(int width) + { + static void RunTest(string serialized) + { + int sampleWidth = FeatureTestRunner.Deserialize(serialized); + const int height = 3; + const float padding = -123F; + using JxlPlane input = new(TestEnvironment.Configuration, sampleWidth, height); + using JxlPlane output = new(TestEnvironment.Configuration, sampleWidth, height); + Span inputSamples = input.GetRow(0); + Span outputSamples = output.GetRow(0); + inputSamples.Clear(); + outputSamples.Fill(padding); + + // Edge and interior impulses have row amplitudes 1, 2 and 3. The interior impulse exercises nonzero SIMD lanes. + for (int y = 0; y < height; y++) + { + inputSamples[y * input.PixelsPerRow] = y + 1; + if (sampleWidth >= 17) + { + inputSamples[(y * input.PixelsPerRow) + (sampleWidth / 2)] = y + 1; + } + } + + JxlWeightsSeparable5 weights = default; + for (int i = 0; i < 4; i++) + { + // The symmetric binomial kernel [1,4,6,4,1]/16 has exact weights and a unit sum. + weights.Horizontal[i] = weights.Vertical[i] = 0.375F; + weights.Horizontal[4 + i] = weights.Vertical[4 + i] = 0.25F; + weights.Horizontal[8 + i] = weights.Vertical[8 + i] = 0.0625F; + } + + Assert.True(JxlConvolve.Separable5(input, new Rectangle(0, 0, sampleWidth, height), weights, output)); + + // Reflected rows produce [23,32,41]/16. The edge impulse distributes as [10,5,1]/16; a single-sample row retains one. + // The separate interior impulse contributes [1,4,6,4,1]/16 without reflection. All expected values remain exact. + ReadOnlySpan vertical = [1.4375F, 2F, 2.5625F]; + for (int y = 0; y < height; y++) + { + ReadOnlySpan row = outputSamples.Slice(y * output.PixelsPerRow, output.PixelsPerRow); + + for (int x = 0; x < sampleWidth; x++) + { + float horizontal = sampleWidth == 1 ? 1F : x == 0 ? 0.625F : x == 1 ? 0.3125F : x == 2 ? 0.0625F : 0F; + if (sampleWidth >= 17) + { + int distance = Math.Abs(x - (sampleWidth / 2)); + horizontal += distance == 0 ? 0.375F : distance == 1 ? 0.25F : distance == 2 ? 0.0625F : 0F; + } + + Assert.Equal(vertical[y] * horizontal, row[x]); + } + + for (int x = sampleWidth; x < row.Length; x++) + { + Assert.Equal(padding, row[x]); + } + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + width, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } + private static JxlImage3 SinglePixelImage(float r, float g, float b) { JxlImage3 img = new(TestEnvironment.Configuration, 1, 1); From c24b00ff4960a1d5883b4489656a6e6e26cbd7ef Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 20:54:32 +1000 Subject: [PATCH 265/328] Correct JPEG XL modular sample and transform ownership --- .../Decoder/Modular/JxlModularFrameDecoder.cs | 377 +++++----- .../Modular/Encoding/JxlModularEncoding.cs | 4 +- .../Processing/Modular/JxlModularChannel.cs | 77 +- .../Jxl/Processing/Modular/JxlModularImage.cs | 92 ++- .../Modular/Transforms/JxlPalette.cs | 664 ++++++++++-------- .../Modular/Transforms/JxlSqueeze.cs | 207 ++++-- .../Modular/Transforms/JxlTransform.cs | 34 +- 7 files changed, 915 insertions(+), 540 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Modular/JxlModularFrameDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Modular/JxlModularFrameDecoder.cs index 16af64c308..3c656fd95c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Modular/JxlModularFrameDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Modular/JxlModularFrameDecoder.cs @@ -17,18 +17,23 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Modular; /// -/// Decodes JPEG-XL images coded in Modular mode. +/// Decodes global modular data and owns its integer sample channels for one frame. /// -/// Global configuration -/// Frame dimensions for the image. -internal sealed class JxlModularFrameDecoder(Configuration configuration, JxlFrameDimensions fd) +/// The configuration used for owned sample allocations. +/// The borrowed parsed frame dimensions. +internal sealed class JxlModularFrameDecoder(Configuration configuration, JxlFrameDimensions frameDimensions) : IDisposable { private const float AlmostZero = 1e-8f; private readonly Configuration configuration = configuration; + + /// + /// The owned global channels. A failed replacement leaves its partial channels local to DecodeGlobalInfo. + /// private JxlModularImage fullImage = new(); - private List globalTransform = []; - private readonly JxlFrameDimensions frameDimensions = fd; + + private readonly List globalTransform = []; + private readonly JxlFrameDimensions frameDimensions = frameDimensions; private bool doColor; private bool haveSomething; private bool useFullImage = true; @@ -246,10 +251,12 @@ public void DecodeQuantizerTable(int requiredSizeX, int requiredSizeY, JxlBitRea for (int c = 0; c < 3; c++) { JxlModularChannel currentChannel = img.Channels[c]; + Span channelSamples = currentChannel.GetRow(0); + int channelStride = currentChannel.Plane.PixelsPerRow; for (int y = 0; y < requiredSizeY; y++) { - Span row = currentChannel.GetRow(y); + Span row = channelSamples[(y * channelStride)..]; for (int x = 0; x < requiredSizeX; x++) { @@ -265,6 +272,12 @@ public void DecodeQuantizerTable(int requiredSizeX, int requiredSizeY, JxlBitRea } } + /// + /// Decodes the global modular channels and replaces this decoder's previous owned image only after success. + /// + /// The global modular bitstream. + /// The parsed frame header that determines channel dimensions and sampling. + /// Whether the existing modular decoder may accept a truncated group. public void DecodeGlobalInfo(JxlBitReader reader, JxlFrameHeader frameHeader, bool allowTruncatedGroup) { bool decodeColor = frameHeader.Encoding == JxlFrameEncoding.Modular; @@ -304,12 +317,15 @@ public void DecodeGlobalInfo(JxlBitReader reader, JxlFrameHeader frameHeader, bo { if (hasTree) { - // Trees cannot be larger than the tree size limit. - // The absolute maximum size of a tree is 1 << 22, - // or 4,194,304. - int treeSizeLimit = Math.Min( - 1 << 22, - 1024 + (this.frameDimensions.XSize * this.frameDimensions.YSize * (numberOfChannels + extraChannelCount) / 16)); + const int initialTreeNodes = 1024; + const long samplesPerTreeNode = 16; + + // The format allows 1024 base nodes and one additional node per 16 samples, up to MaxTreeSize. + // At least one color channel exists here. Limit pixels first: larger counts already reach the node cap with one channel. + // This keeps both multiplications within signed 64-bit bounds for int-sized dimensions and channel counts. + long pixels = Math.Min((long)this.frameDimensions.XSize * this.frameDimensions.YSize, JxlMaConstants.MaxTreeSize * samplesPerTreeNode); + long channels = numberOfChannels + (long)extraChannelCount; + int treeSizeLimit = (int)Math.Min(JxlMaConstants.MaxTreeSize, initialTreeNodes + (pixels * channels / samplesPerTreeNode)); // Decoding the actual MA tree now, followed by the ANS histograms. JxlMaDecoder.DecodeTree(this.configuration, reader, this.tree, treeSizeLimit); @@ -371,190 +387,203 @@ public void DecodeGlobalInfo(JxlBitReader reader, JxlFrameHeader frameHeader, bo (int)metadata.BitDepth.BitsPerSample, numberOfChannels + extraChannelCount); - this.allSameShift = true; - - if (frameHeader.ColorTransform == JxlColorTransform.YCbCr) + try { - JxlYCbCrChromaSubsampling subsampling = frameHeader.ChromaSubsampling!; + this.allSameShift = true; - // For Y'Cb'Cr color transforms, we adjust our image - // for chroma subsampling. - for (int c = 0; c < numberOfChannels; c++) + if (frameHeader.ColorTransform == JxlColorTransform.YCbCr) { + JxlYCbCrChromaSubsampling subsampling = frameHeader.ChromaSubsampling!; + + // For Y'Cb'Cr color transforms, we adjust our image + // for chroma subsampling. + for (int c = 0; c < numberOfChannels; c++) + { + // Reference to the channel of the current index. + JxlModularChannel channel = img.Channels[c]; + + // First, we compute the shifts, which are for the + // chroma subsampling and therefore specific to the + // Y'Cb'Cr chroma transform. + channel.HorizontalShift = subsampling.HShift(c); + channel.VerticalShift = subsampling.VShift(c); + + // Then we compute the widths and heights that are + // divided by the shifts. That's because in Y'Cb'Cr + // chroma subsampling, the luma (Y) part is usually + // the same, while the Chroma (Cb, Cr) part is downsampled. + // So the size for the chroma channel is not the same + // as the original image. + // See https://en.wikipedia.org/wiki/Chroma_subsampling + int xSizeShifted = JxlMath.DivCeil(this.frameDimensions.XSize, 1 << channel.HorizontalShift); + int ySizeShifted = JxlMath.DivCeil(this.frameDimensions.YSize, 1 << channel.VerticalShift); + + // The chroma plane should be shrinked down to its downsampled + // size, as part of chroma subsampling. + // + // Note that if this is channel 0, which is a Luma channel, + // xSizeShifted and ySizeShifted will be the same as the channel's + // original width or height. There's no shift as Luma channels + // do not have downsampling; it's the Chroma channels that are + // downsampled. Meaning, this .Shrink() call wouldn't really do + // anything. + channel.Shrink(xSizeShifted, ySizeShifted); + + // Now we check if the shift is the same for all channels. + // If they are not, then we know that the shift isn't + // the same for all channels. + // + // This wouldn't really work for the 0 channel, but that's + // expected as the 0 channel is the Luma channel which isn't + // really downsampled. For channels 1 and 2 which are chroma + // channels, this should work. + if (channel.HorizontalShift != img.Channels[0].HorizontalShift || + channel.VerticalShift != img.Channels[0].VerticalShift) + { + this.allSameShift = false; + } + } + } + + // For the extra channels, we do almost the same as the + // chroma subsampling for Y'Cb'Cr. Except, extra channels have + // their own upsampling matrix, which we use. + // + // The upsampling matrix is a List, which can be + // slow. So we can use a Span wrapper which is more + // lightweight and faster. + Span extraChannelUpsampling = CollectionsMarshal.AsSpan(frameHeader.ExtraChannelUpsampling); + + // We have two loop variables. + // + // 'ec', short for Extra Channel, is the index of the extra channel. + // We need this to access the upsampling matrix which is specific for extra channels. + // + // 'c', on the other hand, just means channel. It is initially set to + // numberOfChannels, because that's where the first extra channel starts. + // + // Suppose our image is coded using RGB and we have two extra channels, one + // alpha channel and one black channel. In such a case: + // - numberOfChannels would be 3. We have 3 channels - R, G, and B. + // - That also means the first index of the extra channel is the last index + // of the RGB channel (2), plus 1. So that's 3. + // + // From then on, 'c' would keep incrementing, to access a generic list of channels. + for (int ec = 0, c = numberOfChannels; ec < extraChannelCount; ec++, c++) + { + // Upsampling for this specific extra channel + int upsampling = (int)extraChannelUpsampling[ec]; + // Reference to the channel of the current index. JxlModularChannel channel = img.Channels[c]; - // First, we compute the shifts, which are for the - // chroma subsampling and therefore specific to the - // Y'Cb'Cr chroma transform. - channel.HorizontalShift = subsampling.HShift(c); - channel.VerticalShift = subsampling.VShift(c); - - // Then we compute the widths and heights that are - // divided by the shifts. That's because in Y'Cb'Cr - // chroma subsampling, the luma (Y) part is usually - // the same, while the Chroma (Cb, Cr) part is downsampled. - // So the size for the chroma channel is not the same - // as the original image. - // See https://en.wikipedia.org/wiki/Chroma_subsampling - int xSizeShifted = JxlMath.DivCeil(this.frameDimensions.XSize, 1 << channel.HorizontalShift); - int ySizeShifted = JxlMath.DivCeil(this.frameDimensions.YSize, 1 << channel.VerticalShift); - - // The chroma plane should be shrinked down to its downsampled - // size, as part of chroma subsampling. - // - // Note that if this is channel 0, which is a Luma channel, - // xSizeShifted and ySizeShifted will be the same as the channel's - // original width or height. There's no shift as Luma channels - // do not have downsampling; it's the Chroma channels that are - // downsampled. Meaning, this .Shrink() call wouldn't really do - // anything. - channel.Shrink(this.configuration, xSizeShifted, ySizeShifted); - - // Now we check if the shift is the same for all channels. - // If they are not, then we know that the shift isn't - // the same for all channels. - // - // This wouldn't really work for the 0 channel, but that's - // expected as the 0 channel is the Luma channel which isn't - // really downsampled. For channels 1 and 2 which are chroma - // channels, this should work. + // We shrink the channel, so it becomes 'upsampling' times smaller + // than its upsampled size. + channel.Shrink( + JxlMath.DivCeil(this.frameDimensions.XSizeUpsampled, upsampling), + JxlMath.DivCeil(this.frameDimensions.YSizeUpsampled, upsampling)); + + // Then we have to adjust the shifts. + // Both the horizontal shift and the vertical shift are + // equal to Ceil(Log2(upsampling for this channel)) - Ceil(Log2(upsampling for the frame sizes)). + channel.HorizontalShift = channel.VerticalShift = + JxlMath.CeilLog2Nonzero(upsampling) - JxlMath.CeilLog2Nonzero((int)frameHeader.Upsampling); + + // Now we want to make sure whether or not are shifts different + // for each channel. if (channel.HorizontalShift != img.Channels[0].HorizontalShift || channel.VerticalShift != img.Channels[0].VerticalShift) { this.allSameShift = false; } } - } - - // For the extra channels, we do almost the same as the - // chroma subsampling for Y'Cb'Cr. Except, extra channels have - // their own upsampling matrix, which we use. - // - // The upsampling matrix is a List, which can be - // slow. So we can use a Span wrapper which is more - // lightweight and faster. - Span extraChannelUpsampling = CollectionsMarshal.AsSpan(frameHeader.ExtraChannelUpsampling); - - // We have two loop variables. - // - // 'ec', short for Extra Channel, is the index of the extra channel. - // We need this to access the upsampling matrix which is specific for extra channels. - // - // 'c', on the other hand, just means channel. It is initially set to - // numberOfChannels, because that's where the first extra channel starts. - // - // Suppose our image is coded using RGB and we have two extra channels, one - // alpha channel and one black channel. In such a case: - // - numberOfChannels would be 3. We have 3 channels - R, G, and B. - // - That also means the first index of the extra channel is the last index - // of the RGB channel (2), plus 1. So that's 3. - // - // From then on, 'c' would keep incrementing, to access a generic list of channels. - for (int ec = 0, c = numberOfChannels; ec < extraChannelCount; ec++, c++) - { - // Upsampling for this specific extra channel - int upsampling = (int)extraChannelUpsampling[ec]; - - // Reference to the channel of the current index. - JxlModularChannel channel = img.Channels[c]; - // We shrink the channel, so it becomes 'upsampling' times smaller - // than its upsampled size. - channel.Shrink( + // Main Modular decoding logic here. The result is stored + // into img. + JxlModularOptions options = new() + { + // Both the maximum channel size and the group dimension + // is set equal to the group dimension specified in the + // frame dimensions. + MaxChannelSize = this.frameDimensions.GroupDimension, + GroupDimension = this.frameDimensions.GroupDimension + }; + + // Do the actual Modular decode using the JxlModularOptions. + bool decompressedSuccessfully = JxlModularEncoding.ModularGenericDecompress( this.configuration, - JxlMath.DivCeil(this.frameDimensions.XSizeUpsampled, upsampling), - JxlMath.DivCeil(this.frameDimensions.YSizeUpsampled, upsampling)); - - // Then we have to adjust the shifts. - // Both the horizontal shift and the vertical shift are - // equal to Ceil(Log2(upsampling for this channel)) - Ceil(Log2(upsampling for the frame sizes)). - channel.HorizontalShift = channel.VerticalShift = - JxlMath.CeilLog2Nonzero(upsampling) - JxlMath.CeilLog2Nonzero((int)frameHeader.Upsampling); - - // Now we want to make sure whether or not are shifts different - // for each channel. - if (channel.HorizontalShift != img.Channels[0].HorizontalShift || - channel.VerticalShift != img.Channels[0].VerticalShift) + reader, + img, + this.groupHeader, + JxlModularStreamId.Global.GetId(this.frameDimensions), + options, + undoTransforms: false, + this.tree, + this.code, + this.contextMap, + allowTruncatedGroup); + + // The ModularGenericDecompress method returns a boolean indicating + // whether or not has the modular decoding process completed successfully. + // When this is false, we know that the decoding process failed, so + // throw an exception. + if (!decompressedSuccessfully) { - this.allSameShift = false; + throw new InvalidOperationException("Could not decode global Modular info"); } - } - // Main Modular decoding logic here. The result is stored - // into img. - JxlModularOptions options = new() - { - // Both the maximum channel size and the group dimension - // is set equal to the group dimension specified in the - // frame dimensions. - MaxChannelSize = this.frameDimensions.GroupDimension, - GroupDimension = this.frameDimensions.GroupDimension - }; - - // Do the actual Modular decode using the JxlModularOptions. - bool decompressedSuccessfully = JxlModularEncoding.ModularGenericDecompress( - this.configuration, - reader, - img, - this.groupHeader, - JxlModularStreamId.Global.GetId(this.frameDimensions), - options, - undoTransforms: false, - this.tree, - this.code, - this.contextMap, - allowTruncatedGroup); - - // The ModularGenericDecompress method returns a boolean indicating - // whether or not has the modular decoding process completed successfully. - // When this is false, we know that the decoding process failed, so - // throw an exception. - if (!decompressedSuccessfully) - { - throw new InvalidOperationException("Could not decode global Modular info"); - } + // Initially we mark this Modular decoder as empty. + // Then, go through each channel. If each channel is + // within the frame dimensions, then at least one channel is + // therefore usable, so we will mark this Modular + // decoder as not empty. + this.haveSomething = false; + for (int c = 0; c < img.Channels.Count; c++) + { + JxlModularChannel channel = img.Channels[c]; - // Initially we mark this Modular decoder as empty. - // Then, go through each channel. If each channel is - // within the frame dimensions, then at least one channel is - // therefore usable, so we will mark this Modular - // decoder as not empty. - this.haveSomething = false; - for (int c = 0; c < img.Channels.Count; c++) - { - JxlModularChannel channel = img.Channels[c]; + // We want to make sure that the number of channels are >= the number + // of meta channels, and that each dimension fits within group dimensions. + if (c >= img.MetaChannels && + channel.Width <= this.frameDimensions.GroupDimension && + channel.Height <= this.frameDimensions.GroupDimension) + { + this.haveSomething = true; + break; + } + } - // We want to make sure that the number of channels are >= the number - // of meta channels, and that each dimension fits within group dimensions. - if (c >= img.MetaChannels && - channel.Width <= this.frameDimensions.GroupDimension && - channel.Height <= this.frameDimensions.GroupDimension) + // We have to move global transforms to groups, if both of these things happen: + // - Number of channels are < number of meta channels, + // or some channels are larger than group dimensions + // - Chroma subsampling is 4:4:4 or 4:0:0 (if the image uses Y'Cb'Cr), + // there are no extra channels, OR there are extra channels but they + // aren't upsampled - therefore, every channel has the same exact shift. + if (!this.haveSomething && this.allSameShift) { - this.haveSomething = true; - break; + // If we only have one transform and that transform is the Reversible + // Color Transform (RCT), then we set the global transform to the RCT, + // and we remove it from 'img'. + if (img.Transforms.Count == 1 && img.Transforms[0].TransformType == JxlTransformType.Rct) + { + this.globalTransform.Clear(); + this.globalTransform.Add(img.Transforms[0]); + img.Transforms.Clear(); + } } - } - // We have to move global transforms to groups, if both of these things happen: - // - Number of channels are < number of meta channels, - // or some channels are larger than group dimensions - // - Chroma subsampling is 4:4:4 or 4:0:0 (if the image uses Y'Cb'Cr), - // there are no extra channels, OR there are extra channels but they - // aren't upsampled - therefore, every channel has the same exact shift. - if (!this.haveSomething && this.allSameShift) + // Release the previous owned image only after the replacement is complete. No channel storage is borrowed from img. + this.fullImage.Dispose(); + this.fullImage = img; + } + catch { - // If we only have one transform and that transform is the Reversible - // Color Transform (RCT), then we set the global transform to the RCT, - // and we remove it from 'img'. - if (img.Transforms.Count == 1 && img.Transforms[0].TransformType == JxlTransformType.Rct) - { - this.globalTransform = img.Transforms[0]; - img.Transforms.Clear(); - } + // The decoder takes ownership only after global parsing and transform preparation both succeed. + img.Dispose(); + throw; } - - // Finally, set the full image. - this.fullImage = img; } + + /// + public void Dispose() => this.fullImage.Dispose(); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncoding.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncoding.cs index d45e18dce3..6d6f6a0d37 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncoding.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncoding.cs @@ -775,7 +775,7 @@ public static bool ModularDecode( foreach (JxlTransform transform in image.Transforms) { - transform.MetaApply(image); + transform.MetaApply(configuration, image); } if (image.IsInvalid) @@ -956,7 +956,7 @@ public static bool ModularGenericDecompress( if (undoTransforms) { - image.UndoTransforms(header.WeightedHeader); + image.UndoTransforms(configuration, header.WeightedHeader); } if (image.IsInvalid) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularChannel.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularChannel.cs index 6dccb4e9f6..8feea90040 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularChannel.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularChannel.cs @@ -6,10 +6,18 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular; /// -/// A wrapper over for modular operations. +/// Owns one integer sample plane and its modular channel dimensions and shifts. /// internal sealed class JxlModularChannel : IDisposable { + /// + /// Allocates a channel whose logical dimensions initially match its plane allocation. + /// + /// The configuration used for sample allocation. + /// The channel width in samples. + /// The channel height in rows. + /// The horizontal subsampling shift, or -1 for a metachannel. + /// The vertical subsampling shift, or -1 for a metachannel. public JxlModularChannel(Configuration configuration, int width, int height, int horizShift, int vertShift) { this.HorizontalShift = horizShift; @@ -51,33 +59,80 @@ public JxlModularChannel(Configuration configuration, int width, int height, int public int Component { get; set; } = -1; /// - /// Gets or sets the backing plane buffer. + /// Gets the owned plane. Row views and transform inputs borrow this storage. /// - public JxlPlane Plane { get; set; } + public JxlPlane Plane { get; } - public void Shrink(Configuration configuration) + /// + /// Applies reduced channel dimensions before decoding without reallocating or changing the row stride. + /// + /// The metadata transform must only reduce dimensions within this channel's existing plane capacity. + public void Shrink() { - if (this.Plane.XSize == this.Width && this.Plane.YSize == this.Height) + int previousWidth = this.Plane.XSize; + _ = this.Plane.ShrinkTo(this.Width, this.Height); + + // The physical stride stays fixed. Columns removed from the logical width now form padding that SIMD tails can read. + // Existing padding is already zero; clear only the newly exposed columns before decoding writes the remaining samples. + if (this.Width != previousWidth) { - return; + Span samples = this.Plane.GetRow(0); + for (int y = 0; y < this.Height; y++) + { + samples.Slice((y * this.Plane.PixelsPerRow) + this.Width, previousWidth - this.Width).Clear(); + } } - - this.Plane.Dispose(); - this.Plane = new JxlPlane(configuration, this.Width, this.Height); } - public void Shrink(Configuration configuration, int newWidth, int newHeight) + /// + /// Reduces the logical channel dimensions before decoding while retaining its sample allocation. + /// + /// The reduced width within the current plane dimensions. + /// The reduced height within the current plane dimensions. + public void Shrink(int newWidth, int newHeight) { this.Width = newWidth; this.Height = newHeight; - this.Shrink(configuration); + this.Shrink(); } + /// + /// Exchanges owned storage and channel metadata without copying samples. + /// + /// The other owner, which receives this channel's previous storage and metadata. + public void Swap(JxlModularChannel other) + { + this.Plane.Swap(other.Plane); + (this.Width, other.Width) = (other.Width, this.Width); + (this.Height, other.Height) = (other.Height, this.Height); + (this.HorizontalShift, other.HorizontalShift) = (other.HorizontalShift, this.HorizontalShift); + (this.VerticalShift, other.VerticalShift) = (other.VerticalShift, this.VerticalShift); + (this.Component, other.Component) = (other.Component, this.Component); + } + + /// + /// Borrows storage from a row through the end of the plane allocation. + /// + /// The row index. + /// The row, its padding and following rows. public Span GetRow(int y) => this.Plane.GetRow(y); + /// + /// Borrows row storage with a prefix before the row origin. + /// + /// The row index. + /// The prefix length in samples. + /// The remaining allocated samples from the requested position. public Span GetRowMinus(int y, int minus) => this.Plane.GetRowMinus(y, minus); + /// + /// Borrows row storage starting after the row origin. + /// + /// The row index. + /// The offset in samples. + /// The remaining allocated samples from the requested position. public Span GetRowPlus(int y, int plus) => this.Plane.GetRowPlus(y, plus); + /// public void Dispose() => this.Plane.Dispose(); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularImage.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularImage.cs index 9d568cef79..f4175e81b8 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularImage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularImage.cs @@ -6,8 +6,22 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular; +/// +/// Owns the modular channels for one image and the transform metadata applied to them. +/// internal sealed class JxlModularImage : IDisposable { + /// + /// The consumable inverse-transform stack. Parsed header lists retain their own entries. + /// + private List transforms = []; + + /// + /// Initializes image metadata without allocating channel samples. + /// + /// The image width in samples. + /// The image height in rows. + /// The encoded sample precision. public JxlModularImage(int width, int height, int bitDepth) { this.Width = width; @@ -28,14 +42,29 @@ public JxlModularImage() } /// - /// Gets or sets all channels within this image. + /// Gets the owned channels. Removed or replaced channels must be released by the operation that removes them. /// - public List Channels { get; set; } = []; + public List Channels { get; } = []; /// - /// Gets or sets all transforms within this image. + /// Gets the inverse-transform stack, or copies parsed transform metadata without changing the source list or its descriptors. /// - public List Transforms { get; set; } = []; + public List Transforms + { + get => this.transforms; + set + { + // MetaApply can expand an empty squeeze list using this image's channel shapes. The parsed header must retain its encoded defaults. + List replacement = new(value.Count); + foreach (JxlTransform transform in value) + { + replacement.Add(transform.DeepClone()); + } + + // Publish only the complete copy. The source can refer to the current stack; copying must not clear it before reading its entries. + this.transforms = replacement; + } + } /// /// Gets or sets the total number of metachannels in this image. @@ -62,36 +91,66 @@ public JxlModularImage() /// public bool IsInvalid { get; set; } - public bool IsEmpty => this.Channels.Any(x => x.Width > 0 && x.Height > 0); + /// + /// Gets a value indicating whether no channel contains samples. + /// + public bool IsEmpty => !this.Channels.Any(x => x.Width > 0 && x.Height > 0); + /// + /// Applies the recorded inverse transforms to this image's owned channels. + /// + /// The configuration used by transforms that allocate output samples. + /// The weighted-prediction settings. public void UndoTransforms(Configuration configuration, JxlModularHeader wpHeader) { while (this.Transforms.Count > 0) { - JxlTransform transform = this.Transforms.First(); + // Forward transforms compose in list order. Their inverse must consume the final transform first. + JxlTransform transform = this.Transforms[^1]; transform.Inverse(configuration, this, wpHeader); - this.Transforms.RemoveAt(0); + this.Transforms.RemoveAt(this.Transforms.Count - 1); } } + /// + /// Allocates image channels and releases partial allocations if construction fails. + /// + /// The configuration used for channel sample allocations. + /// The image width in samples. + /// The image height in rows. + /// The encoded sample precision. + /// The number of owned channels. + /// The image that owns every completed channel allocation. public static JxlModularImage Create(Configuration configuration, int width, int height, int bitDepth, int channels) { JxlModularImage result = new(width, height, bitDepth); - for (int i = 0; i < channels; i++) + // Reserve references before allocating samples so List.Add cannot strand a completed channel if list growth fails. + result.Channels.EnsureCapacity(channels); + + try { - JxlModularChannel c = new(configuration, width, height, 0, 0); + for (int i = 0; i < channels; i++) + { + JxlModularChannel c = new(configuration, width, height, 0, 0) + { + Component = i + }; - result.Channels.Add(c); - result.Channels[^1].Component = i; - } + result.Channels.Add(c); + } - return result; + return result; + } + catch + { + result.Dispose(); + throw; + } } /// - /// Disposes each channel within this modular image, then sets - /// to an empty list. + /// Releases all owned channel samples and clears the existing channel list for reuse. /// public void Dispose() { @@ -100,6 +159,7 @@ public void Dispose() channel.Dispose(); } - this.Channels = []; + this.Channels.Clear(); + this.Transforms.Clear(); } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs index 92a09bbdcb..473b1ef83c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs @@ -185,6 +185,15 @@ public static int GetPaletteValue(Span palette, int index, int c, int oneRo return palette[(c * oneRow) + index]; } + /// + /// Replaces equal-shaped channels with their index channel and an owned palette metachannel before decoding. + /// + /// The configuration used for palette sample allocation. + /// The image that owns the retained index channel and new palette. + /// The first channel replaced by palette indices. + /// The last channel replaced by palette indices. + /// The number of explicit palette entries. + /// The number of predictive delta entries. public static void MetaPalette(Configuration configuration, JxlModularImage input, int beginC, int endC, int numberOfColors, int numberOfDeltas) { JxlTransform.CheckEqualChannels(input, beginC, endC); @@ -205,15 +214,23 @@ public static void MetaPalette(Configuration configuration, JxlModularImage inpu input.MetaChannels += 2 - nb; } - input.Channels.RemoveRange(beginC + 1, endC - beginC); + // Reserve references before allocating samples. The first selected channel remains as the index plane. + input.Channels.EnsureCapacity(input.Channels.Count + 1); JxlModularChannel ch = new(configuration, numberOfColors + numberOfDeltas, nb, -1, -1); + + for (int c = beginC + 1; c <= endC; c++) + { + input.Channels[c].Dispose(); + } + + input.Channels.RemoveRange(beginC + 1, endC - beginC); input.Channels.Insert(0, ch); } /// /// Decodes palette/indexed images. /// - /// Configuration for parallelism. + /// The configuration used for restored channel samples. /// Input & out images. /// Offset of output channel. /// Number of colors. @@ -253,6 +270,9 @@ public static void InversePalette( throw new InvalidOperationException("Transforms are corrupted"); } + // Each new channel belongs to input as soon as its samples are allocated. A later failure leaves no unattached owner. + input.Channels.EnsureCapacity(input.Channels.Count + nb - 1); + for (int i = 1; i < nb; i++) { JxlModularChannel newChannel = new(configuration, w, h, channel.HorizontalShift, channel.VerticalShift); @@ -262,8 +282,8 @@ public static void InversePalette( JxlModularChannel palette = input.Channels[0]; int oneRow = palette.Plane.PixelsPerRow; - int oneRowImage = channel.Plane.PixelsPerRow; int bitDepth = Math.Min(input.BitDepth, 24); + Span paletteData = palette.GetRow(0); if (w == 0) { @@ -273,10 +293,12 @@ public static void InversePalette( { if (nb == 1) { + Span channel0Samples = channel.GetRow(0); + int channel0Stride = channel.Plane.PixelsPerRow; + for (int y = 0; y < h; y++) { - Span p = channel.GetRow(y); - Span paletteData = palette.GetRow(0); + Span p = channel0Samples[(y * channel0Stride)..]; for (int x = 0; x < w; x++) { @@ -288,11 +310,15 @@ public static void InversePalette( } else if (nb == 2) { + Span channel0Samples = channel.GetRow(0); + int channel0Stride = channel.Plane.PixelsPerRow; + Span channel1Samples = input.Channels[c0 + 1].GetRow(0); + int channel1Stride = input.Channels[c0 + 1].Plane.PixelsPerRow; + for (int y = 0; y < h; y++) { - Span paletteData = palette.GetRow(0); - Span p0 = channel.GetRow(y); - Span p1 = input.Channels[c0 + 1].GetRow(y); + Span p0 = channel0Samples[(y * channel0Stride)..]; + Span p1 = channel1Samples[(y * channel1Stride)..]; for (int x = 0; x < w; x++) { @@ -306,12 +332,18 @@ public static void InversePalette( } else if (nb == 3) { + Span channel0Samples = channel.GetRow(0); + int channel0Stride = channel.Plane.PixelsPerRow; + Span channel1Samples = input.Channels[c0 + 1].GetRow(0); + int channel1Stride = input.Channels[c0 + 1].Plane.PixelsPerRow; + Span channel2Samples = input.Channels[c0 + 2].GetRow(0); + int channel2Stride = input.Channels[c0 + 2].Plane.PixelsPerRow; + for (int y = 0; y < h; y++) { - Span paletteData = palette.GetRow(0); - Span p0 = channel.GetRow(y); - Span p1 = input.Channels[c0 + 1].GetRow(y); - Span p2 = input.Channels[c0 + 2].GetRow(y); + Span p0 = channel0Samples[(y * channel0Stride)..]; + Span p1 = channel1Samples[(y * channel1Stride)..]; + Span p2 = channel2Samples[(y * channel2Stride)..]; for (int x = 0; x < w; x++) { @@ -327,13 +359,21 @@ public static void InversePalette( } else if (nb == 4) { + Span channel0Samples = channel.GetRow(0); + int channel0Stride = channel.Plane.PixelsPerRow; + Span channel1Samples = input.Channels[c0 + 1].GetRow(0); + int channel1Stride = input.Channels[c0 + 1].Plane.PixelsPerRow; + Span channel2Samples = input.Channels[c0 + 2].GetRow(0); + int channel2Stride = input.Channels[c0 + 2].Plane.PixelsPerRow; + Span channel3Samples = input.Channels[c0 + 3].GetRow(0); + int channel3Stride = input.Channels[c0 + 3].Plane.PixelsPerRow; + for (int y = 0; y < h; y++) { - Span paletteData = palette.GetRow(0); - Span p0 = channel.GetRow(y); - Span p1 = input.Channels[c0 + 1].GetRow(y); - Span p2 = input.Channels[c0 + 2].GetRow(y); - Span p3 = input.Channels[c0 + 3].GetRow(y); + Span p0 = channel0Samples[(y * channel0Stride)..]; + Span p1 = channel1Samples[(y * channel1Stride)..]; + Span p2 = channel2Samples[(y * channel2Stride)..]; + Span p3 = channel3Samples[(y * channel3Stride)..]; for (int x = 0; x < w; x++) { @@ -351,14 +391,24 @@ public static void InversePalette( } else if (nb == 5) { + Span channel0Samples = channel.GetRow(0); + int channel0Stride = channel.Plane.PixelsPerRow; + Span channel1Samples = input.Channels[c0 + 1].GetRow(0); + int channel1Stride = input.Channels[c0 + 1].Plane.PixelsPerRow; + Span channel2Samples = input.Channels[c0 + 2].GetRow(0); + int channel2Stride = input.Channels[c0 + 2].Plane.PixelsPerRow; + Span channel3Samples = input.Channels[c0 + 3].GetRow(0); + int channel3Stride = input.Channels[c0 + 3].Plane.PixelsPerRow; + Span channel4Samples = input.Channels[c0 + 4].GetRow(0); + int channel4Stride = input.Channels[c0 + 4].Plane.PixelsPerRow; + for (int y = 0; y < h; y++) { - Span paletteData = palette.GetRow(0); - Span p0 = channel.GetRow(y); - Span p1 = input.Channels[c0 + 1].GetRow(y); - Span p2 = input.Channels[c0 + 2].GetRow(y); - Span p3 = input.Channels[c0 + 3].GetRow(y); - Span p4 = input.Channels[c0 + 4].GetRow(y); + Span p0 = channel0Samples[(y * channel0Stride)..]; + Span p1 = channel1Samples[(y * channel1Stride)..]; + Span p2 = channel2Samples[(y * channel2Stride)..]; + Span p3 = channel3Samples[(y * channel3Stride)..]; + Span p4 = channel4Samples[(y * channel4Stride)..]; for (int x = 0; x < w; x++) { @@ -384,21 +434,29 @@ public static void InversePalette( else { JxlPlane plane = input.Channels[c0].Plane; - JxlPlane indices = new(configuration, plane.XSize, plane.YSize); - input.Channels[c0].Plane = indices; + using JxlPlane indices = new(configuration, plane.XSize, plane.YSize); + + // Keep the original index samples separate from reconstructed output. The channel retains its plane wrapper; + // the scoped temporary receives the old index allocation and releases it after every output channel has read it. + plane.Swap(indices); + Span indexSamples = indices.GetRow(0); + int indexStride = indices.PixelsPerRow; + + // Read each output stride after the swap. The original indices can retain wider storage from a metadata shrink. if (predictor == JxlPredictor.Weighted) { for (int c = 0; c < nb; c++) { JxlModularChannel outputChannel = input.Channels[c0 + c]; + int oneRowImage = outputChannel.Plane.PixelsPerRow; + Span outputSamples = outputChannel.GetRow(0); JxlModularState wpState = new(weightedHeader, outputChannel.Width); - Span paletteData = palette.GetRow(0); for (int y = 0; y < outputChannel.Height; y++) { - Span p = outputChannel.GetRow(y); - Span idx = indices.GetRow(y); + Span p = outputSamples[(y * oneRowImage)..]; + Span idx = indexSamples[(y * indexStride)..]; for (int x = 0; x < outputChannel.Width; x++) { @@ -434,12 +492,13 @@ public static void InversePalette( for (int c = 0; c < nb; c++) { JxlModularChannel outputChannel = input.Channels[c0 + c]; - Span paletteData = palette.GetRow(0); + int oneRowImage = outputChannel.Plane.PixelsPerRow; + Span outputSamples = outputChannel.GetRow(0); for (int y = 0; y < outputChannel.Height; y++) { - Span p = outputChannel.GetRow(y); - Span idx = indices.GetRow(y); + Span p = outputSamples[(y * oneRowImage)..]; + Span idx = indexSamples[(y * indexStride)..]; for (int x = 0; x < outputChannel.Width; x++) { @@ -477,20 +536,23 @@ public static void InversePalette( } else { - if (input.MetaChannels >= 2 - nb) + if (input.MetaChannels < 2 - nb) { throw new InvalidOperationException("Too many meta channels"); } input.MetaChannels -= 2 - nb; - if (beginC + nb - 1 < input.MetaChannels) + if (beginC + nb - 1 >= input.MetaChannels) { throw new InvalidOperationException("Too many meta channels"); } - input.Channels.RemoveAt(0); } + + // Both normal-channel and metachannel palettes consume the leading palette after its final read. + palette.Dispose(); + input.Channels.RemoveAt(0); } private static float ColorDistance(Span a, Span b) @@ -663,6 +725,20 @@ public static void ForwardPalette( paletteIterationData); } + /// + /// Searches palette entries with scoped quantized samples and transfers a completed palette to the image. + /// + /// The configuration used for temporary and output samples. + /// The owner of the source channels and any completed palette. + /// The first source channel. + /// The last source channel. + /// The palette size limit, replaced with the selected count. + /// The delta-entry count, updated by the search. + /// Whether to order selected colors. + /// Whether to quantize source colors. + /// The selected prediction method. + /// The weighted-prediction settings. + /// The search statistics and final-pass selection. private static void ForwardPaletteIteration( Configuration configuration, JxlModularImage input, @@ -688,6 +764,11 @@ private static void ForwardPaletteIteration( JxlModularChannel beginCChannel = input.Channels[beginC]; int w = beginCChannel.Width; int h = beginCChannel.Height; + Span sourceSamples = beginCChannel.GetRow(0); + int sourceStride = beginCChannel.Plane.PixelsPerRow; + + // Every successful palette adds one leading channel. Reserve its reference slot before allocating palette samples. + input.Channels.EnsureCapacity(input.Channels.Count + 1); if (input.BitDepth >= 32) { @@ -717,7 +798,7 @@ private static void ForwardPaletteIteration( for (int y = 0; y < h; y++) { - Span p = beginCChannel.GetRow(y); + Span p = sourceSamples[(y * sourceStride)..]; for (int x = 0; x < w; x++) { @@ -735,8 +816,9 @@ private static void ForwardPaletteIteration( } } - // Don't dispose. The channel is stored into the input. + // Attach the owner before filling its samples. The image releases it if a later encoding step fails. JxlModularChannel modularChannel = new(configuration, idx, 1, -1, -1); + input.Channels.Insert(0, modularChannel); numberOfColors = idx; idx = 0; @@ -750,7 +832,7 @@ private static void ForwardPaletteIteration( for (int y = 0; y < h; y++) { - Span p = beginCChannel.GetRow(y); + Span p = sourceSamples[(y * sourceStride)..]; for (int x = 0; x < w; x++) { @@ -765,8 +847,6 @@ private static void ForwardPaletteIteration( predictor = JxlPredictor.Zero; input.MetaChannels++; - input.Channels.Insert(0, modularChannel); - return; } @@ -775,7 +855,7 @@ private static void ForwardPaletteIteration( for (int y = 0; y < h; y++) { - Span p = beginCChannel.GetRow(y); + Span p = sourceSamples[(y * sourceStride)..]; for (int x = 0; x < w; x++) { if (lookup[p[x] - minValue] == 0) @@ -791,8 +871,9 @@ private static void ForwardPaletteIteration( } } - // Don't dispose. The channel is stored into the input. + // Attach before filling so input owns the palette on success and on partial failure. JxlModularChannel channel = new(configuration, idx, 1, -1, -1); + input.Channels.Insert(0, channel); numberOfColors = idx; idx = 0; Span pPalette = channel.GetRow(0); @@ -809,7 +890,7 @@ private static void ForwardPaletteIteration( for (int y = 0; y < h; y++) { - Span p = beginCChannel.GetRow(y); + Span p = sourceSamples[(y * sourceStride)..]; for (int x = 0; x < w; x++) { @@ -819,18 +900,14 @@ private static void ForwardPaletteIteration( predictor = JxlPredictor.Zero; input.MetaChannels++; - input.Channels.Insert(0, channel); - return; } - JxlModularImage quantizedInput = new(configuration, 1, 1, -1, -1); + // Only the lossy path needs quantized sample channels. Both paths use the real image dimensions and one scoped owner. + using JxlModularImage quantizedInput = JxlModularImage.Create(configuration, w, h, input.BitDepth, lossy ? nb : 0); if (lossy) { - quantizedInput.Dispose(); - quantizedInput = new(configuration, w, h, input.BitDepth, nb); - for (int c = 0; c < nb; c++) { if (!JxlImageOperations.CopyImage(input.Channels[beginC + c].Plane, quantizedInput.Channels[c].Plane)) @@ -989,332 +1066,361 @@ private static void ForwardPaletteIteration( inversePalette[color] = numberOfColors + k; } - // Don't dispose this. + // This owner transfers to input only when the final encoding pass completes. JxlModularChannel newChannel = new(configuration, numberOfColors, nb, -1, -1); - Span palette = newChannel.GetRow(0); - int oneRow = newChannel.Plane.PixelsPerRow; - int oneRowImage = beginCChannel.Plane.PixelsPerRow; - int bitDepth = Math.Min(input.BitDepth, 24); // max. 24 bits, cannot be greater + bool transferred = false; - if (lossy) + try { - for (int i = 0; i < numberOfDeltas; i++) + Span palette = newChannel.GetRow(0); + int oneRow = newChannel.Plane.PixelsPerRow; + int oneRowImage = beginCChannel.Plane.PixelsPerRow; + int bitDepth = Math.Min(input.BitDepth, 24); // max. 24 bits, cannot be greater + + if (lossy) { - for (int c = 0; c < 3; c++) + for (int i = 0; i < numberOfDeltas; i++) { - palette[(c * oneRow) + i] = paletteIterationData.FrequentDeltas[c][i]; + for (int c = 0; c < 3; c++) + { + palette[(c * oneRow) + i] = paletteIterationData.FrequentDeltas[c][i]; + } } } - } - float frequencyThreshold = 4f; - int clr = 0; + float frequencyThreshold = 4f; + int clr = 0; - if (ordered && nb >= 3) - { - candidatePaletteImageOrder.Sort((ap, bp) => + if (ordered && nb >= 3) { - float ay = (0.299f * ap[0]) + (0.587f * ap[1]) + (0.114f * ap[2]) + 0.1f; - - if (ap.Length > 3) + candidatePaletteImageOrder.Sort((ap, bp) => { - ay *= 1f + ap[3]; - } - - float by = (0.299f * bp[0]) + (0.587f * bp[1]) + (0.114f * bp[2]) + 0.1f; + float ay = (0.299f * ap[0]) + (0.587f * ap[1]) + (0.114f * ap[2]) + 0.1f; - if (bp.Length > 3) - { - by *= 1f + bp[3]; - } + if (ap.Length > 3) + { + ay *= 1f + ap[3]; + } - ay = colorFreqMap[ap] > frequencyThreshold ? -ay : ay; - by = colorFreqMap[bp] > frequencyThreshold ? -by : by; + float by = (0.299f * bp[0]) + (0.587f * bp[1]) + (0.114f * bp[2]) + 0.1f; - return ay.CompareTo(by); - }); - } + if (bp.Length > 3) + { + by *= 1f + bp[3]; + } - foreach (int[] pcol in candidatePaletteImageOrder) - { - Span pcolSpan = pcol.AsSpan(); + ay = colorFreqMap[ap] > frequencyThreshold ? -ay : ay; + by = colorFreqMap[bp] > frequencyThreshold ? -by : by; - for (int i = 0; i < nb; i++) - { - palette[numberOfDeltas + (i * oneRow) + clr] = pcolSpan[i]; + return ay.CompareTo(by); + }); } - inversePalette[pcol] = clr++; - } - - List wpStates = []; + foreach (int[] pcol in candidatePaletteImageOrder) + { + Span pcolSpan = pcol.AsSpan(); - for (int c = 0; c < nb; c++) - { - wpStates.Add(new JxlModularState(wpHeader, w)); - } + for (int i = 0; i < nb; i++) + { + palette[numberOfDeltas + (i * oneRow) + clr] = pcolSpan[i]; + } - InlineArray3> errorRow = default; + inversePalette[pcol] = clr++; + } - if (lossy) - { - errorRow[0] = new(nb, w + 4); - errorRow[1] = new(nb, w + 4); - errorRow[2] = new(nb, w + 4); - } + List wpStates = []; - Span tmp = stackalloc int[32]; // Power of 2 - Span bestValue = tmp.Slice(0 * nb, nb); - Span idealResidual = tmp.Slice(1 * nb, nb); - Span quantizedValue = tmp.Slice(2 * nb, nb); - Span predictions = tmp.Slice(30 * nb, nb); + for (int c = 0; c < nb; c++) + { + wpStates.Add(new JxlModularState(wpHeader, w)); + } - // This is a temporary buffer, values are copied here. - // It is so we can swap spans. Since spans are just a view - // of memory, using CopyTo as a swap means we need a - // separate buffer like this for the swapping value. - Span tempBuffer = stackalloc float[w + 4]; + InlineArray3> errorRow = default; - for (int y = 0; y < h; y++) - { - for (int c = 0; c < nb; c++) + if (lossy) { - p_in[c] = input.channel[beginC + c].Row(y); - if (lossy) - p_quant[c] = quantized_input.channel[c].Row(y); + errorRow[0] = new(nb, w + 4); + errorRow[1] = new(nb, w + 4); + errorRow[2] = new(nb, w + 4); } - Span p = beginCChannel.GetRow(y); + Span tmp = stackalloc int[32]; // Power of 2 + Span bestValue = tmp.Slice(0 * nb, nb); + Span idealResidual = tmp.Slice(1 * nb, nb); + Span quantizedValue = tmp.Slice(2 * nb, nb); + Span predictions = tmp.Slice(30 * nb, nb); - for (int x = 0; x < w; x++) + // This is a temporary buffer, values are copied here. + // It is so we can swap spans. Since spans are just a view + // of memory, using CopyTo as a swap means we need a + // separate buffer like this for the swapping value. + Span tempBuffer = stackalloc float[w + 4]; + + for (int y = 0; y < h; y++) { - int index; - if (!lossy) + for (int c = 0; c < nb; c++) { - for (int c = 0; c < nb; c++) - { - color[c] = p_in[c][x]; - } - - index = inversePalette[color]; + p_in[c] = input.channel[beginC + c].Row(y); + if (lossy) + p_quant[c] = quantized_input.channel[c].Row(y); } - else - { - int best_index = 0; - bool best_is_delta = false; - float best_distance = float.PositiveInfinity; - tmp.Clear(); + Span p = sourceSamples[(y * sourceStride)..]; - foreach (double diffusion_multiplier in (Span)[0.55, 0.75]) + for (int x = 0; x < w; x++) + { + int index; + if (!lossy) { for (int c = 0; c < nb; c++) { - colorWithError[c] = - p_in[c][x] + ((paletteIterationData.IsFinalRun ? 1 : 0) * - diffusion_multiplier * errorRow[0][c, x + 2]); - color[c] = (int)Math.Clamp(MathF.Round(colorWithError[c]), 0, (1 << input.BitDepth) - 1); + color[c] = p_in[c][x]; } - for (int c = 0; c < nb; c++) - { - predictions[c] = PredictTreeNoWeightedPrediction(w, p_quant[c] + x, oneRowImage, x, y, predictor, wpStates[c]).Guess; - } + index = inversePalette[color]; + } + else + { + int best_index = 0; + bool best_is_delta = false; + float best_distance = float.PositiveInfinity; + + tmp.Clear(); - void TryIndex(int index, Span predictions, Span idealResidual, Span colorWithError, ref Span bestValue, ref Span quantizedValue, Span palette, ref int numberOfColors, ref int numberOfDeltas) + foreach (double diffusion_multiplier in (Span)[0.55, 0.75]) { for (int c = 0; c < nb; c++) { - quantizedValue[c] = GetPaletteValue(palette, index, c, oneRow, bitDepth); - if (index < numberOfDeltas) - { - quantizedValue[c] += predictions[c]; - } + colorWithError[c] = + p_in[c][x] + ((paletteIterationData.IsFinalRun ? 1 : 0) * + diffusion_multiplier * errorRow[0][c, x + 2]); + color[c] = (int)Math.Clamp(MathF.Round(colorWithError[c]), 0, (1 << input.BitDepth) - 1); } - float color_distance = 32.0f / (1 << Math.Max(0, 2 * (bitDepth - 8))) * ColorDistance(colorWithError, quantizedValue); - - float indexPenalty = 0; - if (index == -1) - { - indexPenalty = -124; - } - else if (index < 0) - { - indexPenalty = -2 * index; - } - else if (index < numberOfDeltas) - { - indexPenalty = 250; - } - else if (index < numberOfColors) + for (int c = 0; c < nb; c++) { - indexPenalty = 150; + predictions[c] = PredictTreeNoWeightedPrediction(w, p_quant[c] + x, oneRowImage, x, y, predictor, wpStates[c]).Guess; } - else if (index < numberOfColors + LargeCubeOffset) + + void TryIndex( + int index, + Span predictions, + Span idealResidual, + Span colorWithError, + ref Span bestValue, + ref Span quantizedValue, + Span palette, + ref int numberOfColors, + ref int numberOfDeltas) { - indexPenalty = 70; + for (int c = 0; c < nb; c++) + { + quantizedValue[c] = GetPaletteValue(palette, index, c, oneRow, bitDepth); + if (index < numberOfDeltas) + { + quantizedValue[c] += predictions[c]; + } + } + + float color_distance = 32.0f / (1 << Math.Max(0, 2 * (bitDepth - 8))) * ColorDistance(colorWithError, quantizedValue); + + float indexPenalty = 0; + if (index == -1) + { + indexPenalty = -124; + } + else if (index < 0) + { + indexPenalty = -2 * index; + } + else if (index < numberOfDeltas) + { + indexPenalty = 250; + } + else if (index < numberOfColors) + { + indexPenalty = 150; + } + else if (index < numberOfColors + LargeCubeOffset) + { + indexPenalty = 70; + } + else + { + indexPenalty = 256; + } + + float distance = color_distance + indexPenalty; + if (distance < best_distance) + { + best_distance = distance; + best_index = index; + best_is_delta = index < numberOfDeltas; + + RuntimeUtility.Swap(ref bestValue, ref quantizedValue); + + for (int c = 0; c < nb; c++) + { + idealResidual[c] = (int)(colorWithError[c] - predictions[c]); + } + } } - else + + for (index = MinimumImplicitPaletteIndex; index < numberOfColors; index++) { - indexPenalty = 256; + TryIndex(index); } - float distance = color_distance + indexPenalty; - if (distance < best_distance) + TryIndex(QuantizeColorToImplicitPaletteIndex(color, numberOfColors, bitDepth, false)); + + if (EncodeToHighQualityImplicitPalette) { - best_distance = distance; - best_index = index; - best_is_delta = index < numberOfDeltas; + TryIndex(QuantizeColorToImplicitPaletteIndex(color, numberOfColors, bitDepth, true)); + } + } - RuntimeUtility.Swap(ref bestValue, ref quantizedValue); + index = best_index; + deltaUsed |= best_is_delta; - for (int c = 0; c < nb; c++) - { - idealResidual[c] = (int)(colorWithError[c] - predictions[c]); - } + if (!paletteIterationData.IsFinalRun) + { + for (int c = 0; c < 3; c++) + { + paletteIterationData.Deltas[c].Add(idealResidual[c]); } + + paletteIterationData.DeltaDistances.Add(best_distance); } - for (index = MinimumImplicitPaletteIndex; index < numberOfColors; index++) + for (int c = 0; c < nb; c++) { - TryIndex(index); + wpStates[c].UpdatePredictionErrors(bestValue[c], x, y, w); + p_quant[c][x] = bestValue[c]; } - TryIndex(QuantizeColorToImplicitPaletteIndex(color, numberOfColors, bitDepth, false)); - - if (EncodeToHighQualityImplicitPalette) + float len_error = 0; + for (int c = 0; c < nb; c++) { - TryIndex(QuantizeColorToImplicitPaletteIndex(color, numberOfColors, bitDepth, true)); + float local_error = colorWithError[c] - bestValue[c]; + len_error += local_error * local_error; } - } - index = best_index; - deltaUsed |= best_is_delta; - - if (!paletteIterationData.IsFinalRun) - { - for (int c = 0; c < 3; c++) + len_error = MathF.Sqrt(len_error); + float modulate = 1f; + long len_limit = 38 << Math.Max(0, bitDepth - 8); + if (len_error > len_limit) { - paletteIterationData.Deltas[c].Add(idealResidual[c]); + modulate *= len_limit / len_error; } - paletteIterationData.DeltaDistances.Add(best_distance); - } + DenseMatrix offsets = new(12, 2); - for (int c = 0; c < nb; c++) - { - wpStates[c].UpdatePredictionErrors(bestValue[c], x, y, w); - p_quant[c][x] = bestValue[c]; - } - - float len_error = 0; - for (int c = 0; c < nb; c++) - { - float local_error = colorWithError[c] - bestValue[c]; - len_error += local_error * local_error; - } - - len_error = MathF.Sqrt(len_error); - float modulate = 1f; - long len_limit = 38 << Math.Max(0, bitDepth - 8); - if (len_error > len_limit) - { - modulate *= len_limit / len_error; - } + for (int c = 0; c < nb; c++) + { + float total_error = colorWithError[c] - bestValue[c]; - DenseMatrix offsets = new(12, 2); + DefaultOffsets.Data.AsSpan().CopyTo(offsets.Data); - for (int c = 0; c < nb; c++) - { - float total_error = colorWithError[c] - bestValue[c]; + float total_available = 0; + for (int i = 0; i < 11; i++) + { + int row = offsets[i, 0]; + int col = offsets[i, 1]; - DefaultOffsets.Data.AsSpan().CopyTo(offsets.Data); + if (Math.Sign(errorRow[row][c, x + col]) != Math.Sign(total_error)) + { + total_available += errorRow[row][c, x + col]; + } + } - float total_available = 0; - for (int i = 0; i < 11; i++) - { - int row = offsets[i, 0]; - int col = offsets[i, 1]; + float weight = MathF.Abs(total_error) / (MathF.Abs(total_available) + 1e-3f); + weight = MathF.Min(weight, 1.0f); - if (Math.Sign(errorRow[row][c, x + col]) != Math.Sign(total_error)) + for (int i = 0; i < 11; ++i) { - total_available += errorRow[row][c, x + col]; - } - } + int row = offsets[i, 0]; + int col = offsets[i, 1]; - float weight = MathF.Abs(total_error) / (MathF.Abs(total_available) + 1e-3f); - weight = MathF.Min(weight, 1.0f); + if (Math.Sign(errorRow[row][c, x + col]) != Math.Sign(total_error)) + { + total_error += weight * errorRow[row][c, x + col]; + errorRow[row][c, x + col] *= 1 - weight; + } + } - for (int i = 0; i < 11; ++i) - { - int row = offsets[i, 0]; - int col = offsets[i, 1]; + total_error *= modulate; + float remaining_error = (1.0f / 14f) * total_error; + errorRow[0][c, x + 3] += 2 * remaining_error; + errorRow[0][c, x + 4] += remaining_error; + errorRow[1][c, x + 0] += remaining_error; - if (Math.Sign(errorRow[row][c, x + col]) != Math.Sign(total_error)) + for (int i = 0; i < 5; ++i) { - total_error += weight * errorRow[row][c, x + col]; - errorRow[row][c, x + col] *= 1 - weight; + errorRow[1][c, x + i] += remaining_error; + errorRow[2][c, x + i] += remaining_error; } } + } - total_error *= modulate; - float remaining_error = (1.0f / 14f) * total_error; - errorRow[0][c, x + 3] += 2 * remaining_error; - errorRow[0][c, x + 4] += remaining_error; - errorRow[1][c, x + 0] += remaining_error; - - for (int i = 0; i < 5; ++i) - { - errorRow[1][c, x + i] += remaining_error; - errorRow[2][c, x + i] += remaining_error; - } + if (paletteIterationData.IsFinalRun) + { + p[x] = index; } } - if (paletteIterationData.IsFinalRun) + if (lossy) { - p[x] = index; + for (int c = 0; c < nb; c++) + { + // Variables for swapping + Span pos0 = errorRow[0].Data.AsSpan(c, w + 4); + Span pos1 = errorRow[1].Data.AsSpan(c, w + 4); + Span pos2 = errorRow[2].Data.AsSpan(c, w + 4); + + // we need to swap: + // error_row[0][c].swap(error_row[1][c]); + pos0.CopyTo(tempBuffer); + pos1.CopyTo(pos0); + tempBuffer.CopyTo(pos1); + + // swap old1, old2 + pos1.CopyTo(tempBuffer); + pos2.CopyTo(pos1); + + pos2.Clear(); + } } } - if (lossy) + if (!deltaUsed) { - for (int c = 0; c < nb; c++) + predictor = JxlPredictor.Zero; + } + + if (paletteIterationData.IsFinalRun) + { + input.MetaChannels++; + + // The first channel now contains indices. Release the other selected source channels before removing their references. + for (int c = beginC + 1; c <= endC; c++) { - // Variables for swapping - Span pos0 = errorRow[0].Data.AsSpan(c, w + 4); - Span pos1 = errorRow[1].Data.AsSpan(c, w + 4); - Span pos2 = errorRow[2].Data.AsSpan(c, w + 4); - - // we need to swap: - // error_row[0][c].swap(error_row[1][c]); - pos0.CopyTo(tempBuffer); - pos1.CopyTo(pos0); - tempBuffer.CopyTo(pos1); - - // swap old1, old2 - pos1.CopyTo(tempBuffer); - pos2.CopyTo(pos1); - - pos2.Clear(); + input.Channels[c].Dispose(); } + + input.Channels.RemoveRange(beginC + 1, endC - beginC); + input.Channels.Insert(0, newChannel); + transferred = true; } - } - if (!deltaUsed) - { - predictor = JxlPredictor.Zero; + numberOfColors -= numberOfDeltas; } - - if (paletteIterationData.IsFinalRun) + finally { - input.MetaChannels++; - input.Channels.RemoveRange(beginC + 1, endC - beginC); - input.Channels.Insert(0, newChannel); + if (!transferred) + { + newChannel.Dispose(); + } } - - numberOfColors -= numberOfDeltas; } /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs index 43506b07b8..87135e7533 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs @@ -186,6 +186,13 @@ private static void FastUnsqueeze(Span pResidual, Span pAvg, Span (output - diff).StoreUnsafe(ref pNOutRef); } + /// + /// Restores a horizontal channel from its average and residual planes, then releases the displaced average storage. + /// + /// The configuration used for the restored samples. + /// The owner of the average and residual channels. + /// The average channel index, which receives the restored samples. + /// The borrowed residual channel index. InverseSqueeze removes it after reconstruction. public static void InverseHorizontalSqueeze(Configuration configuration, JxlModularImage input, int c, int rc) { // Channel offsets should not overflow. @@ -211,8 +218,8 @@ public static void InverseHorizontalSqueeze(Configuration configuration, JxlModu return; } - // Do not dispose. - JxlModularChannel outputChannel = new( + // Keep the image's channel wrapper stable. Swapping at completion transfers the result and releases the old average allocation. + using JxlModularChannel outputChannel = new( configuration, inputChannel.Width + inputResidualChannel.Width, inputChannel.Height, @@ -221,7 +228,7 @@ public static void InverseHorizontalSqueeze(Configuration configuration, JxlModu if (inputResidualChannel.Height == 0) { - input.Channels[c] = outputChannel; + inputChannel.Swap(outputChannel); return; } @@ -238,18 +245,23 @@ public static void InverseHorizontalSqueeze(Configuration configuration, JxlModu Span bpOutOddT = stackalloc int[rowsPerBlock * 8]; int rowBlockCount = JxlMath.DivCeil(inputChannel.Height, rowsPerBlock); + // Borrow all three allocations before traversal. Their strides remain fixed until the completed output is swapped into the channel. + Span residualSamples = inputResidualChannel.GetRow(0); + Span averageSamples = inputChannel.GetRow(0); + Span outputSamples = outputChannel.GetRow(0); + int onerow_in = inputChannel.Plane.PixelsPerRow; + int onerow_inr = inputResidualChannel.Plane.PixelsPerRow; + int onerow_out = outputChannel.Plane.PixelsPerRow; + for (int idx = 0; idx < rowBlockCount; idx++) { int y0 = idx * rowsPerBlock; int rows = Math.Min(rowsPerBlock, inputChannel.Height - y0); int x = 0; - int onerow_in = inputChannel.Plane.PixelsPerRow; - int onerow_inr = inputResidualChannel.Plane.PixelsPerRow; - int onerow_out = outputChannel.Plane.PixelsPerRow; - Span pResidual = inputResidualChannel.GetRow(y0); - Span pAverage = inputChannel.GetRow(y0); - Span pOut = outputChannel.GetRow(y0); + Span pResidual = residualSamples[(y0 * onerow_inr)..]; + Span pAverage = averageSamples[(y0 * onerow_in)..]; + Span pOut = outputSamples[(y0 * onerow_out)..]; ref int pOutRef = ref MemoryMarshal.GetReference(pOut); ref int bpOutEvenTRef = ref MemoryMarshal.GetReference(bpOutEvenT); @@ -309,17 +321,19 @@ public static void InverseHorizontalSqueeze(Configuration configuration, JxlModu for (int y = 0; y < rows; y++) { - UnsqueezeRow(y0 + y, x); + UnsqueezeRow(y0 + y, x, residualSamples, averageSamples, outputSamples); } } - input.Channels[c] = outputChannel; + inputChannel.Swap(outputChannel); - void UnsqueezeRow(int y, int x0) + // Restore the scalar remainder of row y, starting at residual x0. The three spans borrow complete allocations + // at their logical origins. Pass them explicitly because a local function cannot capture spans from its caller. + void UnsqueezeRow(int y, int x0, Span residualStorage, Span averageStorage, Span outputStorage) { - Span residual = inputResidualChannel.GetRow(y); - Span average = inputChannel.GetRow(y); - Span output = outputChannel.GetRow(y); + Span residual = residualStorage[(y * onerow_inr)..]; + Span average = averageStorage[(y * onerow_in)..]; + Span output = outputStorage[(y * onerow_out)..]; int inputChannelWidth = inputChannel.Width; int outputChannelWidth = outputChannel.Width; @@ -349,6 +363,13 @@ void UnsqueezeRow(int y, int x0) } } + /// + /// Restores a vertical channel from its average and residual planes, then releases the displaced average storage. + /// + /// The configuration used for the restored samples. + /// The owner of the average and residual channels. + /// The average channel index, which receives the restored samples. + /// The borrowed residual channel index. InverseSqueeze removes it after reconstruction. public static void InverseVerticalSqueeze(Configuration configuration, JxlModularImage input, int c, int rc) { // Channel offsets should not overflow. @@ -374,8 +395,7 @@ public static void InverseVerticalSqueeze(Configuration configuration, JxlModula return; } - // Do not dispose. - JxlModularChannel outputChannel = new( + using JxlModularChannel outputChannel = new( configuration, inputChannel.Width, inputChannel.Height + inputResidualChannel.Height, @@ -384,7 +404,7 @@ public static void InverseVerticalSqueeze(Configuration configuration, JxlModula if (inputResidualChannel.Width == 0) { - input.Channels[c] = outputChannel; + inputChannel.Swap(outputChannel); return; } @@ -393,6 +413,13 @@ public static void InverseVerticalSqueeze(Configuration configuration, JxlModula const int columnsPerBlock = 8; int columnBlockCount = JxlMath.DivCeil(inputChannel.Width, columnsPerBlock); + Span residualSamples = inputResidualChannel.GetRow(0); + Span averageSamples = inputChannel.GetRow(0); + Span outputSamples = outputChannel.GetRow(0); + int residualStride = inputResidualChannel.Plane.PixelsPerRow; + int averageStride = inputChannel.Plane.PixelsPerRow; + int outputStride = outputChannel.Plane.PixelsPerRow; + for (int idx = 0; idx < columnBlockCount; idx++) { int x0 = idx * columnsPerBlock; @@ -403,12 +430,13 @@ public static void InverseVerticalSqueeze(Configuration configuration, JxlModula { int yLsh1 = y << 1; - Span pResidual = inputResidualChannel.GetRow(y)[x0..]; - Span pAverage = inputChannel.GetRow(y)[x0..]; - Span pNAvg = inputChannel.GetRow(y + 1 < inputChannel.Height ? y + 1 : y)[x0..]; - Span pOut = outputChannel.GetRow(yLsh1)[x0..]; - Span pNOut = outputChannel.GetRow(yLsh1 + 1)[x0..]; - Span pPOut = y > 0 ? outputChannel.GetRow(yLsh1 - 1)[x0..] : pNAvg; + Span pResidual = residualSamples[((y * residualStride) + x0)..]; + Span pAverage = averageSamples[((y * averageStride) + x0)..]; + int nextAverageRow = y + 1 < inputChannel.Height ? y + 1 : y; + Span pNAvg = averageSamples[((nextAverageRow * averageStride) + x0)..]; + Span pOut = outputSamples[((yLsh1 * outputStride) + x0)..]; + Span pNOut = outputSamples[(((yLsh1 + 1) * outputStride) + x0)..]; + Span pPOut = y > 0 ? outputSamples[(((yLsh1 - 1) * outputStride) + x0)..] : pNAvg; int x = 0; for (; x + 7 < w; x += 8) @@ -442,8 +470,8 @@ public static void InverseVerticalSqueeze(Configuration configuration, JxlModula { int y = inputChannel.Height - 1; - Span pAverage = inputChannel.GetRow(y); - Span pOutput = outputChannel.GetRow(y << 1); + Span pAverage = averageSamples[(y * averageStride)..]; + Span pOutput = outputSamples[((y << 1) * outputStride)..]; for (int x = 0; x < inputChannel.Width; x++) { @@ -451,15 +479,20 @@ public static void InverseVerticalSqueeze(Configuration configuration, JxlModula } } - input.Channels[c] = outputChannel; + inputChannel.Swap(outputChannel); } + /// + /// Reconstructs squeezed channels in reverse transform order and releases each consumed residual channel. + /// + /// The configuration used for restored samples. + /// The owner of the transformed channels. + /// The squeeze steps in forward order. public static void InverseSqueeze(Configuration configuration, JxlModularImage input, Span parameters) { - int totalNumberOfChannels = input.Channels.Count; - for (int i = parameters.Length - 1; i >= 0; i--) { + int totalNumberOfChannels = input.Channels.Count; ref JxlSqueezeParameters parameter = ref parameters[i]; CheckMetaSqueezeParameters(parameter, totalNumberOfChannels); @@ -471,7 +504,7 @@ public static void InverseSqueeze(Configuration configuration, JxlModularImage i int offset = inPlace ? endC + 1 - : totalNumberOfChannels + beginC + endC - 1; + : totalNumberOfChannels + beginC - endC - 1; if (beginC < input.MetaChannels) { @@ -509,9 +542,23 @@ public static void InverseSqueeze(Configuration configuration, JxlModularImage i InverseVerticalSqueeze(configuration, input, c, rc); } } + + // Every step appends one residual per selected channel, either after that range or at the image's end. + // Remove only this step's residuals; the next reverse step must see the reduced channel count. + for (int c = offset; c < offset + parameter.NumC; c++) + { + input.Channels[c].Dispose(); + } + + input.Channels.RemoveRange(offset, parameter.NumC); } } + /// + /// Selects squeeze steps that reduce the image to its first preview dimensions. + /// + /// The list replaced with the selected steps. + /// The borrowed channel shapes used to select the steps. public static void DefaultSqueezeParameters(List squeezeParameters, JxlModularImage image) { int numberOfChannels = image.Channels.Count - image.MetaChannels; @@ -522,9 +569,7 @@ public static void DefaultSqueezeParameters(List squeezePa int h = numMetaChannelsChannel.Height; bool wide = w > h; - JxlModularChannel nextNumMetaChannelsChannel = image.Channels[image.MetaChannels + 1]; - - if (numberOfChannels > 2 && nextNumMetaChannelsChannel.Width == w && nextNumMetaChannelsChannel.Height == h) + if (numberOfChannels > 2 && image.Channels[image.MetaChannels + 1].Width == w && image.Channels[image.MetaChannels + 1].Height == h) { JxlSqueezeParameters parameters = new() { @@ -565,7 +610,7 @@ public static void DefaultSqueezeParameters(List squeezePa w = (w + 1) >> 1; } - if (w > MaxFirstPreviewSize) + if (h > MaxFirstPreviewSize) { newParameters.Horizontal = false; squeezeParameters.Add(newParameters); @@ -574,6 +619,11 @@ public static void DefaultSqueezeParameters(List squeezePa } } + /// + /// Validates an encoded squeeze channel range against the current channel count. + /// + /// The encoded step. + /// The channel count before applying that step. private static void CheckMetaSqueezeParameters(in JxlSqueezeParameters parameter, int numChannels) { int c1 = parameter.BeginC; @@ -587,6 +637,12 @@ private static void CheckMetaSqueezeParameters(in JxlSqueezeParameters parameter } } + /// + /// Reduces channel shapes before decoding and attaches the corresponding owned residual planes. + /// + /// The configuration used for residual samples. + /// The image that owns the reduced and residual channels. + /// The encoded steps, or an empty list to receive the default steps. public static void MetaSqueeze(Configuration configuration, JxlModularImage image, List parameters) { if (parameters.Count == 0) @@ -622,11 +678,14 @@ public static void MetaSqueeze(Configuration configuration, JxlModularImage imag ? endC + 1 : image.Channels.Count; + // Reserve references before allocating residual samples. Each completed plane can then attach without list growth. + image.Channels.EnsureCapacity(image.Channels.Count + parameter.NumC); + for (int c = beginC; c <= endC; c++) { JxlModularChannel channel = image.Channels[c]; - if (channel.Height > 30 || channel.VerticalShift > 30) + if (channel.HorizontalShift > 30 || channel.VerticalShift > 30) { throw new InvalidOperationException("Too many squeezes: shift > 30"); } @@ -634,7 +693,7 @@ public static void MetaSqueeze(Configuration configuration, JxlModularImage imag int w = channel.Width; int h = channel.Height; - if ((w & h) == 0) // either w, or h, is 0 + if (w == 0 || h == 0) { throw new InvalidOperationException("Squeezing empty channel"); } @@ -652,7 +711,7 @@ public static void MetaSqueeze(Configuration configuration, JxlModularImage imag } else { - channel.HorizontalShift = (h + 1) >> 1; + channel.Height = (h + 1) >> 1; if (channel.VerticalShift >= 0) { @@ -662,7 +721,7 @@ public static void MetaSqueeze(Configuration configuration, JxlModularImage imag h -= (h + 1) >> 1; } - channel.Shrink(configuration); + channel.Shrink(); JxlModularChannel placeholder = new(configuration, w, h, channel.HorizontalShift, channel.VerticalShift) { @@ -674,22 +733,42 @@ public static void MetaSqueeze(Configuration configuration, JxlModularImage imag } } + /// + /// Splits a horizontal channel into owned average and residual planes and releases its original samples. + /// + /// The configuration used for the output samples. + /// The owner of all input and output channels. + /// The channel to split. + /// The insertion index for the residual channel. public static void ForwardHorizontalSqueeze(Configuration configuration, JxlModularImage input, int c, int rc) { JxlModularChannel inputChannel = input.Channels[c]; - // Do not dispose these. - JxlModularChannel outputChannel = new(configuration, (inputChannel.Width + 1) >> 1, inputChannel.Height, inputChannel.HorizontalShift + 1, inputChannel.VerticalShift); - JxlModularChannel outputChannelResidual = new(configuration, inputChannel.Width - outputChannel.Width, outputChannel.Height, inputChannel.HorizontalShift + 1, inputChannel.VerticalShift); + // Attach the residual before processing. The image then owns it even if reconstruction of another output fails. + input.Channels.EnsureCapacity(input.Channels.Count + 1); + using JxlModularChannel outputChannel = new( + configuration, (inputChannel.Width + 1) >> 1, inputChannel.Height, inputChannel.HorizontalShift + 1, inputChannel.VerticalShift); + + JxlModularChannel outputChannelResidual = new( + configuration, inputChannel.Width - outputChannel.Width, outputChannel.Height, inputChannel.HorizontalShift + 1, inputChannel.VerticalShift); + + input.Channels.Insert(rc, outputChannelResidual); outputChannel.Component = inputChannel.Component; outputChannelResidual.Component = inputChannel.Component; + Span inputSamples = inputChannel.GetRow(0); + Span outputSamples = outputChannel.GetRow(0); + Span residualSamples = outputChannelResidual.GetRow(0); + int inputStride = inputChannel.Plane.PixelsPerRow; + int outputStride = outputChannel.Plane.PixelsPerRow; + int residualStride = outputChannelResidual.Plane.PixelsPerRow; + for (int y = 0; y < outputChannel.Height; y++) { - Span pIn = inputChannel.GetRow(y); - Span pOut = outputChannel.GetRow(y); - Span pRes = outputChannelResidual.GetRow(y); + Span pIn = inputSamples[(y * inputStride)..]; + Span pOut = outputSamples[(y * outputStride)..]; + Span pRes = residualSamples[(y * residualStride)..]; for (int x = 0; x < outputChannelResidual.Width; x++) { @@ -727,28 +806,45 @@ public static void ForwardHorizontalSqueeze(Configuration configuration, JxlModu } } - input.Channels[c] = outputChannel; - input.Channels.Insert(rc, outputChannelResidual); + inputChannel.Swap(outputChannel); } + /// + /// Splits a vertical channel into owned average and residual planes and releases its original samples. + /// + /// The configuration used for the output samples. + /// The owner of all input and output channels. + /// The channel to split. + /// The insertion index for the residual channel. public static void ForwardVerticalSqueeze(Configuration configuration, JxlModularImage input, int c, int rc) { JxlModularChannel inputChannel = input.Channels[c]; - // Do not dispose these. - JxlModularChannel outputChannel = new(configuration, inputChannel.Width, (inputChannel.Height + 1) >> 1, inputChannel.HorizontalShift, inputChannel.VerticalShift + 1); - JxlModularChannel outputResidualChannel = new(configuration, inputChannel.Width, inputChannel.Height - outputChannel.Height, inputChannel.HorizontalShift, inputChannel.VerticalShift + 1); + input.Channels.EnsureCapacity(input.Channels.Count + 1); + using JxlModularChannel outputChannel = new( + configuration, inputChannel.Width, (inputChannel.Height + 1) >> 1, inputChannel.HorizontalShift, inputChannel.VerticalShift + 1); + + JxlModularChannel outputResidualChannel = new( + configuration, inputChannel.Width, inputChannel.Height - outputChannel.Height, inputChannel.HorizontalShift, inputChannel.VerticalShift + 1); + + input.Channels.Insert(rc, outputResidualChannel); outputChannel.Component = inputChannel.Component; outputResidualChannel.Component = inputChannel.Component; int oneRowInput = inputChannel.Plane.PixelsPerRow; + Span inputSamples = inputChannel.GetRow(0); + Span outputSamples = outputChannel.GetRow(0); + Span residualSamples = outputResidualChannel.GetRow(0); + int outputStride = outputChannel.Plane.PixelsPerRow; + int residualStride = outputResidualChannel.Plane.PixelsPerRow; + for (int y = 0; y < outputChannel.Height; y++) { - Span pIn = inputChannel.GetRow(y * 2); - Span pOut = outputChannel.GetRow(y); - Span pResidual = outputResidualChannel.GetRow(y); + Span pIn = inputSamples[(y * 2 * oneRowInput)..]; + Span pOut = outputSamples[(y * outputStride)..]; + Span pResidual = residualSamples[(y * residualStride)..]; for (int x = 0; x < outputChannel.Width; x++) { @@ -781,8 +877,8 @@ public static void ForwardVerticalSqueeze(Configuration configuration, JxlModula { int y = outputChannel.Height - 1; - Span pIn = inputChannel.GetRow(y * 2); - Span pOut = outputChannel.GetRow(y); + Span pIn = inputSamples[(y * 2 * oneRowInput)..]; + Span pOut = outputSamples[(y * outputStride)..]; for (int x = 0; x < outputChannel.Width; x++) { @@ -790,8 +886,7 @@ public static void ForwardVerticalSqueeze(Configuration configuration, JxlModula } } - input.Channels[c] = outputChannel; - input.Channels.Insert(rc, outputResidualChannel); + inputChannel.Swap(outputChannel); } public static void ForwardSqueeze(Configuration configuration, JxlModularImage input, List parameters) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs index 6d8c73f8f2..c51afcdd75 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs @@ -8,7 +8,10 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; -internal sealed class JxlTransform : IJxlFields +/// +/// Stores one modular transform and owns its mutable squeeze steps. +/// +internal sealed class JxlTransform : IJxlFields, IDeepCloneable { public JxlTransform() : this(JxlTransformType.Invalid) @@ -21,6 +24,27 @@ public JxlTransform(JxlTransformType id) this.TransformType = id; } + /// + /// Copies transform metadata without sharing the list that default squeeze expansion can change. + /// + /// The parsed transform to copy. + private JxlTransform(JxlTransform source) + { + this.TransformType = source.TransformType; + this.BeginC = source.BeginC; + this.RctType = source.RctType; + this.NumC = source.NumC; + this.Colors = source.Colors; + this.Deltas = source.Deltas; + this.MaxDeltaError = source.MaxDeltaError; + this.Predictor = source.Predictor; + this.OrderedPalette = source.OrderedPalette; + this.LossyPalette = source.LossyPalette; + + // Squeeze steps are value types. Copying them into this owned list separates all mutable transform metadata. + this.Squeezes.AddRange(source.Squeezes); + } + public JxlTransformType TransformType { get; set; } public int BeginC { get; set; } @@ -33,7 +57,10 @@ public JxlTransform(JxlTransformType id) public int Deltas { get; set; } - public List Squeezes { get; set; } = []; + /// + /// Gets the owned squeeze steps. An empty parsed list selects image-dependent default steps during metadata application. + /// + public List Squeezes { get; } = []; public int MaxDeltaError { get; set; } @@ -43,6 +70,9 @@ public JxlTransform(JxlTransformType id) public bool LossyPalette { get; set; } + /// + public JxlTransform DeepClone() => new(this); + public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); public static void CheckEqualChannels(JxlModularImage image, int c1, int c2) From 382a20803a5be29c35edcead3f0a86f4787bf75e Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 21:13:31 +1000 Subject: [PATCH 266/328] Remove unused JPEG XL utility copies --- .../FrameHeader/JxlColorTransformHelpers.cs | 31 -------------- src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs | 12 ------ .../Formats/Jxl/Processing/JxlSimdUtils.cs | 41 ------------------- 3 files changed, 84 deletions(-) delete mode 100644 src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransformHelpers.cs delete mode 100644 src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransformHelpers.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransformHelpers.cs deleted file mode 100644 index 81bc941870..0000000000 --- a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransformHelpers.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; - -/// -/// Helper methods associated with JxlColorTransform. -/// -internal static class JxlColorTransformHelpers -{ - private static ReadOnlySpan Grayscale => [0, 0, 0]; - - private static ReadOnlySpan YCbCr => [1, 0, 2]; - - private static ReadOnlySpan None => [0, 1, 2]; - - public static ReadOnlySpan GetJpegOrder(JxlColorTransform transform, bool isGraysacle) - { - if (isGraysacle) - { - return Grayscale; - } - - if (transform == JxlColorTransform.YCbCr) - { - return YCbCr; - } - - return None; - } -} diff --git a/src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs b/src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs deleted file mode 100644 index a39dfe707c..0000000000 --- a/src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Diagnostics.CodeAnalysis; - -namespace SixLabors.ImageSharp.Formats.Jxl; - -internal static class JxlThrowHelper -{ - [DoesNotReturn] - public static void ThrowEndOfStream() => throw new EndOfStreamException(); -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs index fddb21dc09..931d717add 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Numerics; -using System.Numerics.Tensors; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; @@ -108,13 +107,6 @@ public static void Transpose8x8Block(Span fromSpan, Span toSpan, int s } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector Pow(Vector @base, Vector exponent) - { - Vector vec = Vector.Log2(@base) * exponent; - return vec * vec; - } - /// /// /// Fills the span so its first value is equal to @@ -508,39 +500,6 @@ public static Vector FastPowf( Vector exponent) => FastPow2f(Vector.Log2(@base) * exponent); - public static uint MaxValue(Span data) - { - int lanes = Vector.Count; - int lastFull = lanes * (data.Length / lanes); - - Vector max = Vector.Zero; - ref uint dataRef = ref MemoryMarshal.GetReference(data); - - for (int i = 0; i < lastFull; i += lanes) - { - max = Vector.Max(max, Vector.LoadUnsafe(ref Unsafe.Add(ref dataRef, i))); - } - - if (lastFull < data.Length) - { - Vector stop = Vector.Create((uint)data.Length); - Vector fence = Iota((uint)lastFull); - Vector take = Vector.LessThan(fence, stop); - max = Vector.Max( - max, - Vector.ConditionalSelect( - take, - Vector.LoadUnsafe(ref Unsafe.Add(ref dataRef, lastFull)), - Vector.Zero)); - } - - // The following part is to find the largest number in - // the vector. - Span copy = stackalloc uint[Vector.Count]; - max.CopyTo(copy); - return TensorPrimitives.Max(copy); - } - /// /// A SIMD utility method which takes in the 128 bit vector /// and duplicates its values to fit in the CPU vector size. From 1cb7f1ce4715f14317065f41bc1e80b5d18fd3fe Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 21:14:23 +1000 Subject: [PATCH 267/328] Limit JPEG XL range reductions to logical samples --- .../Processing/Image/JxlImageOperations.cs | 27 +++++++----- .../Modular/Transforms/JxlPalette.cs | 2 +- .../Modular/Transforms/JxlTransform.cs | 20 --------- .../Jxl/Processing/ImageOperationsTests.cs | 44 +++++++++++++++++++ 4 files changed, 62 insertions(+), 31 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs index 95209cdde3..27fc726dd9 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using System.Numerics.Tensors; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; @@ -459,27 +460,33 @@ public static int Mirror(long x, long xSize) } /// - /// Searches the entire plane to find the smallest and largest value. + /// Finds the smallest and largest logical samples without reading a storage prefix, row padding or unused rows. /// /// The type of the plane. /// The plane where to search for the minimum & maximum values. /// Lowest value found in the plane. /// Largest value found in the plane. + /// An empty plane returns the sample type's maximum value as min and minimum value as max. public static void ImageMinMax(JxlPlane image, out T min, out T max) where T : unmanaged, INumber, IMinMaxValue { - min = T.MaxValue; // Start with opposite - max = T.MinValue; // Start with opposite + min = T.MaxValue; + max = T.MinValue; - for (int y = 0; y < image.YSize; y++) + // Empty planes are valid owners. Retain the existing empty result without passing an empty row to tensor reductions. + if (image.XSize == 0) { - Span row = image.GetRow(y); + return; + } - for (int x = 0; x < image.XSize; x++) - { - min = T.Min(min, row[x]); - max = T.Max(max, row[x]); - } + ReadOnlySpan samples = image.GetRow(0); + + for (int y = 0; y < image.YSize; y++) + { + // Reduce only this logical row. Shared tensor operations supply all supported SIMD widths and scalar remainders. + ReadOnlySpan row = samples.Slice(y * image.PixelsPerRow, image.XSize); + min = T.Min(min, TensorPrimitives.Min(row)); + max = T.Max(max, TensorPrimitives.Max(row)); } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs index 473b1ef83c..58203d1308 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs @@ -789,7 +789,7 @@ private static void ForwardPaletteIteration( throw new InvalidOperationException("No colors"); } - JxlTransform.ComputeMinMax(beginCChannel, out int minValue, out int maxValue); + JxlImageOperations.ImageMinMax(beginCChannel.Plane, out int minValue, out int maxValue); int lookupTableSize = maxValue - minValue + 1; if (lookupTableSize < MaxPaletteLookupTableSize) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs index c51afcdd75..51ca8332b0 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Numerics.Tensors; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; @@ -103,25 +102,6 @@ public static void CheckEqualChannels(JxlModularImage image, int c1, int c2) } } - public static void ComputeMinMax(JxlModularChannel channel, out int min, out int max) - { - // Start with opposite bounds so the first iteration - // guarantees to set these values - min = int.MaxValue; - max = int.MinValue; - - for (int y = 0; y < channel.Height; y++) - { - ReadOnlySpan p = channel.GetRow(y); - - int minRow = TensorPrimitives.Min(p); - int maxRow = TensorPrimitives.Max(p); - - min = Math.Min(minRow, min); - max = Math.Max(maxRow, max); - } - } - public void Inverse(Configuration configuration, JxlModularImage input, JxlModularHeader wpHeader) { switch (this.TransformType) diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/ImageOperationsTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/ImageOperationsTests.cs index a47ebc8c63..8cd2afcf53 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/ImageOperationsTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/ImageOperationsTests.cs @@ -8,11 +8,55 @@ using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Tests.Memory; +using SixLabors.ImageSharp.Tests.TestUtilities; namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; public class ImageOperationsTests { + /// + /// Checks that range scans exclude a prefix, padding and retained rows at every SIMD width and in the scalar path. + /// + [Fact] + public void PlaneMinMax_UsesOnlyLogicalSamples() + { + static void RunTest() + { + using JxlPlane plane = new(TestEnvironment.Configuration, 33, 3, 2); + Span allocation = plane.GetRowMinus(0, 2); + Span samples = plane.GetRow(0); + ReadOnlySpan widths = [0, 3, 17, 33]; + + foreach (int width in widths) + { + Assert.True(plane.ShrinkTo(width, 2)); + + // These extremes belong only to the prefix, padding and unused third row. Logical samples lie between -width and width. + allocation.Fill(int.MaxValue); + allocation[0] = int.MinValue; + for (int x = 0; x < width; x++) + { + samples[x] = x + 1; + samples[plane.PixelsPerRow + x] = -x - 1; + } + + JxlImageOperations.ImageMinMax(plane, out int minimum, out int maximum); + Assert.Equal(width == 0 ? int.MaxValue : -width, minimum); + Assert.Equal(width == 0 ? int.MinValue : width, maximum); + } + + // A logical shrink retains the allocation. Neither the other columns nor the earlier two-row extent belongs to this scan. + Assert.True(plane.ShrinkTo(1, 1)); + JxlImageOperations.ImageMinMax(plane, out int finalMinimum, out int finalMaximum); + Assert.Equal(1, finalMinimum); + Assert.Equal(1, finalMaximum); + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } + /// /// Checks that groups reuse coefficient scratch and release replaced buffers when it grows. /// From 61780d364a28c9b12743f3afa760542b6adce7fe Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 21:28:52 +1000 Subject: [PATCH 268/328] Bind JPEG XL decoder workspace to shared codestream ownership --- .../Decoder/FrameDecoder/JxlFrameDecoder.cs | 199 +++++++------- .../Decoder/Group/JxlGroupDecoder.cs | 4 +- .../Processing/Decoder/JxlDecoderInternal.cs | 127 +++++---- .../Decoder/JxlPassesDecoderState.cs | 202 +++++++++----- .../Decoder/Patch/JxlPatchDictionary.cs | 11 +- .../Decoder/Patch/JxlReferenceFrame.cs | 8 - .../AcStrategy/JxlAcStrategyEncoder.cs | 4 +- .../Encoder/Heuristics/JxlCflHeuristics.cs | 4 +- .../Encoder/JxlAdaptiveQuantization.cs | 112 ++++---- .../Jxl/Processing/JxlCoefficientOrder.cs | 59 +++- .../Jxl/Processing/JxlColorCorrelationMap.cs | 85 ++++-- .../Jxl/Processing/JxlPassesSharedState.cs | 253 ++++++++++++++---- .../Formats/Jxl/Processing/JxlToc.cs | 167 +++--------- .../Primitives/JxlFrameDimensions.cs | 34 ++- .../RenderPipeline/UpsamplingStage.cs | 26 +- 15 files changed, 762 insertions(+), 533 deletions(-) delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlReferenceFrame.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/FrameDecoder/JxlFrameDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/FrameDecoder/JxlFrameDecoder.cs index ccfaade87d..4656c0a9a1 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/FrameDecoder/JxlFrameDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/FrameDecoder/JxlFrameDecoder.cs @@ -3,19 +3,20 @@ using SixLabors.ImageSharp.Formats.Jxl.IO; using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; -using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; -using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Group; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Modular; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.FrameDecoder; -internal sealed class JxlFrameDecoder +/// +/// Owns one frame's section control and modular sample storage while borrowing the codestream's decoder workspace. +/// +internal sealed class JxlFrameDecoder : IDisposable { private JxlPassesDecoderState decoderState; private List toc = []; - private ulong sectionSizesSum; + private long sectionSizesSum; private JxlFrameHeader frameHeader; private JxlFrameDimensions frameDimensions; private JxlImageBundle decoded; @@ -31,12 +32,85 @@ internal sealed class JxlFrameDecoder private long numSectionsDone; private bool isFinalized = true; private bool allocated; - private List groupDecoderCaches = []; - private bool useTaskId; - private bool useSlowRenderingPipeline; private JxlProgressiveDetail progressiveDetail = JxlProgressiveDetail.Frames; private List passesToPause = []; + /// + /// Binds a parsed frame to its decoder workspace and reads its section table. + /// + /// The borrowed workspace whose shared state outlives this controller. + /// The header already read from this frame's bitstream. + /// The validated dimensions derived from that header. + /// The caller-owned destination bundle. + /// The reader immediately after the frame header. It advances to the first section. + public JxlFrameDecoder( + JxlPassesDecoderState decoderState, + JxlFrameHeader frameHeader, + JxlFrameDimensions frameDimensions, + JxlImageBundle decoded, + JxlBitReader reader) + { + this.decoderState = decoderState; + this.frameHeader = frameHeader; + this.frameDimensions = frameDimensions; + this.decoded = decoded; + this.modularFrameDecoder = new(decoderState.Shared.Configuration, frameDimensions); + + int entries = JxlToc.NumberOfTocEntries(frameDimensions.NumGroups, frameDimensions.NumDcGroups, (int)frameHeader.Passes.NumberOfPasses); + List sizes = new(entries); + List permutation = []; + if (!JxlToc.ReadToc(decoderState.Shared.Configuration, entries, reader, sizes, permutation)) + { + throw new InvalidImageContentException("The JPEG XL frame section table is invalid or incomplete."); + } + + this.toc.EnsureCapacity(entries); + for (int i = 0; i < entries; i++) + { + // TOC sizes follow physical stream order. The permutation maps each logical section to its physical slot. + // The U32 distribution limits one size to less than Int32.MaxValue; at most 65,536 entries fit in Int64. + this.toc.Add(new JxlTocEntry { Size = (int)sizes[i], Id = i }); + this.sectionSizesSum += sizes[i]; + } + + if (permutation.Count != 0) + { + for (int i = 0; i < entries; i++) + { + int physicalIndex = (int)permutation[i]; + JxlTocEntry entry = this.toc[physicalIndex]; + entry.Id = i; + this.toc[physicalIndex] = entry; + } + } + + // A reused output bundle can still contain a previous frame with another extent. Only it owns those planes. + decoded.RemoveColor(); + decoded.ClearExtraChannels(); + decoded.Duration = frameHeader.AnimationFrame.Duration; + + if (!frameHeader.IsPreviewFrame && (frameHeader.IsLast || decoded.Duration > 0) + && frameHeader.FrameType is JxlFrameType.RegularFrame or JxlFrameType.SkipProgressive) + { + decoderState.VisibleFrameIndex++; + decoderState.NonvisibleFrameIndex = 0; + } + else + { + decoderState.NonvisibleFrameIndex++; + } + } + + /// + /// Gets section sizes and logical identifiers in their physical stream order. + /// + public IReadOnlyList Toc => this.toc; + + /// + /// Gets the total byte count of the frame's sections, excluding its header and section table. + /// + public long SectionSizesSum => this.sectionSizesSum; + /// /// Gets a value indicating whether there are any DC groups left to decode. /// @@ -110,7 +184,7 @@ public bool HasEverything return false; } - if (this.decodedPassesPerAcGroup.Any(nbp => nbp < this.frameHeader.Passes.NumPasses)) + if (this.decodedPassesPerAcGroup.Any(nbp => nbp < this.frameHeader.Passes.NumberOfPasses)) { return false; } @@ -135,122 +209,33 @@ public static int SavedAs(JxlFrameHeader header) return 0; } - private static int GetStride(int width, JxlPixelFormat format) - { - if (!JxlMath.SafeMultiply(BytesPerChannel(format.DataType), format.Channels, out int xStride)) - { - throw new InvalidOperationException("Image too large"); - } - - if (!JxlMath.SafeMultiply(xStride, width, out int yStride)) - { - throw new InvalidOperationException("Image too large"); - } - - if (!JxlMath.SafeRoundUpTo(yStride, format.Align, yStride)) - { - throw new InvalidOperationException("Image too large"); - } - - return yStride; - } - public static void DecodeGlobalDcInfo(Configuration configuration, JxlBitReader reader, bool isJpeg, JxlPassesDecoderState state) { - state.SharedStorage.Quantizer.Decode(reader); + state.Shared.Quantizer.Decode(reader); - if (!JxlEntropyCoder.DecodeBlockContextMap(configuration, reader, ref state.SharedStorage.BlockContextMap)) + if (!JxlEntropyCoder.DecodeBlockContextMap(configuration, reader, ref state.Shared.BlockContextMap)) { throw new InvalidOperationException("Could not decode block context map"); } - if (!state.SharedStorage.ColorMap.DecodeDc(reader)) + if (!state.Shared.ColorMap.DecodeDc(reader)) { throw new InvalidOperationException("Could not decode DC color correlation map"); } if (isJpeg) { - state.SharedStorage.Quantizer.ClearDcMultipliers(); + state.Shared.Quantizer.ClearDcMultipliers(); } - state.SharedStorage.AcStrategy.FillInvalid(); + state.Shared.AcStrategy.FillInvalid(); } - public static void DecodeFrame(JxlPassesDecoderState decoderState, Stream stream, ref JxlFrameHeader header, JxlImageBundle decoded, JxlCodecMetadata metadata, bool useSlowRenderingPipeline) - { - JxlFrameDecoder frameDecoder = new(decoderState, metadata, useSlowRenderingPipeline); - JxlBitReader reader = new(stream); - - if (!frameDecoder.InitializeFrame(reader, decoded, isPreview: false)) - { - throw new InvalidOperationException("Frame initialization failed"); - } - - if (!frameDecoder.InitializeFrameOutput()) - { - throw new InvalidOperationException("Could not initialize frame output"); - } - - if (header is not null) - { - header = frameDecoder.frameHeader; - } - - bool closeOk = true; - List sectionReaders = []; - List sectionClosers = []; - List sectionInfos = []; - List sectionStatuses = []; - - int index = 0; - - foreach (JxlTocEntry toc in frameDecoder.toc) - { - JxlBitReader br = new(stream); - sectionInfos.Add(new JxlSectionInfo(br, toc.Id, index++)); - sectionClosers.Add(new JxlBitReaderScopedCloser(reader, closeOk)); - sectionReaders.Add(br); - } - - frameDecoder.ProcessSections(sectionInfos, sectionStatuses); - for (int i = 0; i < sectionStatuses.Count; i++) - { - if (sectionStatuses[i] != JxlSectionStatus.Done) - { - throw new InvalidOperationException("Section incomplete"); - } - } - - if (!closeOk) - { - throw new InvalidDataException("Stream cannot be closed"); - } - - frameDecoder.FinalizeFrame(); - } - - private static int BytesPerChannel(JxlDataType dataType) => - dataType == JxlDataType.Byte ? 1 - : dataType == JxlDataType.Single - ? 4 - : 2; - - private int GetStorageLocation(int thread, int task) => this.useTaskId ? task : thread; - - private void PrepareStorage(int numThreads, int numTasks) + /// + /// Releases frame-owned modular channels without disposing borrowed decoder controls or the output bundle. + /// + public void Dispose() { - int storageSize = Math.Min(numThreads, numTasks); - if (storageSize > this.groupDecoderCaches.Count) - { - this.groupDecoderCaches = [.. this.groupDecoderCaches.Take(storageSize)]; - } - - this.useTaskId = numThreads > numTasks; - bool useNoise = (this.frameHeader.Flags & (int)JxlFrameHeaderFlags.Noise) != 0; - bool useGroupIds = this.modularFrameDecoder.UsesFullImage && (this.frameHeader.Encoding == JxlFrameEncoding.VarDct || useNoise); - - this.decoderState.RenderPipeline?.PrepareForThreads(storageSize, useGroupIds); - this.decoderState.Upsampler8x.PrepareForThreads(numThreads); + this.modularFrameDecoder.Dispose(); } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs index 017642826c..49cd05beda 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs @@ -277,8 +277,8 @@ private static bool DecodeGroupCore( JxlAcStrategyRow acsRow = acStrategy.GetRow(blockRect, by); - ReadOnlySpan rowCMap0 = decState.Shared.ColorMap.YToXMap!.GetRow(ty); - ReadOnlySpan rowCMap2 = decState.Shared.ColorMap.YToBMap!.GetRow(ty); + ReadOnlySpan rowCMap0 = decState.Shared.ColorMap.YToXMap.GetRow(ty); + ReadOnlySpan rowCMap2 = decState.Shared.ColorMap.YToBMap.GetRow(ty); Span idctRow0; Span idctRow1; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderInternal.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderInternal.cs index 974d543c32..e407b83dca 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderInternal.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderInternal.cs @@ -243,6 +243,11 @@ internal sealed class JxlDecoderInternal : ImageDecoderCore, IDisposable /// private JxlPassesDecoderState? passesState; + /// + /// Owns shared frame controls and reference caches until this codestream is rewound or disposed. + /// + private JxlPassesSharedState? sharedState; + /// /// State for frame decoder. /// @@ -747,17 +752,45 @@ private sealed record JxlExtraChannelOutput(JxlPixelFormat Format, object? Buffe private sealed record JxlOooEntry(byte[] CodestreamBytes, bool IsLast); + /// + /// Releases the buffers and workspaces owned by this decoder. + /// public void Dispose() { - // RewindDecodingState resets everything, - // including disposal of streams. - this.RewindDecodingState(); - - // Streams like input and output streams may - // be unmanaged. + // Final disposal releases existing storage. Unlike a rewind, it must not allocate metadata or replacement buffers. + this.ReleaseOwnedStorage(); GC.SuppressFinalize(this); } + /// + /// Releases the current input buffers and codec workspace without allocating replacement state. + /// + private void ReleaseOwnedStorage() + { + this.boxOutputBuffer?.Dispose(); + this.boxOutputBuffer = null; + this.exifMetadata?.Dispose(); + this.exifMetadata = null; + this.xmpMetadata?.Dispose(); + this.xmpMetadata = null; + this.imageOutBuffer?.Dispose(); + this.imageOutBuffer = null; + this.nextInput?.Dispose(); + this.nextInput = null; + + // Release consumers before the shared planes they borrow. Reference caches belong only to sharedState. + this.frameDecoder?.Dispose(); + this.frameDecoder = null; + this.passesState?.Dispose(); + this.passesState = null; + this.sharedState?.Dispose(); + this.sharedState = null; + this.imageBundle?.Dispose(); + this.imageBundle = null; + this.codestreamCopy?.Dispose(); + this.codestreamCopy = null; + } + /// /// Ensures that the coordinates are not out of bounds. /// @@ -972,6 +1005,7 @@ public bool TryInjectNextBufferedJxlpBox() value.Deconstruct(out byte[] data, out bool isLast); int length = data.Length; + this.EnsureCodeStreamCopy(); this.codestreamCopy!.Write(data); if (isLast) @@ -1030,17 +1064,12 @@ public long AvailableCodeStream() } /// - /// Ensures that the copy of the code stream is present. + /// Allocates copied codestream storage when an input operation first requires it. /// - /// - /// Thrown if the copy is missing or null. - /// + [MemberNotNull(nameof(codestreamCopy))] private void EnsureCodeStreamCopy() { - if (this.codestreamCopy is null) - { - throw new InvalidOperationException("Copy of the code stream is missing"); - } + this.codestreamCopy ??= new(this.Options.Configuration.MemoryAllocator); } /// @@ -1121,10 +1150,7 @@ public bool TryRequestMoreInput() public Memory? TryGetCodestreamInput() { - if (this.codestreamCopy is null) - { - return null; - } + this.EnsureCodeStreamCopy(); if (this.codestreamCopy.Length == 0 && this.codestreamPos > 0) { @@ -1226,13 +1252,12 @@ public static bool CheckSizeLimit(int width, int height) } /// - /// Resets the decoder state to its default values, and, - /// additionally, releases memory used by buffers and replaces - /// them with new fresh copies. + /// Releases current codec storage and prepares fresh metadata for another codestream. /// [MemberNotNull(nameof(metadata))] public void RewindDecodingState() { + this.ReleaseOwnedStorage(); this.decoderStage = JxlDecoderStage.Initialized; this.gotSignature = false; @@ -1269,16 +1294,10 @@ public void RewindDecodingState() this.boxOutBufferSet = false; this.boxOutBufferSetCurrentBox = false; - this.boxOutputBuffer?.Dispose(); - this.boxOutputBuffer = null; this.boxOutBufferSize = 0; this.boxOutBufferBegin = 0; this.boxOutBufferPos = 0; - this.exifMetadata?.Dispose(); - this.exifMetadata = null; - this.xmpMetadata?.Dispose(); - this.xmpMetadata = null; this.storeExif = 0; this.storeXmp = 0; @@ -1294,8 +1313,6 @@ public void RewindDecodingState() this.downsamplingTarget = 8; this.imageOutBufferSet = false; - this.imageOutBuffer?.Dispose(); - this.imageOutBuffer = null; this.imageOutputInitCallback = null; this.imageOutputRunCallback = null; this.imageOutputDestroyCallback = null; @@ -1308,19 +1325,12 @@ public void RewindDecodingState() this.extraChannelOutputs.Clear(); - this.nextInput?.Dispose(); - this.nextInput = null; - this.availableInput = 0; this.inputClosed = false; - this.passesState?.Reset(); - this.frameDecoder?.Reset(); this.nextSection = 0; this.sectionProcessed.Clear(); - this.imageBundle = null; - // A rewind starts a new codestream. Its frames share this metadata, including the original profile. // Constructing metadata here prevents a later frame from retaining the previous codestream's profile. this.metadata = new JxlCodecMetadata(); @@ -1330,8 +1340,6 @@ public void RewindDecodingState() Metadata = this.metadata }; - this.codestreamCopy?.Dispose(); - this.codestreamCopy = new(this.Options.Configuration.MemoryAllocator); this.codestreamUnconsumed = 0; this.codestreamPos = 0; this.codestreamBitsAhead = 0; @@ -1584,13 +1592,7 @@ public void ReadAllHeaders() this.AdvanceCodeStream(bitReader.TotalBitsConsumed / JxlMath.BitsPerByte); this.codestreamBitsAhead = 0; - this.passesState ??= new(this.frameHeader!, this.Options.Configuration); - this.passesState.OutputEncodingInfo.SetFromMetadata(this.metadata); - - if (this.desiredIntensityTarget > 0f) - { - this.passesState.OutputEncodingInfo.DesiredIntensityTarget = this.desiredIntensityTarget; - } + // Shared frame state starts at the first parsed frame header. Codestream-only queries need no frame dimensions or workspace. } /// @@ -1608,7 +1610,7 @@ public void ProcessSections() List sectionInfo = []; List sectionStatus = []; - for (long i = this.nextSection; i < toc.Size; i++) + for (int i = (int)this.nextSection; i < toc.Count; i++) { if (this.sectionProcessed[(int)i] != 0) { @@ -1725,19 +1727,19 @@ public int ProcessCodestream() this.imageBundle ??= new(this.metadata.ImageMetadata); this.jpegDecoder.SetImageBundleJpegData(this.imageBundle); - this.frameDecoder = new(this.passesState!, this.metadata, useSlowRenderingPipeline: false); this.frameHeader = new() { - Metadata = this.metadata + Metadata = this.metadata, + IsPreviewFrame = this.previewFrame }; Span span = this.GetCodeStreamSpan(); JxlBitReader reader = new(span); - this.frameDecoder.InitializeFrame(reader, this.imageBundle, this.previewFrame); - - this.AdvanceCodeStream(reader.TotalBitsConsumed / JxlMath.BitsPerByte); - this.frameHeader = this.frameDecoder.GetFrameHeader(); + if (!JxlBundle.Read(reader, this.frameHeader)) + { + throw new InvalidImageContentException("The JPEG XL frame header is invalid or incomplete."); + } JxlFrameDimensions dim = this.frameHeader.FrameDimensions; @@ -1746,15 +1748,31 @@ public int ProcessCodestream() throw new InvalidOperationException("Frame is too large"); } + // The first real frame supplies constructor-bound dimensions. Later frames reuse the same codestream caches. + // The controller continues with this reader at the TOC; it must not read the frame header a second time. + JxlPassesSharedState shared = this.sharedState ??= new(this.Options.Configuration, this.metadata, dim); + JxlPassesDecoderState decoderState = this.passesState ??= new(shared); + if (this.desiredIntensityTarget > 0F) + { + decoderState.OutputEncodingInfo.DesiredIntensityTarget = this.desiredIntensityTarget; + } + + this.frameDecoder?.Dispose(); + this.frameDecoder = new(decoderState, this.frameHeader, dim, this.imageBundle, reader); + this.AdvanceCodeStream(reader.TotalBitsConsumed / JxlMath.BitsPerByte); + int outputType = this.previewFrame ? JxlDecoderStatus.PreviewImage : JxlDecoderStatus.FullImage; bool outputNeeded = (this.eventsWanted & outputType) != 0; if (outputNeeded) { + // Only this decoder owns these shared controls. Encoder roundtrips instead borrow their encoder's initialized controls. + shared.InitializeFrame(this.frameHeader, dim, false); + decoderState.InitializeFrame(this.frameHeader); this.frameDecoder.InitializeFrameOutput(); } - this.remainingFrameSize = this.frameDecoder.SumSectionSizes(); + this.remainingFrameSize = this.frameDecoder.SectionSizesSum; this.frameStage = JxlFrameStage.Toc; if (this.previewFrame) @@ -1861,7 +1879,8 @@ public int ProcessCodestream() this.dcFrameProgressionDone = false; this.nextSection = 0; this.sectionProcessed.Clear(); - ResizeSectionProcessed(this.frameDecoder.Toc.Size); + CollectionsMarshal.SetCount(this.sectionProcessed, this.frameDecoder.Toc.Count); + CollectionsMarshal.AsSpan(this.sectionProcessed).Clear(); if (this.previewFrame || (this.eventsWanted & JxlDecoderStatus.FullImage) != 0) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs index 9a2bc7c470..73be2d5705 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs @@ -1,8 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; -using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; @@ -13,96 +13,170 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; -internal sealed class JxlPassesDecoderState +/// +/// Owns decoder workspace and borrows the shared state used for one codestream or encoder roundtrip. +/// +internal sealed class JxlPassesDecoderState : IDisposable { - public JxlPassesDecoderState(JxlFrameHeader header, Configuration configuration) + /// + /// Initializes empty decoder workspace without allocating frame-sized sample buffers. + /// + /// The shared state, which its encoder or decoder caller owns and keeps alive. + public JxlPassesDecoderState(JxlPassesSharedState shared) { - this.XDmMultiplier = MathF.Pow(1 / 1.25f, header.XQmScale - 2f); - this.BDmMultiplier = MathF.Pow(1 / 1.25f, header.BQmScale - 2f); + this.Shared = shared; + this.Coefficients = new(shared.Configuration, 0, 0); + this.FrameStorageForReferencing = new(shared.CodecMetadata.ImageMetadata); + this.OutputEncodingInfo.SetFromMetadata(shared.CodecMetadata); - this.MainOutput.Callback = PixelCallback; - this.MainOutput.Buffer = null; - - this.UndoOrientation = JxlExifOrientation.Identity; - this.Upsampler8x = GetUpsamplingImage(configuration, this.Shared.CodecMetadata.CustomTransformData, 0, 3); - - if (header.LoopFilter?.EpfIterations > 0) - { - this.Sigma = new JxlPlane( - configuration, - (this.Shared.FrameDimensions.XSizeBlocks + 2) * SigmaPadding, - (this.Shared.FrameDimensions.YSizeBlocks + 2) * SigmaPadding); - } - - this.SharedStorage = new(configuration, header, false); - this.Shared = this.SharedStorage; + // Custom upsampling weights belong to the codestream header. Build the DC stage only after Shared is bound to those weights. + this.Upsampler8x = new UpsamplingStage(shared.Configuration, shared.CodecMetadata.CustomTransformData, 0, 3); } - public JxlPassesSharedState SharedStorage { get; set; } - - public JxlPassesSharedState Shared { get; set; } - - public RenderPipelineStageBase Upsampler8x { get; set; } = new(); - - public List Code { get; set; } = []; - - public List> ContextMap { get; set; } = []; - - public float XDmMultiplier { get; set; } - - public float BDmMultiplier { get; set; } - - public JxlPlane? Sigma { get; set; } - - public int Width { get; set; } - - public int Height { get; set; } - - public JxlImageOutput MainOutput { get; set; } - - public List ExtraOutput { get; set; } = []; - - public bool FastXybSRgb8Conversion { get; set; } - - public bool UnpremultiplyAlpha { get; set; } - - public JxlExifOrientation UndoOrientation { get; set; } - + /// + /// Gets the borrowed shared state. Its owner, rather than this workspace, releases its frame and reference storage. + /// + public JxlPassesSharedState Shared { get; } + + /// + /// Gets the owned DC upsampling stage built from the codestream's custom filter weights. + /// + public RenderPipelineStageBase Upsampler8x { get; } + + /// + /// Gets the entropy code for each pass of the current frame. + /// + public List Code { get; } = []; + + /// + /// Gets the entropy context maps for the current frame's passes. + /// + public List> ContextMap { get; } = []; + + /// + /// Gets the X-channel dequantization-matrix multiplier for the current frame. + /// + public float XDmMultiplier { get; private set; } + + /// + /// Gets the B-channel dequantization-matrix multiplier for the current frame. + /// + public float BDmMultiplier { get; private set; } + + /// + /// Gets the owned edge-preserving filter sigma plane, or null when that filter is disabled. + /// + public JxlPlane? Sigma { get; private set; } + + /// + /// Gets or sets the visible-frame index used to seed decoded noise. + /// public int VisibleFrameIndex { get; set; } + /// + /// Gets or sets the nonvisible-frame index used to seed decoded noise. + /// public int NonvisibleFrameIndex { get; set; } + /// + /// Gets or sets the transform-strategy bit mask used by this frame. + /// public int UsedAcs { get; set; } - public JxlDctAcImage Coefficients { get; set; } = []; + /// + /// Gets or sets the owned accumulation buffer. Empty storage selects direct coefficient decoding. + /// + public JxlDctAcImage Coefficients { get; set; } + /// + /// Gets or sets the frame's render pipeline, whose implementation is part of renderer integration. + /// public JxlRenderPipeline RenderPipeline { get; set; } - public JxlImageBundle FrameStorageForReferencing { get; set; } + /// + /// Gets the owned frame storage that can be transferred or copied into a shared reference slot after rendering. + /// + public JxlImageBundle FrameStorageForReferencing { get; } + + /// + /// Gets color-conversion information initialized from the codestream's metadata. + /// + public JxlOutputEncodingInfo OutputEncodingInfo { get; } = new(); + + /// + /// Initializes decoder-only frame workspace after the shared frame controls are available. + /// + /// The parsed header for the current frame. + /// Encoder roundtrips call this after preparing their own shared controls. This operation does not modify those controls. + public void InitializeFrame(JxlFrameHeader header) + { + this.XDmMultiplier = MathF.Pow(1F / 1.25F, header.XQmScale - 2F); + this.BDmMultiplier = MathF.Pow(1F / 1.25F, header.BQmScale - 2F); + this.UsedAcs = 0; + this.Code.Clear(); + this.ContextMap.Clear(); + this.FrameStorageForReferencing.RemoveColor(); + this.FrameStorageForReferencing.ClearExtraChannels(); - public JxlOutputEncodingInfo OutputEncodingInfo { get; set; } = new(); + if (header.LoopFilter?.EpfIterations > 0) + { + // Sigma has one sample per frame block, with two padding columns or rows on EACH edge. + // Add both borders to the block extent; multiplying the extent by the padding would change the sampling grid. + int width = this.Shared.FrameDimensions.XSizeBlocks + (2 * JxlDecoderCache.SigmaPadding); + int height = this.Shared.FrameDimensions.YSizeBlocks + (2 * JxlDecoderCache.SigmaPadding); + if (this.Sigma is null || !this.Sigma.ShrinkTo(width, height)) + { + JxlPlane sigma = new(this.Shared.Configuration, width, height); + this.Sigma?.Dispose(); + this.Sigma = sigma; + } + } + else + { + this.Sigma?.Dispose(); + this.Sigma = null; + } + } + /// + /// Reserves only the decoded coefficient orders needed by this frame's selected transform strategies. + /// + /// The number of progressive passes in this frame. + /// This is a bitstream-decoder operation. Encoder roundtrips already supply their coefficients in memory. public void InitializeForAc(int numPasses) { - this.SharedStorage.CoeffOrderSize = 0; - - for (int o = 0; o < JxlAcStrategy.NumberOfValidStrategies; o++) + this.Shared.CoeffOrderSize = 0; + for (int strategy = 0; strategy < JxlAcStrategy.NumberOfValidStrategies; strategy++) { - if (((1 << o) & this.UsedAcs) == 0) + if (((1 << strategy) & this.UsedAcs) == 0) { continue; } - int order = JxlCoefficientOrder.StrategyOrder[o]; - this.SharedStorage.CoeffOrderSize = Math.Max( + int order = JxlCoefficientOrder.StrategyOrder[strategy]; + this.Shared.CoeffOrderSize = Math.Max( JxlCoefficientOrder.CoefficientOrderOffsets[3 * (order + 1)] * JxlFrameDimensions.DctBlockSize, - this.SharedStorage.CoeffOrderSize); + this.Shared.CoeffOrderSize); } - int sz = numPasses * this.SharedStorage.CoeffOrderSize; - if (sz > this.SharedStorage.CoeffOrders.Count) + int length = numPasses * this.Shared.CoeffOrderSize; + if (length > this.Shared.CoeffOrders.Count) { - this.SharedStorage.CoeffOrders.Resize(sz); + int previousLength = this.Shared.CoeffOrders.Count; + CollectionsMarshal.SetCount(this.Shared.CoeffOrders, length); + CollectionsMarshal.AsSpan(this.Shared.CoeffOrders)[previousLength..].Clear(); } } + + /// + /// Releases decoder-owned workspace without disposing the borrowed shared state or its reference images. + /// + public void Dispose() + { + this.Sigma?.Dispose(); + this.Sigma = null; + this.Upsampler8x.Dispose(); + this.Coefficients.Dispose(); + this.FrameStorageForReferencing.Dispose(); + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchDictionary.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchDictionary.cs index 1c681daf70..375e8a38ed 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchDictionary.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchDictionary.cs @@ -16,7 +16,10 @@ internal sealed class JxlPatchDictionary { private const byte NumPatchBlendModes = (byte)JxlPatchBlendMode.AlphaWeightedAddBelow + 1; - private JxlReferenceFrame[] referenceFrames = new JxlReferenceFrame[4]; + /// + /// Borrows the codestream owner's reference slots. The owner binds them before decoding patches and retains their sample planes. + /// + private (JxlImageBundle Frame, bool ImageBundleIsInXyb)[] referenceFrames = []; private readonly List positions = []; private readonly List referencePositions = []; private readonly List blendings = []; @@ -28,7 +31,11 @@ internal sealed class JxlPatchDictionary public bool HasAny => this.positions.Count > 0; - public void SetShared(JxlReferenceFrame[] referenceFrames) => this.referenceFrames = referenceFrames; + /// + /// Borrows the codestream's reference slots without copying slots or taking ownership of their sample planes. + /// + /// The slots retained by the shared state for this codestream. + public void SetShared((JxlImageBundle Frame, bool ImageBundleIsInXyb)[] referenceFrames) => this.referenceFrames = referenceFrames; /// /// Clears the decoded dictionary and its row index before it is reused for another frame. diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlReferenceFrame.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlReferenceFrame.cs deleted file mode 100644 index 049783385b..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlReferenceFrame.cs +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Patch; - -internal record JxlReferenceFrame(JxlImageBundle Frame, bool ImageBundleIsInXyb = false); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyEncoder.cs index c0d4fe023f..9b56e51e8d 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyEncoder.cs @@ -916,9 +916,9 @@ public static bool ProcessAcStrategyRectangle( int ty = by / JxlChromaFromLuma.ColorTileDimensionInBlocks; InlineArray3 cmapFactors = default; - cmapFactors[0] = cmap.Base.YToXRatio(cmap.YToXMap!.GetRow(ty)[tx]); + cmapFactors[0] = cmap.Base.YToXRatio(cmap.YToXMap.GetRow(ty)[tx]); cmapFactors[1] = 0f; - cmapFactors[2] = cmap.Base.YToBRatio(cmap.YToBMap!.GetRow(ty)[tx]); + cmapFactors[2] = cmap.Base.YToBRatio(cmap.YToBMap.GetRow(ty)[tx]); if (cparams.SpeedTier > JxlSpeedTier.Hare) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlCflHeuristics.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlCflHeuristics.cs index 4dd0a93980..922fd4e4f1 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlCflHeuristics.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlCflHeuristics.cs @@ -110,8 +110,8 @@ public void ComputeTile( rectangle, fast, acStrategy is null, - cmap.YToXMap!, - cmap.YToBMap!, + cmap.YToXMap, + cmap.YToBMap, this.dcValues, scratch); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs index f838356c4c..971d38e412 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs @@ -721,6 +721,15 @@ private static void TileDistMap(JxlPlane distMap, int tileSize, int margi } } + /// + /// Renders an encoder's prepared coefficients while borrowing its shared controls. + /// + /// The configuration that owns temporary decoded samples. + /// The frame settings used to prepare the coefficients. + /// The borrowed encoder input used for the frame extent. + /// The encoder that owns shared controls and coefficient images. + /// The color conversion provider used by the existing encoder pipeline. + /// The decoded bundle, which the caller owns and must dispose. private static JxlImageBundle RoundtripImage( Configuration configuration, JxlFrameHeader frameHeader, @@ -728,11 +737,6 @@ private static JxlImageBundle RoundtripImage( JxlPassesEncoderState encState, JxlCmsInterface cms) { - JxlPassesDecoderState decState = new(frameHeader, configuration); - - decState.OutputEncodingInfo.SetFromMetadata(encState.Shared.Metadata); - decState.Shared = encState.Shared; - if (opsin.YSize % JxlFrameDimensions.BlockDimensions != 0) { throw new InvalidOperationException("The opsin height must be divisible by the block dimension."); @@ -741,83 +745,77 @@ private static JxlImageBundle RoundtripImage( int xSizeGroups = JxlMath.DivCeil(opsin.XSize, JxlFrameDimensions.GroupDimensions); int ySizeGroups = JxlMath.DivCeil(opsin.YSize, JxlFrameDimensions.GroupDimensions); int numGroups = xSizeGroups * ySizeGroups; - int numSpecialFrames = encState.SpecialFrames.Count; - int numPasses = encState.ProgressiveSplitter.GetNumPasses(); - JxlModularFrameEncoder modularFrameEncoder = JxlModularFrameEncoder.Create(configuration, frameHeader, encState.CParams, false); InitializePassesEncoder(configuration, frameHeader, opsin, opsin.GetRectangle(), cms, encState, modularFrameEncoder, null); - decState.Initialize(frameHeader); - decState.InitializeForAc(configuration, numPasses); + // The encoder has prepared its controls. Borrow that exact state instead of allocating another frame's planes and replacing a pointer. + // Memory block loading supplies AC coefficients directly, so this path must not resize or overwrite decoded coefficient orders. + using JxlPassesDecoderState decState = new(encState.Shared); + decState.InitializeFrame(frameHeader); - JxlImageBundle decoded = new(configuration, encState.Shared.Metadata.M) + JxlImageBundle decoded = new(encState.Shared.CodecMetadata.ImageMetadata) { Origin = frameHeader.FrameOrigin }; - JxlImage3 tmp = new(configuration, opsin.XSize, opsin.YSize); - - if (!decoded.SetFromImage(tmp, decState.OutputEncodingInfo.ColorEncoding)) - { - throw new InvalidOperationException("Could not set image bundle"); - } - - JxlPassesDecoderState.PipelineOptions options = new() + try { - UseSlowRenderPipeline = false, - Coalescing = false, - RenderSpotcolors = false, - RenderNoise = false - }; - - JxlImageMetadata metadata = decoded.Metadata; + JxlImage3 tmp = new(configuration, opsin.XSize, opsin.YSize); + if (!decoded.SetFromImage(tmp, decState.OutputEncodingInfo.ColorEncoding)) + { + tmp.Dispose(); + throw new InvalidOperationException("Could not set image bundle"); + } - decState.PreparePipeline(frameHeader, encState.Shared.Metadata.ImageMetadata, decoded, options); + JxlPassesDecoderState.PipelineOptions options = new() + { + UseSlowRenderPipeline = false, + Coalescing = false, + RenderSpotcolors = false, + RenderNoise = false + }; - using JxlGroupDecoderCache groupDecCache = new(configuration); + JxlImageMetadata metadata = decoded.Metadata; + decState.PreparePipeline(frameHeader, encState.Shared.CodecMetadata.ImageMetadata, decoded, options); - decState.RenderPipeline.PrepareForThreads(1, useGroupIds: false); + using JxlGroupDecoderCache groupDecCache = new(configuration); + decState.RenderPipeline.PrepareForThreads(1, false); - void ProcessGroup(int groupIndex) - { - if (frameHeader.LoopFilter!.EpfIterations > 0) + void ProcessGroup(int groupIndex) { - JxlLoopFilter.ComputeSigma( - decState.Shared.FrameDimensions.BlockGroupRect(groupIndex), - decState); - } + if (frameHeader.LoopFilter!.EpfIterations > 0) + { + JxlLoopFilter.ComputeSigma(decState.Shared.FrameDimensions.BlockGroupRect(groupIndex), decState); + } + + RenderPipelineInput input = decState.RenderPipeline.GetInputBuffers(groupIndex, 0); + JxlGroupDecoder.DecodeGroupForRoundtrip(frameHeader, encState.Coeffs, groupIndex, decState, groupDecCache, 0, input, null, null); - RenderPipelineInput input = decState.RenderPipeline.GetInputBuffers(groupIndex, 0); + for (int c = 0; c < metadata.ExtraChannelCount; c++) + { + (JxlPlane plane, Rectangle rect) = input.GetBuffer(3 + c); + JxlImageOperations.FillPlane(0.0f, plane, rect); + } - JxlGroupDecoder.DecodeGroupForRoundtrip( - frameHeader, - encState.Coeffs, - groupIndex, - decState, - groupDecCache, - 0, - input, - null, - null); + input.Done(); + } - for (int c = 0; c < metadata.ExtraChannelCount; c++) + for (int i = 0; i < numGroups; i++) { - (JxlPlane plane, Rectangle rect) = input.GetBuffer(3 + c); - JxlImageOperations.FillPlane(0.0f, plane, rect); + ProcessGroup(i); } - input.Done(); + encState.SpecialFrames.RemoveRange(numSpecialFrames, encState.SpecialFrames.Count - numSpecialFrames); + return decoded; } - - for (int i = 0; i < numGroups; i++) + catch { - ProcessGroup(i); + // Success transfers this bundle to the caller. Failure releases only the locally decoded output, never the encoder's shared planes. + decoded.Dispose(); + throw; } - encState.SpecialFrames.RemoveRange(numSpecialFrames, encState.SpecialFrames.Count - numSpecialFrames); - - return decoded; } public static float InitialQuantDc(float butteraugliTarget) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs index 2ac90e6738..836868e619 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs @@ -1,7 +1,9 @@ // 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.Jxl.IO.Entropy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; @@ -51,9 +53,30 @@ public static uint CoeffOrderContext(uint value) return Math.Min(token, PermutationContexts - 1u); } + /// + /// Reads a complete permutation stream, including its entropy tables and final state. + /// + /// The configuration whose allocator owns entropy history and permutation scratch. + /// The leading entries that keep their original order. + /// The permutation length, already limited by the owning field. + /// The destination permutation. + /// The reader at the permutation's entropy header. + /// Whether the permutation and final entropy state are valid. + public static bool DecodePermutation(Configuration configuration, int skip, int size, Span order, JxlBitReader bitReader) + { + JxlAnsCode code = new(); + List contextMap = []; + JxlAnsReader.DecodeHistograms(configuration, bitReader, PermutationContexts, code, ref contextMap, false); + using JxlAnsSymbolReader reader = JxlAnsSymbolReader.Create(configuration, code, bitReader, 0); + ReadOnlySpan contexts = CollectionsMarshal.AsSpan(contextMap); + + return ReadPermutation(configuration, skip, size, order, bitReader, reader, contexts) && reader.CheckAnsFinalState(); + } + /// /// Reads a coefficient permutation from an initialized entropy stream. /// + /// The configuration whose allocator owns permutation scratch. /// The leading coefficients that keep their original order. /// The coefficient count. /// The destination permutation, or an empty span when only validation is required. @@ -61,34 +84,44 @@ public static uint CoeffOrderContext(uint value) /// The caller-owned entropy reader. /// The stable mapping from permutation contexts to histograms. /// Whether the Lehmer code describes a valid permutation. - public static bool ReadPermutation(int skip, int size, Span order, JxlBitReader bitReader, JxlAnsSymbolReader reader, ReadOnlySpan contextMap) + public static bool ReadPermutation( + Configuration configuration, + int skip, + int size, + Span order, + JxlBitReader bitReader, + JxlAnsSymbolReader reader, + ReadOnlySpan contextMap) { DebugGuard.MustBeLessThanOrEqualTo(size, 65536, nameof(size)); - Span lehmer = stackalloc uint[size]; - lehmer.Clear(); - - Span temp = stackalloc uint[size * 2]; - temp.Clear(); - // Borrow the reader's window once; all coefficient symbols advance the same entropy and LZ77 state. Span window = reader.Lz77Window; - uint end = (uint)reader.ReadHybridUInt((int)CoeffOrderContext((uint)size), bitReader, contextMap, window) + (uint)skip; - - if (end > size) + uint encodedEnd = unchecked((uint)reader.ReadHybridUInt((int)CoeffOrderContext((uint)size), bitReader, contextMap, window)); + if (encodedEnd > (uint)(size - skip)) { - throw new InvalidOperationException("Invalid permutation size"); + return false; } + int end = skip + (int)encodedEnd; + + // The largest TOC has 65,536 entries. Keep its Lehmer codes and tree scratch in one allocator-owned buffer, not on the stack. + // Omitted Lehmer entries mean zero. The tree decoder initializes its own next-power-of-two workspace. + using IMemoryOwner storage = configuration.MemoryAllocator.Allocate(size * 3); + Span scratch = storage.Memory.Span; + Span lehmer = scratch[..size]; + Span temp = scratch[size..]; + lehmer.Clear(); + uint last = 0; for (int i = skip; i < end; i++) { - lehmer[i] = (uint)reader.ReadHybridUInt((int)CoeffOrderContext(last), bitReader, contextMap, window); + lehmer[i] = unchecked((uint)reader.ReadHybridUInt((int)CoeffOrderContext(last), bitReader, contextMap, window)); last = lehmer[i]; if (lehmer[i] >= size - i) { - throw new InvalidOperationException("Invalid lehmer code"); + return false; } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelationMap.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelationMap.cs index 0fb3e1112b..6593788e8a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelationMap.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelationMap.cs @@ -9,34 +9,81 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing; /// /// JPEG XL Color correlation map. /// -internal sealed class JxlColorCorrelationMap +internal sealed class JxlColorCorrelationMap : IDisposable { - public JxlColorCorrelation Base { get; set; } = new(); + /// + /// Allocates both sample planes before publishing a usable correlation map. + /// + /// The configuration that owns plane storage. + /// The frame width in pixels. + /// The frame height in pixels. + /// Whether to use the XYB model's default B correlation. + private JxlColorCorrelationMap(Configuration configuration, int width, int height, bool xyb) + { + int xBlocks = JxlMath.DivCeil(width, JxlChromaFromLuma.ColorTileDimension); + int yBlocks = JxlMath.DivCeil(height, JxlChromaFromLuma.ColorTileDimension); + this.YToXMap = new(configuration, xBlocks, yBlocks); - public JxlPlane? YToXMap { get; set; } + try + { + this.YToBMap = new(configuration, xBlocks, yBlocks); + } + catch + { + // Construction has not published the map. Release the first allocation if the second cannot be acquired. + this.YToXMap.Dispose(); + throw; + } - public JxlPlane? YToBMap { get; set; } + this.YToXMap.Clear(); + this.YToBMap.Clear(); + if (!xyb) + { + this.Base.BaseCorrelationB = 0; + } - public bool DecodeDc(JxlBitReader reader) => this.Base.DecodeDc(reader); + this.Base.RecomputeDcFactors(); + } - public static JxlColorCorrelationMap Create(Configuration configuration, int width, int height, bool xyb = true) - { - JxlColorCorrelationMap map = new(); + /// + /// Gets the scalar correlation factors used with the map samples. + /// + public JxlColorCorrelation Base { get; } = new(); - (int xBlocks, int yBlocks) = (JxlMath.DivCeil(width, JxlChromaFromLuma.ColorTileDimension), JxlMath.DivCeil(height, JxlChromaFromLuma.ColorTileDimension)); + /// + /// Gets the owned Y-to-X sample plane. Callers borrow the plane without taking ownership. + /// + public JxlPlane YToXMap { get; } - map.YToXMap = new JxlPlane(configuration, xBlocks, yBlocks); - map.YToBMap = new JxlPlane(configuration, xBlocks, yBlocks); + /// + /// Gets the owned Y-to-B sample plane. Callers borrow the plane without taking ownership. + /// + public JxlPlane YToBMap { get; } - map.YToXMap.Clear(); - map.YToBMap.Clear(); + /// + /// Reads the frame's scalar DC color-correlation factors. + /// + /// The encoded factor fields. + /// Whether the factors decoded successfully. + public bool DecodeDc(JxlBitReader reader) => this.Base.DecodeDc(reader); - if (!xyb) - { - map.Base.BaseCorrelationB = 0; - } + /// + /// Allocates zeroed correlation planes for the supplied frame extent. + /// + /// The configuration that owns plane storage. + /// The frame width in pixels. + /// The frame height in pixels. + /// Whether the map uses the XYB model's default B correlation. + /// The map that owns both sample planes. + public static JxlColorCorrelationMap Create(Configuration configuration, int width, int height, bool xyb = true) + => new(configuration, width, height, xyb); - map.Base.RecomputeDcFactors(); - return map; + /// + /// Releases the owned color-correlation planes. + /// + public void Dispose() + { + this.YToXMap.Dispose(); + this.YToBMap.Dispose(); } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs index ae426f9d7e..2ffc746c38 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs @@ -1,123 +1,268 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -#pragma warning disable SA1401 // Fields should be private - +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; -using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Patch; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; -internal class JxlPassesSharedState +/// +/// Owns shared frame controls and the DC and reference-frame caches for one codestream. +/// +/// +/// Frame initialization replaces frame controls but retains cached reference images. +/// Decoder workspaces borrow this object. They do not dispose it, including during encoder roundtrips. +/// +internal sealed class JxlPassesSharedState : IDisposable { - // Make this a field so we can get a ref to it - public JxlBlockContextMap BlockContextMap; + /// + /// The decoded block-context map, kept as a field because its decoder replaces it through a reference. + /// +#pragma warning disable SA1401 // Field decoding replaces this shared object through a reference. + public JxlBlockContextMap BlockContextMap = new(); +#pragma warning restore SA1401 + + /// + /// Initializes empty sample storage and reference slots after the first frame header supplies its dimensions. + /// + /// The configuration that owns sample allocations. + /// The metadata shared by all frames in this codestream. + /// The first frame's parsed dimensions. + public JxlPassesSharedState(Configuration configuration, JxlCodecMetadata metadata, JxlFrameDimensions frameDimensions) + { + this.Configuration = configuration; + this.CodecMetadata = metadata; + this.FrameDimensions = frameDimensions; + this.AcStrategy = JxlAcStrategyImage.Create(configuration, 0, 0); + this.ColorMap = JxlColorCorrelationMap.Create(configuration, 0, 0); + this.Quantizer = new(this.Matrices); + this.Dc = this.DcStorage; - public JxlCodecMetadata CodecMetadata { get; set; } = new(); + // Slots belong to the codestream, not to a single frame. Empty planes allocate no sample buffers. + // A decoded DC image can later move into a slot; Dc only borrows whichever owned image the frame selects. + for (int i = 0; i < this.DcFrames.Length; i++) + { + this.DcFrames[i] = new(); + this.ReferenceFrames[i] = (new JxlImageBundle(metadata.ImageMetadata), false); + } - public JxlFrameDimensions FrameDimensions { get; set; } + this.ImageFeatures.PatchDictionary.SetShared(this.ReferenceFrames); + } + + /// + /// Gets the configuration that owns this codestream's sample storage. + /// + public Configuration Configuration { get; } + + /// + /// Gets the original metadata shared by the codestream's frames. + /// + public JxlCodecMetadata CodecMetadata { get; } + + /// + /// Gets the dimensions supplied by the current parsed frame header. + /// + public JxlFrameDimensions FrameDimensions { get; private set; } - public JxlAcStrategyImage AcStrategy { get; set; } + /// + /// Gets the owned transform-selection plane for the current frame. + /// + public JxlAcStrategyImage AcStrategy { get; private set; } - public JxlDequantMatrices Matrices { get; set; } = new(); + /// + /// Gets the current dequantization matrices. + /// + public JxlDequantMatrices Matrices { get; private set; } = new(); - public JxlQuantizer Quantizer { get; set; } + /// + /// Gets the quantizer bound to the current matrices. + /// + public JxlQuantizer Quantizer { get; private set; } - public JxlPlane RawQuantField { get; set; } + /// + /// Gets the current frame's quantization field in block coordinates. + /// + public JxlPlane RawQuantField { get; private set; } = new(); - public JxlPlane EpfSharpness { get; set; } + /// + /// Gets the current frame's edge-preserving filter sharpness values. + /// + public JxlPlane EpfSharpness { get; private set; } = new(); - public JxlColorCorrelationMap ColorMap { get; set; } + /// + /// Gets the owned color-correlation planes for the current frame. + /// + public JxlColorCorrelationMap ColorMap { get; private set; } - public JxlImageFeatures ImageFeatures { get; set; } = new(); + /// + /// Gets decoded patches, splines and noise parameters. + /// + public JxlImageFeatures ImageFeatures { get; } = new(); + /// + /// Gets or sets the coefficient count occupied by one pass's orders. + /// public int CoeffOrderSize { get; set; } /// - /// Gets or sets coefficient indices, arranged as consecutive pass-sized orders. + /// Gets coefficient indices arranged as consecutive pass-sized orders. /// /// Large transforms need indices greater than 255. Keep the indices as integers until coefficient lookup. - public List CoeffOrders { get; set; } = []; + public List CoeffOrders { get; } = []; - public JxlPlane QuantDc { get; set; } + /// + /// Gets the current frame's quantized DC values. + /// + public JxlPlane QuantDc { get; private set; } = new(); - public JxlImage3 DcStorage { get; set; } + /// + /// Gets the owned DC image used when the frame does not reference a cached DC image. + /// + public JxlImage3 DcStorage { get; } = new(); - public JxlImage3 Dc { get; set; } + /// + /// Gets the borrowed DC image, which is either or one entry in . + /// + public JxlImage3 Dc { get; private set; } - public JxlImage3[] DcFrames { get; set; } = new JxlImage3[4]; + /// + /// Gets the four owned DC reference images retained across frame initialization. + /// + public JxlImage3[] DcFrames { get; } = new JxlImage3[JxlShared.MaximumNumberOfReferenceFrames]; - public JxlReferenceFrame[] ReferenceFrames { get; set; } = new JxlReferenceFrame[4]; + /// + /// Gets the four owned reference-frame slots borrowed by patches and frame blending. + /// + /// + /// Each slot owns its bundle. The flag distinguishes samples saved before the color transform from rendered samples. + /// Update slots through this array so patch consumers see the same bundle and flag; modifying a copied tuple does not update a slot. + /// Replacement must transfer or copy planes instead of creating another owner of the same planes. + /// + public (JxlImageBundle Frame, bool ImageBundleIsInXyb)[] ReferenceFrames { get; } = new (JxlImageBundle, bool)[JxlShared.MaximumNumberOfReferenceFrames]; + /// + /// Gets or sets the number of clustered histograms used by each pass. + /// public int NumHistograms { get; set; } - public JxlPassesSharedState(Configuration configuration, JxlFrameHeader frameHeader, bool encoder) + /// + /// Initializes frame-sized controls after the frame header and dimensions have been validated. + /// + /// The parsed frame header, which borrows this codestream's metadata. + /// The validated dimensions already derived from that header. + /// Whether the encoder must reserve the complete coefficient orders before strategy selection. + public void InitializeFrame(JxlFrameHeader frameHeader, JxlFrameDimensions dimensions, bool encoder) { - if (frameHeader.Metadata is null) + this.FrameDimensions = dimensions; + + // Quantizer references must follow their matrices. Neither object owns sample buffers. + // Reset both together so a later frame cannot use the previous frame's decoded quantization parameters. + this.Matrices = new(); + this.Quantizer = new(this.Matrices); + + JxlAcStrategyImage strategy = JxlAcStrategyImage.Create(this.Configuration, dimensions.XSizeBlocks, dimensions.YSizeBlocks); + this.AcStrategy.Dispose(); + this.AcStrategy = strategy; + + // Existing plane storage can serve smaller or equal frames. Reallocate only when its original capacity cannot hold the new dimensions. + if (!this.RawQuantField.ShrinkTo(dimensions.XSizeBlocks, dimensions.YSizeBlocks)) { - throw new InvalidOperationException("The frame header metadata is missing"); + JxlPlane quantField = new(this.Configuration, dimensions.XSizeBlocks, dimensions.YSizeBlocks); + this.RawQuantField.Dispose(); + this.RawQuantField = quantField; } - this.CodecMetadata = frameHeader.Metadata; - this.FrameDimensions = frameHeader.FrameDimensions; - this.ImageFeatures.PatchDictionary.SetShared(this.ImageFeatures.ReferenceFrames); - - JxlFrameDimensions dimensions = frameHeader.FrameDimensions; + if (!this.EpfSharpness.ShrinkTo(dimensions.XSizeBlocks, dimensions.YSizeBlocks)) + { + JxlPlane sharpness = new(this.Configuration, dimensions.XSizeBlocks, dimensions.YSizeBlocks); + this.EpfSharpness.Dispose(); + this.EpfSharpness = sharpness; + } - this.AcStrategy = JxlAcStrategyImage.Create(configuration, dimensions.XSizeBlocks, dimensions.YSizeBlocks); - this.RawQuantField = new JxlPlane(configuration, dimensions.XSizeBlocks, dimensions.YSizeBlocks); - this.EpfSharpness = new JxlPlane(configuration, dimensions.XSizeBlocks, dimensions.YSizeBlocks); - this.ColorMap = JxlColorCorrelationMap.Create(configuration, dimensions.XSize, dimensions.YSize); + JxlColorCorrelationMap colorMap = JxlColorCorrelationMap.Create(this.Configuration, dimensions.XSize, dimensions.YSize); + this.ColorMap.Dispose(); + this.ColorMap = colorMap; this.CoeffOrderSize = JxlCoefficientOrder.CoefficientOrderMaxSize; - - if (encoder && - this.CoeffOrders.Count < ((int)frameHeader.Passes.NumberOfPasses * JxlCoefficientOrder.CoefficientOrderMaxSize) && - frameHeader.Encoding == JxlFrameEncoding.VarDct) + if (encoder && frameHeader.Encoding == JxlFrameEncoding.VarDct) { - // Each progressive pass has one complete coefficient order. Reserve consecutive pass-sized ranges. - int upperBound = (int)frameHeader.Passes.NumberOfPasses * JxlCoefficientOrder.CoefficientOrderMaxSize; - int length = this.CoeffOrders.Count; - int delta = upperBound - length; - - for (int i = 0; i < delta; i++) + int length = (int)frameHeader.Passes.NumberOfPasses * this.CoeffOrderSize; + if (this.CoeffOrders.Count < length) { - this.CoeffOrders.Add(0); // default constant + int previousLength = this.CoeffOrders.Count; + CollectionsMarshal.SetCount(this.CoeffOrders, length); + CollectionsMarshal.AsSpan(this.CoeffOrders)[previousLength..].Clear(); } } - this.QuantDc = new JxlPlane(configuration, dimensions.XSizeBlocks, dimensions.YSizeBlocks); + if (!this.QuantDc.ShrinkTo(dimensions.XSizeBlocks, dimensions.YSizeBlocks)) + { + JxlPlane quantDc = new(this.Configuration, dimensions.XSizeBlocks, dimensions.YSizeBlocks); + this.QuantDc.Dispose(); + this.QuantDc = quantDc; + } bool useDcFrame = (frameHeader.Flags & (ulong)JxlFrameHeaderFlags.Dc) != 0; if (!encoder && useDcFrame) { if (frameHeader.DcLevel == 4) { - throw new InvalidOperationException("DC level for DC frames cannot be equal to 4"); + throw new InvalidImageContentException("JPEG XL cannot reference a DC image at level four."); } - this.DcStorage = new JxlImage3(); - this.Dc = this.DcFrames[(int)frameHeader.DcLevel]; - - if (this.Dc.XSize == 0) + // This index is the format's reference level. Saving a DC frame uses dcLevel - 1 at the controller, not here. + JxlImage3 dc = this.DcFrames[(int)frameHeader.DcLevel]; + if (dc.XSize == 0) { - throw new InvalidOperationException("DC frame was specified for DC Level = " + frameHeader.DcLevel + ", but frame wasn't decoded with level " + frameHeader.DcLevel + 1); + throw new InvalidImageContentException("The referenced JPEG XL DC image has not been decoded."); } + this.DcStorage.Dispose(); + this.Dc = dc; this.QuantDc.Clear(); } else { - this.DcStorage = new JxlImage3(configuration, dimensions.XSizeBlocks, dimensions.YSizeBlocks); + if (!this.DcStorage.ShrinkTo(dimensions.XSizeBlocks, dimensions.YSizeBlocks)) + { + // Swap new storage into the stable owned image. The temporary then owns and releases only the previous storage. + using JxlImage3 dc = new(this.Configuration, dimensions.XSizeBlocks, dimensions.YSizeBlocks); + this.DcStorage.Swap(dc); + } + this.Dc = this.DcStorage; } + } + + /// + /// Releases frame controls, decoded features and every owned reference image for this codestream. + /// + public void Dispose() + { + this.AcStrategy.Dispose(); + this.RawQuantField.Dispose(); + this.EpfSharpness.Dispose(); + this.ColorMap.Dispose(); + this.QuantDc.Dispose(); + this.DcStorage.Dispose(); + this.ImageFeatures.PatchDictionary.Clear(); + this.ImageFeatures.Splines.Clear(); + + // Dc is a borrowed alias. Dispose only the storage owners, so a selected reference is never released twice through that alias. + foreach (JxlImage3 dc in this.DcFrames) + { + dc.Dispose(); + } - this.Quantizer = new(this.Matrices); + foreach ((JxlImageBundle frame, bool _) in this.ReferenceFrames) + { + frame.Dispose(); + } } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlToc.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlToc.cs index d3de060b83..8d5888a9c2 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlToc.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlToc.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; @@ -11,13 +12,11 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing; /// internal static class JxlToc { - private const int BitsPerByte = 8; - private const int MaxTocEntries = 65536; public static readonly JxlU32Enc TocDistribution = new( JxlFieldExpressions.Bits(10), - JxlFieldExpressions.BitsOffset(14, 2024), + JxlFieldExpressions.BitsOffset(14, 1024), JxlFieldExpressions.BitsOffset(22, 17408), JxlFieldExpressions.BitsOffset(30, 4211712)); @@ -30,6 +29,13 @@ public static int MaxBits(int numSizes) public static int AcGroupIndex(int pass, int group, int numGroups, int numDcGroups) => 2 + numDcGroups + (pass * numGroups) + group; + /// + /// Gets the section count for a frame and rejects counts above the format decoder's allocation limit. + /// + /// The number of transform or modular groups. + /// The number of DC groups. + /// The frame's progressive pass count. + /// The section count. public static int NumberOfTocEntries(int numGroups, int numDcGroups, int numPasses) { if (numGroups == 1 && numPasses == 1) @@ -37,69 +43,36 @@ public static int NumberOfTocEntries(int numGroups, int numDcGroups, int numPass return 1; } - return AcGroupIndex(0, 0, numGroups, numDcGroups) + (numGroups * numPasses); - } - - public static bool ReadToc( - Configuration configuration, - int tocEntries, - JxlBitReader reader, - List sizes, - List permutation) - { - if (tocEntries > MaxTocEntries) - { - return false; // too many TOC entries - } - - sizes.Clear(); - sizes.Capacity = tocEntries; - - for (int i = 0; i < tocEntries; i++) - { - sizes.Add(0); - } - - if (reader.TotalBitsConsumed >= reader.TotalBytes * BitsPerByte) + // Frame dimensions originate in untrusted headers. Widen before multiplication, not after an Int32 count has wrapped. + long count = 2L + numDcGroups + ((long)numGroups * numPasses); + if (count > MaxTocEntries) { - return false; // not enough bytes + throw new InvalidImageContentException("The JPEG XL frame has too many sections."); } - bool CheckBitBudget(int numEntries) - { - long minimalBitCost = numEntries * (2 + 10); - long bitBudget = reader.TotalBytes * BitsPerByte; - long expenses = reader.TotalBitsConsumed; - - return expenses <= bitBudget && - minimalBitCost <= bitBudget - expenses; - } + return (int)count; + } - if (tocEntries <= 0) - { - return false; - } + /// + /// Reads physical section sizes and an optional permutation of logical section identifiers. + /// + /// The configuration whose allocator owns permutation scratch and entropy history. + /// The section count validated by from the parsed frame header. + /// The reader at the section table. Truncated fields raise . + /// The decoded sizes, in physical stream order. + /// The logical-to-physical permutation, or an empty list when the table uses natural order. + /// Whether the section count and optional permutation are valid. + public static bool ReadToc(Configuration configuration, int tocEntries, JxlBitReader reader, List sizes, List permutation) + { + CollectionsMarshal.SetCount(sizes, tocEntries); + permutation.Clear(); + // The shared bit reader rejects truncated fields. It needs no seekable input or copied byte-budget state here. + // Permutation indices are UInt32 values: a valid table can contain far more than 256 sections. if (reader.ReadBits32(1) == 1) { - if (!CheckBitBudget(tocEntries)) - { - return false; - } - - permutation.Clear(); - - for (int i = 0; i < tocEntries; i++) - { - permutation.Add(default); - } - - if (!DecodePermutation( - configuration, - 0, - tocEntries, - permutation, - reader)) + CollectionsMarshal.SetCount(permutation, tocEntries); + if (!JxlCoefficientOrder.DecodePermutation(configuration, 0, tocEntries, CollectionsMarshal.AsSpan(permutation), reader)) { return false; } @@ -107,86 +80,14 @@ bool CheckBitBudget(int numEntries) reader.JumpToByteBoundary(); - if (!CheckBitBudget(tocEntries)) - { - return false; - } - + Span sectionSizes = CollectionsMarshal.AsSpan(sizes); for (int i = 0; i < tocEntries; i++) { - sizes[i] = JxlU32Coder.Read(TocDistribution, reader); + sectionSizes[i] = JxlU32Coder.Read(TocDistribution, reader); } reader.JumpToByteBoundary(); - return CheckBitBudget(0); - } - - public static bool ReadGroupOffsets( - Configuration configuration, - int tocEntries, - JxlBitReader reader, - List offsets, - List sizes, - out ulong totalSize) - { - totalSize = 0; - - List permutation = []; - - if (!ReadToc( - configuration, - tocEntries, - reader, - sizes, - permutation)) - { - return false; - } - - offsets.Clear(); - offsets.Capacity = tocEntries; - - for (int i = 0; i < tocEntries; i++) - { - offsets.Add(0); - } - - ulong offset = 0; - - for (int i = 0; i < tocEntries; i++) - { - ulong size = sizes[i]; - - if (offset + size < offset) - { - return false; - } - - offsets[i] = offset; - offset += size; - } - - totalSize = offset; - - if (permutation.Count != 0) - { - List permutedOffsets = new(tocEntries); - List permutedSizes = new(tocEntries); - - foreach (byte index in permutation) - { - permutedOffsets.Add(offsets[index]); - permutedSizes.Add(sizes[index]); - } - - offsets.Clear(); - offsets.AddRange(permutedOffsets); - - sizes.Clear(); - sizes.AddRange(permutedSizes); - } - return true; } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlFrameDimensions.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlFrameDimensions.cs index 7bd67326a3..d46ee1533a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlFrameDimensions.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlFrameDimensions.cs @@ -10,7 +10,24 @@ internal sealed class JxlFrameDimensions public const int GroupDimensions = 256; public const int GroupDimensionsInBlocks = GroupDimensions / BlockDimensions; - public JxlFrameDimensions(int xSizePixel, int ySizePixel, int groupSizeShift, int maxHorizontalShift, int maxVerticalShift, bool modularMode, int upsampling) + /// + /// Derives pixel, block and group extents from a parsed frame header. + /// + /// The frame width in pixels after upsampling. + /// The frame height in pixels after upsampling. + /// The encoded group-size shift, from zero to three. + /// The largest horizontal chroma-subsampling shift. + /// The largest vertical chroma-subsampling shift. + /// Whether pixel extents exclude DCT padding. + /// The encoded upsampling factor, which is a positive power of two up to eight. + public JxlFrameDimensions( + int xSizePixel, + int ySizePixel, + int groupSizeShift, + int maxHorizontalShift, + int maxVerticalShift, + bool modularMode, + int upsampling) { this.GroupDimension = (GroupDimensions >> 1) << groupSizeShift; this.DcGroupDimension = this.GroupDimension * BlockDimensions; @@ -31,12 +48,15 @@ public JxlFrameDimensions(int xSizePixel, int ySizePixel, int groupSizeShift, in this.XSizeUpsampledPadded = this.XSizePadded * upsampling; this.YSizeUpsampledPadded = this.YSizePadded * upsampling; - this.XSizeGroups = JxlMath.DivCeil(this.XSize, GroupDimensions); - this.YSizeGroups = JxlMath.DivCeil(this.YSize, GroupDimensions); - this.XSizeDcGroups = JxlMath.DivCeil(this.XSizeBlocks, GroupDimensions); - this.YSizeDcGroups = JxlMath.DivCeil(this.YSizeBlocks, GroupDimensions); - this.NumGroups = this.XSizeGroups * this.YSizeGroups; - this.NumDcGroups = this.XSizeDcGroups * this.YSizeDcGroups; + + // GroupSizeShift selects 128, 256, 512 or 1024 pixels per group side. The fixed 256-pixel default is not the parsed group size. + // DC groups span that same number of blocks per side, so their count divides the block extent rather than the pixel extent. + this.XSizeGroups = JxlMath.DivCeil(this.XSize, this.GroupDimension); + this.YSizeGroups = JxlMath.DivCeil(this.YSize, this.GroupDimension); + this.XSizeDcGroups = JxlMath.DivCeil(this.XSizeBlocks, this.GroupDimension); + this.YSizeDcGroups = JxlMath.DivCeil(this.YSizeBlocks, this.GroupDimension); + this.NumGroups = checked(this.XSizeGroups * this.YSizeGroups); + this.NumDcGroups = checked(this.XSizeDcGroups * this.YSizeDcGroups); } public int XSize { get; set; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/UpsamplingStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/UpsamplingStage.cs index d4b28a1226..b492ea728f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/UpsamplingStage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/UpsamplingStage.cs @@ -13,21 +13,29 @@ internal class UpsamplingStage : RenderPipelineStageBase private const int ChunkSize = 1024; private readonly float[] kernel = new float[64 * 25]; // 1600 floats, 6400 bytes (6.25 KB) - private readonly int c; + private readonly int channelIndex; private readonly InlineArray3>> temp = default; - public UpsamplingStage(JxlCustomTransformData upsFactors, int c, int shift) + /// + /// Initializes a new instance of the UpsamplingStage class. + /// + /// The configuration used by the render stage. + /// The custom upsampling filter weights. + /// The channel processed by this stage. + /// The base-two logarithm of the upsampling factor. + public UpsamplingStage(Configuration configuration, JxlCustomTransformData transformData, int channelIndex, int upsamplingShift) + : base(configuration) { - this.c = c; - this.Settings = RenderPipelineStageConfiguration.CreateSymmetric(shift, 2); + this.channelIndex = channelIndex; + this.Settings = RenderPipelineStageConfiguration.CreateSymmetric(upsamplingShift, 2); Span kernel = this.kernel; - Span weights = shift == 1 ? upsFactors.Upsampling2Weights - : shift == 2 ? upsFactors.Upsampling4Weights - : upsFactors.Upsampling8Weights; + Span weights = upsamplingShift == 1 ? transformData.Upsampling2Weights + : upsamplingShift == 2 ? transformData.Upsampling4Weights + : transformData.Upsampling8Weights; - int n = 1 << shift; + int n = 1 << upsamplingShift; int h = n / 2; for (int ky = 0; ky < h; ++ky) @@ -62,7 +70,7 @@ public UpsamplingStage(JxlCustomTransformData upsFactors, int c, int shift) public override string Name => "Upsample"; public override RenderPipelineChannelMode GetChannelMode(int channel) => - channel == this.c + channel == this.channelIndex ? RenderPipelineChannelMode.InOut : RenderPipelineChannelMode.Ignored; } From 602f8f000a44f354cf23f2c385c8a923177ff881 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 22:00:26 +1000 Subject: [PATCH 269/328] Reuse JPEG XL weighted prediction storage and vectorize error histories --- .../Encoder/Modular/JxlModularFrameEncoder.cs | 81 --- .../Formats/Jxl/Processing/JxlScopeGuard.cs | 26 - .../ContextPrediction/JxlContextPrediction.cs | 593 ++++++++++++++---- .../ContextPrediction/JxlModularState.cs | 267 ++++++-- .../Modular/Encoding/JxlModularEncoding.cs | 477 ++++++++------ .../Encoding/JxlModularEncodingEncoder.cs | 320 ++++++++-- .../Modular/Transforms/JxlPalette.cs | 38 +- .../Jxl/Processing/EntropyCoderTests.cs | 149 +++++ 8 files changed, 1386 insertions(+), 565 deletions(-) delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlScopeGuard.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Modular/JxlModularFrameEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Modular/JxlModularFrameEncoder.cs index 6957420abe..81fec58c01 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Modular/JxlModularFrameEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Modular/JxlModularFrameEncoder.cs @@ -236,87 +236,6 @@ private static bool FloatToInt(ReadOnlySpan rowIn, Span rowOut, int return true; } - private static float EstimateWeightedPredictionCost(JxlModularImage img, int i) - { - int extraBits = 0; - float histoCost = 0; - JxlAnsHybridUIntConfiguration config = new(); - - ReadOnlySpan cutoffs = - [ - -500, -392, -255, -191, -127, -95, -63, -47, -31, - -23, -15, -11, -7, -4, -3, -1, 0, 1, - 3, 5, 7, 11, 15, 23, 31, 47, 63, - 95, 127, 191, 255, 392, 500 - ]; - - int nc = cutoffs.Length + 1; - JxlHistogram[] histo = new JxlHistogram[nc]; - - for (int j = 0; j < nc; j++) - { - histo[j] = new JxlHistogram(); - } - - JxlModularHeader wpHeader = new(); - JxlContextPrediction.SetPredictorMode(i, wpHeader); - - foreach (JxlModularChannel ch in img.Channels) - { - int oneRow = ch.Plane.PixelsPerRow; - JxlModularState wpState = new(wpHeader, ch.Width); - Span properties = [1]; - bool unhealthy = false; - - for (int y = 0; y < ch.Height; y++) - { - ReadOnlySpan r = ch.GetRow(y); - - for (int x = 0; x < ch.Width; x++) - { - int offset = 0; - - int left = x != 0 ? r[x - 1] : y != 0 ? r[x - oneRow] : 0; - int top = y != 0 ? r[x - oneRow] : left; - int topleft = x != 0 && y != 0 ? r[x - 1 - oneRow] : left; - int topright = x + 1 < ch.Width && y != 0 ? r[x + 1 - oneRow] : top; - int toptop = y > 1 ? r[x - oneRow - oneRow] : top; - - long guess = wpState.Predict(true, x, y, ch.Width, top, left, topright, topleft, toptop, properties, offset); - - int ctx = 0; - - foreach (int c in cutoffs) - { - ctx += c >= properties[0] ? 1 : 0; - } - - unhealthy |= JxlMath.SubOverflow((uint)r[x], (uint)guess, out uint res); - - config.Encode(JxlPackSigned.PackUnsigned((int)res), out uint token, out uint nbits, out uint bits); - - histo[ctx].Add((int)token); - extraBits += (int)nbits; - - wpState.UpdatePredictionErrors(r[x], x, y, ch.Width); - } - } - - if (unhealthy) - { - return float.MaxValue; - } - - foreach (JxlHistogram h in histo) - { - histoCost += h.ComputeShannonEntropy(); - h.Clear(); - } - } - - return histoCost + extraBits; - } - private static bool DoTransform(Configuration configuration, JxlModularImage image, JxlTransform tr, JxlGroupHeader wpHeader, bool forceJxlArt = false) { JxlTransform t = tr; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlScopeGuard.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlScopeGuard.cs deleted file mode 100644 index de553f17cd..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlScopeGuard.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; - -/// -/// Conditionally invokes the specified method when -/// appropriate and when it goes out of scope. -/// -internal struct JxlScopeGuard(Action action) : IDisposable -{ - /// - /// When true the action will be invoked when Dispose() is called. - /// - private bool isArmed = true; - - public void Disarm() => this.isArmed = false; - - public readonly void Dispose() - { - if (this.isArmed) - { - action(); - } - } -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlContextPrediction.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlContextPrediction.cs index b2add731ad..df3618c44b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlContextPrediction.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlContextPrediction.cs @@ -151,15 +151,30 @@ public static int Select(int a, int b, int c) return pa < pb ? a : b; } - public static void PrecomputeReferences(JxlModularChannel channel, int y, JxlModularImage image, int i, JxlModularChannel references) + /// + /// Prepares the current row's reference properties in storage borrowed before the channel traversal. + /// + /// The channel whose samples will be predicted. + /// The current row. + /// The image containing already reconstructed reference channels. + /// The current channel index. Only earlier channels can supply references. + /// The reference-record dimensions and physical stride. + /// The caller-borrowed reference plane, cleared and reused for this row. + public static void PrecomputeReferences( + JxlModularChannel channel, + int y, + JxlModularImage image, + int i, + JxlModularChannel references, + Span referenceStorage) { - references.Plane.Clear(); + referenceStorage.Clear(); int offset = 0; int numExtraProps = references.Width; int oneRow = references.Plane.PixelsPerRow; JxlModularChannel channelI = image.Channels[i]; - for (int j = i - 1; i >= 0 && offset < numExtraProps; j--) + for (int j = i - 1; j >= 0 && offset < numExtraProps; j--) { JxlModularChannel channelJ = image.Channels[j]; @@ -174,12 +189,15 @@ public static void PrecomputeReferences(JxlModularChannel channel, int y, JxlMod continue; } - Span rp = references.GetRow(0)[offset..]; - Span rpp = channelJ.GetRow(y); - Span rpprev = channelJ.GetRow(y > 0 ? y - 1 : 0); + Span source = channelJ.GetRow(0); + int sourceStride = channelJ.Plane.PixelsPerRow; + Span rpp = source[(y * sourceStride)..]; + Span rpprev = source[((y > 0 ? y - 1 : 0) * sourceStride)..]; - for (int x = 0; x < channel.Width; x++, rp = rp[oneRow..]) + for (int x = 0; x < channel.Width; x++) { + // A reference record is one row of this transposed plane. Address it directly; advancing a final prefixed span can exceed the allocation. + Span rp = referenceStorage.Slice((x * oneRow) + offset, ExtraPropertiesPerChannel); int v = rpp[x]; rp[0] = Numerics.Abs(v); rp[1] = v; @@ -200,18 +218,18 @@ public static void PrecomputeReferences(JxlModularChannel channel, int y, JxlMod } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void InitializePropertiesForRow(Span p, InlineArray2 staticProperties, int y) + public static void InitializePropertiesForRow(Span properties, InlineArray2 staticProperties, int y) { - p[0] = staticProperties[0]; - p[1] = staticProperties[1]; - p[2] = y; - p[9] = 0; // Local gradient + properties[0] = staticProperties[0]; + properties[1] = staticProperties[1]; + properties[2] = y; + properties[9] = 0; // Local gradient } // Prediction for one pixel using neighbors [MethodImpl(InliningOptions.HotPath)] // This method is called frequently public static int PredictOne( - JxlPredictor p, + JxlPredictor predictor, int left, int top, int toptop, @@ -219,13 +237,13 @@ public static int PredictOne( int topright, int leftleft, int toprightright, - int wpPred) => p switch + int weightedPrediction) => predictor switch { JxlPredictor.Zero => 0, JxlPredictor.Left => left, JxlPredictor.Top => top, JxlPredictor.Select => Select(left, top, topleft), - JxlPredictor.Weighted => wpPred, + JxlPredictor.Weighted => weightedPrediction, JxlPredictor.Gradient => ClampedGradient(left, top, topleft), JxlPredictor.TopLeft => topleft, JxlPredictor.TopRight => topright, @@ -240,90 +258,112 @@ public static int PredictOne( _ => 0, }; + /// + /// Predicts a modular sample and evaluates the context properties selected by the mode. + /// + /// The prediction and property-evaluation flags. + /// The destination context properties. + /// The number of samples in the current row. + /// The reference used as the origin for neighboring samples. + /// The distance between row starts, in samples. + /// The current sample column. + /// The current sample row. + /// The predictor used when tree selection is disabled. + /// The optional prediction tree. + /// The exact precomputed reference-property record for this pixel, empty when no reference properties are used. + /// The optional weighted-prediction owner. + /// The borrowed prediction storage, empty when weighted prediction is disabled. + /// The independently live channel slot when weighted prediction is enabled. + /// The destination for all predictor results when requested by the mode. + /// The predicted value and entropy context. public static JxlPredictionResult Predict( JxlPredictorMode mode, - Span p, // contains properties - int w, // block width - ref int pp, // This is a reference to the output pixel stored in row-major order. Negative offsets are accessed to reference other pixels in the image, specifically neighboring pixles. - int oneRow, // Number of pixels on one row + Span properties, + int width, + ref int currentSample, + int rowStride, int x, int y, JxlPredictor predictor, - JxlMaTreeLookup? lookup, - JxlModularChannel? references, - JxlModularState? wpState, + JxlMaTreeLookup? treeLookup, + ReadOnlySpan referenceProperties, + JxlModularState? weightedPredictorState, + Span weightedStorage, + int weightedStateIndex, Span predictions) { int offset = 3; // Start at position 3 because of 2 static properties + y // Status flags - // computeProperties = should the p (properties) variable be updated? - // nec = are there no edge cases? bool computeProperties = (mode & JxlPredictorMode.UseTree) != 0 || (mode & JxlPredictorMode.ForceComputeProperties) != 0; - bool nec = (mode & JxlPredictorMode.NoEdgeCases) != 0; - - // The following variables are neighboring pixels relative to the pixel to predict. - // Pixels may be unavailable and therefore replaced with default values. For example, - // at Y=0, the top pixel may not be available because we're already at the very top - // of the image, there's no "above" of that. - int left = nec || x > 0 ? Unsafe.Subtract(ref pp, 1) : (y > 0 ? Unsafe.Subtract(ref pp, oneRow) : 0); // ⬅️ (or 0 if unavailable) - int top = nec || y > 0 ? Unsafe.Subtract(ref pp, oneRow) : left; // ⬆️ (or ⬅️ if unavailable) - int topleft = nec || (x > 0 && y > 0) ? Unsafe.Add(ref pp, -1 - oneRow) : left; // ↗️ (or ⬅️ if unavailable) - int topright = nec || (x + 1 < w && y > 0) ? Unsafe.Add(ref pp, 1 - oneRow) : top; // ↖️ (or ⬆️ if unavailable) - int leftleft = nec || x > 1 ? Unsafe.Subtract(ref pp, 2) : left; // ⬅️⬅️ (or ⬅️ if unavailable) - int toptop = nec || y > 1 ? Unsafe.Add(ref pp, -oneRow - oneRow) : top; // ⬆️⬆️ (or ⬆️ if unavailable) - int toprightright = nec || (x + 2 < w && y > 0) ? Unsafe.Add(ref pp, 2 - oneRow) : topright; // ↗️➡️ (or ↗️ if unavailable) + bool skipEdgeChecks = (mode & JxlPredictorMode.NoEdgeCases) != 0; + + // Interior callers omit edge checks only when every addressed neighbor exists. + // At the left edge use north, or zero on the first row. Missing north and diagonal samples use the nearest available neighbor. + int left = skipEdgeChecks || x > 0 ? Unsafe.Subtract(ref currentSample, 1) : (y > 0 ? Unsafe.Subtract(ref currentSample, rowStride) : 0); + int top = skipEdgeChecks || y > 0 ? Unsafe.Subtract(ref currentSample, rowStride) : left; + int topleft = skipEdgeChecks || (x > 0 && y > 0) ? Unsafe.Add(ref currentSample, -1 - rowStride) : left; + int topright = skipEdgeChecks || (x + 1 < width && y > 0) ? Unsafe.Add(ref currentSample, 1 - rowStride) : top; + int leftleft = skipEdgeChecks || x > 1 ? Unsafe.Subtract(ref currentSample, 2) : left; + int toptop = skipEdgeChecks || y > 1 ? Unsafe.Add(ref currentSample, -rowStride - rowStride) : top; + int toprightright = skipEdgeChecks || (x + 2 < width && y > 0) ? Unsafe.Add(ref currentSample, 2 - rowStride) : topright; if (computeProperties) { - p[offset++] = x; - p[offset++] = top > 0 ? top : -top; - p[offset++] = left > 0 ? left : -left; - p[offset++] = top; - p[offset++] = left; + properties[offset++] = x; + properties[offset++] = top > 0 ? top : -top; + properties[offset++] = left > 0 ? left : -left; + properties[offset++] = top; + properties[offset++] = left; // Local gradient - p[offset] = left - p[offset + 1]; + properties[offset] = left - properties[offset + 1]; offset++; // Local gradient - p[offset++] = left + top - topleft; + properties[offset++] = left + top - topleft; // FFV1 context properties - p[offset++] = left - topleft; - p[offset++] = topleft - top; - p[offset++] = top - topright; - p[offset++] = top - toptop; - p[offset++] = left - leftleft; + properties[offset++] = left - topleft; + properties[offset++] = topleft - top; + properties[offset++] = top - topright; + properties[offset++] = top - toptop; + properties[offset++] = left - leftleft; } // Predicted weighted prediction value - int wpPred = 0; + int weightedPrediction = 0; if ((mode & JxlPredictorMode.UseWeightedPrediction) != 0) { - if (wpState is null) + if (weightedPredictorState is null) { throw new InvalidOperationException("Weighted prediction state is missing"); } - wpPred = unchecked((int)wpState.Predict(computeProperties, x, y, w, top, left, topright, topleft, toptop, p, offset)); + weightedPrediction = unchecked((int)weightedPredictorState.Predict( + computeProperties, + x, + y, + weightedStorage, + weightedStateIndex, + top, + left, + topright, + topleft, + toptop, + properties, + offset)); } - if (!nec && computeProperties) + if (!skipEdgeChecks && computeProperties) { - if (references is null) - { - throw new InvalidOperationException("References are missing"); - } - offset += NumberOfProperties; - // Extra properties - Span rp = references.GetRow(x); - for (int i = 0; i < references.Width; i++) + // The caller slices one pixel's reference record from storage borrowed before the row loop. + foreach (int value in referenceProperties) { - p[offset++] = rp[i]; + properties[offset++] = value; } } @@ -331,12 +371,12 @@ public static JxlPredictionResult Predict( if ((mode & JxlPredictorMode.UseTree) != 0) { - if (lookup is null) + if (treeLookup is null) { throw new InvalidOperationException("Lookup is missing"); } - JxlMaTreeLookupResult result = lookup.Lookup(p); + JxlMaTreeLookupResult result = treeLookup.Lookup(properties); predictor = result.Predictor; predResult = new((int)result.Context, result.Offset, default, result.Multiplier); } @@ -345,13 +385,13 @@ public static JxlPredictionResult Predict( { for (int i = 0; i < JxlPredictorFacts.ModularPredictors; i++) { - predictions[i] = PredictOne((JxlPredictor)i, left, top, toptop, topleft, topright, leftleft, toprightright, wpPred); + predictions[i] = PredictOne((JxlPredictor)i, left, top, toptop, topleft, topright, leftleft, toprightright, weightedPrediction); } } predResult = new( predResult.Context, - predResult.Guess + PredictOne(predictor, left, top, toptop, topleft, topright, leftleft, toprightright, wpPred), + predResult.Guess + PredictOne(predictor, left, top, toptop, topleft, topright, leftleft, toprightright, weightedPrediction), predictor, predResult.Multiplier); @@ -361,125 +401,412 @@ public static JxlPredictionResult Predict( // The following methods are just wrappers over the Predict // method. // See https://github.com/libjxl/libjxl/blob/main/lib/jxl/modular/encoding/context_predict.h#L593-L709 + /// + /// Predicts a sample using the fixed mode selected by this entry point. + /// + /// The number of samples in the current row. + /// The reference used as the origin for neighboring samples. + /// The distance between row starts, in samples. + /// The current sample column. + /// The current sample row. + /// The predictor to evaluate. + /// The predicted sample and entropy context. public static JxlPredictionResult PredictNoTreeNoWeightedPrediction( - int w, - ref int pp, - int oneRow, + int width, + ref int currentSample, + int rowStride, int x, int y, JxlPredictor predictor) - => Predict(0, [], w, ref pp, oneRow, x, y, predictor, null, null, null, []); + => Predict(0, [], width, ref currentSample, rowStride, x, y, predictor, null, [], null, [], 0, []); + /// + /// Predicts a sample using the fixed mode selected by this entry point. + /// + /// The number of samples in the current row. + /// The reference used as the origin for neighboring samples. + /// The distance between row starts, in samples. + /// The current sample column. + /// The current sample row. + /// The predictor to evaluate. + /// The operation's weighted-prediction owner. + /// The storage borrowed when the channel or palette histories were reset. + /// The independently live channel slot, zero for a sequential channel. + /// The predicted sample and entropy context. public static JxlPredictionResult PredictNoTreeWeightedPrediction( - int w, - ref int pp, - int oneRow, + int width, + ref int currentSample, + int rowStride, int x, int y, JxlPredictor predictor, - JxlModularState wpState) - => Predict(JxlPredictorMode.UseTree, [], w, ref pp, oneRow, x, y, predictor, null, null, wpState, []); + JxlModularState weightedPredictorState, + Span weightedStorage, + int weightedStateIndex) + => Predict( + JxlPredictorMode.UseWeightedPrediction, + [], + width, + ref currentSample, + rowStride, + x, + y, + predictor, + null, + [], + weightedPredictorState, + weightedStorage, + weightedStateIndex, + []); + /// + /// Predicts a sample using the fixed mode selected by this entry point. + /// + /// The destination context properties. + /// The number of samples in the current row. + /// The reference used as the origin for neighboring samples. + /// The distance between row starts, in samples. + /// The current sample column. + /// The current sample row. + /// The prediction tree. + /// The exact precomputed reference-property record for this pixel, empty when no reference properties are used. + /// The predicted sample and entropy context. public static JxlPredictionResult PredictTreeNoWeightedPrediction( - Span p, - int w, - ref int pp, - int oneRow, + Span properties, + int width, + ref int currentSample, + int rowStride, int x, int y, JxlMaTreeLookup treeLookup, - JxlModularChannel references) - => Predict(JxlPredictorMode.UseTree, p, w, ref pp, oneRow, x, y, JxlPredictor.Zero, treeLookup, references, null, []); + ReadOnlySpan referenceProperties) + => Predict( + JxlPredictorMode.UseTree, + properties, + width, + ref currentSample, + rowStride, + x, + y, + JxlPredictor.Zero, + treeLookup, + referenceProperties, + null, + [], + 0, + []); + /// + /// Predicts a sample using the fixed mode selected by this entry point. + /// + /// The destination context properties. + /// The number of samples in the current row. + /// The reference used as the origin for neighboring samples. + /// The distance between row starts, in samples. + /// The current sample column. + /// The current sample row. + /// The prediction tree. + /// The exact precomputed reference-property record for this pixel, empty when no reference properties are used. + /// The predicted sample and entropy context. public static JxlPredictionResult PredictTreeNoWeightedPredictionNoEdgeCases( - Span p, - int w, - ref int pp, - int oneRow, + Span properties, + int width, + ref int currentSample, + int rowStride, int x, int y, JxlMaTreeLookup treeLookup, - JxlModularChannel references) - => Predict(JxlPredictorMode.UseTree | JxlPredictorMode.NoEdgeCases, p, w, ref pp, oneRow, x, y, JxlPredictor.Zero, treeLookup, references, null, []); + ReadOnlySpan referenceProperties) + => Predict( + JxlPredictorMode.UseTree | JxlPredictorMode.NoEdgeCases, + properties, + width, + ref currentSample, + rowStride, + x, + y, + JxlPredictor.Zero, + treeLookup, + referenceProperties, + null, + [], + 0, + []); + /// + /// Predicts a sample using the fixed mode selected by this entry point. + /// + /// The destination context properties. + /// The number of samples in the current row. + /// The reference used as the origin for neighboring samples. + /// The distance between row starts, in samples. + /// The current sample column. + /// The current sample row. + /// The prediction tree. + /// The exact precomputed reference-property record for this pixel, empty when no reference properties are used. + /// The operation's weighted-prediction owner. + /// The storage borrowed when the channel or palette histories were reset. + /// The independently live channel slot, zero for a sequential channel. + /// The predicted sample and entropy context. public static JxlPredictionResult PredictTreeWeightedPrediction( - Span p, - int w, - ref int pp, - int oneRow, + Span properties, + int width, + ref int currentSample, + int rowStride, int x, int y, JxlMaTreeLookup treeLookup, - JxlModularChannel references, - JxlModularState wpState) - => Predict(JxlPredictorMode.UseTree | JxlPredictorMode.UseWeightedPrediction, p, w, ref pp, oneRow, x, y, JxlPredictor.Zero, treeLookup, references, wpState, []); + ReadOnlySpan referenceProperties, + JxlModularState weightedPredictorState, + Span weightedStorage, + int weightedStateIndex) + => Predict( + JxlPredictorMode.UseTree | JxlPredictorMode.UseWeightedPrediction, + properties, + width, + ref currentSample, + rowStride, + x, + y, + JxlPredictor.Zero, + treeLookup, + referenceProperties, + weightedPredictorState, + weightedStorage, + weightedStateIndex, + []); + /// + /// Predicts a sample using the fixed mode selected by this entry point. + /// + /// The destination context properties. + /// The number of samples in the current row. + /// The reference used as the origin for neighboring samples. + /// The distance between row starts, in samples. + /// The current sample column. + /// The current sample row. + /// The prediction tree. + /// The exact precomputed reference-property record for this pixel, empty when no reference properties are used. + /// The operation's weighted-prediction owner. + /// The storage borrowed when the channel or palette histories were reset. + /// The independently live channel slot, zero for a sequential channel. + /// The predicted sample and entropy context. public static JxlPredictionResult PredictTreeWeightedPredictionNoEdgeCases( - Span p, - int w, - ref int pp, - int oneRow, + Span properties, + int width, + ref int currentSample, + int rowStride, int x, int y, JxlMaTreeLookup treeLookup, - JxlModularChannel references, - JxlModularState wpState) - => Predict(JxlPredictorMode.UseTree | JxlPredictorMode.UseWeightedPrediction | JxlPredictorMode.NoEdgeCases, p, w, ref pp, oneRow, x, y, JxlPredictor.Zero, treeLookup, references, wpState, []); + ReadOnlySpan referenceProperties, + JxlModularState weightedPredictorState, + Span weightedStorage, + int weightedStateIndex) + => Predict( + JxlPredictorMode.UseTree | JxlPredictorMode.UseWeightedPrediction | JxlPredictorMode.NoEdgeCases, + properties, + width, + ref currentSample, + rowStride, + x, + y, + JxlPredictor.Zero, + treeLookup, + referenceProperties, + weightedPredictorState, + weightedStorage, + weightedStateIndex, + []); + /// + /// Predicts a sample using the fixed mode selected by this entry point. + /// + /// The destination context properties. + /// The number of samples in the current row. + /// The reference used as the origin for neighboring samples. + /// The distance between row starts, in samples. + /// The current sample column. + /// The current sample row. + /// The predictor to evaluate. + /// The exact precomputed reference-property record for this pixel, empty when no reference properties are used. + /// The operation's weighted-prediction owner. + /// The storage borrowed when the channel or palette histories were reset. + /// The independently live channel slot, zero for a sequential channel. + /// The predicted sample and entropy context. public static JxlPredictionResult PredictLearn( - Span p, - int w, - ref int pp, - int oneRow, + Span properties, + int width, + ref int currentSample, + int rowStride, int x, int y, JxlPredictor predictor, - JxlModularChannel references, - JxlModularState wpState) - => Predict(JxlPredictorMode.ForceComputeProperties | JxlPredictorMode.UseWeightedPrediction, p, w, ref pp, oneRow, x, y, predictor, null, references, wpState, []); + ReadOnlySpan referenceProperties, + JxlModularState weightedPredictorState, + Span weightedStorage, + int weightedStateIndex) + => Predict( + JxlPredictorMode.ForceComputeProperties | JxlPredictorMode.UseWeightedPrediction, + properties, + width, + ref currentSample, + rowStride, + x, + y, + predictor, + null, + referenceProperties, + weightedPredictorState, + weightedStorage, + weightedStateIndex, + []); + /// + /// Predicts a sample using the fixed mode selected by this entry point. + /// + /// The destination context properties. + /// The number of samples in the current row. + /// The reference used as the origin for neighboring samples. + /// The distance between row starts, in samples. + /// The current sample column. + /// The current sample row. + /// The exact precomputed reference-property record for this pixel, empty when no reference properties are used. + /// The operation's weighted-prediction owner. + /// The storage borrowed when the channel or palette histories were reset. + /// The independently live channel slot, zero for a sequential channel. + /// The destination for all predictor results. + /// The predicted sample and entropy context. public static JxlPredictionResult PredictLearnAll( - Span p, - int w, - ref int pp, - int oneRow, + Span properties, + int width, + ref int currentSample, + int rowStride, int x, int y, - JxlModularChannel references, - JxlModularState wpState, + ReadOnlySpan referenceProperties, + JxlModularState weightedPredictorState, + Span weightedStorage, + int weightedStateIndex, Span predictions) - => Predict(JxlPredictorMode.ForceComputeProperties | JxlPredictorMode.UseWeightedPrediction | JxlPredictorMode.AllPredictions, p, w, ref pp, oneRow, x, y, JxlPredictor.Zero, null, references, wpState, predictions); + => Predict( + JxlPredictorMode.ForceComputeProperties | JxlPredictorMode.UseWeightedPrediction | JxlPredictorMode.AllPredictions, + properties, + width, + ref currentSample, + rowStride, + x, + y, + JxlPredictor.Zero, + null, + referenceProperties, + weightedPredictorState, + weightedStorage, + weightedStateIndex, + predictions); + /// + /// Predicts a sample using the fixed mode selected by this entry point. + /// + /// The destination context properties. + /// The number of samples in the current row. + /// The reference used as the origin for neighboring samples. + /// The distance between row starts, in samples. + /// The current sample column. + /// The current sample row. + /// The predictor to evaluate. + /// The exact precomputed reference-property record for this pixel, empty when no reference properties are used. + /// The operation's weighted-prediction owner. + /// The storage borrowed when the channel or palette histories were reset. + /// The independently live channel slot, zero for a sequential channel. + /// The predicted sample and entropy context. public static JxlPredictionResult PredictLearnNoEdgeCases( - Span p, - int w, - ref int pp, - int oneRow, + Span properties, + int width, + ref int currentSample, + int rowStride, int x, int y, JxlPredictor predictor, - JxlModularChannel references, - JxlModularState wpState) - => Predict(JxlPredictorMode.ForceComputeProperties | JxlPredictorMode.UseWeightedPrediction | JxlPredictorMode.NoEdgeCases, p, w, ref pp, oneRow, x, y, predictor, null, references, wpState, []); + ReadOnlySpan referenceProperties, + JxlModularState weightedPredictorState, + Span weightedStorage, + int weightedStateIndex) + => Predict( + JxlPredictorMode.ForceComputeProperties | JxlPredictorMode.UseWeightedPrediction | JxlPredictorMode.NoEdgeCases, + properties, + width, + ref currentSample, + rowStride, + x, + y, + predictor, + null, + referenceProperties, + weightedPredictorState, + weightedStorage, + weightedStateIndex, + []); + /// + /// Predicts a sample using the fixed mode selected by this entry point. + /// + /// The destination context properties. + /// The number of samples in the current row. + /// The reference used as the origin for neighboring samples. + /// The distance between row starts, in samples. + /// The current sample column. + /// The current sample row. + /// The exact precomputed reference-property record for this pixel, empty when no reference properties are used. + /// The operation's weighted-prediction owner. + /// The storage borrowed when the channel or palette histories were reset. + /// The independently live channel slot, zero for a sequential channel. + /// The destination for all predictor results. + /// The predicted sample and entropy context. public static JxlPredictionResult PredictLearnAllNoEdgeCases( - Span p, - int w, - ref int pp, - int oneRow, + Span properties, + int width, + ref int currentSample, + int rowStride, int x, int y, - JxlModularChannel references, - JxlModularState wpState, + ReadOnlySpan referenceProperties, + JxlModularState weightedPredictorState, + Span weightedStorage, + int weightedStateIndex, Span predictions) - => Predict(JxlPredictorMode.ForceComputeProperties | JxlPredictorMode.UseWeightedPrediction | JxlPredictorMode.AllPredictions | JxlPredictorMode.NoEdgeCases, p, w, ref pp, oneRow, x, y, JxlPredictor.Zero, null, references, wpState, predictions); + => Predict( + JxlPredictorMode.ForceComputeProperties | JxlPredictorMode.UseWeightedPrediction | JxlPredictorMode.AllPredictions | JxlPredictorMode.NoEdgeCases, + properties, + width, + ref currentSample, + rowStride, + x, + y, + JxlPredictor.Zero, + null, + referenceProperties, + weightedPredictorState, + weightedStorage, + weightedStateIndex, + predictions); + /// + /// Predicts a sample using the fixed mode selected by this entry point. + /// + /// The number of samples in the current row. + /// The reference used as the origin for neighboring samples. + /// The distance between row starts, in samples. + /// The current sample column. + /// The current sample row. + /// The destination for all predictor results. + /// The predicted sample and entropy context. public static JxlPredictionResult PredictAllNoWeightedPrediction( - int w, - ref int pp, - int oneRow, + int width, + ref int currentSample, + int rowStride, int x, int y, Span predictions) - => Predict(JxlPredictorMode.AllPredictions, [], w, ref pp, oneRow, x, y, JxlPredictor.Zero, null, null, null, predictions); + => Predict(JxlPredictorMode.AllPredictions, [], width, ref currentSample, rowStride, x, y, JxlPredictor.Zero, null, [], null, [], 0, predictions); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs index 2b112ff3cc..412075a283 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs @@ -1,38 +1,89 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; /// -/// State for context prediction +/// Owns reusable weighted-prediction histories for one modular operation. /// -internal sealed class JxlModularState +/// +/// Sequential channels reset and reuse one slot. General palette search keeps one slot per interleaved channel. +/// Reset binds the current header and clears active history before prediction starts. The owner is not shared by concurrent operations. +/// +internal sealed class JxlModularState : IDisposable { /// /// Rounding constant for predictions. /// private const long PredictionRound = ((1 << 3) >> 1) - 1; - private InlineArray4 prediction; - private long pred; - private readonly uint[][] predErrors = new uint[4][]; - private readonly int[] error; - private readonly JxlModularHeader header; + // Each slot starts with four candidate predictions and the final prediction (five Int64 values = ten UInt32 words). + // It then stores two signed-error rows and two predictor-error rows. Four predictor errors are adjacent at each position. + private const int PredictorCount = 4; + private const int HistoryRows = 2; + + // Keep the native two-position row margin. Error updates can accumulate into the column immediately after the logical row. + private const int RowMargin = 2; + private const int PredictionWords = (PredictorCount + 1) * (sizeof(long) / sizeof(uint)); + private const int HistoryWordsPerPosition = HistoryRows * (PredictorCount + 1); + private readonly IMemoryOwner storageOwner; + private readonly int wordsPerSlot; + private readonly int channelCount; + private JxlModularHeader header; + private int width; + private int rowLength; - public JxlModularState(JxlModularHeader header, int width) + /// + /// Allocates fixed-capacity prediction storage for one operation. Sequential channels reuse one slot; + /// interleaved palette channels retain distinct slots through their complete pixel traversal. + /// + /// The configuration that owns the numeric storage. + /// The initial weighted-prediction settings. Reset binds the settings for each new history. + /// The widest channel this operation will process. + /// The number of concurrently live channel histories, normally one. + public JxlModularState(Configuration configuration, JxlModularHeader header, int maximumWidth, int channelCount) + { + this.header = header; + this.channelCount = channelCount; + this.wordsPerSlot = checked(PredictionWords + (HistoryWordsPerPosition * (maximumWidth + RowMargin))); + this.storageOwner = configuration.MemoryAllocator.Allocate(checked(this.wordsPerSlot * channelCount)); + } + + /// + /// Binds the current header and clears every active history without allocating or changing capacity. + /// + /// The parsed or encoder-selected settings for the new channel or palette iteration. + /// The active channel width, within the maximum prepared by the operation owner. + /// The complete borrowed storage. Pass this span through prediction loops without acquiring Memory.Span again. + /// + /// Call once before an independent sequential channel, or once before all interleaved palette channels. + /// Borrowed storage must not outlive this owner. Reset invalidates prior prediction history, not the allocation. + /// + public Span Reset(JxlModularHeader header, int width) { this.header = header; + this.width = width; + this.rowLength = width + RowMargin; + Span storage = this.storageOwner.GetSpan()[..(this.wordsPerSlot * this.channelCount)]; + int activeWords = PredictionWords + (HistoryWordsPerPosition * this.rowLength); - for (int i = 0; i < 4; i++) + for (int channel = 0; channel < this.channelCount; channel++) { - this.predErrors[i] = new uint[(width + 2) * 2]; + storage.Slice(channel * this.wordsPerSlot, activeWords).Clear(); } - this.error = new int[(width + 2) * 2]; + return storage; } + /// + public void Dispose() => this.storageOwner.Dispose(); + /// /// Gets a table for approximating division by a number from /// 1 to 64. It is defined as follows. @@ -63,59 +114,122 @@ public JxlModularState(JxlModularHeader header, int width) [MethodImpl(MethodImplOptions.AggressiveInlining)] private static long AddBits(long x) => x << 3; + /// + /// Approximates the fixed-point inverse error weight without losing the unsigned error range. + /// + /// The accumulated UInt32 error. The calculation widens it before adding one. + /// The four-bit maximum weight from the parsed header. + /// The approximated weight, always at least four. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static uint ErrorWeight(int x, uint maxWeight) + public static uint ErrorWeight(uint error, uint maxWeight) { - int shift = Math.Max(0, JxlMath.FloorLog2Nonzero(x + 1) - 5); // Math.Max call ensures the value isn't negative - return 4 + ((maxWeight * DivisionLookup[x >> shift]) >> shift); + int shift = Math.Max(0, (int)JxlMath.FloorLog2Nonzero((ulong)error + 1) - 5); + return 4 + ((maxWeight * DivisionLookup[(int)(error >> shift)]) >> shift); } - public static long WeightedAverage(Span p, Span w) + /// + /// Combines four integer predictions with the native fixed-point normalization and rounding. + /// + /// Four predictions retaining three fractional bits. + /// Four weights of at least four, replaced by their normalized values. + /// The weighted prediction, still retaining its fractional bits. + private static long WeightedAverage(Span predictions, Span weights) { - uint weightSum = (w[0] + w[1]) + (w[2] + w[3]); - - if (weightSum <= 15) - { - throw new InvalidOperationException("Weight sum is too low"); - } + // ErrorWeight returns at least four for each of the four predictors, so the sum is at least sixteen. + // Four-bit header weights also bound the sum below UInt32 overflow before normalization. + uint weightSum = (weights[0] + weights[1]) + (weights[2] + weights[3]); int logWeight = (int)JxlMath.FloorLog2Nonzero(weightSum); weightSum = 0; - for (int i = 0; i < 4; i++) + for (int i = 0; i < PredictorCount; i++) { - w[i] >>= logWeight - 4; - weightSum += w[i]; + weights[i] >>= logWeight - 4; + weightSum += weights[i]; } long sum = (weightSum >> 1) - 1; - for (int i = 0; i < 4; i++) + for (int i = 0; i < PredictorCount; i++) { // Dot product - sum += p[i] * w[i]; + sum += predictions[i] * weights[i]; } return (sum * DivisionLookup[(int)weightSum - 1]) >> 24; } - public long Predict(bool computeProperties, int x, int y, int width, long n, long w, long ne, long nw, long nn, Span properties, int offset) + /// + /// Predicts one sample from an explicitly borrowed channel history. Pixels remain sequential; SIMD lanes represent the four predictors. + /// + /// Whether to store the signed neighbor error with the greatest magnitude. + /// The sample column. + /// The sample row. + /// The span returned by Reset for this operation. + /// The live channel slot, zero for a sequential channel. + /// The north sample. + /// The west sample. + /// The north-east sample. + /// The north-west sample. + /// The sample two rows above. + /// The destination for requested context properties. + /// The weighted property's position. + /// The weighted integer prediction after removing its three fractional bits. + public long Predict( + bool computeProperties, + int x, + int y, + Span storage, + int stateIndex, + long n, + long w, + long ne, + long nw, + long nn, + Span properties, + int offset) { + Span slot = storage.Slice(stateIndex * this.wordsPerSlot, this.wordsPerSlot); + Span predictions = MemoryMarshal.Cast(slot[..PredictionWords]); + Span errors = MemoryMarshal.Cast(slot.Slice(PredictionWords, HistoryRows * this.rowLength)); + Span predictorErrors = slot.Slice(PredictionWords + (HistoryRows * this.rowLength), HistoryRows * PredictorCount * this.rowLength); + int width = this.width; bool yIsOdd = (y & 1) != 0; - int cur_row = yIsOdd ? 0 : (width + 2); - int prev_row = yIsOdd ? (width + 2) : 0; + int cur_row = yIsOdd ? 0 : this.rowLength; + int prev_row = yIsOdd ? this.rowLength : 0; int posN = prev_row + x; int posNE = x < width - 1 ? posN + 1 : posN; int posNW = x > 0 ? posN - 1 : posN; - Span weights = [0, 0, 0, 0]; - Span headerW = this.header.GetW(); + InlineArray4 weightStorage = default; + Span weights = weightStorage; + ReadOnlySpan headerWeights = this.header.GetW(); + + // Four lanes hold one error from each predictor. One Vector128 covers the complete record; there is no vector remainder. + // UInt32 addition deliberately wraps as in the bitstream algorithm. Wider vectors would cross into another sample's record. + if (Vector128.IsHardwareAccelerated) + { + ref uint first = ref MemoryMarshal.GetReference(predictorErrors); + Vector128 north = Vector128.LoadUnsafe(ref first, (nuint)(PredictorCount * posN)); + Vector128 northEast = Vector128.LoadUnsafe(ref first, (nuint)(PredictorCount * posNE)); + Vector128 northWest = Vector128.LoadUnsafe(ref first, (nuint)(PredictorCount * posNW)); + (north + northEast + northWest).StoreUnsafe(ref MemoryMarshal.GetReference(weights)); + } + else + { + for (int i = 0; i < PredictorCount; i++) + { + weights[i] = unchecked( + predictorErrors[(PredictorCount * posN) + i] + + predictorErrors[(PredictorCount * posNE) + i] + + predictorErrors[(PredictorCount * posNW) + i]); + } + } - for (int i = 0; i < 4; i++) + for (int i = 0; i < PredictorCount; i++) { - Span error = this.predErrors[i].AsSpan(); - weights[i] = error[posN] + error[posNE] + error[posNW]; - weights[i] = ErrorWeight((int)weights[i], headerW[i]); + // Widen before ErrorWeight adds one. The largest encoded UInt32 error must not become a signed value or wrap to zero. + weights[i] = ErrorWeight(weights[i], headerWeights[i]); } n = AddBits(n); @@ -124,28 +238,27 @@ public long Predict(bool computeProperties, int x, int y, int width, long n, lon nw = AddBits(nw); nn = AddBits(nn); - long teW = x == 0 ? 0 : this.error[cur_row + x - 1]; - long teN = this.error[posN]; - long teNW = this.error[posNW]; + long teW = x == 0 ? 0 : errors[cur_row + x - 1]; + long teN = errors[posN]; + long teNW = errors[posNW]; long sumWN = teN + teW; - long teNE = this.error[posNE]; + long teNE = errors[posNE]; if (computeProperties) { long p = teW; - long absP = Math.Abs(p); - if (Math.Abs(teN) > absP) + if (Math.Abs(teN) > Math.Abs(p)) { p = teN; } - if (Math.Abs(teNW) > absP) + if (Math.Abs(teNW) > Math.Abs(p)) { p = teNW; } - if (Math.Abs(teNE) > absP) + if (Math.Abs(teNE) > Math.Abs(p)) { p = teNE; } @@ -153,40 +266,72 @@ public long Predict(bool computeProperties, int x, int y, int width, long n, lon properties[offset++] = (int)p; } - this.prediction[0] = w + ne - n; - this.prediction[1] = n - (((sumWN + teNE) * this.header.P1C) >> 5); - this.prediction[2] = w - (((sumWN + teNW) * this.header.P2C) >> 5); - this.prediction[3] = + predictions[0] = w + ne - n; + predictions[1] = n - (((sumWN + teNE) * this.header.P1C) >> 5); + predictions[2] = w - (((sumWN + teNW) * this.header.P2C) >> 5); + predictions[3] = n - (((teNW * this.header.P3Ca) + (teN * this.header.P3Cb) + (teNE * this.header.P3Cc) + ((nn - n) * this.header.P3Cd) + ((nw - w) * this.header.P3Ce)) >> 5); - this.pred = WeightedAverage(this.prediction, weights); + predictions[PredictorCount] = WeightedAverage(predictions[..PredictorCount], weights); if (((teN ^ teW) | (teN ^ teNW)) > 0) { - return (this.pred + PredictionRound) >> 3; + return (predictions[PredictorCount] + PredictionRound) >> 3; } long mx = Math.Max(w, Math.Max(ne, n)); long mn = Math.Min(w, Math.Min(ne, n)); - this.pred = Math.Max(mn, Math.Min(mx, this.pred)); - return (this.pred + PredictionRound) >> 3; + predictions[PredictorCount] = Math.Max(mn, Math.Min(mx, predictions[PredictorCount])); + return (predictions[PredictorCount] + PredictionRound) >> 3; } - public void UpdatePredictionErrors(long value, int x, int y, int width) + /// + /// Updates one channel's signed and four-predictor errors after its actual sample is known. + /// + /// The reconstructed or source sample. + /// The sample column. + /// The sample row. + /// The span returned by Reset for this operation. + /// The live channel slot, zero for a sequential channel. + public void UpdatePredictionErrors(long value, int x, int y, Span storage, int stateIndex) { - bool yIsOdd = (y & 1) != 0; - - long curRow = yIsOdd ? 0 : (width + 2); - long prevRow = yIsOdd ? (width + 2) : 0; + Span slot = storage.Slice(stateIndex * this.wordsPerSlot, this.wordsPerSlot); + ReadOnlySpan predictions = MemoryMarshal.Cast(slot[..PredictionWords]); + Span errors = MemoryMarshal.Cast(slot.Slice(PredictionWords, HistoryRows * this.rowLength)); + Span predictorErrors = slot.Slice(PredictionWords + (HistoryRows * this.rowLength), HistoryRows * PredictorCount * this.rowLength); + int currentRow = (y & 1) != 0 ? 0 : this.rowLength; + int previousRow = (y & 1) != 0 ? this.rowLength : 0; value = AddBits(value); - this.error[curRow + x] = (int)(this.pred - value); - for (int i = 0; i < 4; i++) + errors[currentRow + x] = unchecked((int)(predictions[PredictorCount] - value)); + InlineArray4 errorStorage = default; + Span currentErrors = errorStorage; + + for (int i = 0; i < PredictorCount; i++) + { + currentErrors[i] = unchecked((uint)((Math.Abs(predictions[i] - value) + PredictionRound) >> 3)); + } + + int current = PredictorCount * (currentRow + x); + int previous = PredictorCount * (previousRow + x + 1); + + // Current and previous records are in different row halves. Vector addition and the scalar fallback both retain UInt32 wrapping. + if (Vector128.IsHardwareAccelerated) { - long err = (Math.Abs(this.prediction[i] - value) + PredictionRound) >> 3; - this.predErrors[i][curRow + x] = (uint)err; - this.predErrors[i][prevRow + x + 1] += (uint)err; + ref uint first = ref MemoryMarshal.GetReference(predictorErrors); + Vector128 values = Vector128.LoadUnsafe(ref MemoryMarshal.GetReference(currentErrors)); + Vector128 previousValues = Vector128.LoadUnsafe(ref first, (nuint)previous); + values.StoreUnsafe(ref first, (nuint)current); + (previousValues + values).StoreUnsafe(ref first, (nuint)previous); + } + else + { + for (int i = 0; i < PredictorCount; i++) + { + predictorErrors[current + i] = currentErrors[i]; + predictorErrors[previous + i] = unchecked(predictorErrors[previous + i] + currentErrors[i]); + } } } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncoding.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncoding.cs index 6d6f6a0d37..29e3346623 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncoding.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncoding.cs @@ -251,6 +251,8 @@ void MarkProperty(int p, ref bool gradientOnly) /// The image that owns the channel. /// The remaining prefix-coded repetition count. /// The repeated prefix-coded value. + /// The operation-owned scratch, prepared on its first weighted channel and reused by later channels. + /// The maximum channel width prepared by the enclosing modular operation. public static void DecodeModularChannelMAANS( Configuration configuration, bool usesLz77, @@ -265,7 +267,9 @@ public static void DecodeModularChannelMAANS( JxlTreeLut treeLookup, JxlModularImage image, ref uint flRun, - ref uint flV) + ref uint flV, + ref JxlModularState? weightedState, + int maximumWidth) { JxlModularChannel channel = image.Channels[channelIdx]; @@ -273,10 +277,9 @@ public static void DecodeModularChannelMAANS( staticProps[0] = channelIdx; staticProps[1] = groupId; - if ((channel.Width & channel.Height) == 0) // equivalent to (channel.Width == 0 || channel.Height == 0) - { - return; - } + // Keep the physical stride after metadata shrink. Borrow the allocation once; row and neighbor views use this span. + Span channelSamples = channel.GetRow(0); + int onerow = channel.Plane.PixelsPerRow; bool treeHasWeightedPredictionPropOrPred = false; bool isWeightedPredictionOnly = false; @@ -337,7 +340,7 @@ bool GlobalTreeIsAllGradientNoOp() for (int y = 0; y < channel.Height; y++) { - Span r = channel.GetRow(y); + Span r = channelSamples[(y * onerow)..]; r[..channel.Width].Fill(v); } } @@ -347,7 +350,7 @@ bool GlobalTreeIsAllGradientNoOp() { for (int y = 0; y < channel.Height; y++) { - Span r = channel.GetRow(y); + Span r = channelSamples[(y * onerow)..]; for (int x = 0; x < channel.Width; x++) { @@ -361,7 +364,7 @@ bool GlobalTreeIsAllGradientNoOp() { for (int y = 0; y < channel.Height; y++) { - Span r = channel.GetRow(y); + Span r = channelSamples[(y * onerow)..]; for (int x = 0; x < channel.Width; x++) { @@ -381,9 +384,8 @@ bool GlobalTreeIsAllGradientNoOp() for (int y = 0; y < channel.Height; y++) { - Span r = channel.GetRow(y); - Span rtop = y > 0 ? channel.GetRow(y - 1) : channel.GetRowMinus(y, 1); - Span rtopleft = y > 0 ? channel.GetRowMinus(y - 1, 1) : channel.GetRowMinus(y, 1); + Span r = channelSamples[(y * onerow)..]; + Span rtop = y > 0 ? channelSamples[((y - 1) * onerow)..] : r; int guess0 = y > 0 ? rtop[0] : 0; if (flRun == 0) @@ -401,8 +403,8 @@ bool GlobalTreeIsAllGradientNoOp() for (int x = 1; x < channel.Width; x++) { int left = r[x - 1]; - int top = rtop[x]; - int topleft = rtopleft[x]; + int top = y > 0 ? rtop[x] : left; + int topleft = y > 0 ? rtop[x - 1] : left; int guess = JxlContextPrediction.ClampedGradient(top, left, topleft); if (flRun == 0) @@ -423,18 +425,16 @@ bool GlobalTreeIsAllGradientNoOp() } else if (predictor == JxlPredictor.Gradient && offset == 0 && multiplier == 1) { - int onerow = channel.Plane.PixelsPerRow; - for (int y = 0; y < channel.Height; y++) { - Span r = channel.GetRow(y); + Span r = channelSamples[(y * onerow)..]; for (int x = 0; x < channel.Width; x++) { // Neighbors - int left = x > 0 ? r[x - 1] : y > 0 ? channel.GetRowMinus(y, x - onerow)[0] : 0; - int top = y > 0 ? channel.GetRowMinus(y, x - onerow)[0] : left; - int topleft = x > 0 && y > 0 ? channel.GetRowMinus(y, x - 1 - onerow)[0] : left; + int left = x > 0 ? r[x - 1] : y > 0 ? channelSamples[((y - 1) * onerow) + x] : 0; + int top = y > 0 ? channelSamples[((y - 1) * onerow) + x] : left; + int topleft = x > 0 && y > 0 ? channelSamples[((y - 1) * onerow) + x - 1] : left; int guess = JxlContextPrediction.ClampedGradient(top, left, topleft); uint v = (uint)reader.ReadHybridUIntClustered(usesLz77, (int)ctx_id, bitReader, window); @@ -459,18 +459,16 @@ bool GlobalTreeIsAllGradientNoOp() if (isGradientOnly) { - int onerow = channel.Plane.PixelsPerRow; - for (int y = 0; y < channel.Height; y++) { - Span r = channel.GetRow(y); + Span r = channelSamples[(y * onerow)..]; for (int x = 0; x < channel.Width; x++) { // Neighbors - int left = x > 0 ? channel.GetRowPlus(y, x - 1)[0] : y > 0 ? channel.GetRowPlus(y, x - onerow)[0] : 0; - int top = y > 0 ? channel.GetRowPlus(y, x - onerow)[0] : left; - int topleft = x > 0 && y > 0 ? channel.GetRowPlus(y, x - 1 - onerow)[0] : left; + int left = x > 0 ? r[x - 1] : y > 0 ? channelSamples[((y - 1) * onerow) + x] : 0; + int top = y > 0 ? channelSamples[((y - 1) * onerow) + x] : left; + int topleft = x > 0 && y > 0 ? channelSamples[((y - 1) * onerow) + x - 1] : left; int guess = JxlContextPrediction.ClampedGradient(top, left, topleft); int pos = @@ -487,16 +485,24 @@ bool GlobalTreeIsAllGradientNoOp() } else if (!usesLz77 && isWeightedPredictionOnly && channel.Width > 8) { - JxlModularState wpState = new(wpHeader, channel.Width); + // Allocate only when this operation first selects a weighted channel. Later channels reset the same fixed-capacity owner. + weightedState ??= new(configuration, wpHeader, maximumWidth, 1); + JxlModularState wpState = weightedState; + Span weightedStorage = wpState.Reset(wpHeader, channel.Width); Span properties = [0]; for (int y = 0; y < channel.Height; y++) { - Span r = channel.GetRow(y); - Span rtop = y > 0 ? channel.GetRow(y - 1) : channel.GetRowMinus(y, 1); - Span rtoptop = y > 1 ? channel.GetRow(y - 2) : rtop; - Span rtopleft = y > 0 ? channel.GetRowMinus(y - 1, 1) : channel.GetRowMinus(y, 1); - Span rtopright = y > 0 ? channel.GetRow(y - 1)[1..] : channel.GetRowMinus(y, 1); + Span r = channelSamples[(y * onerow)..]; + Span rtop = y > 0 ? channelSamples[((y - 1) * onerow)..] : r; + Span rtoptop = y > 1 ? channelSamples[((y - 2) * onerow)..] : rtop; + + // Interior columns have a valid west sample. Select unavailable first-row neighbors as west once per row. + // For row one, the unavailable two-up sample is north. These offsets are only loaded for 1 <= x < width-1. + int northOffset = y > 0 ? -onerow : -1; + int northWestOffset = y > 0 ? -onerow - 1 : -1; + int northEastOffset = y > 0 ? -onerow + 1 : -1; + int twoUpOffset = y > 1 ? -2 * onerow : northOffset; int x = 0; { @@ -505,39 +511,75 @@ bool GlobalTreeIsAllGradientNoOp() int toptop = y > 0 ? rtoptop[x] : 0; int topright = x + 1 < channel.Width && y > 0 ? rtop[x + 1] : left; - int guess = (int)wpState.Predict(true, x, y, channel.Width, left, left, topright, left, toptop, properties, offset); - int pos = JxlMaConstants.PropertyRangeFast + Math.Clamp(properties[0], -JxlMaConstants.PropertyRangeFast, JxlMaConstants.PropertyRangeFast - 1); + int guess = (int)wpState.Predict(true, x, y, weightedStorage, 0, left, left, topright, left, toptop, properties, offset); + int pos = JxlMaConstants.PropertyRangeFast + Math.Clamp( + properties[0], + -JxlMaConstants.PropertyRangeFast, + JxlMaConstants.PropertyRangeFast - 1); uint ctx_id = treeLookup.ContextLookup[pos]; uint v = (uint)reader.ReadHybridUIntClustered(usesLz77, (int)ctx_id, bitReader, window); r[x] = MakePixel(v, 1, guess); - wpState.UpdatePredictionErrors(r[x], x, y, channel.Width); + wpState.UpdatePredictionErrors(r[x], x, y, weightedStorage, 0); } for (x = 1; x + 1 < channel.Width; x++) { int offset = 0; - int guess = (int)wpState.Predict(true, x, y, channel.Width, rtop[x], r[x - 1], rtopright[x], rtopleft[x], rtoptop[x], properties, offset); - int pos = JxlMaConstants.PropertyRangeFast + Math.Clamp(properties[0], -JxlMaConstants.PropertyRangeFast, JxlMaConstants.PropertyRangeFast - 1); + ref int current = ref r[x]; + int guess = (int)wpState.Predict( + true, + x, + y, + weightedStorage, + 0, + Unsafe.Add(ref current, northOffset), + r[x - 1], + Unsafe.Add(ref current, northEastOffset), + Unsafe.Add(ref current, northWestOffset), + Unsafe.Add(ref current, twoUpOffset), + properties, + offset); + + int pos = JxlMaConstants.PropertyRangeFast + Math.Clamp( + properties[0], + -JxlMaConstants.PropertyRangeFast, + JxlMaConstants.PropertyRangeFast - 1); int ctx_id = treeLookup.ContextLookup[pos]; uint v = (uint)reader.ReadHybridUIntClustered(usesLz77, (int)ctx_id, bitReader, window); r[x] = MakePixel(v, 1, guess); - wpState.UpdatePredictionErrors(r[x], x, y, channel.Width); + wpState.UpdatePredictionErrors(r[x], x, y, weightedStorage, 0); } { int offset = 0; - int guess = (int)wpState.Predict(true, x, y, channel.Width, rtop[x], r[x - 1], rtop[x], rtopleft[x], rtoptop[x], properties, offset); - int pos = JxlMaConstants.PropertyRangeFast + Math.Clamp(properties[0], -JxlMaConstants.PropertyRangeFast, JxlMaConstants.PropertyRangeFast - 1); + int guess = (int)wpState.Predict( + true, + x, + y, + weightedStorage, + 0, + y > 0 ? rtop[x] : r[x - 1], + r[x - 1], + y > 0 ? rtop[x] : r[x - 1], + y > 0 ? rtop[x - 1] : r[x - 1], + y > 0 ? rtoptop[x] : r[x - 1], + properties, + offset); + + int pos = JxlMaConstants.PropertyRangeFast + Math.Clamp( + properties[0], + -JxlMaConstants.PropertyRangeFast, + JxlMaConstants.PropertyRangeFast - 1); int ctx_id = treeLookup.ContextLookup[pos]; uint v = (uint)reader.ReadHybridUIntClustered(usesLz77, (int)ctx_id, bitReader, window); r[x] = MakePixel(v, 1, guess); - wpState.UpdatePredictionErrors(r[x], x, y, channel.Width); + wpState.UpdatePredictionErrors(r[x], x, y, weightedStorage, 0); } } } @@ -545,35 +587,62 @@ bool GlobalTreeIsAllGradientNoOp() { JxlMaTreeLookup tree_lookup = new(tree); Span properties = stackalloc int[numProps]; - int onerow = channel.Plane.PixelsPerRow; - - JxlModularChannel references = new(configuration, properties.Length - NumNonrefProperties, channel.Width, 0, 0); + using JxlModularChannel references = new(configuration, properties.Length - JxlMaConstants.NumNonrefProperties, channel.Width, 0, 0); + Span referenceStorage = references.GetRow(0); + int referenceStride = references.Plane.PixelsPerRow; for (int y = 0; y < channel.Height; y++) { - Span p = channel.GetRow(y); - PrecomputeReferences(channel, y, image, channelIdx, references); - InitPropsRow(properties, staticProps, y); + Span p = channelSamples[(y * onerow)..]; + JxlContextPrediction.PrecomputeReferences(channel, y, image, channelIdx, references, referenceStorage); + JxlContextPrediction.InitializePropertiesForRow(properties, staticProps, y); if (y > 1 && channel.Width > 8 && references.Width == 0) { for (int x = 0; x < 2; x++) { - JxlPredictionResult res = JxlContextPrediction.PredictTreeNoWeightedPrediction(properties, channel.Width, ref p[x], onerow, x, y, tree_lookup, references); + JxlPredictionResult res = JxlContextPrediction.PredictTreeNoWeightedPrediction( + properties, + channel.Width, + ref p[x], + onerow, + x, + y, + tree_lookup, + referenceStorage.Slice(x * referenceStride, references.Width)); + uint v = (uint)reader.ReadHybridUIntClustered(usesLz77, res.Context, bitReader, window); p[x] = MakePixel(v, res.Multiplier, res.Guess); } for (int x = 2; x < channel.Width - 2; x++) { - JxlPredictionResult res = JxlContextPrediction.PredictTreeNoWeightedPredictionNoEdgeCases(properties, channel.Width, ref p[x], onerow, x, y, tree_lookup, references); + JxlPredictionResult res = JxlContextPrediction.PredictTreeNoWeightedPredictionNoEdgeCases( + properties, + channel.Width, + ref p[x], + onerow, + x, + y, + tree_lookup, + referenceStorage.Slice(x * referenceStride, references.Width)); + uint v = (uint)reader.ReadHybridUIntClustered(usesLz77, res.Context, bitReader, window); p[x] = MakePixel(v, res.Multiplier, res.Guess); } for (int x = channel.Width - 2; x < channel.Width; x++) { - JxlPredictionResult res = JxlContextPrediction.PredictTreeNoWeightedPrediction(properties, channel.Width, ref p[x], onerow, x, y, tree_lookup, references); + JxlPredictionResult res = JxlContextPrediction.PredictTreeNoWeightedPrediction( + properties, + channel.Width, + ref p[x], + onerow, + x, + y, + tree_lookup, + referenceStorage.Slice(x * referenceStride, references.Width)); + uint v = (uint)reader.ReadHybridUIntClustered(usesLz77, res.Context, bitReader, window); p[x] = MakePixel(v, res.Multiplier, res.Guess); } @@ -582,7 +651,16 @@ bool GlobalTreeIsAllGradientNoOp() { for (int x = 0; x < channel.Width; x++) { - JxlPredictionResult res = JxlContextPrediction.PredictTreeNoWeightedPrediction(properties, channel.Width, ref p[x], onerow, x, y, tree_lookup, references); + JxlPredictionResult res = JxlContextPrediction.PredictTreeNoWeightedPrediction( + properties, + channel.Width, + ref p[x], + onerow, + x, + y, + tree_lookup, + referenceStorage.Slice(x * referenceStride, references.Width)); + uint v = (uint)reader.ReadHybridUIntClustered(usesLz77, res.Context, bitReader, window); p[x] = MakePixel(v, res.Multiplier, res.Guess); } @@ -593,107 +671,112 @@ bool GlobalTreeIsAllGradientNoOp() { JxlMaTreeLookup tree_lookup = new(tree); Span properties = stackalloc int[numProps]; - int onerow = channel.Plane.PixelsPerRow; - - JxlModularChannel references = new(configuration, properties.Length - NumNonrefProperties, channel.Width, 0, 0); - JxlModularState wpState = new(wpHeader, channel.Width); + using JxlModularChannel references = new(configuration, properties.Length - JxlMaConstants.NumNonrefProperties, channel.Width, 0, 0); + Span referenceStorage = references.GetRow(0); + int referenceStride = references.Plane.PixelsPerRow; + // Allocate only when this operation first selects a weighted channel. Later channels reset the same fixed-capacity owner. + weightedState ??= new(configuration, wpHeader, maximumWidth, 1); + JxlModularState wpState = weightedState; + Span weightedStorage = wpState.Reset(wpHeader, channel.Width); for (int y = 0; y < channel.Height; y++) { - Span p = channel.GetRow(y); - InitPropsRow(properties, staticProps, y); - PrecomputeReferences(channel, y, image, channelIdx, references); + Span p = channelSamples[(y * onerow)..]; + JxlContextPrediction.InitializePropertiesForRow(properties, staticProps, y); + JxlContextPrediction.PrecomputeReferences(channel, y, image, channelIdx, references, referenceStorage); if (!usesLz77 && y > 1 && channel.Width > 8 && references.Width == 0) { for (int x = 0; x < 2; x++) { - JxlPredictionResult res = JxlContextPrediction.PredictTreeWeightedPrediction(properties, channel.Width, ref p[x], onerow, x, y, tree_lookup, references, wpState); + JxlPredictionResult res = JxlContextPrediction.PredictTreeWeightedPrediction( + properties, + channel.Width, + ref p[x], + onerow, + x, + y, + tree_lookup, + referenceStorage.Slice(x * referenceStride, references.Width), + wpState, + weightedStorage, + 0); + uint v = (uint)reader.ReadHybridUIntClustered(usesLz77, res.Context, bitReader, window); p[x] = MakePixel(v, res.Multiplier, res.Guess); - wpState.UpdatePredictionErrors(p[x], x, y, channel.Width); + wpState.UpdatePredictionErrors(p[x], x, y, weightedStorage, 0); } for (int x = 2; x < channel.Width - 2; x++) { - JxlPredictionResult res = JxlContextPrediction.PredictTreeWeightedPredictionNoEdgeCases(properties, channel.Width, ref p[x], onerow, x, y, tree_lookup, references, wpState); + JxlPredictionResult res = JxlContextPrediction.PredictTreeWeightedPredictionNoEdgeCases( + properties, + channel.Width, + ref p[x], + onerow, + x, + y, + tree_lookup, + referenceStorage.Slice(x * referenceStride, references.Width), + wpState, + weightedStorage, + 0); + uint v = (uint)reader.ReadHybridUIntClustered(usesLz77, res.Context, bitReader, window); p[x] = MakePixel(v, res.Multiplier, res.Guess); - wpState.UpdatePredictionErrors(p[x], x, y, channel.Width); + wpState.UpdatePredictionErrors(p[x], x, y, weightedStorage, 0); } for (int x = channel.Width - 2; x < channel.Width; x++) { - JxlPredictionResult res = JxlContextPrediction.PredictTreeWeightedPrediction(properties, channel.Width, ref p[x], onerow, x, y, tree_lookup, references, wpState); + JxlPredictionResult res = JxlContextPrediction.PredictTreeWeightedPrediction( + properties, + channel.Width, + ref p[x], + onerow, + x, + y, + tree_lookup, + referenceStorage.Slice(x * referenceStride, references.Width), + wpState, + weightedStorage, + 0); + uint v = (uint)reader.ReadHybridUIntClustered(usesLz77, res.Context, bitReader, window); p[x] = MakePixel(v, res.Multiplier, res.Guess); - wpState.UpdatePredictionErrors(p[x], x, y, channel.Width); + wpState.UpdatePredictionErrors(p[x], x, y, weightedStorage, 0); } } else { for (int x = 0; x < channel.Width; x++) { - JxlPredictionResult res = JxlContextPrediction.PredictTreeWeightedPrediction(properties, channel.Width, ref p[x], onerow, x, y, tree_lookup, references, wpState); + JxlPredictionResult res = JxlContextPrediction.PredictTreeWeightedPrediction( + properties, + channel.Width, + ref p[x], + onerow, + x, + y, + tree_lookup, + referenceStorage.Slice(x * referenceStride, references.Width), + wpState, + weightedStorage, + 0); + uint v = (uint)reader.ReadHybridUIntClustered(usesLz77, res.Context, bitReader, window); p[x] = MakePixel(v, res.Multiplier, res.Guess); - wpState.UpdatePredictionErrors(p[x], x, y, channel.Width); + wpState.UpdatePredictionErrors(p[x], x, y, weightedStorage, 0); } } } } } - /// - /// Decodes one channel using the entropy reader's LZ77 selection and caller-borrowed storage. - /// - /// The configuration that owns prediction storage. - /// The encoded channel data. - /// The caller-owned entropy reader. - /// The stable prediction-context mapping. - /// The LZ77 window borrowed before decoding channels. - /// The shared prediction tree. - /// The weighted-prediction settings. - /// The channel index. - /// The group identifier. - /// The reusable prediction lookup storage. - /// The image that owns the channel. - /// The remaining prefix-coded repetition count. - /// The repeated prefix-coded value. - public static void DecodeModularChannelMAANS( - Configuration configuration, - JxlBitReader bitReader, - JxlAnsSymbolReader reader, - ReadOnlySpan contextMap, - Span window, - Tree globalTree, - JxlModularHeader wpHeader, - int channelIdx, - int groupId, - JxlTreeLut treeLut, - JxlModularImage image, - ref uint flRun, - ref uint flV) - => DecodeModularChannelMAANS( - configuration, - reader.UsesLz77, - bitReader, - reader, - contextMap, - window, - globalTree, - wpHeader, - channelIdx, - groupId, - treeLut, - image, - ref flRun, - ref flV); - public static void ValidateChannelDimensions(JxlModularImage image, JxlModularOptions options) { int nbChannels = image.Channels.Count; @@ -816,110 +899,124 @@ public static bool ModularDecode( } int nextChannel = 0; - using JxlScopeGuard clearGuard = new(() => + bool clearPendingChannels = !allowTruncatedGroup; + try { - for (int c = nextChannel; c < image.Channels.Count; c++) - { - image.Channels[c].Plane.Clear(); - } - }); + Tree treeStorage = []; + List contextMapStorage = []; + JxlAnsCode codeStorage = new(); - if (allowTruncatedGroup) - { - clearGuard.Disarm(); - } + Tree tree = treeStorage; + JxlAnsCode code = codeStorage; + List contextMap = contextMapStorage; - Tree treeStorage = []; - List contextMapStorage = []; - JxlAnsCode codeStorage = new(); + if (!header.UseGlobalTree) + { + ulong maxTreeSize = 1024; - Tree tree = treeStorage; - JxlAnsCode code = codeStorage; - List contextMap = contextMapStorage; + for (int i = 0; i < numberOfChannels; i++) + { + JxlModularChannel channel = image.Channels[i]; + if (i >= image.MetaChannels && (channel.Width > options.MaxChannelSize || channel.Height > options.MaxChannelSize)) + { + break; + } - if (!header.UseGlobalTree) - { - ulong maxTreeSize = 1024; + ulong pixels = (ulong)channel.Width * (ulong)channel.Height; + maxTreeSize += pixels; + } - for (int i = 0; i < numberOfChannels; i++) + maxTreeSize = Math.Min(1uL << 20, maxTreeSize); + JxlMaDecoder.DecodeTree(configuration, br, treeStorage, (int)maxTreeSize); + JxlAnsReader.DecodeHistograms(configuration, br, (treeStorage.Count + 1) / 2, codeStorage, ref contextMapStorage, false); + } + else { - JxlModularChannel channel = image.Channels[i]; - if (i >= image.MetaChannels && (channel.Width > options.MaxChannelSize || channel.Height > options.MaxChannelSize)) + if (globalTree.Count == 0) { - break; + throw new InvalidOperationException("No global tree available but one was requested"); } - ulong pixels = (ulong)channel.Width * (ulong)channel.Height; - maxTreeSize += pixels; + tree = globalTree; + code = globalCode; + contextMap = globalContextMap; } - maxTreeSize = Math.Min(1uL << 20, maxTreeSize); - JxlMaDecoder.DecodeTree(configuration, br, treeStorage, (int)maxTreeSize); - JxlAnsReader.DecodeHistograms(configuration, br, (treeStorage.Count + 1) / 2, codeStorage, ref contextMapStorage, false); - } - else - { - if (globalTree.Count == 0) - { - throw new InvalidOperationException("No global tree available but one was requested"); - } + using JxlAnsSymbolReader reader = JxlAnsSymbolReader.Create(configuration, code, br, distanceMultiplier); + JxlTreeLut treeLut = new(false, false); - tree = globalTree; - code = globalCode; - contextMap = globalContextMap; - } + // All channels share this reader and distance history. Borrow storage before the channel loop, + // then pass the same spans through prediction loops without acquiring Memory.Span for each symbol. + Span window = reader.Lz77Window; + bool usesLz77 = reader.UsesLz77; + ReadOnlySpan contexts = CollectionsMarshal.AsSpan(contextMap); - using JxlAnsSymbolReader reader = JxlAnsSymbolReader.Create(configuration, code, br, distanceMultiplier); - JxlTreeLut treeLut = new(false, false); + uint flRun = 0; + uint flV = 0; - // All channels share this reader and distance history. Borrow storage before the channel loop, - // then pass the same spans through prediction loops without acquiring Memory.Span for each symbol. - Span window = reader.Lz77Window; - ReadOnlySpan contexts = CollectionsMarshal.AsSpan(contextMap); + JxlModularState? weightedState = null; + try + { + for (; nextChannel < numberOfChannels; nextChannel++) + { + JxlModularChannel channel = image.Channels[nextChannel]; + if (nextChannel >= image.MetaChannels && + (channel.Width > options.MaxChannelSize || + channel.Height > options.MaxChannelSize)) + { + break; + } - uint flRun = 0; - uint flV = 0; + if (channel.Width == 0 || channel.Height == 0) + { + continue; // skip empty channels + } - for (; nextChannel < numberOfChannels; nextChannel++) - { - JxlModularChannel channel = image.Channels[nextChannel]; - if (nextChannel >= image.MetaChannels && - (channel.Width > options.MaxChannelSize || - channel.Height > options.MaxChannelSize)) + DecodeModularChannelMAANS( + configuration, + usesLz77, + br, + reader, + contexts, + window, + tree, + header.WeightedHeader, + nextChannel, + groupId, + treeLut, + image, + ref flRun, + ref flV, + ref weightedState, + distanceMultiplier); + } + } + finally { - break; + weightedState?.Dispose(); } - if (channel.Width == 0 || channel.Height == 0) + clearPendingChannels = false; + + if (!reader.CheckAnsFinalState()) { - continue; // skip empty channels + throw new InvalidOperationException("ANS decode final state failed"); } - DecodeModularChannelMAANS( - configuration, - br, - reader, - contexts, - window, - tree, - header.WeightedHeader, - nextChannel, - groupId, - treeLut, - image, - ref flRun, - ref flV); - + return true; } - - clearGuard.Disarm(); - - if (!reader.CheckAnsFinalState()) + finally { - throw new InvalidOperationException("ANS decode final state failed"); + // Strict failures clear channels that decoding has not completed. Truncated-group mode retains the decoded samples. + // The success path disables this cleanup before the final ANS check, matching the native decoder's boundary. + if (clearPendingChannels) + { + for (int c = nextChannel; c < image.Channels.Count; c++) + { + image.Channels[c].Plane.Clear(); + } + } } - - return true; } public static bool ModularGenericDecompress( @@ -942,8 +1039,6 @@ public static bool ModularGenericDecompress( reqSizes.Add(new(c.Width, c.Height)); } - header ??= new JxlGroupHeader(); - bool decStatus = ModularDecode(configuration, br, image, header, groupId, options, tree, code, contextMap, allowTruncatedGroup); if (!allowTruncatedGroup) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncodingEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncodingEncoder.cs index 6753df9180..e8123f36c1 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncodingEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncodingEncoder.cs @@ -121,7 +121,28 @@ public static Tree MakeFixedTree(int property, Span cutoffs, JxlPredictor p return tree; } - public static void GatherTreeData(Configuration configuration, JxlModularImage image, int channelIndex, int groupId, JxlModularHeader wpHeader, JxlModularOptions options, JxlTreeSamples treeSamples, ref int totalPixels) + /// + /// Collects prediction samples for one channel, using scratch shared by the complete tree-learning operation. + /// + /// The configuration used for channel-reference storage. + /// The source modular image. + /// The source channel index. + /// The group used for static context properties. + /// The weighted-prediction settings for this image. + /// The sampling and predictor options. + /// The destination prediction samples. + /// The operation-owned scratch, reset for this channel without allocating. + /// The running number of visited samples. + public static void GatherTreeData( + Configuration configuration, + JxlModularImage image, + int channelIndex, + int groupId, + JxlModularHeader wpHeader, + JxlModularOptions options, + JxlTreeSamples treeSamples, + JxlModularState wpState, + ref int totalPixels) { JxlModularChannel channel = image.Channels[channelIndex]; @@ -153,24 +174,57 @@ bool UseSample() return (bits >> 32) <= threshold; } + Span channelSamples = channel.GetRow(0); int pixelsPerRow = channel.Plane.PixelsPerRow; using JxlModularChannel references = new(configuration, properties.Length - JxlMaConstants.NumNonrefProperties, channel.Width, 0, 0); - JxlModularState wpState = new(wpHeader, channel.Width); + Span referenceStorage = references.GetRow(0); + int referenceStride = references.Plane.PixelsPerRow; + Span weightedStorage = wpState.Reset(wpHeader, channel.Width); treeSamples.PrepareForSamples((int)(pixelFraction * channel.Height * channel.Width) + 64); bool haveMultiplePredictors = treeSamples.NumberOfPredictors != 1; - void ComputeSample(Span p, int x, int y, Span properties, ref int totalPixels) + // Pass spans into the local operation. Its captured state contains no borrowed spans. + void ComputeSample( + Span p, + int x, + int y, + Span properties, + Span predictionStorage, + ReadOnlySpan referenceProperties, + ref int totalPixels) { Span pred = stackalloc int[JxlPredictorFacts.ModularPredictorsAlignment].Slice(0, JxlPredictorFacts.ModularPredictors); if (haveMultiplePredictors) { - _ = JxlContextPrediction.PredictLearnAll(properties, channel.Width, ref p[x], pixelsPerRow, x, y, references, wpState, pred); + _ = JxlContextPrediction.PredictLearnAll( + properties, + channel.Width, + ref p[x], + pixelsPerRow, + x, + y, + referenceProperties, + wpState, + predictionStorage, + 0, + pred); } else { - pred[(int)treeSamples.PredictorFromIndex(0)] = JxlContextPrediction.PredictLearn(properties, channel.Width, ref p[x], pixelsPerRow, x, y, treeSamples.PredictorFromIndex(0), references, wpState).Guess; + pred[(int)treeSamples.PredictorFromIndex(0)] = JxlContextPrediction.PredictLearn( + properties, + channel.Width, + ref p[x], + pixelsPerRow, + x, + y, + treeSamples.PredictorFromIndex(0), + referenceProperties, + wpState, + predictionStorage, + 0).Guess; } totalPixels++; @@ -180,33 +234,55 @@ void ComputeSample(Span p, int x, int y, Span properties, ref int tota treeSamples.AddSample(p[x], properties, pred); } - wpState.UpdatePredictionErrors(p[x], x, y, channel.Width); + wpState.UpdatePredictionErrors(p[x], x, y, predictionStorage, 0); } Span pred = stackalloc int[JxlPredictorFacts.ModularPredictorsAlignment].Slice(0, JxlPredictorFacts.ModularPredictors); for (int y = 0; y < channel.Height; y++) { - Span p = channel.GetRow(y); - JxlContextPrediction.PrecomputeReferences(channel, y, image, channelIndex, references); + Span p = channelSamples[(y * pixelsPerRow)..]; + JxlContextPrediction.PrecomputeReferences(channel, y, image, channelIndex, references, referenceStorage); JxlContextPrediction.InitializePropertiesForRow(properties, staticProperties, y); if (y > 1 && channel.Width > 8 && references.Width == 0) { for (int x = 0; x < 2; x++) { - ComputeSample(p, x, y, properties, ref totalPixels); + ComputeSample(p, x, y, properties, weightedStorage, referenceStorage.Slice(x * referenceStride, references.Width), ref totalPixels); } for (int x = 2; x < channel.Width - 2; x++) { if (haveMultiplePredictors) { - _ = JxlContextPrediction.PredictLearnAllNoEdgeCases(properties, channel.Width, ref p[x], pixelsPerRow, x, y, references, wpState, pred); + _ = JxlContextPrediction.PredictLearnAllNoEdgeCases( + properties, + channel.Width, + ref p[x], + pixelsPerRow, + x, + y, + referenceStorage.Slice(x * referenceStride, references.Width), + wpState, + weightedStorage, + 0, + pred); } else { - pred[(int)treeSamples.PredictorFromIndex(0)] = JxlContextPrediction.PredictLearnNoEdgeCases(properties, channel.Width, ref p[x], pixelsPerRow, x, y, treeSamples.PredictorFromIndex(0), references, wpState).Guess; + pred[(int)treeSamples.PredictorFromIndex(0)] = JxlContextPrediction.PredictLearnNoEdgeCases( + properties, + channel.Width, + ref p[x], + pixelsPerRow, + x, + y, + treeSamples.PredictorFromIndex(0), + referenceStorage.Slice(x * referenceStride, references.Width), + wpState, + weightedStorage, + 0).Guess; } totalPixels++; @@ -216,19 +292,19 @@ void ComputeSample(Span p, int x, int y, Span properties, ref int tota treeSamples.AddSample(p[x], properties, pred); } - wpState.UpdatePredictionErrors(p[x], x, y, channel.Width); + wpState.UpdatePredictionErrors(p[x], x, y, weightedStorage, 0); } for (int x = channel.Width - 2; x < channel.Width; x++) { - ComputeSample(p, x, y, properties, ref totalPixels); + ComputeSample(p, x, y, properties, weightedStorage, referenceStorage.Slice(x * referenceStride, references.Width), ref totalPixels); } } else { for (int x = 0; x < channel.Width; x++) { - ComputeSample(p, x, y, properties, ref totalPixels); + ComputeSample(p, x, y, properties, weightedStorage, referenceStorage.Slice(x * referenceStride, references.Width), ref totalPixels); } } } @@ -269,7 +345,30 @@ public static Tree LearnTree(JxlTreeSamples treeSamples, int totalPixels, JxlMod return tree; } - public static void EncodeModularChannelMAANS(Configuration configuration, JxlModularImage image, int channelIndex, JxlModularHeader wpHeader, Tree globalTree, Span tokens, int groupId, bool skipEncoderFastPath) + /// + /// Encodes one channel's prediction residuals into the caller's token storage. + /// + /// The configuration that owns prediction and reference storage. + /// The source modular image. + /// The source channel index. + /// The weighted-prediction settings for this group. + /// The prediction tree shared by the encoded channels. + /// The destination token storage. + /// The group used for static context properties. + /// Whether to use general prediction instead of the specialized channel paths. + /// The operation-owned scratch, prepared on its first weighted channel and reused by later channels. + /// The maximum channel width prepared by the enclosing modular operation. + public static void EncodeModularChannelMAANS( + Configuration configuration, + JxlModularImage image, + int channelIndex, + JxlModularHeader wpHeader, + Tree globalTree, + Span tokens, + int groupId, + bool skipEncoderFastPath, + ref JxlModularState? weightedState, + int maximumWidth) { JxlModularChannel channel = image.Channels[channelIndex]; @@ -303,38 +402,46 @@ public static void EncodeModularChannelMAANS(Configuration configuration, JxlMod isGradientOnly = JxlModularEncoding.TreeToLookupTable(tree, treeLut); } + // Neighbor references remain inside this allocation, including physical row padding after metadata shrink. + Span channelSamples = channel.GetRow(0); int onerow = channel.Plane.PixelsPerRow; int treeCount = tree.Count; if (isWpOnly && !skipEncoderFastPath) { - JxlModularState wpState = new(wpHeader, channel.Width); + // Prepare once on the first weighted channel. The enclosing operation owns disposal and fixed maximum-width capacity. + weightedState ??= new(configuration, wpHeader, maximumWidth, 1); + JxlModularState wpState = weightedState; + Span weightedStorage = wpState.Reset(wpHeader, channel.Width); Span properties = [0]; bool unhealthy = false; for (int y = 0; y < channel.Height; y++) { - Span r = channel.GetRow(y); + Span r = channelSamples[(y * onerow)..]; for (int x = 0; x < channel.Width; x++) { int offset = 0; // Neighbors - int left = x > 0 ? channel.GetRowPlus(y, x - 1)[0] : y > 0 ? channel.GetRowPlus(y, x - onerow)[0] : 0; - int top = y > 0 ? channel.GetRowPlus(y, x - onerow)[0] : left; - int topleft = x > 0 && y > 0 ? channel.GetRowPlus(y, x - 1 - onerow)[0] : left; - int topright = x + 1 < channel.Width && y > 0 ? channel.GetRowPlus(y, x + 1 - onerow)[0] : top; - int toptop = y > 1 ? channel.GetRowPlus(y, x - onerow - onerow)[0] : top; - - int guess = (int)wpState.Predict(true, x, y, channel.Width, top, left, topright, topleft, toptop, properties, offset); - int pos = JxlMaConstants.PropertyRangeFast + Math.Clamp(properties[0], -JxlMaConstants.PropertyRangeFast, JxlMaConstants.PropertyRangeFast - 1); + int left = x > 0 ? r[x - 1] : y > 0 ? channelSamples[((y - 1) * onerow) + x] : 0; + int top = y > 0 ? channelSamples[((y - 1) * onerow) + x] : left; + int topleft = x > 0 && y > 0 ? channelSamples[((y - 1) * onerow) + x - 1] : left; + int topright = x + 1 < channel.Width && y > 0 ? channelSamples[((y - 1) * onerow) + x + 1] : top; + int toptop = y > 1 ? channelSamples[((y - 2) * onerow) + x] : top; + + int guess = (int)wpState.Predict(true, x, y, weightedStorage, 0, top, left, topright, topleft, toptop, properties, offset); + int pos = JxlMaConstants.PropertyRangeFast + Math.Clamp( + properties[0], + -JxlMaConstants.PropertyRangeFast, + JxlMaConstants.PropertyRangeFast - 1); int ctxId = treeLut.ContextLookup[pos]; unhealthy |= JxlMath.SubOverflow(r[x], guess, out int residual); tokens[tokenp++] = new JxlToken((JxlMaTreeContext)ctxId, JxlPackSigned.PackUnsigned(residual)); - wpState.UpdatePredictionErrors(r[x], x, y, channel.Width); + wpState.UpdatePredictionErrors(r[x], x, y, weightedStorage, 0); } } @@ -346,13 +453,13 @@ public static void EncodeModularChannelMAANS(Configuration configuration, JxlMod for (int y = 0; y < channel.Height; y++) { - Span r = channel.GetRow(y); + Span r = channelSamples[(y * onerow)..]; for (int x = 0; x < channel.Width; x++) { - int left = x > 0 ? channel.GetRowPlus(y, x - 1)[0] : y > 0 ? channel.GetRowPlus(y, x - onerow)[0] : 0; - int top = y > 0 ? channel.GetRowPlus(y, x - onerow)[0] : left; - int topleft = x > 0 && y > 0 ? channel.GetRowPlus(y, x - 1 - onerow)[0] : left; + int left = x > 0 ? r[x - 1] : y > 0 ? channelSamples[((y - 1) * onerow) + x] : 0; + int top = y > 0 ? channelSamples[((y - 1) * onerow) + x] : left; + int topleft = x > 0 && y > 0 ? channelSamples[((y - 1) * onerow) + x - 1] : left; int guess = JxlContextPrediction.ClampedGradient(top, left, topleft); unhealthy |= JxlMath.SubOverflow(r[x], guess, out int residual); @@ -369,13 +476,13 @@ public static void EncodeModularChannelMAANS(Configuration configuration, JxlMod for (int y = 0; y < channel.Height; y++) { - Span r = channel.GetRow(y); + Span r = channelSamples[(y * onerow)..]; for (int x = 0; x < channel.Width; x++) { - int left = x > 0 ? channel.GetRowPlus(y, x - 1)[0] : y > 0 ? channel.GetRowPlus(y, x - onerow)[0] : 0; - int top = y > 0 ? channel.GetRowPlus(y, x - onerow)[0] : left; - int topleft = x > 0 && y > 0 ? channel.GetRowPlus(y, x - 1 - onerow)[0] : left; + int left = x > 0 ? r[x - 1] : y > 0 ? channelSamples[((y - 1) * onerow) + x] : 0; + int top = y > 0 ? channelSamples[((y - 1) * onerow) + x] : left; + int topleft = x > 0 && y > 0 ? channelSamples[((y - 1) * onerow) + x - 1] : left; int guess = JxlContextPrediction.ClampedGradient(top, left, topleft); int pos = JxlMaConstants.PropertyRangeFast + Math.Min( @@ -395,7 +502,7 @@ public static void EncodeModularChannelMAANS(Configuration configuration, JxlMod { for (int y = 0; y < channel.Height; y++) { - Span p = channel.GetRow(y); + Span p = channelSamples[(y * onerow)..]; for (int x = 0; x < channel.Width; x++) { @@ -409,7 +516,7 @@ public static void EncodeModularChannelMAANS(Configuration configuration, JxlMod for (int y = 0; y < channel.Height; y++) { - Span r = channel.GetRow(y); + Span r = channelSamples[(y * onerow)..]; for (int x = 0; x < channel.Width; x++) { @@ -429,17 +536,28 @@ public static void EncodeModularChannelMAANS(Configuration configuration, JxlMod { Span properties = stackalloc int[numProps]; using JxlModularChannel references = new(configuration, properties.Length - JxlMaConstants.NumNonrefProperties, channel.Width, 0, 0); + Span referenceStorage = references.GetRow(0); + int referenceStride = references.Plane.PixelsPerRow; for (int y = 0; y < channel.Height; y++) { - Span p = channel.GetRow(y); + Span p = channelSamples[(y * onerow)..]; - JxlContextPrediction.PrecomputeReferences(channel, y, image, channelIndex, references); + JxlContextPrediction.PrecomputeReferences(channel, y, image, channelIndex, references, referenceStorage); JxlContextPrediction.InitializePropertiesForRow(properties, staticProperties, y); for (int x = 0; x < channel.Width; x++) { - JxlPredictionResult res = JxlContextPrediction.PredictTreeNoWeightedPrediction(properties, channel.Width, ref p[x], onerow, x, y, treeLookup, references); + JxlPredictionResult res = JxlContextPrediction.PredictTreeNoWeightedPrediction( + properties, + channel.Width, + ref p[x], + onerow, + x, + y, + treeLookup, + referenceStorage.Slice(x * referenceStride, references.Width)); + int residual = p[x] - res.Guess; if ((residual % res.Multiplier) != 0) @@ -455,18 +573,34 @@ public static void EncodeModularChannelMAANS(Configuration configuration, JxlMod { Span properties = stackalloc int[numProps]; using JxlModularChannel references = new(configuration, properties.Length - JxlMaConstants.NumNonrefProperties, channel.Width, 0, 0); + Span referenceStorage = references.GetRow(0); + int referenceStride = references.Plane.PixelsPerRow; - JxlModularState wpState = new(wpHeader, channel.Width); + // Prepare once on the first weighted channel. The enclosing operation owns disposal and fixed maximum-width capacity. + weightedState ??= new(configuration, wpHeader, maximumWidth, 1); + JxlModularState wpState = weightedState; + Span weightedStorage = wpState.Reset(wpHeader, channel.Width); for (int y = 0; y < channel.Height; y++) { - Span p = channel.GetRow(y); - JxlContextPrediction.PrecomputeReferences(channel, y, image, channelIndex, references); + Span p = channelSamples[(y * onerow)..]; + JxlContextPrediction.PrecomputeReferences(channel, y, image, channelIndex, references, referenceStorage); JxlContextPrediction.InitializePropertiesForRow(properties, staticProperties, y); for (int x = 0; x < channel.Width; x++) { - JxlPredictionResult res = JxlContextPrediction.PredictTreeWeightedPrediction(properties, channel.Width, ref p[x], onerow, x, y, treeLookup, references, wpState); + JxlPredictionResult res = JxlContextPrediction.PredictTreeWeightedPrediction( + properties, + channel.Width, + ref p[x], + onerow, + x, + y, + treeLookup, + referenceStorage.Slice(x * referenceStride, references.Width), + wpState, + weightedStorage, + 0); int residual = p[x] - res.Guess; @@ -476,7 +610,7 @@ public static void EncodeModularChannelMAANS(Configuration configuration, JxlMod } tokens[tokenp++] = new JxlToken((JxlMaTreeContext)res.Context, JxlPackSigned.PackUnsigned(residual / res.Multiplier)); - wpState.UpdatePredictionErrors(p[x], x, y, channel.Width); + wpState.UpdatePredictionErrors(p[x], x, y, weightedStorage, 0); } } } @@ -564,7 +698,23 @@ public static Tree PredefinedTree(JxlTreeKind treeKind, int totalPixels, int bit throw new InvalidOperationException("Invalid tree type: " + treeKind); } - public static Tree LearnTree(Configuration configuration, JxlModularImage[] images, Span options, int start, int stop, List? multiplierInfo = null) + /// + /// Learns a prediction tree from the selected images, reusing weighted-prediction storage across their channels. + /// + /// The configuration that owns temporary prediction storage. + /// The modular source images. + /// The matching sampling and prediction options. + /// The first image index to include. + /// The first image index to exclude. + /// The optional learned multiplier information. + /// The learned prediction tree. + public static Tree LearnTree( + Configuration configuration, + JxlModularImage[] images, + Span options, + int start, + int stop, + List? multiplierInfo = null) { multiplierInfo ??= []; @@ -573,6 +723,7 @@ public static Tree LearnTree(Configuration configuration, JxlModularImage[] imag samples.SetProperties(options[start].SplittingHeuristicsProperties, options[start].WpTreeMode); int maxC = 0; + int maximumWidth = 0; List pixelSamples = []; List diffSamples = []; @@ -582,6 +733,19 @@ public static Tree LearnTree(Configuration configuration, JxlModularImage[] imag for (int i = start; i < stop; i++) { maxC = Math.Max(images[i].Channels.Count, maxC); + + for (int c = 0; c < images[i].Channels.Count; c++) + { + JxlModularChannel channel = images[i].Channels[c]; + + if (c >= images[i].MetaChannels && (channel.Width > options[i].MaxChannelSize || channel.Height > options[i].MaxChannelSize)) + { + break; + } + + maximumWidth = Math.Max(maximumWidth, channel.Width); + } + JxlMaEncoder.CollectPixelSamples(configuration, images[i], options[i], i, groupPixelCount, channelPixelCount, pixelSamples, diffSamples); } @@ -616,6 +780,7 @@ public static Tree LearnTree(Configuration configuration, JxlModularImage[] imag totalPixels = Math.Max(totalPixels, 1); JxlModularHeader wpHeader = new(); + using JxlModularState weightedState = new(configuration, wpHeader, maximumWidth, 1); for (int i = start; i < stop; i++) { @@ -657,7 +822,7 @@ public static Tree LearnTree(Configuration configuration, JxlModularImage[] imag continue; // skip empty channels } - GatherTreeData(configuration, images[i], c, i, wpHeader, options[i], samples, totalPixels); + GatherTreeData(configuration, images[i], c, i, wpHeader, options[i], samples, weightedState, ref totalPixels); } } @@ -666,7 +831,26 @@ public static Tree LearnTree(Configuration configuration, JxlModularImage[] imag return tree; } - public static void ModularCompress(JxlModularImage image, JxlModularOptions options, int groupId, Tree tree, JxlGroupHeader header, List tokens, ref int width) + /// + /// Prepares one modular group's prediction header and tokens, sharing weighted scratch across its sequential channels. + /// + /// The configuration that owns temporary prediction storage. + /// The source modular image. + /// The prediction and channel-selection options. + /// The group used for static context properties. + /// The prediction tree shared by this group. + /// The destination group header. + /// The destination token list. + /// The maximum encoded channel width, used by the entropy encoder. + public static void ModularCompress( + Configuration configuration, + JxlModularImage image, + JxlModularOptions options, + int groupId, + Tree tree, + JxlGroupHeader header, + List tokens, + ref int width) { int numChannels = image.Channels.Count; @@ -721,21 +905,39 @@ public static void ModularCompress(JxlModularImage image, JxlModularOptions opti int tokenp = pos; - for (int i = 0; i < numChannels; i++) + JxlModularState? weightedState = null; + try { - if (i >= image.MetaChannels && - (image.Channels[i].Width > options.MaxChannelSize || - image.Channels[i].Height > options.MaxChannelSize)) + for (int i = 0; i < numChannels; i++) { - break; - } + if (i >= image.MetaChannels && + (image.Channels[i].Width > options.MaxChannelSize || + image.Channels[i].Height > options.MaxChannelSize)) + { + break; + } - if (image.Channels[i].Width == 0 || image.Channels[i].Height == 0) - { - continue; - } + if (image.Channels[i].Width == 0 || image.Channels[i].Height == 0) + { + continue; + } - EncodeModularChannelMAANS(configuration, image, i, header.WeightedHeader, tree, CollectionsMarshal.AsSpan(tokens)[tokenp..], groupId, options.SkipEncoderFastPath); + EncodeModularChannelMAANS( + configuration, + image, + i, + header.WeightedHeader, + tree, + CollectionsMarshal.AsSpan(tokens)[tokenp..], + groupId, + options.SkipEncoderFastPath, + ref weightedState, + imageWidth); + } + } + finally + { + weightedState?.Dispose(); } if (tokenp != tokens.Count) @@ -829,7 +1031,7 @@ public static void ModularGenericCompress(Configuration configuration, JxlModula int imageWidth = 0; List> tokens = [[]]; - ModularCompress(image, options, groupId, tree, groupHeader, tokens[0], ref imageWidth); + ModularCompress(configuration, image, options, groupId, tree, groupHeader, tokens[0], ref imageWidth); code = default; JxlHistogramParameters histoParameters = options.HistogramParameters; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs index 58203d1308..dc99a3a13b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs @@ -446,12 +446,14 @@ public static void InversePalette( // Read each output stride after the swap. The original indices can retain wider storage from a metadata shrink. if (predictor == JxlPredictor.Weighted) { + using JxlModularState wpState = new(configuration, weightedHeader, indices.XSize, 1); + for (int c = 0; c < nb; c++) { JxlModularChannel outputChannel = input.Channels[c0 + c]; int oneRowImage = outputChannel.Plane.PixelsPerRow; Span outputSamples = outputChannel.GetRow(0); - JxlModularState wpState = new(weightedHeader, outputChannel.Width); + Span weightedStorage = wpState.Reset(weightedHeader, outputChannel.Width); for (int y = 0; y < outputChannel.Height; y++) { @@ -463,14 +465,16 @@ public static void InversePalette( int index = idx[x]; int value = 0; int paletteEntry = GetPaletteValue(paletteData, index, c, oneRow, bitDepth); - JxlPredictionResult pred = JxlContextPrediction.PredictTreeNoWeightedPrediction( + JxlPredictionResult pred = JxlContextPrediction.PredictNoTreeWeightedPrediction( outputChannel.Width, - p[x..], + ref p[x], oneRowImage, x, y, predictor, - wpState); + wpState, + weightedStorage, + 0); if (index < nbDeltas) { @@ -482,7 +486,7 @@ public static void InversePalette( } p[x] = value; - wpState.UpdatePredictionErrors(p[x], x, y, outputChannel.Width); + wpState.UpdatePredictionErrors(p[x], x, y, weightedStorage, 0); } } } @@ -1128,12 +1132,9 @@ private static void ForwardPaletteIteration( inversePalette[pcol] = clr++; } - List wpStates = []; - - for (int c = 0; c < nb; c++) - { - wpStates.Add(new JxlModularState(wpHeader, w)); - } + // Lossy palette search interleaves channels within each pixel. Their independent histories share one tracked allocation. + using JxlModularState? wpState = lossy ? new(configuration, wpHeader, w, nb) : null; + Span weightedStorage = wpState is not null ? wpState.Reset(wpHeader, w) : default; InlineArray3> errorRow = default; @@ -1170,7 +1171,7 @@ private static void ForwardPaletteIteration( for (int x = 0; x < w; x++) { int index; - if (!lossy) + if (wpState is null) { for (int c = 0; c < nb; c++) { @@ -1199,7 +1200,16 @@ private static void ForwardPaletteIteration( for (int c = 0; c < nb; c++) { - predictions[c] = PredictTreeNoWeightedPrediction(w, p_quant[c] + x, oneRowImage, x, y, predictor, wpStates[c]).Guess; + predictions[c] = JxlContextPrediction.PredictNoTreeWeightedPrediction( + w, + ref p_quant[c][x], + oneRowImage, + x, + y, + predictor, + wpState, + weightedStorage, + c).Guess; } void TryIndex( @@ -1294,7 +1304,7 @@ void TryIndex( for (int c = 0; c < nb; c++) { - wpStates[c].UpdatePredictionErrors(bestValue[c], x, y, w); + wpState.UpdatePredictionErrors(bestValue[c], x, y, weightedStorage, c); p_quant[c][x] = bestValue[c]; } diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/EntropyCoderTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/EntropyCoderTests.cs index 2cba3f429b..4ff95135be 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/EntropyCoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/EntropyCoderTests.cs @@ -4,12 +4,161 @@ using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using SixLabors.ImageSharp.Tests.TestUtilities; namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; public class EntropyCoderTests { + /// + /// Checks reference records when fewer matching channels exist than the requested property capacity. + /// + [Fact] + public void PredictionReferences_StopAtFirstChannelAndClearUnusedProperties() + { + using JxlModularImage image = new(2, 1, 8); + image.Channels.Add(new(TestEnvironment.Configuration, 2, 1, 0, 0)); + image.Channels.Add(new(TestEnvironment.Configuration, 2, 1, 0, 0)); + image.Channels.Add(new(TestEnvironment.Configuration, 2, 1, 0, 0)); + Span source = image.Channels[0].GetRow(0); + source[0] = -2; + source[1] = 3; + + using JxlModularChannel references = new(TestEnvironment.Configuration, 8, 2, 0, 0); + Span records = references.GetRow(0); + records.Fill(99); + JxlContextPrediction.PrecomputeReferences(image.Channels[1], 0, image, 1, references, records); + + // Each record is [abs(value), value, abs(residual), residual]. Row-zero prediction uses west, initially zero. + // Only one earlier channel exists, so the second four-property group remains zero. + ReadOnlySpan first = [2, -2, 2, -2, 0, 0, 0, 0]; + ReadOnlySpan second = [3, 3, 5, 5, 0, 0, 0, 0]; + Assert.Equal(first, records[..8]); + Assert.Equal(second, records.Slice(references.Plane.PixelsPerRow, 8)); + + Span next = image.Channels[1].GetRow(0); + next[0] = 4; + next[1] = -1; + JxlContextPrediction.PrecomputeReferences(image.Channels[2], 0, image, 2, references, records); + + // References visit the nearest earlier channel first. Both property groups must fit through the final pixel. + ReadOnlySpan firstWithTwoReferences = [4, 4, 4, 4, 2, -2, 2, -2]; + ReadOnlySpan secondWithTwoReferences = [1, -1, 5, -5, 3, 3, 5, 5]; + Assert.Equal(firstWithTwoReferences, records[..8]); + Assert.Equal(secondWithTwoReferences, records.Slice(references.Plane.PixelsPerRow, 8)); + + JxlContextPrediction.PrecomputeReferences(image.Channels[0], 0, image, 0, references, records); + + // The first channel has no earlier references. Preparing it must clear the previous records without indexing channel -1. + ReadOnlySpan empty = [0, 0, 0, 0, 0, 0, 0, 0]; + Assert.Equal(empty, records[..8]); + Assert.Equal(empty, records.Slice(references.Plane.PixelsPerRow, 8)); + } + + /// + /// Checks the fixed-point error-weight approximation at zero and at unsigned error boundaries. + /// + /// The unsigned accumulated prediction error. + /// The four-bit weight from the prediction header. + /// The independently calculated fixed-point weight. + [Theory] + [InlineData(0U, 13U, 218103812U)] + [InlineData(16777215U, 13U, 17U)] + [InlineData(2147483648U, 13U, 4U)] + [InlineData(uint.MaxValue, 13U, 4U)] + public void WeightedPrediction_ErrorWeightPreservesUnsignedErrors(uint error, uint maximumWeight, uint expected) + { + // Zero uses 13*2^24+4. At 2^24-1 the scaled lookup gives 13+4; the larger errors reduce to the minimum weight four. + // The largest error must be widened before adding one, rather than becoming signed -1 and then zero. + Assert.Equal(expected, JxlModularState.ErrorWeight(error, maximumWeight)); + } + + /// + /// Checks signed-error selection, independent channel histories and header rebinding when storage is reused. + /// + [Fact] + public void WeightedPrediction_SelectsLargestErrorAndResetsHistories() + { + static void RunTest() + { + JxlModularHeader header = new() + { + P1C = 0, + P2C = 0, + P3Ca = 0, + P3Cb = 0, + P3Cc = 0, + P3Cd = 0, + P3Ce = 0 + }; + + // Zero header weights give four equal weights of four. These are valid custom header values. + header.GetW().Clear(); + using JxlModularState state = new(TestEnvironment.Configuration, header, 3, 2); + Span storage = state.Reset(header, 3); + ReadOnlySpan firstRow = [10, 0, 5]; + + for (int x = 0; x < firstRow.Length; x++) + { + long left = x == 0 ? 0 : firstRow[x - 1]; + _ = state.Predict(false, x, 0, storage, 0, left, left, left, left, left, [], 0); + state.UpdatePredictionErrors(firstRow[x], x, 0, storage, 0); + + // A second interleaved channel stays zero. It must not replace the first channel's predictions or error history. + _ = state.Predict(false, x, 0, storage, 1, 0, 0, 0, 0, 0, [], 0); + state.UpdatePredictionErrors(0, x, 0, storage, 1); + } + + // The first row's signed errors are -80, +80 and -40 in eighth-sample units. + // The next prediction is 60/8, rounded to seven; storing seven leaves west error +4. + Assert.Equal(7L, state.Predict(false, 0, 1, storage, 0, 10, 10, 0, 10, 10, [], 0)); + state.UpdatePredictionErrors(7, 0, 1, storage, 0); + _ = state.Predict(false, 0, 1, storage, 1, 0, 0, 0, 0, 0, [], 0); + state.UpdatePredictionErrors(0, 0, 1, storage, 1); + Span properties = stackalloc int[2]; + _ = state.Predict(true, 1, 1, storage, 0, 0, 7, 5, 10, 0, properties, 0); + _ = state.Predict(true, 1, 1, storage, 1, 0, 0, 0, 0, 0, properties, 1); + + // +80 is the first greatest magnitude. The equal -80 and smaller -40 must not replace it merely because both exceed west's +4. + Assert.Equal(80, properties[0]); + Assert.Equal(0, properties[1]); + + JxlModularHeader replacement = new() + { + P1C = 0, + P2C = 31, + P3Ca = 0, + P3Cb = 0, + P3Cc = 0, + P3Cd = 0, + P3Ce = 0 + }; + + replacement.GetW().Clear(); + storage = state.Reset(replacement, 3); + + for (int x = 0; x < firstRow.Length; x++) + { + long left = x == 0 ? 0 : firstRow[x - 1]; + _ = state.Predict(x == 0, x, 0, storage, 0, left, left, left, left, left, properties, 0); + if (x == 0) + { + Assert.Equal(0, properties[0]); + } + + state.UpdatePredictionErrors(firstRow[x], x, 0, storage, 0); + } + + // P2C=31 changes the west candidate to 235 eighth-sample units. The resulting prediction clamps to north's ten. + Assert.Equal(10L, state.Predict(false, 0, 1, storage, 0, 10, 10, 0, 10, 10, [], 0)); + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature(RunTest, HwIntrinsics.AllowAll | HwIntrinsics.DisableHWIntrinsic); + } + [Fact] public void PackUnpack() { From 8f5dd14ba0abfa5502914aa312884d6f3b5b5c30 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 22:00:34 +1000 Subject: [PATCH 270/328] Prepare only consumed JPEG XL comparison reference levels --- .../Butteraugli/ButteraugliComparator.cs | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs index 181f864a09..6e72110ec3 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs @@ -90,12 +90,12 @@ public ButteraugliComparator(Configuration configuration, int xsize, int ysize, public JxlImage3 Temp => this.temp; /// - /// Builds the owned reference decomposition and its lower-resolution comparisons from borrowed RGB samples. + /// Builds the owned reference decomposition and the half-resolution reference used by comparison. /// /// The configuration used for owned sample allocations. /// The borrowed reference pixels. /// The perceptual comparison parameters. - /// The comparator that owns the completed reference decomposition. + /// The comparator that owns both required reference decompositions. public static ButteraugliComparator Make(Configuration configuration, JxlImage3 rgb0, ButteraugliParameters parameters) { int xSize = rgb0.XSize; @@ -110,22 +110,17 @@ public static ButteraugliComparator Make(Configuration configuration, JxlImage3< return result; } - if (!Butteraugli.OpsinDynamicsImage(configuration, rgb0, parameters, result.Temp, result.blurTemp, result.transformedActual)) - { - throw new InvalidOperationException("OpsinDynamicsImage failed"); - } - - if (!Butteraugli.SeparateFrequencies(configuration, result.blurTemp, result.transformedActual, result.pi0)) - { - throw new InvalidOperationException("Could not separate frequencies"); - } + result.InitializeReference(configuration, rgb0); // Smaller comparisons return only zero. Do not construct their frequency bands or blur workspace. if ((xSize + 1) / 2 >= 8 && (ySize + 1) / 2 >= 8) { using JxlImage3 subsampledRgb0 = Butteraugli.SubSample2x(configuration, rgb0); - result.sub = Make(configuration, subsampledRgb0, parameters); + result.sub = new(configuration, subsampledRgb0.XSize, subsampledRgb0.YSize, parameters); + result.sub.InitializeReference(configuration, subsampledRgb0); + // Diffmap reads the child's frequency bands directly. It does not run the child's lower-resolution comparison. + // Prepare only that half-size reference; quarter-size and smaller decompositions would remain unread. using JxlPlane subResult = new(configuration, result.sub.xSize, result.sub.ySize); result.subsampledDifference.Swap(subResult); } @@ -403,4 +398,22 @@ public void Dispose() this.sub = null; this.blurTemp.Dispose(); } + + /// + /// Prepares this comparator's reference bands in its existing buffers without constructing another resolution level. + /// + /// The configuration used by the comparison stages. + /// The borrowed reference, with the dimensions supplied to this comparator's constructor. + private void InitializeReference(Configuration configuration, JxlImage3 rgb) + { + if (!Butteraugli.OpsinDynamicsImage(configuration, rgb, this.parameters, this.Temp, this.blurTemp, this.transformedActual)) + { + throw new InvalidOperationException("OpsinDynamicsImage failed"); + } + + if (!Butteraugli.SeparateFrequencies(configuration, this.blurTemp, this.transformedActual, this.pi0)) + { + throw new InvalidOperationException("Could not separate frequencies"); + } + } } From a8b1507f5bb9fe68ef2b4d94b554586389b54a3a Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 22:12:22 +1000 Subject: [PATCH 271/328] Keep JPEG XL reservoir loading in the concrete bit reader --- .../Processing/Decoder/Ans/JxlAnsReader.cs | 2 - .../Decoder/Ans/JxlAnsSymbolReader.cs | 42 ++++++------ .../Jxl/Processing/Decoder/JxlBitReader.cs | 68 +++++++++---------- .../Jxl/Processing/EntropyCoderTests.cs | 62 +++++++---------- 4 files changed, 78 insertions(+), 96 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsReader.cs index 1cdde7ff67..4644fbff3d 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsReader.cs @@ -385,8 +385,6 @@ public static void DecodeAnsCodes(Configuration configuration, int numHistograms public static void DecodeUIntConfiguration(int logAlphaSize, out JxlAnsHybridUIntConfiguration uintConfig, JxlBitReader br) { - br.Refill(); - uint splitExponent = br.ReadBits32((uint)JxlMath.CeilLog2Nonzero(logAlphaSize + 1)); uint msbInToken = 0; uint lsbInToken = 0; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsSymbolReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsSymbolReader.cs index c5b6532f69..eada7d1724 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsSymbolReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsSymbolReader.cs @@ -153,7 +153,13 @@ public static JxlAnsSymbolReader Create(Configuration configuration, JxlAnsCode /// Whether the stream ended at the format's signature state. public bool CheckAnsFinalState() => this.state == JxlAnsConstants.AnsSignature << 16; - public int ReadSymbolAnsWithoutRefill(int histogramIndex, JxlBitReader br) + /// + /// Reads one ANS symbol and consumes a 16-bit normalization field only when the state needs it. + /// + /// The decoded histogram table to use. + /// The encoded normalization fields. The bit reader owns reservoir refill. + /// The decoded symbol. + private int ReadSymbolAns(int histogramIndex, JxlBitReader br) { uint res = this.state & (JxlAnsConstants.AnsTableSize - 1); @@ -175,27 +181,24 @@ public int ReadSymbolAnsWithoutRefill(int histogramIndex, JxlBitReader br) return symbol.Value; } - public int ReadSymbolHuffmanWithoutRefill(int histogramIndex, JxlBitReader br) - => this.huffmanDecoders[histogramIndex].ReadSymbol(br); - - public int ReadSymbolWithoutRefill(int histogramIndex, JxlBitReader br) + /// + /// Reads one symbol using the prefix or ANS coding selected by the entropy header. + /// + /// The decoded histogram table to use. + /// The encoded symbol fields. Reads and peeks refill their own reservoir. + /// The decoded symbol. + public int ReadSymbol(int histogramIndex, JxlBitReader br) { if (this.usePrefixCode) { - return this.ReadSymbolHuffmanWithoutRefill(histogramIndex, br); + return this.huffmanDecoders[histogramIndex].ReadSymbol(br); } else { - return this.ReadSymbolAnsWithoutRefill(histogramIndex, br); + return this.ReadSymbolAns(histogramIndex, br); } } - public int ReadSymbol(int histogramIndex, JxlBitReader br) - { - br.Refill(); - return this.ReadSymbolWithoutRefill(histogramIndex, br); - } - /// /// Expands a hybrid-integer token and its payload without changing entropy or history state. /// @@ -244,9 +247,7 @@ public static int ReadHybridUIntConfiguration(JxlAnsHybridUIntConfiguration conf public void ReadHybridUIntClusteredHuffRleOnly(int context, JxlBitReader br, ref uint value, ref uint run) { DebugGuard.IsTrue(this.IsHuffRleOnly, "This method is meant for huffman RLE only"); - br.Refill(); - - int token = this.ReadSymbolHuffmanWithoutRefill(context, br); + int token = this.huffmanDecoders[context].ReadSymbol(br); if (token >= this.lz77Threshold) { run = (uint)( @@ -283,17 +284,14 @@ public int ReadHybridUIntClustered(bool usesLz77, int context, JxlBitReader br, } } - br.Refill(); - int token = this.ReadSymbolWithoutRefill(context, br); + int token = this.ReadSymbol(context, br); if (usesLz77) { if (token >= this.lz77Threshold) { this.numToCopy = (uint)ReadHybridUIntConfiguration(this.lz77LengthUInt, token - this.lz77Threshold, br) + (uint)this.lz77MinLength; - br.Refill(); - - int distanceToken = this.ReadSymbolWithoutRefill(this.lz77Context, br); + int distanceToken = this.ReadSymbol(this.lz77Context, br); // Hybrid integers retain all 32 bits. Widen before adding one so the largest distance // cannot wrap to zero. The format then bounds it by the available history and ring size. @@ -418,4 +416,4 @@ public bool IsSingleValueAndAdvance(int context, int count, out uint value, Span /// Releases the allocator-owned LZ77 history. /// public void Dispose() => this.lz77Window?.Dispose(); -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs index 2a28e772ff..2d7bcc29d0 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs @@ -13,7 +13,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; /// Prefetch can advance the stream, but only reads and skips advance the logical bit position. /// The caller owns the stream and must keep it alive while using this reader. /// -internal class JxlBitReader(Stream stream) +internal sealed class JxlBitReader(Stream stream) { /// /// The next unread bits, starting at bit zero. @@ -49,8 +49,8 @@ internal class JxlBitReader(Stream stream) /// Reads up to one complete reservoir block, including streams that return short reads. /// /// The little-endian block, with unused high bits cleared. - /// The number of valid bits in the block. - private void RefillCore(out ulong value, out int bits) + /// The number of valid bits in the block. + private void RefillCore(out ulong value, out int bitCount) { Span bytes = stackalloc byte[sizeof(ulong)]; int count = 0; @@ -71,29 +71,29 @@ private void RefillCore(out ulong value, out int bits) // result ends the refill. Clear unused high bytes so no uninitialized stack data is read. bytes[count..].Clear(); value = BinaryPrimitives.ReadUInt64LittleEndian(bytes); - bits = count * 8; + bitCount = count * 8; this.bytesFetched += count; } /// /// Ensures that the requested field is buffered without consuming any bits. /// - /// The field width, from one to 64 bits. - private void EnsureBits(int bits) + /// The field width, from one to 64 bits. + private void EnsureBits(int bitCount) { if (this.bufferRemainingBits == 0) { this.RefillCore(out this.buffer, out this.bufferRemainingBits); } - if (bits > this.bufferRemainingBits && this.lookaheadBits == 0) + if (bitCount > this.bufferRemainingBits && this.lookaheadBits == 0) { this.RefillCore(out this.lookahead, out this.lookaheadBits); } // A short final block cannot satisfy a larger field. Preserve the buffered bits on // failure; neither their count nor the logical position may wrap or advance. - if (bits > this.bufferRemainingBits + this.lookaheadBits) + if (bitCount > this.bufferRemainingBits + this.lookaheadBits) { throw new EndOfStreamException(); } @@ -102,45 +102,45 @@ private void EnsureBits(int bits) /// /// Reads or peeks one field using the same reservoir and bit ordering. /// - /// The field width, from zero to 64 bits. + /// The field width, from zero to 64 bits. /// Whether to leave the logical position unchanged. /// The unsigned field value. - private ulong ReadBitsCore(int bits, bool peek) + private ulong ReadBitsCore(int bitCount, bool peek) { - if (bits == 0) + if (bitCount == 0) { return 0; } - this.EnsureBits(bits); + this.EnsureBits(bitCount); // Current bits occupy the low end. A field crossing the block boundary continues // into lookahead. Keep both blocks until consumption so repeated peeks are identical. ulong value = this.buffer; - if (bits > this.bufferRemainingBits) + if (bitCount > this.bufferRemainingBits) { value |= this.lookahead << this.bufferRemainingBits; } // This mask also handles a full 64-bit field. Shifting 1UL by 64 would wrap to bit zero. - value &= ulong.MaxValue >> (64 - bits); + value &= ulong.MaxValue >> (64 - bitCount); if (peek) { return value; } - if (bits < this.bufferRemainingBits) + if (bitCount < this.bufferRemainingBits) { - this.buffer >>= bits; - this.bufferRemainingBits -= bits; + this.buffer >>= bitCount; + this.bufferRemainingBits -= bitCount; } else { // Promote remaining lookahead bits after consuming the current block. The shift // is at most 63: a crossing field always consumes at least one current-buffer bit. - int consumedLookahead = bits - this.bufferRemainingBits; + int consumedLookahead = bitCount - this.bufferRemainingBits; this.buffer = this.lookahead >> consumedLookahead; this.bufferRemainingBits = this.lookaheadBits - consumedLookahead; this.lookahead = 0; @@ -167,55 +167,55 @@ public void JumpToByteBoundary() /// /// Reads an unsigned field of up to 32 bits. /// - /// The field width, from zero to 32 bits. + /// The field width, from zero to 32 bits. /// The field value. - public virtual uint ReadBits32(uint bits) => (uint)this.ReadBitsCore((int)bits, false); + public uint ReadBits32(uint bitCount) => (uint)this.ReadBitsCore((int)bitCount, false); /// /// Peeks an unsigned field of up to 32 bits without consuming it. /// - /// The field width, from zero to 32 bits. + /// The field width, from zero to 32 bits. /// The field value. - public virtual uint PeekBits32(uint bits) => (uint)this.ReadBitsCore((int)bits, true); + public uint PeekBits32(uint bitCount) => (uint)this.ReadBitsCore((int)bitCount, true); /// /// Skips the requested number of bits. /// - /// The number of bits to skip. - public virtual void SkipBits32(uint bits) => this.SkipBits64(bits); + /// The number of bits to skip. + public void SkipBits32(uint bitCount) => this.SkipBits64(bitCount); /// /// Reads an unsigned field of up to 64 bits. /// - /// The field width, from zero to 64 bits. + /// The field width, from zero to 64 bits. /// The field value. - public virtual ulong ReadBits64(ulong bits) => this.ReadBitsCore((int)bits, false); + public ulong ReadBits64(ulong bitCount) => this.ReadBitsCore((int)bitCount, false); /// /// Peeks an unsigned field of up to 64 bits without consuming it. /// - /// The field width, from zero to 64 bits. + /// The field width, from zero to 64 bits. /// The field value. - public virtual ulong PeekBits64(ulong bits) => this.ReadBitsCore((int)bits, true); + public ulong PeekBits64(ulong bitCount) => this.ReadBitsCore((int)bitCount, true); /// /// Skips a bit range, including ranges longer than one reservoir block. /// - /// The number of bits to skip. - public virtual void SkipBits64(ulong bits) + /// The number of bits to skip. + public void SkipBits64(ulong bitCount) { - while (bits >= 64) + while (bitCount >= 64) { this.ReadBitsCore(64, false); - bits -= 64; + bitCount -= 64; } - this.ReadBitsCore((int)bits, false); + this.ReadBitsCore((int)bitCount, false); } /// /// Reads the next Boolean field. /// /// Whether the next bit is one. - public virtual bool ReadBoolean() => this.ReadBits32(1) != 0; + public bool ReadBoolean() => this.ReadBits32(1) != 0; } diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/EntropyCoderTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/EntropyCoderTests.cs index 4ff95135be..3b0a8f50f8 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/EntropyCoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/EntropyCoderTests.cs @@ -4,6 +4,7 @@ using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular; using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; @@ -174,33 +175,12 @@ public void PackUnpack() } } - private sealed class MockBitReader : JxlBitReader - { - public ulong NBits { get; set; } - - public ulong Bits { get; set; } - - public MockBitReader(Stream stream) - : base(stream) - { - } - - public override uint PeekBits32(uint bitCount) - { - Assert.Equal(this.NBits, bitCount); - return (uint)this.Bits; - } - - public override uint ReadBits32(uint bitCount) - { - Assert.Equal(this.NBits, bitCount); - return (uint)this.Bits; - } - } - - private static void HybridUintRoundtrip( - JxlAnsHybridUIntConfiguration config, - uint limit = 1u << 24) + /// + /// Checks hybrid integers through one real bitstream, including each token's exact number of remaining bits. + /// + /// The split and token-bit settings. + /// The largest generated integer. + private static void HybridUintRoundtrip(JxlAnsHybridUIntConfiguration config, uint limit) { Rng rng = new(0); @@ -209,7 +189,8 @@ private static void HybridUintRoundtrip( uint[] integers = new uint[numIntegers]; uint[] token = new uint[numIntegers]; uint[] nbits = new uint[numIntegers]; - uint[] bits = new uint[numIntegers]; + using MemoryStream input = new(); + JxlBitWriter writer = new(input); for (int i = 0; i < numIntegers; i++) { @@ -218,31 +199,36 @@ private static void HybridUintRoundtrip( integers[i], out token[i], out nbits[i], - out bits[i]); + out uint bits); + + // Tokens belong to the entropy stream. Only their remaining low bits go into this bitstream. + writer.Write((int)nbits[i], bits); } + writer.ZeroPadToByte(); + input.Position = 0; + JxlBitReader reader = new(input); + for (int i = 0; i < numIntegers; i++) { - MockBitReader br = new(Stream.Null) - { - NBits = nbits[i], - Bits = bits[i] - }; + long before = reader.TotalBitsConsumed; Assert.Equal( integers[i], - (uint)JxlAnsSymbolReader.ReadHybridUIntConfiguration(config, (int)token[i], br)); + (uint)JxlAnsSymbolReader.ReadHybridUIntConfiguration(config, (int)token[i], reader)); + + Assert.Equal((long)nbits[i], reader.TotalBitsConsumed - before); } } [Fact] - public void Test000() => HybridUintRoundtrip(new JxlAnsHybridUIntConfiguration(0, 0, 0)); + public void Test000() => HybridUintRoundtrip(new JxlAnsHybridUIntConfiguration(0, 0, 0), 1U << 24); [Fact] - public void Test411() => HybridUintRoundtrip(new JxlAnsHybridUIntConfiguration(4, 1, 1)); + public void Test411() => HybridUintRoundtrip(new JxlAnsHybridUIntConfiguration(4, 1, 1), 1U << 24); [Fact] - public void Test420() => HybridUintRoundtrip(new JxlAnsHybridUIntConfiguration(4, 2, 0)); + public void Test420() => HybridUintRoundtrip(new JxlAnsHybridUIntConfiguration(4, 2, 0), 1U << 24); [Fact] public void Test421() => HybridUintRoundtrip( From e80e3941cb282a17a9c0abf6a61a14f30f3a0fe2 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 22:14:56 +1000 Subject: [PATCH 272/328] Implement JPEG XL modular group and transform fields --- .../ContextPrediction/JxlModularHeader.cs | 21 +- .../Modular/Encoding/JxlGroupHeader.cs | 62 +++++- .../Transforms/JxlSqueezeParameters.cs | 29 ++- .../Modular/Transforms/JxlTransform.cs | 195 +++++++++++++++++- .../Formats/Jxl/IO/FieldsTests.cs | 147 +++++++++++++ 5 files changed, 439 insertions(+), 15 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularHeader.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularHeader.cs index bda4751e83..dfe763a339 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularHeader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularHeader.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Fields; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; @@ -23,6 +22,11 @@ internal sealed class JxlModularHeader : IJxlFields private int p3Ce; private InlineArray4 w; + /// + /// Initializes the weighted-prediction coefficients to their serialized defaults. + /// + public JxlModularHeader() => JxlBundle.Init(this); + /// /// Gets or sets a value indicating whether all values are default. /// @@ -101,6 +105,7 @@ public int P3Ce /// Reference to w public Span GetW() => this.w; + /// public bool Visit(JxlVisitor v) { if (v.AllDefault(this, ref this.allDefault)) @@ -126,10 +131,18 @@ public bool Visit(JxlVisitor v) return true; - bool VisitP(int value, ref int p) + // Coefficients use unsigned five-bit fields. Keep the wire local separate from the signed predictor coefficient. + bool VisitP(int defaultValue, ref int coefficient) { - ref uint unsignedP = ref Unsafe.As(ref p); - return v.Bits(5, (uint)value, ref unsignedP); + uint value = (uint)coefficient; + + if (!v.Bits(5, (uint)defaultValue, ref value)) + { + return false; + } + + coefficient = (int)value; + return true; } } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlGroupHeader.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlGroupHeader.cs index e9d11b036f..2d5cb90a37 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlGroupHeader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlGroupHeader.cs @@ -8,20 +8,54 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding; +/// +/// Stores the global-tree flag, weighted-prediction settings and owned transform metadata for one modular group. +/// internal sealed class JxlGroupHeader : IJxlFields { private bool useGlobalTree; + private List transforms = []; + /// + /// Initializes the serialized defaults and the owned weighted-prediction header. + /// + public JxlGroupHeader() => JxlBundle.Init(this); + + /// + /// Gets or sets a value indicating whether the group uses the global prediction tree. + /// public bool UseGlobalTree { get => this.useGlobalTree; set => this.useGlobalTree = value; } - internal JxlModularHeader WeightedHeader { get; set; } = new(); + /// + /// Gets the owned weighted-prediction settings. + /// + public JxlModularHeader WeightedHeader { get; } = new(); - internal List Transforms { get; set; } = []; + /// + /// Gets the owned transforms, or copies transform metadata from another owner without sharing mutable squeeze steps. + /// + public List Transforms + { + get => this.transforms; + set + { + List replacement = new(value.Count); + + foreach (JxlTransform transform in value) + { + replacement.Add(transform.DeepClone()); + } + + // Copy before publishing. The source can be this header's own list, and failure must retain the previous metadata. + this.transforms = replacement; + } + } + /// public bool Visit(JxlVisitor visitor) { if (!visitor.Boolean(false, ref this.useGlobalTree)) @@ -36,20 +70,34 @@ public bool Visit(JxlVisitor visitor) uint numTransforms = (uint)this.Transforms.Count; - _ = visitor.U32( + if (!visitor.U32( JxlFieldExpressions.Value(0), JxlFieldExpressions.Value(1), JxlFieldExpressions.BitsOffset(4, 2), JxlFieldExpressions.BitsOffset(8, 18), 0, - ref numTransforms); + ref numTransforms)) + { + return false; + } + + // U32 bounds the count to 273. Reading must create actual transform objects, not just list capacity. + // A default visitor also sets the count to zero, so apply the count for every visitor that changes it. + int count = (int)numTransforms; + + if (this.transforms.Count > count) + { + this.transforms.RemoveRange(count, this.transforms.Count - count); + } + + this.transforms.EnsureCapacity(count); - if (visitor.IsReading) + while (this.transforms.Count < count) { - this.Transforms = new List((int)numTransforms); + this.transforms.Add(new JxlTransform()); } - Span sp = CollectionsMarshal.AsSpan(this.Transforms); + Span sp = CollectionsMarshal.AsSpan(this.transforms); for (int i = 0; i < numTransforms; i++) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueezeParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueezeParameters.cs index 1d4c332770..fd5b8b03bd 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueezeParameters.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueezeParameters.cs @@ -15,7 +15,17 @@ internal struct JxlSqueezeParameters : IJxlFields private int beginC; private int numC; - public JxlSqueezeParameters() => JxlBundle.Init(this); + /// + /// Initializes one vertical, non-in-place squeeze step starting at channel zero and covering two channels. + /// + public JxlSqueezeParameters() + { + // Calling Bundle.Init through IJxlFields would initialize a boxed copy of this value, not the stored step. + this.horizontal = false; + this.inPlace = false; + this.beginC = 0; + this.numC = 2; + } /// /// Gets or sets a value indicating whether the transform is horizontal. @@ -35,20 +45,30 @@ public bool InPlace set => this.inPlace = value; } + /// + /// Gets or sets the first selected channel index. + /// public int BeginC { readonly get => this.beginC; set => this.beginC = value; } + /// + /// Gets or sets the number of selected channels. + /// public int NumC { readonly get => this.numC; set => this.numC = value; } + /// public bool Visit(JxlVisitor visitor) { + uint beginC = (uint)this.beginC; + uint numC = (uint)this.numC; + if (!visitor.Boolean(false, ref this.horizontal) || !visitor.Boolean(false, ref this.inPlace) || !visitor.U32( @@ -57,18 +77,21 @@ public bool Visit(JxlVisitor visitor) JxlFieldExpressions.BitsOffset(10, 72), JxlFieldExpressions.BitsOffset(13, 1096), 0, - ref this.beginC) || + ref beginC) || !visitor.U32( JxlFieldExpressions.Value(1), JxlFieldExpressions.Value(2), JxlFieldExpressions.Value(3), JxlFieldExpressions.BitsOffset(4, 4), 2, - ref this.numC)) + ref numC)) { return false; } + // Both wire distributions fit in Int32. Publish the decoded values into the actual value-type fields. + this.beginC = (int)beginC; + this.numC = (int)numC; return true; } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs index 51ca8332b0..eca30c09b5 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs @@ -12,11 +12,18 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; /// internal sealed class JxlTransform : IJxlFields, IDeepCloneable { + /// + /// Initializes default parameters and marks the transform invalid until its identifier is read or assigned. + /// public JxlTransform() : this(JxlTransformType.Invalid) { } + /// + /// Initializes serialized parameter defaults and selects a transform identifier. + /// + /// The transform identifier. public JxlTransform(JxlTransformType id) { JxlBundle.Init(this); @@ -44,16 +51,34 @@ private JxlTransform(JxlTransform source) this.Squeezes.AddRange(source.Squeezes); } + /// + /// Gets or sets the encoded transform kind that selects the remaining parameters. + /// public JxlTransformType TransformType { get; set; } + /// + /// Gets or sets the first selected channel index for an RCT or palette transform. + /// public int BeginC { get; set; } + /// + /// Gets or sets the reversible color-transform mode, from 0 through 41. Mode 6 selects YCoCg. + /// public int RctType { get; set; } + /// + /// Gets or sets the number of source channels combined by a palette transform. + /// public int NumC { get; set; } + /// + /// Gets or sets the number of explicit palette colors, excluding predictive delta entries. + /// public int Colors { get; set; } + /// + /// Gets or sets the number of predictive delta entries before the palette's color entries. + /// public int Deltas { get; set; } /// @@ -61,18 +86,186 @@ private JxlTransform(JxlTransform source) /// public List Squeezes { get; } = []; + /// + /// Gets or sets the encoder's maximum delta error. This setting is not serialized. + /// public int MaxDeltaError { get; set; } + /// + /// Gets or sets the prediction method applied to palette delta entries. + /// public JxlPredictor Predictor { get; set; } + /// + /// Gets or sets a value indicating whether the encoder orders palette colors. This setting is not serialized. + /// public bool OrderedPalette { get; set; } = true; + /// + /// Gets or sets a value indicating whether palette encoding can quantize colors. This setting is not serialized. + /// public bool LossyPalette { get; set; } /// public JxlTransform DeepClone() => new(this); - public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); + /// + public bool Visit(JxlVisitor visitor) + { + // Transform identifiers use four exact values, not the broader JPEG XL enum distribution. + // Keep unsigned wire locals separate from the existing signed channel/count properties. + uint value = (uint)this.TransformType; + + if (!visitor.U32( + JxlFieldExpressions.Value((uint)JxlTransformType.Rct), + JxlFieldExpressions.Value((uint)JxlTransformType.Palette), + JxlFieldExpressions.Value((uint)JxlTransformType.Squeeze), + JxlFieldExpressions.Value((uint)JxlTransformType.Invalid), + (uint)JxlTransformType.Rct, + ref value) + || value == (uint)JxlTransformType.Invalid) + { + return false; + } + + this.TransformType = (JxlTransformType)value; + + if (visitor.Conditional(this.TransformType is JxlTransformType.Rct or JxlTransformType.Palette)) + { + value = (uint)this.BeginC; + + if (!visitor.U32( + JxlFieldExpressions.Bits(3), + JxlFieldExpressions.BitsOffset(6, 8), + JxlFieldExpressions.BitsOffset(10, 72), + JxlFieldExpressions.BitsOffset(13, 1096), + 0, + ref value)) + { + return false; + } + + this.BeginC = (int)value; + } + + if (visitor.Conditional(this.TransformType == JxlTransformType.Rct)) + { + value = (uint)this.RctType; + + if (!visitor.U32( + JxlFieldExpressions.Value(6), + JxlFieldExpressions.Bits(2), + JxlFieldExpressions.BitsOffset(4, 2), + JxlFieldExpressions.BitsOffset(6, 10), + 6, + ref value) + || value >= 42) + { + return false; + } + + this.RctType = (int)value; + } + + if (visitor.Conditional(this.TransformType == JxlTransformType.Palette)) + { + value = (uint)this.NumC; + + if (!visitor.U32( + JxlFieldExpressions.Value(1), + JxlFieldExpressions.Value(3), + JxlFieldExpressions.Value(4), + JxlFieldExpressions.BitsOffset(13, 1), + 3, + ref value)) + { + return false; + } + + this.NumC = (int)value; + value = (uint)this.Colors; + + if (!visitor.U32( + JxlFieldExpressions.Bits(8), + JxlFieldExpressions.BitsOffset(10, 256), + JxlFieldExpressions.BitsOffset(12, 1280), + JxlFieldExpressions.BitsOffset(16, 5376), + 256, + ref value)) + { + return false; + } + + this.Colors = (int)value; + value = (uint)this.Deltas; + + if (!visitor.U32( + JxlFieldExpressions.Value(0), + JxlFieldExpressions.BitsOffset(8, 1), + JxlFieldExpressions.BitsOffset(10, 257), + JxlFieldExpressions.BitsOffset(16, 1281), + 0, + ref value)) + { + return false; + } + + this.Deltas = (int)value; + value = (uint)this.Predictor; + + if (!visitor.Bits(4, (uint)JxlPredictor.Zero, ref value) || value >= (uint)JxlPredictor.Best) + { + return false; + } + + this.Predictor = (JxlPredictor)value; + } + + if (visitor.Conditional(this.TransformType == JxlTransformType.Squeeze)) + { + value = (uint)this.Squeezes.Count; + + if (!visitor.U32( + JxlFieldExpressions.Value(0), + JxlFieldExpressions.BitsOffset(4, 1), + JxlFieldExpressions.BitsOffset(6, 9), + JxlFieldExpressions.BitsOffset(8, 41), + 0, + ref value)) + { + return false; + } + + // The decoded count is at most 296. A default visitor also clears an earlier explicit sequence to select the default squeeze. + int count = (int)value; + + if (this.Squeezes.Count > count) + { + this.Squeezes.RemoveRange(count, this.Squeezes.Count - count); + } + + this.Squeezes.EnsureCapacity(count); + + while (this.Squeezes.Count < count) + { + this.Squeezes.Add(new JxlSqueezeParameters()); + } + + // Squeeze steps have only primitive fields, with no all-default flag or extensions. Visit the actual value entries + // directly so decoded fields persist without boxing a struct through IJxlFields. The current bundle owns their bit budget. + Span squeezes = CollectionsMarshal.AsSpan(this.Squeezes); + + for (int i = 0; i < squeezes.Length; i++) + { + if (!squeezes[i].Visit(visitor)) + { + return false; + } + } + } + + return true; + } public static void CheckEqualChannels(JxlModularImage image, int c1, int c2) { diff --git a/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs index 42a267dd3b..a6602d3a18 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs @@ -18,6 +18,153 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jxl.IO; public class FieldsTests { + /// + /// Checks modular group counts and transform parameters against fixed native wire encodings. + /// + /// The fixed least-significant-bit-first group header. + /// The header length before byte padding. + /// The single encoded transform identifier, or Invalid when the count is zero. + [Theory] + [InlineData(new byte[] { 0x02 }, 4, (byte)JxlTransformType.Invalid)] + [InlineData(new byte[] { 0x06, 0x00 }, 13, (byte)JxlTransformType.Rct)] + [InlineData(new byte[] { 0x16, 0x28, 0x00, 0x00 }, 31, (byte)JxlTransformType.Palette)] + [InlineData(new byte[] { 0x66, 0x30, 0x03 }, 21, (byte)JxlTransformType.Squeeze)] + public void ModularGroupHeader_ReadAndWriteMatchesFixedWire(byte[] wire, int bitCount, byte expectedType) + { + JxlTransformType type = (JxlTransformType)expectedType; + + // The common prefix is global-tree=false, weighted-prediction defaults=true, then the transform count. + // RCT starts at channel 0 and uses mode 6. Palette starts at channel 0 with three channels, 256 colors, no deltas and predictor Zero. + // Squeeze has one horizontal in-place step beginning at channel 3 and covering one channel. + JxlGroupHeader header = new() { UseGlobalTree = true }; + header.WeightedHeader.P1C = 1; + + if (type == JxlTransformType.Invalid) + { + // The zero-count wire must also remove entries left by a previous group header. + header.Transforms.Add(new JxlTransform(JxlTransformType.Rct)); + header.Transforms.Add(new JxlTransform(JxlTransformType.Palette)); + } + + using MemoryStream input = new(wire, false); + JxlBitReader reader = new(input); + + Assert.True(JxlBundle.Read(reader, header)); + Assert.Equal(bitCount, reader.TotalBitsConsumed); + Assert.False(header.UseGlobalTree); + Assert.Equal(16, header.WeightedHeader.P1C); + Assert.Equal(type == JxlTransformType.Invalid ? 0 : 1, header.Transforms.Count); + + if (type != JxlTransformType.Invalid) + { + JxlTransform transform = header.Transforms[0]; + Assert.Equal(type, transform.TransformType); + + switch (type) + { + case JxlTransformType.Rct: + Assert.Equal(0, transform.BeginC); + Assert.Equal(6, transform.RctType); + break; + + case JxlTransformType.Palette: + Assert.Equal(0, transform.BeginC); + Assert.Equal(3, transform.NumC); + Assert.Equal(256, transform.Colors); + Assert.Equal(0, transform.Deltas); + Assert.Equal(JxlPredictor.Zero, transform.Predictor); + break; + + case JxlTransformType.Squeeze: + JxlSqueezeParameters step = Assert.Single(transform.Squeezes); + Assert.True(step.Horizontal); + Assert.True(step.InPlace); + Assert.Equal(3, step.BeginC); + Assert.Equal(1, step.NumC); + break; + } + } + + using MemoryStream output = new(); + JxlBitWriter writer = new(output); + + Assert.True(JxlBundle.Write(header, writer, JxlLayerType.Header, null)); + Assert.Equal(bitCount, writer.BitsWritten); + + writer.ZeroPadToByte(); + + Assert.Equal(wire, output.ToArray()); + } + + /// + /// Checks owned predictor defaults and resets a previously populated transform list through the existing visitor. + /// + [Fact] + public void ModularGroupHeader_DefaultsRestoreOwnedFields() + { + JxlGroupHeader header = new(); + JxlModularHeader weighted = header.WeightedHeader; + + Assert.Equal( + (16, 10, 7, 7, 7, 0, 0), + (weighted.P1C, weighted.P2C, weighted.P3Ca, weighted.P3Cb, weighted.P3Cc, weighted.P3Cd, weighted.P3Ce)); + + Assert.Equal(new uint[] { 13, 12, 12, 12 }, weighted.GetW().ToArray()); + Assert.True(JxlBundle.AllDefault(header)); + + header.UseGlobalTree = true; + weighted.P1C = 1; + header.Transforms.Add(new JxlTransform(JxlTransformType.Rct)); + JxlBundle.SetDefault(header); + + Assert.False(header.UseGlobalTree); + Assert.Same(weighted, header.WeightedHeader); + Assert.Equal(16, weighted.P1C); + Assert.Empty(header.Transforms); + + // A value-type constructor must set the stored step, rather than an IJxlFields boxed copy. + JxlSqueezeParameters step = new(); + Assert.Equal((false, false, 0, 2), (step.Horizontal, step.InPlace, step.BeginC, step.NumC)); + } + + /// + /// Rejects reserved transform identifiers, RCT modes and prediction modes from the encoded input. + /// + /// The fixed group header with one invalid transform field. + [Theory] + [InlineData(new byte[] { 0x36 })] + [InlineData(new byte[] { 0x06, 0x18, 0x04 })] + [InlineData(new byte[] { 0x16, 0x28, 0x00, 0x70 })] + public void ModularGroupHeader_ReadRejectsReservedTransformValues(byte[] wire) + { + // These encode transform ID 3, RCT mode 42, and predictor 14 (Best), respectively. + using MemoryStream input = new(wire, false); + JxlBitReader reader = new(input); + + Assert.False(JxlBundle.Read(reader, new JxlGroupHeader())); + } + + /// + /// Checks that header ownership separates mutable squeeze metadata and supports assigning its own list. + /// + [Fact] + public void ModularGroupHeader_AssigningTransformsPreservesSourceMetadata() + { + JxlTransform source = new(JxlTransformType.Squeeze); + source.Squeezes.Add(new JxlSqueezeParameters { Horizontal = true, NumC = 1 }); + JxlGroupHeader header = new() { Transforms = [source] }; + + header.Transforms = header.Transforms; + + JxlTransform stored = Assert.Single(header.Transforms); + stored.Squeezes.Clear(); + + JxlSqueezeParameters originalStep = Assert.Single(source.Squeezes); + Assert.True(originalStep.Horizontal); + Assert.Equal(1, originalStep.NumC); + Assert.Equal(JxlTransformType.Squeeze, stored.TransformType); + } + /// /// Checks progressive pass counts, shifts and transition indices against fixed native field encodings. /// From 1f09e90e8d287527ba9e4b9c430416a8fac28b22 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 22:20:30 +1000 Subject: [PATCH 273/328] Complete JPEG XL frame and transform metadata defaults --- .../Formats/Jxl/Cms/JxlOpsinConstants.cs | 10 + .../Formats/Jxl/Fields/JxlVisitor.cs | 58 ++++ .../Jxl/IO/FrameHeader/JxlAnimationFrame.cs | 10 +- .../Jxl/IO/FrameHeader/JxlFrameHeader.cs | 141 +++++--- .../FrameHeader/JxlYCbCrChromaSubsampling.cs | 9 + .../Jxl/IO/Metadata/JxlCustomTransformData.cs | 20 +- .../Jxl/IO/Metadata/JxlExtraChannelInfo.cs | 45 ++- .../Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs | 49 ++- .../Formats/Jxl/Processing/JxlLoopFilter.cs | 302 ++++++++++++++++-- .../Formats/Jxl/IO/FieldsTests.cs | 179 +++++++++++ 10 files changed, 733 insertions(+), 90 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlOpsinConstants.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlOpsinConstants.cs index 2b304885d1..246affd45f 100644 --- a/src/ImageSharp/Formats/Jxl/Cms/JxlOpsinConstants.cs +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlOpsinConstants.cs @@ -23,4 +23,14 @@ internal static class JxlOpsinConstants public const float OpsinAbsorbanceBias2 = OpsinAbsorbanceBias0; public static ReadOnlySpan OpsinAbsorbanceBias => [OpsinAbsorbanceBias0, OpsinAbsorbanceBias1, OpsinAbsorbanceBias2]; + + /// + /// Gets the frozen default inverse opsin matrix in row-major order, as defined by the JPEG XL bitstream. + /// + public static ReadOnlySpan DefaultInverseOpsinAbsorbanceMatrix => + [ + 11.031566901960783f, -9.866943921568629f, -0.16462299647058826f, + -3.254147380392157f, 4.418770392156863f, -0.16462299647058826f, + -3.6588512862745097f, 2.7129230470588235f, 1.9459282392156863f + ]; } diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlVisitor.cs index 9d15ad7854..df108a66f5 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlVisitor.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlVisitor.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; +using System.Text; namespace SixLabors.ImageSharp.Formats.Jxl.Fields; @@ -170,6 +171,63 @@ public virtual bool Enum(T defaultValue, ref T value) /// Whether the field operation succeeded. public abstract bool F16(float defaultValue, ref float value); + /// + /// Visits the UTF-8 name shared by frame and extra-channel headers. + /// + /// The name to read, write, compare or reset. Null and empty names encode identically. + /// Whether the byte length and every name byte can be processed. + public bool Name(ref string? value) + { + // Name lengths count encoded bytes, not UTF-16 characters. The longest wire alternative stores 48 plus ten bits. + const int MaximumNameBytes = 1071; + uint byteLength = this.IsReading ? 0U : (uint)Encoding.UTF8.GetByteCount(value.AsSpan()); + + if (!this.U32( + JxlFieldExpressions.Value(0), + JxlFieldExpressions.Bits(4), + JxlFieldExpressions.BitsOffset(5, 16), + JxlFieldExpressions.BitsOffset(10, 48), + 0, + ref byteLength) + || byteLength > MaximumNameBytes) + { + return false; + } + + // Reset visitors also set the byte length to zero. Do not encode an earlier name into their empty destination. + if (byteLength == 0) + { + value = string.Empty; + return true; + } + + Span bytes = stackalloc byte[(int)byteLength]; + + if (!this.IsReading) + { + Encoding.UTF8.GetBytes(value.AsSpan(), bytes); + } + + for (int i = 0; i < bytes.Length; i++) + { + uint encoded = this.IsReading ? 0U : bytes[i]; + + if (!this.Bits(8, 0, ref encoded)) + { + return false; + } + + bytes[i] = (byte)encoded; + } + + if (this.IsReading) + { + value = Encoding.UTF8.GetString(bytes); + } + + return true; + } + /// /// Selects whether a conditional group of fields is visited. /// diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlAnimationFrame.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlAnimationFrame.cs index be422cc297..eeefa3cc07 100644 --- a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlAnimationFrame.cs +++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlAnimationFrame.cs @@ -21,6 +21,11 @@ internal sealed class JxlAnimationFrame : IJxlFields /// private uint timecode; + /// + /// Initializes the frame duration and timecode to zero. + /// + public JxlAnimationFrame() => JxlBundle.Init(this); + /// /// Gets or sets the duration of the animation. /// @@ -45,9 +50,10 @@ public uint Timecode /// public JxlCodecMetadata? CodecMetadata { get; set; } + /// public bool Visit(JxlVisitor visitor) { - if (visitor.Conditional(this.CodecMetadata?.ImageMetadata?.HaveAnimation == true)) + if (visitor.Conditional(this.CodecMetadata?.ImageMetadata.HaveAnimation == true)) { if (!visitor.U32( JxlFieldExpressions.Value(0), @@ -61,7 +67,7 @@ public bool Visit(JxlVisitor visitor) } } - if (visitor.Conditional(this.CodecMetadata?.ImageMetadata?.Animation?.ContainsTimecodes == true)) + if (visitor.Conditional(this.CodecMetadata?.ImageMetadata.Animation.ContainsTimecodes == true)) { if (!visitor.Bits(32, 0u, ref this.timecode)) { diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs index d02b283eaf..fea3477a3f 100644 --- a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs +++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs @@ -20,11 +20,12 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; internal sealed class JxlFrameHeader : IJxlFields { // The following are backing fields for properties. + private bool allDefault; private JxlFrameEncoding encoding = JxlFrameEncoding.Modular; private JxlFrameType frameType = JxlFrameType.RegularFrame; private ulong flags; private JxlColorTransform colorTransform = JxlColorTransform.Xyb; - private JxlYCbCrChromaSubsampling? chromaSubsampling; + private JxlYCbCrChromaSubsampling chromaSubsampling = new(); private uint groupSizeShift; private uint xQmScale; private uint bQmScale; @@ -34,19 +35,26 @@ internal sealed class JxlFrameHeader : IJxlFields private uint upsampling; private List extraChannelUpsampling = []; private Point frameOrigin; - private JxlBlendingInfo? blendingInfo; + private JxlBlendingInfo blendingInfo = new(); private List extraChannelBlendingInfo = []; - private readonly JxlAnimationFrame? animationFrame; + private readonly JxlAnimationFrame animationFrame = new(); + private readonly JxlPasses passes = new(); private bool isLast; private uint saveAsReference; private bool saveBeforeColorTransform; private uint dcLevel; private JxlCodecMetadata? metadata; - private JxlLoopFilter? loopFilter; + private JxlLoopFilter loopFilter = new(); private ulong extensions; private bool isPreviewFrame; // Non-serialized + /// + /// Initializes the frame fields and their owned nested headers to the bitstream defaults. + /// + /// Set before reading a frame so conditional fields use the codestream's metadata. + public JxlFrameHeader() => JxlBundle.Init(this); + /// /// Gets or sets the frame encoding method (e.g., Modular or VarDCT). /// @@ -86,7 +94,7 @@ public JxlColorTransform ColorTransform /// /// Gets or sets the chroma subsampling information. /// - public JxlYCbCrChromaSubsampling? ChromaSubsampling + public JxlYCbCrChromaSubsampling ChromaSubsampling { get => this.chromaSubsampling; set => this.chromaSubsampling = value; @@ -176,7 +184,7 @@ public Point FrameOrigin /// /// Gets or sets the blending information for the frame. /// - public JxlBlendingInfo? BlendingInfo + public JxlBlendingInfo BlendingInfo { get => this.blendingInfo; set => this.blendingInfo = value; @@ -192,9 +200,14 @@ public List ExtraChannelBlendingInfo } /// - /// Gets the associated animation frame, if any. + /// Gets the animation timing fields owned by this frame header. + /// + public JxlAnimationFrame AnimationFrame => this.animationFrame; + + /// + /// Gets the progressive passes owned by this frame header. /// - public JxlAnimationFrame? AnimationFrame => this.animationFrame; + public JxlPasses Passes => this.passes; /// /// Gets or sets a value indicating whether this is the last frame. @@ -238,13 +251,17 @@ public uint DcLevel public JxlCodecMetadata? Metadata { get => this.metadata; - set => this.metadata = value; + set + { + this.metadata = value; + this.animationFrame.CodecMetadata = value; + } } /// /// Gets or sets the loop filter applied to the frame. /// - public JxlLoopFilter? LoopFilter + public JxlLoopFilter LoopFilter { get => this.loopFilter; set => this.loopFilter = value; @@ -268,6 +285,9 @@ public ulong Extensions set => this.extensions = value; } + /// + /// Gets the image or preview width used when the frame does not supply a custom width. + /// public int DefaultXSize { get @@ -279,13 +299,16 @@ public int DefaultXSize if (this.isPreviewFrame) { - return this.metadata.ImageMetadata?.PreviewSize?.XSize ?? 0; + return this.metadata.ImageMetadata.PreviewSize.XSize; } return this.metadata.XSize; } } + /// + /// Gets the image or preview height used when the frame does not supply a custom height. + /// public int DefaultYSize { get @@ -297,7 +320,7 @@ public int DefaultYSize if (this.isPreviewFrame) { - return this.metadata.ImageMetadata?.PreviewSize?.YSize ?? 0; + return this.metadata.ImageMetadata.PreviewSize.YSize; } return this.metadata.YSize; @@ -324,8 +347,8 @@ public JxlFrameDimensions FrameDimensions xsize, ysize, (int)this.groupSizeShift, - this.chromaSubsampling?.MaxHShift ?? 0, - this.chromaSubsampling?.MaxVShift ?? 0, + this.chromaSubsampling.MaxHShift, + this.chromaSubsampling.MaxVShift, this.encoding == JxlFrameEncoding.Modular, (int)this.upsampling); @@ -345,7 +368,7 @@ public JxlFrameDimensions FrameDimensions // A non-duration 0 frame may or may not be referenced. !this.isLast && this.frameType != JxlFrameType.DcFrame && - (this.animationFrame?.Duration == 0 || this.saveAsReference != 0); + (this.animationFrame.Duration == 0 || this.saveAsReference != 0); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void JpegOrder(JxlColorTransform transform, bool isGray, out InlineArray3 result) @@ -397,20 +420,31 @@ private void UpdateFlag(bool condition, ulong flag) } } + /// public bool Visit(JxlVisitor visitor) { - bool allDefault = false; - if (visitor.AllDefault(this, ref allDefault)) + if (visitor.AllDefault(this, ref this.allDefault)) { visitor.SetDefault(this); return true; } - if (!VisitFrameType(visitor, JxlFrameType.RegularFrame, ref this.frameType)) + // All four frame types have a direct two-bit selector. This is not the generic enum distribution. + uint encodedFrameType = (uint)this.frameType; + + if (!visitor.U32( + JxlFieldExpressions.Value((uint)JxlFrameType.RegularFrame), + JxlFieldExpressions.Value((uint)JxlFrameType.DcFrame), + JxlFieldExpressions.Value((uint)JxlFrameType.ReferenceOnly), + JxlFieldExpressions.Value((uint)JxlFrameType.SkipProgressive), + (uint)JxlFrameType.RegularFrame, + ref encodedFrameType)) { return false; } + this.frameType = (JxlFrameType)encodedFrameType; + if (visitor.IsReading && this.isPreviewFrame && this.frameType != JxlFrameType.RegularFrame) { throw new InvalidOperationException("Only regular frame could be a preview"); @@ -434,7 +468,8 @@ public bool Visit(JxlVisitor visitor) } // Color transform - bool xybEncoded = this.metadata?.ImageMetadata?.XybEncoded == true; + // Construction initializes defaults before codestream metadata is bound. The format's default color transform is XYB. + bool xybEncoded = this.metadata is null || this.metadata.ImageMetadata.XybEncoded; if (xybEncoded) { this.colorTransform = JxlColorTransform.Xyb; @@ -456,13 +491,13 @@ public bool Visit(JxlVisitor visitor) if (visitor.Conditional(this.colorTransform == JxlColorTransform.YCbCr && ((this.flags & (ulong)JxlFrameHeaderFlags.Dc) == 0))) { - if (!visitor.VisitNested(this.chromaSubsampling!)) + if (!visitor.VisitNested(this.chromaSubsampling)) { return false; } } - int numExtraChannels = this.metadata?.ImageMetadata?.ExtraChannelCount ?? 0; + int numExtraChannels = this.metadata?.ImageMetadata.ExtraChannelCount ?? 0; // Upsampling if (visitor.Conditional((this.flags & (ulong)JxlFrameHeaderFlags.Dc) == 0)) @@ -478,16 +513,25 @@ public bool Visit(JxlVisitor visitor) return false; } - if (this.metadata != null && visitor.Conditional(numExtraChannels != 0)) + if (this.metadata is not null && visitor.Conditional(numExtraChannels != 0)) { - List extraChannels = this.metadata!.ImageMetadata?.ExtraChannels ?? []; - this.extraChannelUpsampling = new List(extraChannels.Count); + List extraChannels = this.metadata.ImageMetadata.ExtraChannels; + + // Preserve encoder settings when measuring and writing. Reading or resetting updates these same owned entries. + if (this.extraChannelUpsampling.Count > extraChannels.Count) + { + this.extraChannelUpsampling.RemoveRange(extraChannels.Count, this.extraChannelUpsampling.Count - extraChannels.Count); + } + + while (this.extraChannelUpsampling.Count < extraChannels.Count) + { + this.extraChannelUpsampling.Add(1); + } for (int i = 0; i < extraChannels.Count; i++) { uint dimShift = (uint)extraChannels[i].DimensionShift; - uint ecUpsampling = 1; - ecUpsampling >>= (int)dimShift; + uint ecUpsampling = this.extraChannelUpsampling[i] >> (int)dimShift; if (!visitor.U32( JxlFieldExpressions.Value(1), @@ -512,7 +556,7 @@ public bool Visit(JxlVisitor visitor) throw new InvalidOperationException("EC upsampling too large"); } - this.extraChannelUpsampling.Add(ecUpsampling); + this.extraChannelUpsampling[i] = ecUpsampling; } } else @@ -571,7 +615,9 @@ public bool Visit(JxlVisitor visitor) return false; } } - else + + // Default visitors visit every conditional field. Only a DC frame may retain the resulting pyramid level. + if (this.frameType != JxlFrameType.DcFrame) { this.dcLevel = 0; } @@ -625,6 +671,9 @@ public bool Visit(JxlVisitor visitor) return false; } + // The 30-bit size alternative plus its offset fits Int32. Publish both dimensions before checking the crop. + this.frameSize = new Size((int)frameSizeWidth, (int)frameSizeHeight); + if (this.customSizeOrOrigin && (this.frameSize.Width == 0 || this.frameSize.Height == 0)) { throw new InvalidOperationException("Invalid crop dimensions for frame"); @@ -646,7 +695,7 @@ public bool Visit(JxlVisitor visitor) // Blending, animation, last frame if (visitor.Conditional(this.frameType is JxlFrameType.RegularFrame or JxlFrameType.SkipProgressive)) { - this.blendingInfo!.ExtraChannelCount = numExtraChannels; + this.blendingInfo.ExtraChannelCount = numExtraChannels; this.blendingInfo.IsPartialFrame = isPartialFrame; if (!visitor.VisitNested(this.blendingInfo)) @@ -656,21 +705,28 @@ public bool Visit(JxlVisitor visitor) bool replaceAll = this.blendingInfo.BlendMode == JxlBlendMode.Replace; - this.extraChannelBlendingInfo = new List(numExtraChannels); + // These headers belong to the frame. Replacing the list here would discard the encoder's selected blend modes. + if (this.extraChannelBlendingInfo.Count > numExtraChannels) + { + this.extraChannelBlendingInfo.RemoveRange(numExtraChannels, this.extraChannelBlendingInfo.Count - numExtraChannels); + } + + while (this.extraChannelBlendingInfo.Count < numExtraChannels) + { + this.extraChannelBlendingInfo.Add(new JxlBlendingInfo()); + } + for (int i = 0; i < numExtraChannels; i++) { - JxlBlendingInfo ecBlendingInfo = new() - { - IsPartialFrame = isPartialFrame, - ExtraChannelCount = numExtraChannels - }; + JxlBlendingInfo ecBlendingInfo = this.extraChannelBlendingInfo[i]; + ecBlendingInfo.IsPartialFrame = isPartialFrame; + ecBlendingInfo.ExtraChannelCount = numExtraChannels; if (!visitor.VisitNested(ecBlendingInfo)) { return false; } - this.extraChannelBlendingInfo.Add(ecBlendingInfo); replaceAll &= ecBlendingInfo.BlendMode == JxlBlendMode.Replace; } @@ -682,11 +738,11 @@ public bool Visit(JxlVisitor visitor) } } - if (visitor.Conditional(this.metadata?.ImageMetadata?.HaveAnimation == true)) + if (visitor.Conditional(this.metadata?.ImageMetadata.HaveAnimation == true)) { - this.animationFrame!.CodecMetadata = this.metadata; + this.animationFrame.CodecMetadata = this.metadata; - if (!visitor.VisitNested(this.animationFrame!)) + if (!visitor.VisitNested(this.animationFrame)) { return false; } @@ -762,13 +818,14 @@ public bool Visit(JxlVisitor visitor) this.saveBeforeColorTransform = true; } - if (!VisitNameString(visitor, ref this.name)) + if (!visitor.Name(ref this.name)) { return false; } - this.loopFilter!.IsModular = isModular; - if (!visitor.VisitNested(this.loopFilter!)) + this.loopFilter.IsModular = isModular; + + if (!visitor.VisitNested(this.loopFilter)) { return false; } diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlYCbCrChromaSubsampling.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlYCbCrChromaSubsampling.cs index b10d8818aa..8953c4b851 100644 --- a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlYCbCrChromaSubsampling.cs +++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlYCbCrChromaSubsampling.cs @@ -13,6 +13,11 @@ internal sealed class JxlYCbCrChromaSubsampling : IJxlFields { private readonly int[] channelMode = new int[3]; + /// + /// Initializes three full-resolution channels and their derived sampling shifts. + /// + public JxlYCbCrChromaSubsampling() => JxlBundle.Init(this); + private static ReadOnlySpan HShiftData => [0, 1, 1, 0]; private static ReadOnlySpan VShiftData => [0, 1, 0, 1]; @@ -125,6 +130,7 @@ public override string ToString() } } + /// public bool Visit(JxlVisitor visitor) { for (int i = 0; i < 3; i++) @@ -142,6 +148,9 @@ public bool Visit(JxlVisitor visitor) this.channelMode[i] = (int)unsignedChannel; } + // The frame dimensions use these maxima. Recompute them after reading or resetting the serialized channel modes. + this.Recompute(); + return true; } } diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCustomTransformData.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCustomTransformData.cs index 4d897e43c1..4db8d4e9be 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCustomTransformData.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCustomTransformData.cs @@ -14,6 +14,9 @@ internal sealed class JxlCustomTransformData : IJxlFields private uint customWeightsMask; private bool allDefault; + /// + /// Initializes the transform and upsampling coefficients to the bitstream defaults. + /// public JxlCustomTransformData() => JxlBundle.Init(this); private static ReadOnlySpan Weights2 => @@ -84,22 +87,35 @@ internal sealed class JxlCustomTransformData : IJxlFields 0.11452620f, -0.03348048f, -0.01605681f, -0.02070339f, -0.00458223f ]; + /// + /// Gets or sets whether the owning image header uses XYB. This selects the inverse-matrix fields and is not serialized here. + /// public bool NonserializedXybEncoded { get; set; } + /// + /// Gets or sets whether the wire header uses its one-bit default shortcut. + /// public bool AllDefault { get => this.allDefault; set => this.allDefault = value; } - public JxlOpsinInverseMatrix? OpsinInverseMatrix { get; set; } + /// + /// Gets or sets the inverse XYB transform owned by this codestream header, including its default coefficients. + /// + public JxlOpsinInverseMatrix OpsinInverseMatrix { get; set; } = new(); + /// + /// Gets or sets the three flags that select custom 2x, 4x and 8x upsampling weights. + /// public uint CustomWeightsMask { get => this.customWeightsMask; set => this.customWeightsMask = value; } + /// public bool Visit(JxlVisitor visitor) { if (visitor.AllDefault(this, ref this.allDefault)) @@ -110,7 +126,7 @@ public bool Visit(JxlVisitor visitor) if (visitor.Conditional(this.NonserializedXybEncoded)) { - if (!visitor.VisitNested(this.OpsinInverseMatrix!)) + if (!visitor.VisitNested(this.OpsinInverseMatrix)) { return false; } diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs index da134d8940..5e2375917d 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs @@ -6,10 +6,11 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; -// One field should be public because we can't change -// an InlineArray values if it's a property #pragma warning disable SA1401 // Fields should be private +/// +/// Stores one extra channel's sample representation, dimensions, name and type-specific fields. +/// internal sealed class JxlExtraChannelInfo : IJxlFields { private bool allDefault; @@ -19,62 +20,90 @@ internal sealed class JxlExtraChannelInfo : IJxlFields private bool alphaAssociated; private int cfaChannel; + /// + /// The spot color's three RGB components followed by its solidity. These are codec values, not pixel alpha. + /// public InlineArray4 SpotColor; + /// + /// Initializes an eight-bit, full-resolution, unassociated alpha channel with no name. + /// + public JxlExtraChannelInfo() => JxlBundle.Init(this); + + /// + /// Gets or sets whether the wire header uses its one-bit default shortcut. + /// public bool AllDefault { get => this.allDefault; set => this.allDefault = value; } + /// + /// Gets or sets the extra-channel type. + /// public JxlExtraChannel Type { get => this.type; set => this.type = value; } - public JxlBitDepthMetadata? BitDepth { get; set; } + /// + /// Gets or sets the sample representation owned by this channel header. + /// + public JxlBitDepthMetadata BitDepth { get; set; } = new(); + /// + /// Gets or sets the base-two reduction in channel dimensions relative to the color image. + /// public int DimensionShift { get => this.dimensionShift; set => this.dimensionShift = value; } + /// + /// Gets or sets the channel name. Empty means that no name is present. + /// public string? Name { get => this.name; set => this.name = value; } + /// + /// Gets or sets whether color components are associated with this alpha channel. + /// public bool AlphaAssociated { get => this.alphaAssociated; set => this.alphaAssociated = value; } + /// + /// Gets or sets the channel index in the color-filter array. + /// public int CfaChannel { get => this.cfaChannel; set => this.cfaChannel = value; } + /// public bool Visit(JxlVisitor visitor) { if (visitor.AllDefault(this, ref this.allDefault)) { - // Overwrite all serialized fields, but not any nonserialized_*. visitor.SetDefault(this); return true; } - // General if (!visitor.Enum(JxlExtraChannel.Alpha, ref this.type)) { return false; } - if (!visitor.VisitNested(this.BitDepth!)) + if (!visitor.VisitNested(this.BitDepth)) { return false; } @@ -95,12 +124,12 @@ ref Unsafe.As(ref this.dimensionShift))) return false; } - if (!VisitNameString(visitor, ref this.name)) + if (!visitor.Name(ref this.name)) { return false; } - // Conditional + // Only the selected channel type supplies its conditional fields. The default visitor initializes all three groups. if (visitor.Conditional(this.type == JxlExtraChannel.Alpha)) { if (!visitor.Boolean(false, ref this.alphaAssociated)) diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs index 9148e7dc77..a8793106ac 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs @@ -1,78 +1,97 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -#pragma warning disable SA1401 // Fields should be private - -using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Jxl.Cms; using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +/// +/// Stores the inverse XYB matrix and bias values supplied by the JPEG XL transform header. +/// internal sealed class JxlOpsinInverseMatrix : IJxlFields { private InlineArray3 opsinBiases; - private InlineArray3 quantBiases; + private InlineArray4 quantBiases; private bool allDefault; private JxlMatrix3x3F inverseMatrix; + /// + /// Initializes the transform values to the exact bitstream defaults. + /// + public JxlOpsinInverseMatrix() => JxlBundle.Init(this); + + /// + /// Gets or sets the three negative opsin absorbance biases, in channel order. + /// public InlineArray3 OpsinBiases { get => this.opsinBiases; set => this.opsinBiases = value; } - public InlineArray3 QuantBiases + /// + /// Gets or sets the three channel quantization biases followed by the shared bias numerator. + /// + public InlineArray4 QuantBiases { get => this.quantBiases; set => this.quantBiases = value; } + /// + /// Gets or sets whether the wire header uses its one-bit default shortcut. + /// public bool AllDefault { get => this.allDefault; set => this.allDefault = value; } + /// + /// Gets or sets the inverse opsin absorbance matrix in row-major order. + /// public JxlMatrix3x3F InverseMatrix { get => this.inverseMatrix; set => this.inverseMatrix = value; } + /// public bool Visit(JxlVisitor visitor) { if (visitor.AllDefault(this, ref this.allDefault)) { - // Overwrite all serialized fields, but not any nonserialized_*. visitor.SetDefault(this); return true; } - JxlMatrix3x3F defaultInverse = JxlCms.DefaultInverseOpsinAbsorbanceMatrix(); + // The default matrix is frozen by the format. Recomputing an inverse can produce different rounded coefficients. + ReadOnlySpan defaultInverse = JxlOpsinConstants.DefaultInverseOpsinAbsorbanceMatrix; + Span inverse = this.inverseMatrix.AsSpan(); - for (int j = 0; j < 3; j++) + for (int i = 0; i < inverse.Length; i++) { - for (int i = 0; i < 3; i++) + if (!visitor.F16(defaultInverse[i], ref inverse[i])) { - if (!visitor.F16(defaultInverse[j][i], ref this.inverseMatrix[j][i])) - { - return false; - } + return false; } } for (int i = 0; i < 3; i++) { - if (!visitor.F16(JxlCms.NegOpsinAbsorbanceBiasRgb[i], ref this.opsinBiases[i])) + if (!visitor.F16(-JxlOpsinConstants.OpsinAbsorbanceBias[i], ref this.opsinBiases[i])) { return false; } } + // The fourth value is shared by all channels. It is serialized after the three channel biases, not an alpha value. for (int i = 0; i < 4; i++) { - if (!visitor.F16(DefaultQuantBias[i], ref this.quantBiases[i])) + if (!visitor.F16(JxlQuantizer.DefaultQuantBias[i], ref this.quantBiases[i])) { return false; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs index c7ea5b8365..5f72f5d2ab 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs @@ -32,47 +32,117 @@ internal sealed class JxlLoopFilter : IJxlFields /// public const int EpfSharpEntries = 8; + /// + /// Default Gaborish weights. Constant evaluation preserves the native defaults without doing double arithmetic while parsing. + /// + private const float DefaultGaborishWeight1 = (float)(1.1 * 0.104699568F); + private const float DefaultGaborishWeight2 = (float)(1.1 * 0.055680538F); + + private bool allDefault; + private bool useGaborishConvolution; + private bool gaborishCustom; + private float gaborishXWeight1; + private float gaborishXWeight2; + private float gaborishYWeight1; + private float gaborishYWeight2; + private float gaborishBWeight1; + private float gaborishBWeight2; + private int epfIterations; + private bool epfSharpCustom; + private bool epfCustomWeights; + private float epfPass1ZeroFlush; + private float epfPass2ZeroFlush; + private bool epfCustomSigma; + private float epfQuantMultiplier; + private float epfPass0SigmaScale; + private float epfPass2SigmaScale; + private float epfBorderSadMul; + private float epfSigmaForModular; + private ulong extensions; + + /// + /// Initializes the loop-filter fields to the bitstream defaults. + /// + public JxlLoopFilter() => JxlBundle.Init(this); + + /// + /// Gets or sets whether the owning frame uses Modular encoding. This controls conditional EPF fields and is not serialized here. + /// + public bool IsModular { get; set; } + /// /// Gets or sets a value indicating whether gaborish /// convolution is preferred. /// - public bool UseGaborishConvolution { get; set; } + public bool UseGaborishConvolution + { + get => this.useGaborishConvolution; + set => this.useGaborishConvolution = value; + } /// /// Gets or sets a value indicating whether custom /// gaborish weights are used. /// - public bool GaborishCustom { get; set; } + public bool GaborishCustom + { + get => this.gaborishCustom; + set => this.gaborishCustom = value; + } /// /// Gets or sets the first custom X gaborish weight. /// - public float GaborishXWeight1 { get; set; } + public float GaborishXWeight1 + { + get => this.gaborishXWeight1; + set => this.gaborishXWeight1 = value; + } /// /// Gets or sets the second custom X gaborish weight. /// - public float GaborishXWeight2 { get; set; } + public float GaborishXWeight2 + { + get => this.gaborishXWeight2; + set => this.gaborishXWeight2 = value; + } /// /// Gets or sets the first custom Y gaborish weight. /// - public float GaborishYWeight1 { get; set; } + public float GaborishYWeight1 + { + get => this.gaborishYWeight1; + set => this.gaborishYWeight1 = value; + } /// /// Gets or sets the second custom Y gaborish weight. /// - public float GaborishYWeight2 { get; set; } + public float GaborishYWeight2 + { + get => this.gaborishYWeight2; + set => this.gaborishYWeight2 = value; + } /// /// Gets or sets the first custom B gaborish weight. /// - public float GaborishBWeight1 { get; set; } + public float GaborishBWeight1 + { + get => this.gaborishBWeight1; + set => this.gaborishBWeight1 = value; + } /// /// Gets or sets the second custom B gaborish weight. /// - public float GaborishBWeight2 { get; set; } + public float GaborishBWeight2 + { + get => this.gaborishBWeight2; + set => this.gaborishBWeight2 = value; + } /// /// Gets or sets the number of EPF (Edge-preserving filter) steps. @@ -80,13 +150,21 @@ internal sealed class JxlLoopFilter : IJxlFields /// 2 applies both stages and 3 applies the first stage twice /// and the second stage once. /// - public int EpfIterations { get; set; } + public int EpfIterations + { + get => this.epfIterations; + set => this.epfIterations = value; + } /// /// Gets or sets a value indicating whether custom EPF sharpness /// is used. /// - public bool EpfSharpCustom { get; set; } + public bool EpfSharpCustom + { + get => this.epfSharpCustom; + set => this.epfSharpCustom = value; + } /// /// Gets or sets a value with 8 elements representing EPF sharpness lookup tables (LUTs). @@ -96,7 +174,11 @@ internal sealed class JxlLoopFilter : IJxlFields /// /// Gets or sets a value indicating whether custom EPF weights are used. /// - public bool EpfCustomWeights { get; set; } + public bool EpfCustomWeights + { + get => this.epfCustomWeights; + set => this.epfCustomWeights = value; + } /// /// Gets or sets the relative weight of each channel. @@ -106,48 +188,84 @@ internal sealed class JxlLoopFilter : IJxlFields /// /// Gets or sets the value that represents the minimum weight for first pass. /// - public float EpfPass1ZeroFlush { get; set; } + public float EpfPass1ZeroFlush + { + get => this.epfPass1ZeroFlush; + set => this.epfPass1ZeroFlush = value; + } /// /// Gets or sets the value that represents the minimum weight for second pass. /// - public float EpfPass2ZeroFlush { get; set; } + public float EpfPass2ZeroFlush + { + get => this.epfPass2ZeroFlush; + set => this.epfPass2ZeroFlush = value; + } /// /// Gets or sets a value indicating whether custom sigma parameters are used for EPF. /// - public bool EpfCustomSigma { get; set; } + public bool EpfCustomSigma + { + get => this.epfCustomSigma; + set => this.epfCustomSigma = value; + } /// /// Gets or sets the quant multiplier. /// - public float EpfQuantMultiplier { get; set; } + public float EpfQuantMultiplier + { + get => this.epfQuantMultiplier; + set => this.epfQuantMultiplier = value; + } /// /// Gets or sets the multiplier for sigma in pass 0. /// - public float EpfPass0SigmaScale { get; set; } + public float EpfPass0SigmaScale + { + get => this.epfPass0SigmaScale; + set => this.epfPass0SigmaScale = value; + } /// /// Gets or sets the multiplier for sigma in pass 2. /// - public float EpfPass2SigmaScale { get; set; } + public float EpfPass2SigmaScale + { + get => this.epfPass2SigmaScale; + set => this.epfPass2SigmaScale = value; + } /// /// Gets or sets the inverse multiplier for sigma on borders. /// - public float EpfBorderSadMul { get; set; } + public float EpfBorderSadMul + { + get => this.epfBorderSadMul; + set => this.epfBorderSadMul = value; + } /// /// Gets or sets the EPF sigma for modular. /// // NOTE: This value is not documented by libjxl. - public float EpfSigmaForModular { get; set; } + public float EpfSigmaForModular + { + get => this.epfSigmaForModular; + set => this.epfSigmaForModular = value; + } /// /// Gets or sets the number of extensions. /// - public long Extensions { get; set; } + public ulong Extensions + { + get => this.extensions; + set => this.extensions = value; + } /// /// Mirror n floats starting at *span and store them before span. @@ -261,5 +379,147 @@ public bool ComputeSigma(Rectangle blockRect, JxlPassesDecoderState state) return true; } - public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); + /// + public bool Visit(JxlVisitor visitor) + { + if (visitor.AllDefault(this, ref this.allDefault)) + { + visitor.SetDefault(this); + return true; + } + + if (!visitor.Boolean(true, ref this.useGaborishConvolution)) + { + return false; + } + + // Each Gaborish kernel has a fixed center weight of one and four copies of each surrounding weight. + // Reject a near-zero total before the renderer normalizes the kernel by that total. + if (visitor.Conditional(this.useGaborishConvolution)) + { + if (!visitor.Boolean(false, ref this.gaborishCustom)) + { + return false; + } + + if (visitor.Conditional(this.gaborishCustom)) + { + if (!visitor.F16(DefaultGaborishWeight1, ref this.gaborishXWeight1) || !visitor.F16(DefaultGaborishWeight2, ref this.gaborishXWeight2)) + { + return false; + } + + if (MathF.Abs(1F + ((this.gaborishXWeight1 + this.gaborishXWeight2) * 4F)) < 1E-8F) + { + return false; + } + + if (!visitor.F16(DefaultGaborishWeight1, ref this.gaborishYWeight1) || !visitor.F16(DefaultGaborishWeight2, ref this.gaborishYWeight2)) + { + return false; + } + + if (MathF.Abs(1F + ((this.gaborishYWeight1 + this.gaborishYWeight2) * 4F)) < 1E-8F) + { + return false; + } + + if (!visitor.F16(DefaultGaborishWeight1, ref this.gaborishBWeight1) || !visitor.F16(DefaultGaborishWeight2, ref this.gaborishBWeight2)) + { + return false; + } + + if (MathF.Abs(1F + ((this.gaborishBWeight1 + this.gaborishBWeight2) * 4F)) < 1E-8F) + { + return false; + } + } + } + + if (!visitor.Bits(2, 2, ref Unsafe.As(ref this.epfIterations))) + { + return false; + } + + if (visitor.Conditional(this.epfIterations > 0)) + { + // VarDCT derives sigma from quantization and the sharpness table. Modular supplies sigma directly below. + if (visitor.Conditional(!this.IsModular)) + { + if (!visitor.Boolean(false, ref this.epfSharpCustom)) + { + return false; + } + + if (visitor.Conditional(this.epfSharpCustom)) + { + for (int i = 0; i < EpfSharpEntries; i++) + { + if (!visitor.F16((float)i / (EpfSharpEntries - 1), ref this.EpfSharpLookup[i])) + { + return false; + } + } + } + } + + if (!visitor.Boolean(false, ref this.epfCustomWeights)) + { + return false; + } + + if (visitor.Conditional(this.epfCustomWeights)) + { + if (!visitor.F16(40F, ref this.EpfChannelScale[0]) + || !visitor.F16(5F, ref this.EpfChannelScale[1]) + || !visitor.F16(3.5F, ref this.EpfChannelScale[2]) + || !visitor.F16(0.45F, ref this.epfPass1ZeroFlush) + || !visitor.F16(0.6F, ref this.epfPass2ZeroFlush)) + { + return false; + } + } + + if (!visitor.Boolean(false, ref this.epfCustomSigma)) + { + return false; + } + + if (visitor.Conditional(this.epfCustomSigma)) + { + if (visitor.Conditional(!this.IsModular) && !visitor.F16(0.46F, ref this.epfQuantMultiplier)) + { + return false; + } + + if (!visitor.F16(0.9F, ref this.epfPass0SigmaScale) + || !visitor.F16(6.5F, ref this.epfPass2SigmaScale) + || !visitor.F16(0.6666666666666666F, ref this.epfBorderSadMul)) + { + return false; + } + } + + if (visitor.Conditional(this.IsModular)) + { + if (!visitor.F16(1F, ref this.epfSigmaForModular)) + { + return false; + } + + // The Modular sigma is used as a divisor. Match the native minimum rather than accepting a zero or negative divisor. + if (this.epfSigmaForModular < 1E-8F) + { + return false; + } + } + } + + if (!visitor.BeginExtensions(ref this.extensions)) + { + return false; + } + + return visitor.EndExtensions(); + } } diff --git a/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs index a6602d3a18..5c5beea7a8 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/IO/FieldsTests.cs @@ -11,6 +11,10 @@ using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Tests.ColorProfiles.Icc; @@ -1579,6 +1583,181 @@ public void MetadataHeaders_DefaultsRestoreAfterReuse() Assert.Equal((1U, 1U, 0U, false), (animation.TpsNumerator, animation.TpsDenominator, animation.LoopCount, animation.ContainsTimecodes)); } + /// + /// Checks the owned bit-depth header, UTF-8 byte length and default reset of an extra-channel header. + /// + [Fact] + public void ExtraChannelHeader_ReadNameAndRestoreDefaults() + { + // all_default=0, Depth, integer8, dimension shift0, two-byte UTF-8 name C3 A9. The complete header occupies 30 bits. + byte[] wire = [0x02, 0xC9, 0x70, 0x2A]; + JxlExtraChannelInfo channel = new(); + JxlBitDepthMetadata ownedDepth = channel.BitDepth; + using MemoryStream input = new(wire, false); + JxlBitReader reader = new(input); + + Assert.True(JxlBundle.Read(reader, channel)); + Assert.Equal(30, reader.TotalBitsConsumed); + Assert.Equal(JxlExtraChannel.Depth, channel.Type); + Assert.Equal("é", channel.Name); + Assert.Equal(8U, channel.BitDepth.BitsPerSample); + + using MemoryStream output = new(); + JxlBitWriter writer = new(output); + + Assert.True(JxlBundle.Write(channel, writer, JxlLayerType.Header, null)); + Assert.Equal(30, writer.BitsWritten); + + writer.ZeroPadToByte(); + + Assert.Equal(wire, output.ToArray()); + + // A one-bit shortcut resets the existing child. It does not replace it with a separate bit-depth model. + using MemoryStream defaults = new([0x01]); + + Assert.True(JxlBundle.Read(new JxlBitReader(defaults), channel)); + Assert.Same(ownedDepth, channel.BitDepth); + Assert.Equal(JxlExtraChannel.Alpha, channel.Type); + Assert.Equal(string.Empty, channel.Name); + Assert.Equal(8U, channel.BitDepth.BitsPerSample); + } + + /// + /// Checks all sixteen transform coefficients, including the fourth quantization bias, and the native default shortcut. + /// + [Fact] + public void OpsinHeader_ReadAndWriteRetainsAllCoefficients() + { + // all_default=0, nine row-major matrix values 1..9, three opsin biases, then four quantization biases. + // Each coefficient is an independently specified IEEE half value. The complete header occupies 257 bits. + byte[] wire = + [ + 0x00, 0x78, 0x00, 0x80, 0x00, 0x84, 0x00, 0x88, 0x00, 0x8A, 0x00, + 0x8C, 0x00, 0x8E, 0x00, 0x90, 0x00, 0x91, 0x00, 0x68, 0x00, 0x70, + 0x01, 0x78, 0x00, 0x70, 0x00, 0x74, 0x00, 0x78, 0x00, 0x80, 0x00 + ]; + + JxlOpsinInverseMatrix transform = new(); + using MemoryStream input = new(wire, false); + JxlBitReader reader = new(input); + + Assert.True(JxlBundle.Read(reader, transform)); + Assert.Equal(257, reader.TotalBitsConsumed); + + JxlMatrix3x3F matrix = transform.InverseMatrix; + InlineArray3 opsinBiases = transform.OpsinBiases; + InlineArray4 quantBiases = transform.QuantBiases; + ReadOnlySpan opsin = opsinBiases; + ReadOnlySpan quantization = quantBiases; + + Assert.True(matrix.AsReadOnlySpan().SequenceEqual(new float[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 })); + Assert.True(opsin.SequenceEqual(new float[] { 0.25F, -0.5F, 1 })); + Assert.True(quantization.SequenceEqual(new float[] { 0.5F, 0.75F, 1, 2 })); + + using MemoryStream output = new(); + JxlBitWriter writer = new(output); + + Assert.True(JxlBundle.Write(transform, writer, JxlLayerType.Header, null)); + Assert.Equal(257, writer.BitsWritten); + + writer.ZeroPadToByte(); + + Assert.Equal(wire, output.ToArray()); + + using MemoryStream defaults = new([0x01]); + + Assert.True(JxlBundle.Read(new JxlBitReader(defaults), transform)); + Assert.Equal(11.031566901960783F, transform.InverseMatrix[0, 0]); + Assert.Equal(0.145F, transform.QuantBiases[3]); + } + + /// + /// Checks disabled filters and default restoration without changing the owning frame's Modular context. + /// + [Fact] + public void LoopFilterHeader_ReadAndRestoreDefaults() + { + // all_default=0, Gaborish disabled, zero EPF iterations, zero extensions: six zero bits. + JxlLoopFilter filter = new() { IsModular = true }; + using MemoryStream input = new([0x00]); + JxlBitReader reader = new(input); + + Assert.True(JxlBundle.Read(reader, filter)); + Assert.Equal(6, reader.TotalBitsConsumed); + Assert.False(filter.UseGaborishConvolution); + Assert.Equal(0, filter.EpfIterations); + + using MemoryStream output = new(); + JxlBitWriter writer = new(output); + + Assert.True(JxlBundle.Write(filter, writer, JxlLayerType.Header, null)); + Assert.Equal(6, writer.BitsWritten); + + writer.ZeroPadToByte(); + + Assert.Equal(new byte[] { 0x00 }, output.ToArray()); + + using MemoryStream defaults = new([0x01]); + + Assert.True(JxlBundle.Read(new JxlBitReader(defaults), filter)); + Assert.True(filter.IsModular); + Assert.True(filter.UseGaborishConvolution); + Assert.Equal(2, filter.EpfIterations); + Assert.Equal(1F, filter.EpfSigmaForModular); + } + + /// + /// Checks native rejection of unusable kernel normalization and Modular sigma divisors. + /// + /// The fixed filter header containing an invalid divisor. + /// Whether the owning frame uses Modular encoding. + [Theory] + [InlineData(new byte[] { 0x06, 0xA0, 0x05, 0x00, 0x00 }, false)] + [InlineData(new byte[] { 0x04, 0x00, 0x00 }, true)] + public void ReadLoopFilterHeader_RejectsInvalidDivisors(byte[] wire, bool modular) + { + // The first kernel has weights -0.25 and zero, so its total is 1 + 4 * -0.25 = 0. + // The Modular header enables one EPF pass with a zero sigma. + using MemoryStream input = new(wire, false); + + Assert.False(JxlBundle.Read(new JxlBitReader(input), new JxlLoopFilter { IsModular = modular })); + } + + /// + /// Checks that a frame's one-bit shortcut resets its owned headers before frame decoding uses them. + /// + [Fact] + public void FrameHeader_DefaultShortcutRestoresOwnedHeaders() + { + // Frame defaults are initialized before metadata is available. That state selects XYB and its default X quantization scale. + JxlFrameHeader frame = new(); + Assert.Equal(JxlColorTransform.Xyb, frame.ColorTransform); + Assert.Equal(3U, frame.XQmScale); + Assert.Equal(0U, frame.DcLevel); + + frame.Metadata = new JxlCodecMetadata(); + frame.Encoding = JxlFrameEncoding.Modular; + frame.CustomSizeOrOrigin = true; + frame.FrameSize = new Size(5, 7); + frame.Name = "previous"; + + frame.AnimationFrame.Duration = 5; + frame.Passes.NumberOfPasses = 2; + frame.LoopFilter.EpfIterations = 0; + using MemoryStream input = new([0x01]); + JxlBitReader reader = new(input); + + Assert.True(JxlBundle.Read(reader, frame)); + Assert.Equal(1, reader.TotalBitsConsumed); + Assert.Equal(JxlFrameEncoding.VarDct, frame.Encoding); + Assert.Equal(0U, frame.DcLevel); + Assert.Equal(default, frame.FrameSize); + Assert.Equal(string.Empty, frame.Name); + Assert.Equal(0U, frame.AnimationFrame.Duration); + Assert.Equal(1U, frame.Passes.NumberOfPasses); + Assert.Equal(2, frame.LoopFilter.EpfIterations); + } + [Fact] public void TestRoundtripFrame() { From 825fefb1703349255fb9887b642f73d4c04d80a7 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 22:29:18 +1000 Subject: [PATCH 274/328] Correct JPEG XL terminal Huffman lookahead and lookup expansion --- .../Jxl/Processing/Decoder/JxlBitReader.cs | 36 +++++++++------- .../Processing/Decoder/JxlHuffmanDecoder.cs | 5 ++- .../Formats/Jxl/Processing/BitsTests.cs | 2 + .../Jxl/Processing/EntropyCoderTests.cs | 41 +++++++++++++++++++ 4 files changed, 68 insertions(+), 16 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs index 2d7bcc29d0..d6a8b00e5f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs @@ -11,6 +11,8 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; /// The stream bounded to the current codestream or section. /// /// Prefetch can advance the stream, but only reads and skips advance the logical bit position. +/// Peeks return zero for unavailable high bits so a Huffman lookup can select a shorter terminal code. +/// Reads and skips still require every consumed bit to exist in the input. /// The caller owns the stream and must keep it alive while using this reader. /// internal sealed class JxlBitReader(Stream stream) @@ -40,6 +42,11 @@ internal sealed class JxlBitReader(Stream stream) /// private long bytesFetched; + /// + /// Whether the bounded input has ended. Repeated terminal Huffman peeks must not repeat stream reads. + /// + private bool endOfInput; + /// /// Gets the number of bits consumed, excluding prefetched bits. /// @@ -61,6 +68,7 @@ private void RefillCore(out ulong value, out int bitCount) if (read == 0) { + this.endOfInput = true; break; } @@ -76,27 +84,20 @@ private void RefillCore(out ulong value, out int bitCount) } /// - /// Ensures that the requested field is buffered without consuming any bits. + /// Buffers the available portion of a field without consuming bits or fabricating input length. /// /// The field width, from one to 64 bits. - private void EnsureBits(int bitCount) + private void PrefetchBits(int bitCount) { - if (this.bufferRemainingBits == 0) + if (this.bufferRemainingBits == 0 && !this.endOfInput) { this.RefillCore(out this.buffer, out this.bufferRemainingBits); } - if (bitCount > this.bufferRemainingBits && this.lookaheadBits == 0) + if (bitCount > this.bufferRemainingBits && this.lookaheadBits == 0 && !this.endOfInput) { this.RefillCore(out this.lookahead, out this.lookaheadBits); } - - // A short final block cannot satisfy a larger field. Preserve the buffered bits on - // failure; neither their count nor the logical position may wrap or advance. - if (bitCount > this.bufferRemainingBits + this.lookaheadBits) - { - throw new EndOfStreamException(); - } } /// @@ -112,7 +113,14 @@ private ulong ReadBitsCore(int bitCount, bool peek) return 0; } - this.EnsureBits(bitCount); + this.PrefetchBits(bitCount); + + // Huffman table lookups can inspect more bits than their selected code consumes. Missing high bits remain zero, + // as in the native reader. Only consumption must fit the actual buffered length; failure retains the available bits. + if (!peek && bitCount > this.bufferRemainingBits + this.lookaheadBits) + { + throw new EndOfStreamException(); + } // Current bits occupy the low end. A field crossing the block boundary continues // into lookahead. Keep both blocks until consumption so repeated peeks are identical. @@ -172,7 +180,7 @@ public void JumpToByteBoundary() public uint ReadBits32(uint bitCount) => (uint)this.ReadBitsCore((int)bitCount, false); /// - /// Peeks an unsigned field of up to 32 bits without consuming it. + /// Peeks an unsigned field of up to 32 bits without consuming it. Unavailable high bits are zero. /// /// The field width, from zero to 32 bits. /// The field value. @@ -192,7 +200,7 @@ public void JumpToByteBoundary() public ulong ReadBits64(ulong bitCount) => this.ReadBitsCore((int)bitCount, false); /// - /// Peeks an unsigned field of up to 64 bits without consuming it. + /// Peeks an unsigned field of up to 64 bits without consuming it. Unavailable high bits are zero. /// /// The field width, from zero to 64 bits. /// The field value. diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlHuffmanDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlHuffmanDecoder.cs index 7490ee9c3e..6515233ce4 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlHuffmanDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlHuffmanDecoder.cs @@ -137,7 +137,7 @@ public static bool ReadHuffmanCodeLengths(Span codeLengthCodeLengths, int /// /// Alphabet size (256 at most) /// Bit-stream reader - /// Output table (must have at most 8 items) + /// The 256-entry output lookup table. /// Status of the operation public static bool ReadSimpleCode(int alphabetSize, JxlBitReader br, Span table) { @@ -248,7 +248,8 @@ public static bool ReadSimpleCode(int alphabetSize, JxlBitReader br, Span(() => reader.ReadBits32(17)); Assert.Equal(0, reader.TotalBitsConsumed); Assert.Equal(0x3412U, reader.ReadBits32(16)); diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/EntropyCoderTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/EntropyCoderTests.cs index 3b0a8f50f8..f141a75602 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/EntropyCoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/EntropyCoderTests.cs @@ -14,6 +14,47 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; public class EntropyCoderTests { + /// + /// Checks terminal zero-bit and one-bit Huffman symbols through the encoded table and bounded bit reader. + /// + /// The fixed simple-code header and final symbol bits. + /// The exact header length. + /// The number of bits in each decoded code. + /// The first decoded symbol. + /// The second decoded symbol. + [Theory] + [InlineData((byte)0x11, 5, 0, (ushort)1, (ushort)1)] + [InlineData((byte)0xA5, 6, 1, (ushort)0, (ushort)1)] + public void HuffmanSymbols_ConsumeOnlyEncodedBitsAtEnd(byte wire, int headerBits, int codeBits, ushort first, ushort second) + { + // 0x11 declares the sole symbol 1, whose code needs no bits. 0xA5 declares symbols 0 and 1, followed by codes 0 and 1. + using MemoryStream input = new([wire]); + JxlBitReader reader = new(input); + JxlHuffmanDecoder decoder = new(); + + Assert.True(decoder.ReadFromBitStream(2, reader)); + Assert.Equal(headerBits, reader.TotalBitsConsumed); + + if (codeBits == 0) + { + // Move past the unused byte padding. A zero-bit symbol remains valid with no input left to peek. + reader.SkipBits32((uint)(8 - headerBits)); + } + + long before = reader.TotalBitsConsumed; + Assert.Equal(first, decoder.ReadSymbol(reader)); + Assert.Equal(codeBits, reader.TotalBitsConsumed - before); + Assert.Equal(second, decoder.ReadSymbol(reader)); + Assert.Equal(8, reader.TotalBitsConsumed); + Assert.Throws(() => reader.ReadBoolean()); + + if (codeBits != 0) + { + // Zero-filled lookahead must not let the next one-bit code consume a nonexistent bit. + Assert.Throws(() => decoder.ReadSymbol(reader)); + } + } + /// /// Checks reference records when fewer matching channels exist than the requested property capacity. /// From afa7e486d15c0025d0a089de57115d0d35ea55c2 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 22:55:31 +1000 Subject: [PATCH 275/328] Reuse shared vector blending for JPEG XL native planes --- src/ImageSharp/Common/Helpers/Numerics.cs | 49 +- .../Common/Helpers/SimdUtils.FloatPlanes.cs | 238 ++++-- .../Blending/JxlAlphaBlendingInputLayer.cs | 15 - .../Blending/JxlAlphaBlendingOutput.cs | 15 - .../Jxl/Processing/Blending/JxlAlphaHelper.cs | 216 ++---- .../Jxl/Processing/Blending/JxlBlendInput.cs | 45 ++ .../Jxl/Processing/Blending/JxlBlendOutput.cs | 45 ++ .../Blending/JxlBlending.Arithmetic.cs | 196 +++++ .../Blending/JxlBlending.Operators.cs | 269 +++++++ .../Blending/JxlBlending.SourceOver.cs | 143 ++++ .../Jxl/Processing/Blending/JxlBlending.cs | 435 ++++------- .../Decoder/Patch/JxlPatchDictionary.cs | 205 +++--- .../RenderPipeline/PatchDictionaryStage.cs | 71 +- .../RenderPipeline/RenderPipelineStageBase.cs | 57 +- .../PixelBlenders/PixelBlenders.cs | 690 ++++++++++++++++++ .../PixelBlender{TPixel,TOperator}.cs | 561 +------------- .../Common/SimdUtilsTests.FloatPlanes.cs | 18 + .../Jxl/Processing/AlphaBlendingTests.cs | 372 +++++++--- 18 files changed, 2316 insertions(+), 1324 deletions(-) delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaBlendingInputLayer.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaBlendingOutput.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlendInput.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlendOutput.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlending.Arithmetic.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlending.Operators.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlending.SourceOver.cs create mode 100644 src/ImageSharp/PixelFormats/PixelBlenders/PixelBlenders.cs diff --git a/src/ImageSharp/Common/Helpers/Numerics.cs b/src/ImageSharp/Common/Helpers/Numerics.cs index c616e37cd7..c27f6de9a3 100644 --- a/src/ImageSharp/Common/Helpers/Numerics.cs +++ b/src/ImageSharp/Common/Helpers/Numerics.cs @@ -406,8 +406,7 @@ public static Vector128 UnPremultiply(Vector128 source, Vector128< Vector128 result = Vector128.ConditionalSelect(zeroMask, source, source / alpha); // Division would replace W with one, so restore the original alpha lane exactly. - Vector128 alphaMask = Vector128.Create(0, 0, 0, -1).AsSingle(); - return Vector128.ConditionalSelect(alphaMask, alpha, result); + return WithW(result, alpha); } /// @@ -425,7 +424,7 @@ public static Vector256 UnPremultiply(Vector256 source, Vector256< Vector256 result = Avx.BlendVariable(Avx.Divide(source, alpha), source, zeroMask); // Division would replace W with one, so restore both original alpha lanes exactly. - return Avx.Blend(result, alpha, BlendAlphaControl); + return WithW(result, alpha); } /// @@ -443,8 +442,7 @@ public static Vector512 UnPremultiply(Vector512 source, Vector512< Vector512 result = Vector512.ConditionalSelect(zeroMask, source, source / alpha); // Division would replace W with one, so restore all four original alpha lanes exactly. - Vector512 alphaMask = Vector512.Create(0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1).AsSingle(); - return Vector512.ConditionalSelect(alphaMask, alpha, result); + return WithW(result, alpha); } /// @@ -489,6 +487,47 @@ public static Vector4 WithW(Vector4 value, Vector4 w) return value; } + /// + /// Replaces the fourth component in each four-component group without changing the other component bits. + /// + /// The vectors to update. + /// The vectors that supply each fourth component. + /// The vectors with their fourth components replaced. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 WithW(Vector128 value, Vector128 w) + { + // Select only W in each four-lane group. This retains signed zeros and NaN payload bits in all other lanes. + Vector128 mask = Vector128.Create(0, 0, 0, -1).AsSingle(); + return Vector128.ConditionalSelect(mask, w, value); + } + + /// + /// Replaces the fourth component in each four-component group without changing the other component bits. + /// + /// The vectors to update. + /// The vectors that supply each fourth component. + /// The vectors with their fourth components replaced. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 WithW(Vector256 value, Vector256 w) + { + // Lanes 3 and 7 are W. The immediate blend preserves every other lane without arithmetic. + return Avx.Blend(value, w, BlendAlphaControl); + } + + /// + /// Replaces the fourth component in each four-component group without changing the other component bits. + /// + /// The vectors to update. + /// The vectors that supply each fourth component. + /// The vectors with their fourth components replaced. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 WithW(Vector512 value, Vector512 w) + { + // Select only W in each four-lane group. This retains signed zeros and NaN payload bits in all other lanes. + Vector512 mask = Vector512.Create(0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1).AsSingle(); + return Vector512.ConditionalSelect(mask, w, value); + } + /// /// Calculates the cube pow of all the XYZ channels of the input vectors. /// diff --git a/src/ImageSharp/Common/Helpers/SimdUtils.FloatPlanes.cs b/src/ImageSharp/Common/Helpers/SimdUtils.FloatPlanes.cs index d26e277230..91cb47ea05 100644 --- a/src/ImageSharp/Common/Helpers/SimdUtils.FloatPlanes.cs +++ b/src/ImageSharp/Common/Helpers/SimdUtils.FloatPlanes.cs @@ -15,23 +15,23 @@ namespace SixLabors.ImageSharp; internal static partial class SimdUtils { /// - /// Interleaves three equally sized component planes and an optional fourth plane into four-component vectors. - /// Values are copied without changing their floating-point representation. + /// Interleaves equally sized component planes into four-component vectors. + /// Empty Y or Z planes supply zero. An empty W plane supplies one. /// - /// The values for . - /// The values for . - /// The values for . - /// The values for , or an empty span to use 1 for every value. - /// The destination vectors. - internal static void InterleaveFloatPlanes( + /// The X components. Their count determines the number of output vectors. + /// The Y components, or an empty span to supply zero. + /// The Z components, or an empty span to supply zero. + /// The W components, or an empty span to supply one. + /// The output vectors. Its capacity must cover every nonempty input plane. + public static void InterleaveFloatPlanes( ReadOnlySpan component0, ReadOnlySpan component1, ReadOnlySpan component2, ReadOnlySpan component3, Span destination) { - Guard.IsTrue(component1.Length == component0.Length, nameof(component1), "Components must be of same size!"); - Guard.IsTrue(component2.Length == component0.Length, nameof(component2), "Components must be of same size!"); + Guard.IsTrue(component1.IsEmpty || component1.Length == component0.Length, nameof(component1), "Components must be of same size!"); + Guard.IsTrue(component2.IsEmpty || component2.Length == component0.Length, nameof(component2), "Components must be of same size!"); Guard.IsTrue(component3.IsEmpty || component3.Length == component0.Length, nameof(component3), "Components must be of same size!"); Guard.DestinationShouldNotBeTooShort(component0, destination, nameof(destination)); @@ -39,112 +39,196 @@ internal static void InterleaveFloatPlanes( ref float c1 = ref MemoryMarshal.GetReference(component1); ref float c2 = ref MemoryMarshal.GetReference(component2); ref float c3 = ref MemoryMarshal.GetReference(component3); - ref float d = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); - bool hasComponent3 = !component3.IsEmpty; + ref float output = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); int i = 0; + if (Vector512.IsHardwareAccelerated) + { + nuint count = (component0.Length - i).Vector512Count(); + + // Registers hold consecutive values from each plane. The existing transpose writes complete [X,Y,Z,W] groups. + // Missing planes use constants in registers; no dummy rows or copied samples are needed. + for (nuint block = 0; block < count; block++, i += Vector512.Count) + { + Vector512 x = Vector512.LoadUnsafe(ref c0, (uint)i); + Vector512 y = component1.IsEmpty ? Vector512.Zero : Vector512.LoadUnsafe(ref c1, (uint)i); + Vector512 z = component2.IsEmpty ? Vector512.Zero : Vector512.LoadUnsafe(ref c2, (uint)i); + Vector512 w = component3.IsEmpty ? Vector512.Create(1F) : Vector512.LoadUnsafe(ref c3, (uint)i); + Interleave4(x, y, z, w, ref Unsafe.Add(ref output, (uint)i * 4)); + } + } + + if (Vector256.IsHardwareAccelerated) + { + nuint count = (component0.Length - i).Vector256Count(); + + // Registers hold consecutive values from each plane. The existing transpose writes complete [X,Y,Z,W] groups. + // Missing planes use constants in registers; no dummy rows or copied samples are needed. + for (nuint block = 0; block < count; block++, i += Vector256.Count) + { + Vector256 x = Vector256.LoadUnsafe(ref c0, (uint)i); + Vector256 y = component1.IsEmpty ? Vector256.Zero : Vector256.LoadUnsafe(ref c1, (uint)i); + Vector256 z = component2.IsEmpty ? Vector256.Zero : Vector256.LoadUnsafe(ref c2, (uint)i); + Vector256 w = component3.IsEmpty ? Vector256.Create(1F) : Vector256.LoadUnsafe(ref c3, (uint)i); + Interleave4(x, y, z, w, ref Unsafe.Add(ref output, (uint)i * 4)); + } + } + if (Vector128.IsHardwareAccelerated) { - // Load four values from each plane: v0=[c0.0,c0.1,c0.2,c0.3] through - // v3=[c3.0,c3.1,c3.2,c3.3]. An absent fourth plane supplies four 1.0 values. - // The unpack helpers transpose component bits without arithmetic, preserving - // signed zero, infinities, and NaN payload bits. - for (; i <= component0.Length - 4; i += 4) + nuint count = (component0.Length - i).Vector128Count(); + + // Registers hold consecutive values from each plane. The existing transpose writes complete [X,Y,Z,W] groups. + // Missing planes use constants in registers; no dummy rows or copied samples are needed. + for (nuint block = 0; block < count; block++, i += Vector128.Count) { - Vector128 v0 = Vector128.LoadUnsafe(ref c0, (nuint)i); - Vector128 v1 = Vector128.LoadUnsafe(ref c1, (nuint)i); - Vector128 v2 = Vector128.LoadUnsafe(ref c2, (nuint)i); - Vector128 v3 = hasComponent3 ? Vector128.LoadUnsafe(ref c3, (nuint)i) : Vector128.Create(1F); - - // The 32-bit zips produce c01Low=[c0.0,c1.0,c0.1,c1.1] and - // c01High=[c0.2,c1.2,c0.3,c1.3], with equivalent pairs for components 2 and 3. - Vector128 c01Low = Vector128_.UnpackLow(v0, v1); - Vector128 c01High = Vector128_.UnpackHigh(v0, v1); - Vector128 c23Low = Vector128_.UnpackLow(v2, v3); - Vector128 c23High = Vector128_.UnpackHigh(v2, v3); - - // Each 64-bit zip joins the two pairs for one Vector4. The stores emit - // four consecutive vectors in component order 0, 1, 2, 3. - Vector128.StoreUnsafe(Vector128_.UnpackLow(c01Low.AsDouble(), c23Low.AsDouble()).AsSingle(), ref d, (nuint)(i * 4)); - Vector128.StoreUnsafe(Vector128_.UnpackHigh(c01Low.AsDouble(), c23Low.AsDouble()).AsSingle(), ref d, (nuint)((i + 1) * 4)); - Vector128.StoreUnsafe(Vector128_.UnpackLow(c01High.AsDouble(), c23High.AsDouble()).AsSingle(), ref d, (nuint)((i + 2) * 4)); - Vector128.StoreUnsafe(Vector128_.UnpackHigh(c01High.AsDouble(), c23High.AsDouble()).AsSingle(), ref d, (nuint)((i + 3) * 4)); + Vector128 x = Vector128.LoadUnsafe(ref c0, (uint)i); + Vector128 y = component1.IsEmpty ? Vector128.Zero : Vector128.LoadUnsafe(ref c1, (uint)i); + Vector128 z = component2.IsEmpty ? Vector128.Zero : Vector128.LoadUnsafe(ref c2, (uint)i); + Vector128 w = component3.IsEmpty ? Vector128.Create(1F) : Vector128.LoadUnsafe(ref c3, (uint)i); + Interleave4(x, y, z, w, ref Unsafe.Add(ref output, (uint)i * 4)); } } - // Fewer than four remaining pixels cannot be loaded as a full register. The tail - // writes the identical component order, including the implicit fourth value. + // The scalar remainder handles short tails and all samples without hardware intrinsics. It copies the same bits and constants. for (; i < component0.Length; i++) { - destination[i] = new Vector4(component0[i], component1[i], component2[i], hasComponent3 ? component3[i] : 1F); + destination[i] = new Vector4( + component0[i], + component1.IsEmpty ? 0F : component1[i], + component2.IsEmpty ? 0F : component2[i], + component3.IsEmpty ? 1F : component3[i]); } } /// - /// Deinterleaves four-component vectors into equally sized component planes. - /// Values are copied without changing their floating-point representation. + /// Deinterleaves four-component vectors into the supplied component planes without changing component bits. + /// Empty destination planes discard their corresponding components. /// - /// The source vectors. - /// The destination for values. - /// The destination for values. - /// The destination for values. - /// The destination for values. - internal static void DeinterleaveFloatPlanes( + /// The input vectors. + /// The X destination. Its capacity must cover the source vectors. + /// The Y destination, or an empty span to discard Y. + /// The Z destination, or an empty span to discard Z. + /// The W destination, or an empty span to discard W. + public static void DeinterleaveFloatPlanes( ReadOnlySpan source, Span component0, Span component1, Span component2, Span component3) { - Guard.IsTrue(component1.Length == component0.Length, nameof(component1), "Components must be of same size!"); - Guard.IsTrue(component2.Length == component0.Length, nameof(component2), "Components must be of same size!"); - Guard.IsTrue(component3.Length == component0.Length, nameof(component3), "Components must be of same size!"); + Guard.IsTrue(component1.IsEmpty || component1.Length == component0.Length, nameof(component1), "Components must be of same size!"); + Guard.IsTrue(component2.IsEmpty || component2.Length == component0.Length, nameof(component2), "Components must be of same size!"); + Guard.IsTrue(component3.IsEmpty || component3.Length == component0.Length, nameof(component3), "Components must be of same size!"); Guard.DestinationShouldNotBeTooShort(source, component0, nameof(component0)); - ref float s = ref Unsafe.As(ref MemoryMarshal.GetReference(source)); ref float c0 = ref MemoryMarshal.GetReference(component0); ref float c1 = ref MemoryMarshal.GetReference(component1); ref float c2 = ref MemoryMarshal.GetReference(component2); ref float c3 = ref MemoryMarshal.GetReference(component3); int i = 0; + if (Vector512.IsHardwareAccelerated) + { + nuint count = (source.Length - i).Vector512Count(); + + // Read a complete interleaved block before storing any plane. Optional outputs add no temporary storage. + for (nuint block = 0; block < count; block++, i += Vector512.Count) + { + Deinterleave4(source, i, out Vector512 x, out Vector512 y, out Vector512 z, out Vector512 w); + x.StoreUnsafe(ref c0, (uint)i); + + if (!component1.IsEmpty) + { + y.StoreUnsafe(ref c1, (uint)i); + } + + if (!component2.IsEmpty) + { + z.StoreUnsafe(ref c2, (uint)i); + } + + if (!component3.IsEmpty) + { + w.StoreUnsafe(ref c3, (uint)i); + } + } + } + + if (Vector256.IsHardwareAccelerated) + { + nuint count = (source.Length - i).Vector256Count(); + + // Read a complete interleaved block before storing any plane. Optional outputs add no temporary storage. + for (nuint block = 0; block < count; block++, i += Vector256.Count) + { + Deinterleave4(source, i, out Vector256 x, out Vector256 y, out Vector256 z, out Vector256 w); + x.StoreUnsafe(ref c0, (uint)i); + + if (!component1.IsEmpty) + { + y.StoreUnsafe(ref c1, (uint)i); + } + + if (!component2.IsEmpty) + { + z.StoreUnsafe(ref c2, (uint)i); + } + + if (!component3.IsEmpty) + { + w.StoreUnsafe(ref c3, (uint)i); + } + } + } + if (Vector128.IsHardwareAccelerated) { - // Each loaded register is one vector: p0=[c0.0,c1.0,c2.0,c3.0] through - // p3=[c0.3,c1.3,c2.3,c3.3]. The unpack helpers make this a bitwise - // transpose, preserving nonfinite values and signed zero exactly. - for (; i <= source.Length - 4; i += 4) + nuint count = (source.Length - i).Vector128Count(); + + // Read a complete interleaved block before storing any plane. Optional outputs add no temporary storage. + for (nuint block = 0; block < count; block++, i += Vector128.Count) { - Vector128 p0 = Vector128.LoadUnsafe(ref s, (nuint)(i * 4)); - Vector128 p1 = Vector128.LoadUnsafe(ref s, (nuint)((i + 1) * 4)); - Vector128 p2 = Vector128.LoadUnsafe(ref s, (nuint)((i + 2) * 4)); - Vector128 p3 = Vector128.LoadUnsafe(ref s, (nuint)((i + 3) * 4)); - - // The first 32-bit zips pair adjacent vectors. c01Low contains - // [c0.0,c0.1,c1.0,c1.1], and c01High contains the next two vectors. - // c23Low and c23High hold the corresponding third and fourth components. - Vector128 c01Low = Vector128_.UnpackLow(p0, p1); - Vector128 c01High = Vector128_.UnpackLow(p2, p3); - Vector128 c23Low = Vector128_.UnpackHigh(p0, p1); - Vector128 c23High = Vector128_.UnpackHigh(p2, p3); - - // A 64-bit zip combines the matching two-component groups into - // four values for each component plane in source vector order. - Vector128.StoreUnsafe(Vector128_.UnpackLow(c01Low.AsDouble(), c01High.AsDouble()).AsSingle(), ref c0, (nuint)i); - Vector128.StoreUnsafe(Vector128_.UnpackHigh(c01Low.AsDouble(), c01High.AsDouble()).AsSingle(), ref c1, (nuint)i); - Vector128.StoreUnsafe(Vector128_.UnpackLow(c23Low.AsDouble(), c23High.AsDouble()).AsSingle(), ref c2, (nuint)i); - Vector128.StoreUnsafe(Vector128_.UnpackHigh(c23Low.AsDouble(), c23High.AsDouble()).AsSingle(), ref c3, (nuint)i); + Deinterleave4(source, i, out Vector128 x, out Vector128 y, out Vector128 z, out Vector128 w); + x.StoreUnsafe(ref c0, (uint)i); + + if (!component1.IsEmpty) + { + y.StoreUnsafe(ref c1, (uint)i); + } + + if (!component2.IsEmpty) + { + z.StoreUnsafe(ref c2, (uint)i); + } + + if (!component3.IsEmpty) + { + w.StoreUnsafe(ref c3, (uint)i); + } } } - // The scalar remainder uses the same component mapping for up to three pixels. + // The scalar remainder uses the same optional-plane rules for the final samples. for (; i < source.Length; i++) { Vector4 value = source[i]; component0[i] = value.X; - component1[i] = value.Y; - component2[i] = value.Z; - component3[i] = value.W; + + if (!component1.IsEmpty) + { + component1[i] = value.Y; + } + + if (!component2.IsEmpty) + { + component2[i] = value.Z; + } + + if (!component3.IsEmpty) + { + component3[i] = value.W; + } } } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaBlendingInputLayer.cs b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaBlendingInputLayer.cs deleted file mode 100644 index b749a5ba26..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaBlendingInputLayer.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Blending; - -internal ref struct JxlAlphaBlendingInputLayer(ReadOnlySpan singleSpan) -{ - public ReadOnlySpan R = singleSpan; - - public ReadOnlySpan G = singleSpan; - - public ReadOnlySpan B = singleSpan; - - public ReadOnlySpan A = singleSpan; -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaBlendingOutput.cs b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaBlendingOutput.cs deleted file mode 100644 index 76a10d2158..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaBlendingOutput.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Blending; - -internal ref struct JxlAlphaBlendingOutput(Span singleSpan) -{ - public Span R = singleSpan; - - public Span G = singleSpan; - - public Span B = singleSpan; - - public Span A = singleSpan; -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaHelper.cs b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaHelper.cs index 1393e5cfa6..3a946dba6a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaHelper.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaHelper.cs @@ -1,189 +1,71 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Runtime.CompilerServices; +using System.Numerics.Tensors; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Blending; -// TODO: SIMD support -internal sealed class JxlAlphaHelper +/// +/// Applies JPEG XL's native alpha representation using its nonzero divisor floor. +/// +internal static class JxlAlphaHelper { - private const float SmallAlpha = 1f / (1 << 26); + // JPEG XL retains color at zero alpha by using this nonzero factor for both directions. + // Use the same floor as libjxl alpha.h so association can be reversed without losing those samples. + private const float SmallAlpha = 1F / (1 << 26); - // Force x to stay within the range of 0 through 1 - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static float Clamp(float x) => Math.Clamp(x, 0f, 1f); - - public static void PerformAlphaBlending( - JxlAlphaBlendingInputLayer background, - JxlAlphaBlendingInputLayer foreground, - JxlAlphaBlendingOutput output, - int pixelCount, - bool alphaIsPremultiplied, - bool clamp) + /// + /// Associates planar RGB samples using JPEG XL's nonzero alpha floor. + /// + /// The red samples, modified in place. + /// The green samples, modified in place. + /// The blue samples, modified in place. + /// The unchanged alpha samples. + /// The number of samples per channel. + public static void PremultiplyAlpha(Span red, Span green, Span blue, ReadOnlySpan alpha, int pixelCount) { - ReadOnlySpan fgR = foreground.R; - ReadOnlySpan fgG = foreground.G; - ReadOnlySpan fgB = foreground.B; - ReadOnlySpan fgA = foreground.A; - - ReadOnlySpan bgR = background.R; - ReadOnlySpan bgG = background.G; - ReadOnlySpan bgB = background.B; - ReadOnlySpan bgA = background.A; + // Reuse one register-sized block for the three channels. This bounds stack storage and avoids a full alpha-row copy. + InlineArray16 storage = default; + Span factors = storage; - Span outR = output.R; - Span outG = output.G; - Span outB = output.B; - Span outA = output.A; - - if (alphaIsPremultiplied) - { - for (int x = 0; x < pixelCount; x++) - { - float fga = clamp ? Clamp(fgA[x]) : fgA[x]; - outR[x] = fgR[x] + (bgR[x] * (1f - fga)); - outG[x] = fgG[x] + (bgG[x] * (1f - fga)); - outB[x] = fgB[x] + (bgB[x] * (1f - fga)); - outA[x] = 1f - ((1f - fga) * (1f - bgA[x])); - } - } - else + for (int offset = 0; offset < pixelCount; offset += factors.Length) { - for (int x = 0; x < pixelCount; x++) - { - float fga = clamp ? Clamp(fgA[x]) : fgA[x]; - float newA = 1f - ((1f - fga) * (1f - bgA[x])); - float rnewA = newA > 0 ? 1f / newA : 0f; - outR[x] = ((fgR[x] * fga) + (bgR[x] * bgA[x] * (1f - fga))) * rnewA; - outG[x] = ((fgG[x] * fga) + (bgG[x] * bgA[x] * (1f - fga))) * rnewA; - outB[x] = ((fgB[x] * fga) + (bgB[x] * bgA[x] * (1f - fga))) * rnewA; - outA[x] = newA; - } - } - } + int count = Math.Min(factors.Length, pixelCount - offset); + Span block = factors[..count]; + // Native std::max places the finite floor first, so NaN selects that floor rather than propagating. + TensorPrimitives.MaxNumber(alpha.Slice(offset, count), SmallAlpha, block); - public static void PerformAlphaBlending( - ReadOnlySpan bg, - ReadOnlySpan bga, - ReadOnlySpan fg, - ReadOnlySpan fga, - Span output, - int pixelCount, - bool alphaIsPremultiplied, - bool clamp) - { - if (bg == bga && fg == fga) - { - for (int x = 0; x < pixelCount; x++) - { - float fa = clamp ? Clamp(fga[x]) : fga[x]; - output[x] = 1f - ((1f - fa) * (1f - bga[x])); - } - } - else - { - if (alphaIsPremultiplied) - { - for (int x = 0; x < pixelCount; x++) - { - float fa = clamp ? Clamp(fga[x]) : fga[x]; - output[x] = fg[x] + (bg[x] * (1f - fa)); - } - } - else - { - for (int x = 0; x < pixelCount; x++) - { - float fa = clamp ? Clamp(fga[x]) : fga[x]; - float new_a = 1f - ((1f - fa) * (1f - bga[x])); - float rnew_a = new_a > 0 ? 1f / new_a : 0f; - output[x] = ((fg[x] * fa) + (bg[x] * bga[x] * (1f - fa))) * rnew_a; - } - } - } - } - - public static void PerformAlphaWeightedAdd( - ReadOnlySpan bg, - ReadOnlySpan fg, - ReadOnlySpan fga, - Span output, - int pixelCount, - bool clamp) - { - if (fg == fga) - { - bg[pixelCount..].CopyTo(output); - } - else if (clamp) - { - for (int x = 0; x < pixelCount; x++) - { - output[x] = bg[x] + (fg[x] * Clamp(fga[x])); - } - } - else - { - for (int x = 0; x < pixelCount; ++x) - { - output[x] = bg[x] + (fg[x] * fga[x]); - } + TensorPrimitives.Multiply(red.Slice(offset, count), block, red.Slice(offset, count)); + TensorPrimitives.Multiply(green.Slice(offset, count), block, green.Slice(offset, count)); + TensorPrimitives.Multiply(blue.Slice(offset, count), block, blue.Slice(offset, count)); } } - public static void PerformMultiplyBlending( - ReadOnlySpan bg, - ReadOnlySpan fg, - Span output, - int pixelCount, - bool clamp) + /// + /// Restores unassociated planar RGB samples using JPEG XL's nonzero alpha floor. + /// + /// The red samples, modified in place. + /// The green samples, modified in place. + /// The blue samples, modified in place. + /// The unchanged alpha samples. + /// The number of samples per channel. + public static void UnpremultiplyAlpha(Span red, Span green, Span blue, ReadOnlySpan alpha, int pixelCount) { - if (clamp) - { - for (int x = 0; x < pixelCount; x++) - { - output[x] = bg[x] * Clamp(fg[x]); - } - } - else - { - for (int x = 0; x < pixelCount; x++) - { - output[x] = bg[x] * fg[x]; - } - } - } + // Reuse one register-sized block for the three channels. This bounds stack storage and avoids a full alpha-row copy. + InlineArray16 storage = default; + Span factors = storage; - public static void PremultiplyAlpha( - Span r, - Span g, - Span b, - ReadOnlySpan a, - int pixelCount) - { - for (int x = 0; x < pixelCount; x++) + for (int offset = 0; offset < pixelCount; offset += factors.Length) { - float multiplier = Math.Max(SmallAlpha, a[x]); - r[x] *= multiplier; - g[x] *= multiplier; - b[x] *= multiplier; - } - } + int count = Math.Min(factors.Length, pixelCount - offset); + Span block = factors[..count]; + // Use the same finite floor for both directions, including native NaN handling. + TensorPrimitives.MaxNumber(alpha.Slice(offset, count), SmallAlpha, block); + TensorPrimitives.Divide(1F, block, block); - public static void UnpremultiplyAlpha( - Span r, - Span g, - Span b, - ReadOnlySpan a, - int pixelCount) - { - for (int x = 0; x < pixelCount; x++) - { - float multiplier = 1f / Math.Max(SmallAlpha, a[x]); - r[x] *= multiplier; - g[x] *= multiplier; - b[x] *= multiplier; + TensorPrimitives.Multiply(red.Slice(offset, count), block, red.Slice(offset, count)); + TensorPrimitives.Multiply(green.Slice(offset, count), block, green.Slice(offset, count)); + TensorPrimitives.Multiply(blue.Slice(offset, count), block, blue.Slice(offset, count)); } } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlendInput.cs b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlendInput.cs new file mode 100644 index 0000000000..68fabe2334 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlendInput.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Blending; + +/// +/// Borrows native component planes and their selected reconstruction-weight plane. +/// +internal readonly ref struct JxlBlendInput +{ + /// + /// Initializes a new instance of the JxlBlendInput structure. + /// + /// The first native component row. Its length determines the sample count. + /// The second native component row, or an empty span to supply zero. + /// The third native component row, or an empty span to supply zero. + /// The selected reconstruction weights. These are not pixel-format alpha values. + public JxlBlendInput(ReadOnlySpan component0, ReadOnlySpan component1, ReadOnlySpan component2, ReadOnlySpan weight) + { + this.Component0 = component0; + this.Component1 = component1; + this.Component2 = component2; + this.Weight = weight; + } + + /// + /// Gets native component row 0. + /// + public ReadOnlySpan Component0 { get; } + + /// + /// Gets native component row 1. + /// + public ReadOnlySpan Component1 { get; } + + /// + /// Gets native component row 2. + /// + public ReadOnlySpan Component2 { get; } + + /// + /// Gets the selected native reconstruction-weight row. + /// + public ReadOnlySpan Weight { get; } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlendOutput.cs b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlendOutput.cs new file mode 100644 index 0000000000..701678b6b5 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlendOutput.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Blending; + +/// +/// Borrows native component planes and their selected reconstruction-weight plane. +/// +internal readonly ref struct JxlBlendOutput +{ + /// + /// Initializes a new instance of the JxlBlendOutput structure. + /// + /// The first output component row. Its length determines the sample count. + /// The second output component row, or an empty span to discard it. + /// The third output component row, or an empty span to discard it. + /// The output reconstruction weights, or an empty span to discard them. + public JxlBlendOutput(Span component0, Span component1, Span component2, Span weight) + { + this.Component0 = component0; + this.Component1 = component1; + this.Component2 = component2; + this.Weight = weight; + } + + /// + /// Gets native component row 0. + /// + public Span Component0 { get; } + + /// + /// Gets native component row 1. + /// + public Span Component1 { get; } + + /// + /// Gets native component row 2. + /// + public Span Component2 { get; } + + /// + /// Gets the selected native reconstruction-weight row. + /// + public Span Weight { get; } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlending.Arithmetic.cs b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlending.Arithmetic.cs new file mode 100644 index 0000000000..3e98511494 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlending.Arithmetic.cs @@ -0,0 +1,196 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics.Tensors; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Blending; + +/// +/// Applies native weighted addition and multiplication with the shared finite clamp. +/// +internal static partial class JxlBlending +{ + // These native modes operate on independent component values. They do not perform Porter-Duff alpha composition. + // Shared tensor arithmetic supplies the component operations; encoded clamps apply only to the named source inputs. + + /// + /// Adds source samples weighted by the selected extra channel. + /// + /// The background samples. + /// The source samples. + /// The selected source weights. + /// The output samples. All inputs must cover its length. + /// Whether to bound source weights to [0,1] with the shared finite clamp. + public static void AddWeighted( + ReadOnlySpan background, + ReadOnlySpan source, + ReadOnlySpan sourceWeight, + Span destination, + bool clampWeight) + { + if (source == sourceWeight) + { + // The selected weight channel retains its background in this native mode. It must not be added to itself. + background[..destination.Length].CopyTo(destination); + return; + } + + if (!clampWeight) + { + TensorPrimitives.MultiplyAdd(source[..destination.Length], sourceWeight[..destination.Length], background[..destination.Length], destination); + return; + } + + ref float backgroundRef = ref MemoryMarshal.GetReference(background); + ref float sourceRef = ref MemoryMarshal.GetReference(source); + ref float weightRef = ref MemoryMarshal.GetReference(sourceWeight); + ref float destinationRef = ref MemoryMarshal.GetReference(destination); + int i = 0; + + if (Vector512.IsHardwareAccelerated) + { + nuint count = (destination.Length - i).Vector512Count(); + + // Each lane is one independent sample. Clamp and multiply in registers without copying a weight row. + for (nuint block = 0; block < count; block++, i += Vector512.Count) + { + Vector512 weight = ColorNumerics.ClampToFinite( + Vector512.LoadUnsafe(ref weightRef, (uint)i), + Vector512.Zero, + Vector512.Create(1F)); + + Vector512 result = Vector512.LoadUnsafe(ref backgroundRef, (uint)i) + + (Vector512.LoadUnsafe(ref sourceRef, (uint)i) * weight); + + result.StoreUnsafe(ref destinationRef, (uint)i); + } + } + + if (Vector256.IsHardwareAccelerated) + { + nuint count = (destination.Length - i).Vector256Count(); + + // Each lane is one independent sample. Clamp and multiply in registers without copying a weight row. + for (nuint block = 0; block < count; block++, i += Vector256.Count) + { + Vector256 weight = ColorNumerics.ClampToFinite( + Vector256.LoadUnsafe(ref weightRef, (uint)i), + Vector256.Zero, + Vector256.Create(1F)); + + Vector256 result = Vector256.LoadUnsafe(ref backgroundRef, (uint)i) + + (Vector256.LoadUnsafe(ref sourceRef, (uint)i) * weight); + + result.StoreUnsafe(ref destinationRef, (uint)i); + } + } + + if (Vector128.IsHardwareAccelerated) + { + nuint count = (destination.Length - i).Vector128Count(); + + // Each lane is one independent sample. Clamp and multiply in registers without copying a weight row. + for (nuint block = 0; block < count; block++, i += Vector128.Count) + { + Vector128 weight = ColorNumerics.ClampToFinite( + Vector128.LoadUnsafe(ref weightRef, (uint)i), + Vector128.Zero, + Vector128.Create(1F)); + + Vector128 result = Vector128.LoadUnsafe(ref backgroundRef, (uint)i) + + (Vector128.LoadUnsafe(ref sourceRef, (uint)i) * weight); + + result.StoreUnsafe(ref destinationRef, (uint)i); + } + } + + // Complete-register reads precede each store. The scalar tail also permits exact in-place output. + for (; i < destination.Length; i++) + { + float weight = ColorNumerics.ClampToFinite(Unsafe.Add(ref weightRef, (uint)i), 0F, 1F); + Unsafe.Add(ref destinationRef, (uint)i) = Unsafe.Add(ref backgroundRef, (uint)i) + (Unsafe.Add(ref sourceRef, (uint)i) * weight); + } + } + + /// + /// Multiplies native component rows with the format's optional clamp on source samples. + /// + /// The background samples. + /// The source samples. + /// The output samples. Both inputs must cover its length. + /// Whether to bound source samples to [0,1] with the shared finite clamp. + public static void Multiply(ReadOnlySpan background, ReadOnlySpan source, Span destination, bool clampSource) + { + if (!clampSource) + { + TensorPrimitives.Multiply(background[..destination.Length], source[..destination.Length], destination); + return; + } + + ref float backgroundRef = ref MemoryMarshal.GetReference(background); + ref float sourceRef = ref MemoryMarshal.GetReference(source); + ref float destinationRef = ref MemoryMarshal.GetReference(destination); + int i = 0; + + if (Vector512.IsHardwareAccelerated) + { + nuint count = (destination.Length - i).Vector512Count(); + + // Only source samples use the encoded clamp. Background samples and products retain their native range. + for (nuint block = 0; block < count; block++, i += Vector512.Count) + { + Vector512 value = ColorNumerics.ClampToFinite( + Vector512.LoadUnsafe(ref sourceRef, (uint)i), + Vector512.Zero, + Vector512.Create(1F)); + + Vector512 result = Vector512.LoadUnsafe(ref backgroundRef, (uint)i) * value; + result.StoreUnsafe(ref destinationRef, (uint)i); + } + } + + if (Vector256.IsHardwareAccelerated) + { + nuint count = (destination.Length - i).Vector256Count(); + + // Only source samples use the encoded clamp. Background samples and products retain their native range. + for (nuint block = 0; block < count; block++, i += Vector256.Count) + { + Vector256 value = ColorNumerics.ClampToFinite( + Vector256.LoadUnsafe(ref sourceRef, (uint)i), + Vector256.Zero, + Vector256.Create(1F)); + + Vector256 result = Vector256.LoadUnsafe(ref backgroundRef, (uint)i) * value; + result.StoreUnsafe(ref destinationRef, (uint)i); + } + } + + if (Vector128.IsHardwareAccelerated) + { + nuint count = (destination.Length - i).Vector128Count(); + + // Only source samples use the encoded clamp. Background samples and products retain their native range. + for (nuint block = 0; block < count; block++, i += Vector128.Count) + { + Vector128 value = ColorNumerics.ClampToFinite( + Vector128.LoadUnsafe(ref sourceRef, (uint)i), + Vector128.Zero, + Vector128.Create(1F)); + + Vector128 result = Vector128.LoadUnsafe(ref backgroundRef, (uint)i) * value; + result.StoreUnsafe(ref destinationRef, (uint)i); + } + } + + // The scalar remainder uses the same clamp. Both inputs are read before an exact in-place output write. + for (; i < destination.Length; i++) + { + float value = ColorNumerics.ClampToFinite(Unsafe.Add(ref sourceRef, (uint)i), 0F, 1F); + Unsafe.Add(ref destinationRef, (uint)i) = Unsafe.Add(ref backgroundRef, (uint)i) * value; + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlending.Operators.cs b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlending.Operators.cs new file mode 100644 index 0000000000..cc808d7942 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlending.Operators.cs @@ -0,0 +1,269 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.PixelFormats.PixelBlenders; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Blending; + +/// +/// Keeps native reconstruction-weight rules outside shared pixel blending and pixel alpha conversion. +/// +internal static partial class JxlBlending +{ + /// + /// Selects the fourth component in each packed four-component group. + /// + private const byte WeightShuffleControl = 0b_11_11_11_11; + + /// + /// Uses shared associated source-over composition and supplies the native output weight. + /// + private readonly struct ReconstructionSourceOverOperator : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + { + Vector4 result = AssociatedAlphaPixelBlenderOperators.NormalSrcOver.Invoke(background, source, amount); + + // Native reconstruction specifies this evaluation order, including weights outside [0,1]. + float outputWeight = GetOutputWeight(background.W, source.W * amount); + + result.W = outputWeight; + return result; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + { + Vector256 result = AssociatedAlphaPixelBlenderOperators.NormalSrcOver.Invoke(background, source, amount); + + // Native reconstruction specifies this evaluation order, including weights outside [0,1]. + Vector256 sourceWeight = Vector256_.ShuffleNative(source, WeightShuffleControl) * amount; + Vector256 backgroundWeight = Vector256_.ShuffleNative(background, WeightShuffleControl); + Vector256 outputWeight = GetOutputWeight(backgroundWeight, sourceWeight); + + return Numerics.WithW(result, outputWeight); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + { + Vector512 result = AssociatedAlphaPixelBlenderOperators.NormalSrcOver.Invoke(background, source, amount); + + // Native reconstruction specifies this evaluation order, including weights outside [0,1]. + Vector512 sourceWeight = Vector512_.ShuffleNative(source, WeightShuffleControl) * amount; + Vector512 backgroundWeight = Vector512_.ShuffleNative(background, WeightShuffleControl); + Vector512 outputWeight = GetOutputWeight(backgroundWeight, sourceWeight); + + return Numerics.WithW(result, outputWeight); + } + } + + /// + /// Calculates native output weights in the format's complement-product evaluation order. + /// + /// The original background weights. + /// The selected source weights after any encoded clamp. + /// The native output weights without pixel-range conversion. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float GetOutputWeight(float background, float source) + => 1F - ((1F - source) * (1F - background)); + + /// + /// Calculates native output weights in the format's complement-product evaluation order. + /// + /// The original background weights. + /// The selected source weights after any encoded clamp. + /// The native output weights without pixel-range conversion. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 GetOutputWeight(Vector128 background, Vector128 source) + => Vector128.Create(1F) - ((Vector128.Create(1F) - source) * (Vector128.Create(1F) - background)); + + /// + /// Calculates native output weights in the format's complement-product evaluation order. + /// + /// The original background weights. + /// The selected source weights after any encoded clamp. + /// The native output weights without pixel-range conversion. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 GetOutputWeight(Vector256 background, Vector256 source) + => Vector256.Create(1F) - ((Vector256.Create(1F) - source) * (Vector256.Create(1F) - background)); + + /// + /// Calculates native output weights in the format's complement-product evaluation order. + /// + /// The original background weights. + /// The selected source weights after any encoded clamp. + /// The native output weights without pixel-range conversion. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 GetOutputWeight(Vector512 background, Vector512 source) + => Vector512.Create(1F) - ((Vector512.Create(1F) - source) * (Vector512.Create(1F) - background)); + + /// + /// Writes only native output weights, without color packing or component composition. + /// + /// The background weights. + /// The source weights. + /// The output weights. Both inputs must cover its length. + /// Whether to apply the encoded source clamp to [0,1]. + private static void WriteOutputWeights(ReadOnlySpan background, ReadOnlySpan source, Span destination, bool clampWeight) + { + ref float backgroundRef = ref MemoryMarshal.GetReference(background); + ref float sourceRef = ref MemoryMarshal.GetReference(source); + ref float destinationRef = ref MemoryMarshal.GetReference(destination); + int i = 0; + + if (Vector512.IsHardwareAccelerated) + { + nuint count = (destination.Length - i).Vector512Count(); + + // Each lane is one weight. Both complete input blocks are read before an exact in-place output store. + for (nuint block = 0; block < count; block++, i += Vector512.Count) + { + Vector512 top = Vector512.LoadUnsafe(ref sourceRef, (uint)i); + if (clampWeight) + { + top = ColorNumerics.ClampToFinite(top, Vector512.Zero, Vector512.Create(1F)); + } + + GetOutputWeight(Vector512.LoadUnsafe(ref backgroundRef, (uint)i), top).StoreUnsafe(ref destinationRef, (uint)i); + } + } + + if (Vector256.IsHardwareAccelerated) + { + nuint count = (destination.Length - i).Vector256Count(); + + // Each lane is one weight. Both complete input blocks are read before an exact in-place output store. + for (nuint block = 0; block < count; block++, i += Vector256.Count) + { + Vector256 top = Vector256.LoadUnsafe(ref sourceRef, (uint)i); + if (clampWeight) + { + top = ColorNumerics.ClampToFinite(top, Vector256.Zero, Vector256.Create(1F)); + } + + GetOutputWeight(Vector256.LoadUnsafe(ref backgroundRef, (uint)i), top).StoreUnsafe(ref destinationRef, (uint)i); + } + } + + if (Vector128.IsHardwareAccelerated) + { + nuint count = (destination.Length - i).Vector128Count(); + + // Each lane is one weight. Both complete input blocks are read before an exact in-place output store. + for (nuint block = 0; block < count; block++, i += Vector128.Count) + { + Vector128 top = Vector128.LoadUnsafe(ref sourceRef, (uint)i); + if (clampWeight) + { + top = ColorNumerics.ClampToFinite(top, Vector128.Zero, Vector128.Create(1F)); + } + + GetOutputWeight(Vector128.LoadUnsafe(ref backgroundRef, (uint)i), top).StoreUnsafe(ref destinationRef, (uint)i); + } + } + + // The encoded clamp and weight equation are identical for the remaining scalar samples. + for (; i < destination.Length; i++) + { + float top = Unsafe.Add(ref sourceRef, (uint)i); + if (clampWeight) + { + top = ColorNumerics.ClampToFinite(top, 0F, 1F); + } + + Unsafe.Add(ref destinationRef, (uint)i) = GetOutputWeight(Unsafe.Add(ref backgroundRef, (uint)i), top); + } + } + + /// + /// Normalizes associated results using the native output-weight reciprocal and nonpositive-weight rule. + /// + /// Associated results with their native output weights already in W. + private static void UnassociateResult(Span values) + { + ref float first = ref Unsafe.As(ref MemoryMarshal.GetReference(values)); + int offset = 0; + int length = values.Length * 4; + + if (Vector512.IsHardwareAccelerated) + { + nuint count = (length - offset).Vector512Count(); + + // Each packed register contains complete four-component samples; no lane crosses a sample boundary. + for (nuint block = 0; block < count; block++, offset += Vector512.Count) + { + Vector512 value = Vector512.LoadUnsafe(ref first, (uint)offset); + Vector512 weight = Vector512_.ShuffleNative(value, WeightShuffleControl); + Vector512 positive = Vector512.GreaterThan(weight, Vector512.Zero); + Vector512 reciprocal = Vector512.ConditionalSelect(positive, Vector512.Create(1F) / weight, Vector512.Zero); + + // Native alpha.cc first calculates a reciprocal, then multiplies. Preserve that rounding order. + // Weights can cancel to zero with a nonzero numerator; nonpositive weights use a zero reciprocal. + value *= reciprocal; + Numerics.WithW(value, weight).StoreUnsafe(ref first, (uint)offset); + } + } + + if (Vector256.IsHardwareAccelerated) + { + nuint count = (length - offset).Vector256Count(); + + // Each packed register contains complete four-component samples; no lane crosses a sample boundary. + for (nuint block = 0; block < count; block++, offset += Vector256.Count) + { + Vector256 value = Vector256.LoadUnsafe(ref first, (uint)offset); + Vector256 weight = Vector256_.ShuffleNative(value, WeightShuffleControl); + Vector256 positive = Vector256.GreaterThan(weight, Vector256.Zero); + Vector256 reciprocal = Vector256.ConditionalSelect(positive, Vector256.Create(1F) / weight, Vector256.Zero); + + // Native alpha.cc first calculates a reciprocal, then multiplies. Preserve that rounding order. + // Weights can cancel to zero with a nonzero numerator; nonpositive weights use a zero reciprocal. + value *= reciprocal; + Numerics.WithW(value, weight).StoreUnsafe(ref first, (uint)offset); + } + } + + if (Vector128.IsHardwareAccelerated) + { + nuint count = (length - offset).Vector128Count(); + + // Each packed register contains complete four-component samples; no lane crosses a sample boundary. + for (nuint block = 0; block < count; block++, offset += Vector128.Count) + { + Vector128 value = Vector128.LoadUnsafe(ref first, (uint)offset); + Vector128 weight = Vector128_.ShuffleNative(value, WeightShuffleControl); + Vector128 positive = Vector128.GreaterThan(weight, Vector128.Zero); + Vector128 reciprocal = Vector128.ConditionalSelect(positive, Vector128.Create(1F) / weight, Vector128.Zero); + + // Native alpha.cc first calculates a reciprocal, then multiplies. Preserve that rounding order. + // Weights can cancel to zero with a nonzero numerator; nonpositive weights use a zero reciprocal. + value *= reciprocal; + Numerics.WithW(value, weight).StoreUnsafe(ref first, (uint)offset); + } + } + + // Complete Vector4 samples remain when hardware intrinsics are disabled. Use the identical native reciprocal rule. + for (int i = offset / 4; i < values.Length; i++) + { + Vector4 value = values[i]; + float weight = value.W; + float reciprocal = weight > 0F ? 1F / weight : 0F; + value *= reciprocal; + value.W = weight; + values[i] = value; + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlending.SourceOver.cs b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlending.SourceOver.cs new file mode 100644 index 0000000000..e38692f9e9 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlending.SourceOver.cs @@ -0,0 +1,143 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.PixelFormats.PixelBlenders; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Blending; + +/// +/// Adapts native channel planes to shared vector blending without pixel storage conversion. +/// +internal static partial class JxlBlending +{ + /// + /// Composes native component rows and their selected reconstruction weights. + /// + /// The original background components and weights. + /// The original source components and weights. + /// The output planes. Component0 is required. Empty Component1, Component2, or Weight planes discard those components. + /// Whether components already include their selected weight. + /// Whether the bitstream requires source weights to be bounded to [0,1]. + /// Component0 determines the sample count. Every nonempty plane must cover that count. + public static void SourceOver(JxlBlendInput background, JxlBlendInput source, JxlBlendOutput destination, bool associated, bool clampWeight) + { + // The shared blender consumes complete four-component vectors. Keep only one fixed block for each input + // and reuse the source block for its result. Sixteen samples fill one 512-bit plane register, independent of image dimensions. + InlineArray16 backgroundStorage = default; + InlineArray16 sourceStorage = default; + InlineArray16 boundedWeightStorage = default; + Span backgroundVectors = backgroundStorage; + Span sourceVectors = sourceStorage; + Span boundedWeights = boundedWeightStorage; + int length = destination.Component0.Length; + + for (int offset = 0; offset < length; offset += backgroundVectors.Length) + { + int count = Math.Min(backgroundVectors.Length, length - offset); + ReadOnlySpan sourceWeight = source.Weight.Slice(offset, count); + + if (clampWeight) + { + // The source frame is borrowed. Bound only this small weight block; never mutate its stored samples. + sourceWeight.CopyTo(boundedWeights); + ColorNumerics.ClampToFinite(boundedWeights[..count], 0F, 1F); + sourceWeight = boundedWeights[..count]; + } + + SimdUtils.InterleaveFloatPlanes( + Slice(background.Component0, offset, count), + Slice(background.Component1, offset, count), + Slice(background.Component2, offset, count), + background.Weight.Slice(offset, count), + backgroundVectors[..count]); + + SimdUtils.InterleaveFloatPlanes( + Slice(source.Component0, offset, count), + Slice(source.Component1, offset, count), + Slice(source.Component2, offset, count), + sourceWeight, + sourceVectors[..count]); + + if (!associated) + { + // Native weights can be outside [0,1]. Raw association preserves them without invoking pixel storage conversion. + Numerics.Premultiply(backgroundVectors[..count]); + Numerics.Premultiply(sourceVectors[..count]); + } + + // Reuse the existing blender's SIMD loops and associated source-over operator. The codec adapter + // only replaces the output weight with the format's required complement-product calculation. + // The shared loop loads both inputs before storing a result, so the source block can be reused in place. + PixelBlenders.Blend(sourceVectors[..count], backgroundVectors[..count], sourceVectors[..count], 1F); + + if (!associated) + { + UnassociateResult(sourceVectors[..count]); + } + + SimdUtils.DeinterleaveFloatPlanes( + sourceVectors[..count], + Slice(destination.Component0, offset, count), + Slice(destination.Component1, offset, count), + Slice(destination.Component2, offset, count), + Slice(destination.Weight, offset, count)); + } + } + + /// + /// Composes one native extra channel through the same vector-blending path as the color channels. + /// + /// The background samples. + /// The background reconstruction weights. + /// The source samples. + /// The source reconstruction weights. + /// The output samples. All inputs must cover its length. + /// Whether the samples already include their selected weight. + /// Whether to bound the source weights with the shared finite clamp. + public static void SourceOver( + ReadOnlySpan background, + ReadOnlySpan backgroundWeight, + ReadOnlySpan source, + ReadOnlySpan sourceWeight, + Span destination, + bool associated, + bool clampWeight) + { + if (background == backgroundWeight && source == sourceWeight) + { + // The selected weight channel needs only the native output-weight calculation. Do not pack or blend unused components. + WriteOutputWeights(backgroundWeight, sourceWeight, destination, clampWeight); + return; + } + + JxlBlendInput bottom = new(background, default, default, backgroundWeight); + JxlBlendInput top = new(source, default, default, sourceWeight); + JxlBlendOutput result = new(destination, default, default, default); + + // An extra channel occupies X. Missing Y/Z planes use zero registers and are discarded on output. + // This uses the shared blender without allocating duplicate channel rows or changing the pixel alpha contract. + SourceOver(bottom, top, result, associated, clampWeight); + } + + /// + /// Borrows a block from a native input plane, retaining an absent component plane. + /// + /// The input plane, or an empty span for an absent component. + /// The first sample in a present plane. + /// The number of samples to borrow. + /// The selected block or an empty span. + private static ReadOnlySpan Slice(ReadOnlySpan values, int offset, int count) + => values.IsEmpty ? default : values.Slice(offset, count); + + /// + /// Borrows a block from a native output plane, retaining a discarded component plane. + /// + /// The output plane, or an empty span to discard the component. + /// The first output sample in a present plane. + /// The number of samples to borrow. + /// The selected block or an empty span. + private static Span Slice(Span values, int offset, int count) + => values.IsEmpty ? default : values.Slice(offset, count); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlending.cs b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlending.cs index 5d447c9e9d..3404bcf246 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlending.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlending.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics.Tensors; using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Memory; @@ -8,8 +9,13 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Blending; -internal static class JxlBlending +internal static partial class JxlBlending { + /// + /// Determines whether the frame requires composition with the existing image canvas. + /// + /// The decoded frame header and per-channel composition settings. + /// Whether the frame type, bounds, or blend modes require canvas composition. public static bool NeedsBlending(JxlFrameHeader header) { if (header.FrameType is not JxlFrameType.RegularFrame and not JxlFrameType.SkipProgressive) @@ -17,12 +23,7 @@ public static bool NeedsBlending(JxlFrameHeader header) return false; } - JxlBlendingInfo? blendingInfo = header.BlendingInfo; - if (blendingInfo is null) - { - return false; - } - + JxlBlendingInfo blendingInfo = header.BlendingInfo; bool replaceAll = blendingInfo.BlendMode == JxlBlendMode.Replace; foreach (JxlBlendingInfo info in header.ExtraChannelBlendingInfo) @@ -41,327 +42,193 @@ public static bool NeedsBlending(JxlFrameHeader header) return true; } + /// + /// Blends a row segment and commits all color and extra-channel results after composition. + /// + /// The background rows, with one memory entry per channel. + /// The foreground rows, each starting at the first source sample to blend. + /// The destination rows, which can share storage with either input. + /// The first background and destination column. + /// The number of samples per channel. + /// The settings for the three color channels. + /// The settings for each extra channel. + /// The extra-channel types and alpha representations. + /// The caller-owned plane, with one row per channel and at least pixelCount columns. public static void PerformBlending( - Configuration configuration, - Span> bg, - Span> fg, + ReadOnlySpan> background, + ReadOnlySpan> foreground, Span> output, - int x0, - int xsize, + int startX, + int pixelCount, JxlPatchBlending colorBlending, - Span ecBlending, - List extraChannelInfo) + ReadOnlySpan extraChannelBlending, + List extraChannelInfo, + JxlPlane scratch) { - bool hasAlpha = extraChannelInfo.Any(x => x.Type == JxlExtraChannel.Alpha); - - int numEc = extraChannelInfo.Count; - using JxlPlane tmp = new(configuration, xsize, 3 + numEc); + bool hasAlpha = false; - for (int i = 0; i < numEc; i++) + foreach (JxlExtraChannelInfo channel in extraChannelInfo) { - int i3 = 3 + i; - - switch (ecBlending[i].Mode) + if (channel.Type == JxlExtraChannel.Alpha) { - case JxlPatchBlendMode.Add: - { - Span row = tmp.GetRow(i3); - for (int x = 0; x < xsize; x++) - { - row[x] = bg[i3].Span[x + x0] + fg[i3].Span[x + x0]; - } - - continue; - } - - case JxlPatchBlendMode.BlendAbove: - { - int alpha = ecBlending[i].AlphaChannel; - bool isPremultiplied = extraChannelInfo[alpha].AlphaAssociated; - - Span bgSpan3 = bg[i3].Span[x0..]; - Span bgSpan3Alpha = bg[3 + alpha].Span[x0..]; - Span fgSpan3 = fg[i3].Span[x0..]; - Span fgSpan3Alpha = fg[3 + alpha].Span[x0..]; - - JxlAlphaHelper.PerformAlphaBlending( - bgSpan3, - bgSpan3Alpha, - fgSpan3, - fgSpan3Alpha, - tmp.GetRow(i3), - xsize, - isPremultiplied, - ecBlending[i].Clamp); - - continue; - } - - case JxlPatchBlendMode.BlendBelow: - { - int alpha = ecBlending[i].AlphaChannel; - bool isPremultiplied = extraChannelInfo[alpha].AlphaAssociated; - - Span bgSpan3 = bg[i3].Span[x0..]; - Span bgSpan3Alpha = bg[3 + alpha].Span[x0..]; - Span fgSpan3 = fg[i3].Span[x0..]; - Span fgSpan3Alpha = fg[3 + alpha].Span[x0..]; - - JxlAlphaHelper.PerformAlphaBlending( - bgSpan3, - bgSpan3Alpha, - fgSpan3, - fgSpan3Alpha, - tmp.GetRow(3 + i), - xsize, - isPremultiplied, - ecBlending[i].Clamp); - - continue; - } - - case JxlPatchBlendMode.AlphaWeightedAddAbove: - { - int alpha = ecBlending[i].AlphaChannel; - - Span bgSpan3 = bg[i3].Span[x0..]; - Span bgSpan3Alpha = bg[3 + alpha].Span[x0..]; - Span fgSpan3 = fg[i3].Span[x0..]; - - JxlAlphaHelper.PerformAlphaWeightedAdd( - bgSpan3, - fgSpan3, - bgSpan3Alpha, - tmp.GetRow(3 + i), - xsize, - ecBlending[i].Clamp); - - continue; - } - - case JxlPatchBlendMode.AlphaWeightedAddBelow: - { - int alpha = ecBlending[i].AlphaChannel; - - Span bgSpan3 = bg[i3].Span[x0..]; - Span bgSpan3Alpha = bg[3 + alpha].Span[x0..]; - Span fgSpan3 = fg[i3].Span[x0..]; - - JxlAlphaHelper.PerformAlphaWeightedAdd( - fgSpan3, - bgSpan3, - bgSpan3Alpha, - tmp.GetRow(3 + i), - xsize, - ecBlending[i].Clamp); - - continue; - } - - case JxlPatchBlendMode.Multiply: - { - Span bgSpan3 = bg[i3].Span[x0..]; - Span fgSpan3 = fg[i3].Span[x0..]; - - JxlAlphaHelper.PerformMultiplyBlending( - bgSpan3, - fgSpan3, - tmp.GetRow(i3), - xsize, - ecBlending[i].Clamp); - - continue; - } - - case JxlPatchBlendMode.Replace: - if (xsize > 0) - { - Span fgSpan3 = fg[i3].Span[x0..]; - fgSpan3.Slice(0, xsize).CopyTo(tmp.GetRow(i3)); - } - - continue; - - case JxlPatchBlendMode.None: - if (xsize > 0) - { - Span bgSpan3 = bg[i3].Span[x0..]; - bgSpan3.Slice(0, xsize).CopyTo(tmp.GetRow(i3)); - } - - continue; + hasAlpha = true; + break; } } - int colorBlendingAlpha = colorBlending.AlphaChannel; - - void Add(Span> bg, Span> fg) + // Read original input alpha throughout composition. Destination rows can be the background rows, + // and different extra channels can use different alpha rows. Publish only after every channel is complete. + for (int channel = 0; channel < extraChannelInfo.Count; channel++) { - for (int p = 0; p < 3; p++) - { - Span output = tmp.GetRow(p); - Span bgSpan = bg[p].Span; - Span fgSpan = fg[p].Span; - - for (int x = 0; x < xsize; x++) - { - int xPlusX0 = x + x0; - - output[x] = bgSpan[xPlusX0] + fgSpan[xPlusX0]; - } - } + BlendChannel(background, foreground, startX, pixelCount, 3 + channel, extraChannelBlending[channel], extraChannelInfo, true, scratch); } - void BlendWeighted(Span bottom, Span top) - { - bool isPremultiplied = extraChannelInfo[colorBlendingAlpha].AlphaAssociated; + bool colorSourceOver = colorBlending.Mode is JxlPatchBlendMode.BlendAbove or JxlPatchBlendMode.BlendBelow; - JxlAlphaHelper.PerformAlphaBlending( - new JxlAlphaBlendingInputLayer() - { - R = bottom[x0..], - G = bottom[(x0 + 1)..], - B = bottom[(2 + x0)..], - A = bottom[(3 + colorBlendingAlpha + x0)..] - }, - new JxlAlphaBlendingInputLayer() - { - R = top[x0..], - G = top[(x0 + 1)..], - B = top[(x0 + 2)..], - A = top[(3 + colorBlendingAlpha + x0)..] - }, - new JxlAlphaBlendingOutput() - { - R = tmp.GetRow(0), - G = tmp.GetRow(1), - B = tmp.GetRow(2), - A = tmp.GetRow(3) - }, - xsize, - isPremultiplied, - colorBlending.Clamp); - } - - void AddWeighted(Span bottom, Span top) + if (hasAlpha && colorSourceOver) { - for (int c = 0; c < 3; c++) + int weightChannel = 3 + colorBlending.AlphaChannel; + JxlBlendInput bottom = new( + background[0].Span.Slice(startX, pixelCount), + background[1].Span.Slice(startX, pixelCount), + background[2].Span.Slice(startX, pixelCount), + background[weightChannel].Span.Slice(startX, pixelCount)); + + JxlBlendInput top = new( + foreground[0].Span[..pixelCount], + foreground[1].Span[..pixelCount], + foreground[2].Span[..pixelCount], + foreground[weightChannel].Span[..pixelCount]); + + JxlBlendOutput result = new( + scratch.GetRow(0)[..pixelCount], + scratch.GetRow(1)[..pixelCount], + scratch.GetRow(2)[..pixelCount], + scratch.GetRow(weightChannel)[..pixelCount]); + + // Color composition also defines its selected output weight, overriding that extra channel's separate mode. + bool associated = extraChannelInfo[colorBlending.AlphaChannel].AlphaAssociated; + + if (colorBlending.Mode == JxlPatchBlendMode.BlendBelow) + { + SourceOver(top, bottom, result, associated, colorBlending.Clamp); + } + else { - JxlAlphaHelper.PerformAlphaWeightedAdd(bottom[(c + x0)..], top[(c + x0)..], top[(3 + colorBlendingAlpha + x0)..], tmp.GetRow(c), xsize, colorBlending.Clamp); + SourceOver(bottom, top, result, associated, colorBlending.Clamp); } } - - void Copy(Span src) + else { - for (int p = 0; p < 3; p++) + for (int channel = 0; channel < 3; channel++) { - src.Slice(p + x0, xsize).CopyTo(tmp.GetRow(p)); + BlendChannel(background, foreground, startX, pixelCount, channel, colorBlending, extraChannelInfo, hasAlpha, scratch); } } - switch (colorBlending.Mode) + for (int channel = 0; channel < 3 + extraChannelInfo.Count; channel++) { - case JxlPatchBlendMode.Add: - { - Add(bg, fg); - break; - } + scratch.GetRow(channel)[..pixelCount].CopyTo(output[channel].Span.Slice(startX, pixelCount)); + } + } - case JxlPatchBlendMode.AlphaWeightedAddAbove: - { - if (hasAlpha) - { - AddWeighted(bg.DangerousGetSingleSpan(), fg.DangerousGetSingleSpan()); - } - else - { - Add(bg, fg); - } + /// + /// Selects the source rows and applies one channel's blend mode through the shared bulk APIs. + /// + /// The unchanged background rows. + /// The unchanged foreground rows, starting at the blend segment. + /// The starting column within the background rows. + /// The number of samples to blend. + /// The color or extra-channel index. + /// The channel's blend mode and alpha selection. + /// The alpha representations selected by the metadata. + /// Whether the mode can use an alpha row. + /// The destination scratch rows. + private static void BlendChannel( + ReadOnlySpan> background, + ReadOnlySpan> foreground, + int startX, + int pixelCount, + int channelIndex, + JxlPatchBlending blending, + List extraChannelInfo, + bool hasAlpha, + JxlPlane scratch) + { + ReadOnlySpan backgroundValues = background[channelIndex].Span.Slice(startX, pixelCount); + ReadOnlySpan foregroundValues = foreground[channelIndex].Span[..pixelCount]; + Span destination = scratch.GetRow(channelIndex)[..pixelCount]; + switch (blending.Mode) + { + case JxlPatchBlendMode.None: + backgroundValues.CopyTo(destination); break; - } - + case JxlPatchBlendMode.Replace: + foregroundValues.CopyTo(destination); + break; + case JxlPatchBlendMode.Add: + TensorPrimitives.Add(backgroundValues, foregroundValues, destination); + break; + case JxlPatchBlendMode.Multiply: + Multiply(backgroundValues, foregroundValues, destination, blending.Clamp); + break; + case JxlPatchBlendMode.BlendAbove: + case JxlPatchBlendMode.BlendBelow: + case JxlPatchBlendMode.AlphaWeightedAddAbove: case JxlPatchBlendMode.AlphaWeightedAddBelow: { - if (hasAlpha) - { - AddWeighted(fg.DangerousGetSingleSpan(), bg.DangerousGetSingleSpan()); - } - else + bool sourceOver = blending.Mode is JxlPatchBlendMode.BlendAbove or JxlPatchBlendMode.BlendBelow; + + if (!hasAlpha) { - Add(bg, fg); + // Without alpha, native JPEG XL treats source-over as replacement and weighted addition as ordinary addition. + if (sourceOver) + { + foregroundValues.CopyTo(destination); + } + else + { + TensorPrimitives.Add(backgroundValues, foregroundValues, destination); + } + + break; } - break; - } + int alphaIndex = 3 + blending.AlphaChannel; + ReadOnlySpan backgroundAlpha = background[alphaIndex].Span.Slice(startX, pixelCount); + ReadOnlySpan foregroundAlpha = foreground[alphaIndex].Span[..pixelCount]; - case JxlPatchBlendMode.BlendAbove: - { - if (hasAlpha) + // Below reverses both samples and their alpha rows. Swapping only color would apply the wrong coverage. + if (blending.Mode is JxlPatchBlendMode.BlendBelow or JxlPatchBlendMode.AlphaWeightedAddBelow) { - BlendWeighted(bg.DangerousGetSingleSpan(), fg.DangerousGetSingleSpan()); - } - else - { - Copy(fg.DangerousGetSingleSpan()); + ReadOnlySpan values = backgroundValues; + backgroundValues = foregroundValues; + foregroundValues = values; + ReadOnlySpan alpha = backgroundAlpha; + backgroundAlpha = foregroundAlpha; + foregroundAlpha = alpha; } - break; - } - - case JxlPatchBlendMode.BlendBelow: - { - if (hasAlpha) + if (sourceOver) { - BlendWeighted(fg.DangerousGetSingleSpan(), bg.DangerousGetSingleSpan()); + bool associatedAlpha = extraChannelInfo[blending.AlphaChannel].AlphaAssociated; + SourceOver( + backgroundValues, + backgroundAlpha, + foregroundValues, + foregroundAlpha, + destination, + associatedAlpha, + blending.Clamp); } else { - Copy(fg.DangerousGetSingleSpan()); - } - - break; - } - - case JxlPatchBlendMode.Multiply: - { - Span bgSpan = bg.DangerousGetSingleSpan(); - Span fgSpan = fg.DangerousGetSingleSpan(); - - for (int p = 0; p < 3; p++) - { - JxlAlphaHelper.PerformMultiplyBlending( - bgSpan[(p + x0)..], - fgSpan[(p + x0)..], - tmp.GetRow(p), - xsize, - colorBlending.Clamp); + AddWeighted(backgroundValues, foregroundValues, foregroundAlpha, destination, blending.Clamp); } break; } - - case JxlPatchBlendMode.Replace: - { - Copy(fg.DangerousGetSingleSpan()); - break; - } - - case JxlPatchBlendMode.None: - { - Copy(bg.DangerousGetSingleSpan()); - break; - } - } - - if (xsize != 0) - { - Span outputSpan = output.DangerousGetSingleSpan(); - - for (int i = 0; i < 3; i++) - { - tmp.GetRow(i).Slice(0, xsize).CopyTo(outputSpan[(i + x0)..]); - } } } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchDictionary.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchDictionary.cs index 375e8a38ed..f93cd6e4b0 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchDictionary.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchDictionary.cs @@ -4,6 +4,7 @@ using System.Buffers; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Blending; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; @@ -29,6 +30,11 @@ internal sealed class JxlPatchDictionary private readonly List sortedPatchesY0 = []; private readonly List sortedPatchesY1 = []; + /// + /// Gets the maximum number of patch indices needed for any row. + /// + public int MaximumPatchesPerRow { get; private set; } + public bool HasAny => this.positions.Count > 0; /// @@ -51,6 +57,7 @@ public void Clear() this.numPatches.Clear(); this.sortedPatchesY0.Clear(); this.sortedPatchesY1.Clear(); + this.MaximumPatchesPerRow = 0; } public int GetReferences() @@ -278,7 +285,7 @@ public void Decode( throw new InvalidOperationException("ANS checksum failure."); } - this.ComputePatchTree(); + this.ComputePatchTree(configuration); } private static bool UsesAlpha(JxlPatchBlendMode mode) => @@ -290,19 +297,26 @@ JxlPatchBlendMode.AlphaWeightedAddAbove or private static bool UsesClamp(JxlPatchBlendMode mode) => UsesAlpha(mode) || mode == JxlPatchBlendMode.Multiply; - public void ComputePatchTree() + /// + /// Builds the row-interval index for the decoded patches. + /// + /// The configuration that owns temporary interval storage. + public void ComputePatchTree(Configuration configuration) { this.patchTree.Clear(); this.numPatches.Clear(); this.sortedPatchesY0.Clear(); this.sortedPatchesY1.Clear(); + this.MaximumPatchesPerRow = 0; if (this.positions.Count == 0) { return; } - PatchInterval[] intervals = ArrayPool.Shared.Rent(this.positions.Count); + // Only decoded intervals participate in sorting. Allocator padding must never become a patch. + using IMemoryOwner intervalStorage = configuration.MemoryAllocator.Allocate(this.positions.Count); + Span intervals = intervalStorage.GetSpan()[..this.positions.Count]; for (int i = 0; i < this.positions.Count; i++) { @@ -312,23 +326,18 @@ public void ComputePatchTree() intervals[i].Y1 = pos.Y + this.referencePositions[pos.ReferencePositionIndex].YSize; } - void SortByY0(int start, int end) => intervals.AsSpan()[start..end].Sort(static (i0, i1) => i0.Y0.CompareTo(i1.Y0)); + intervals.Sort(static (left, right) => left.Y1.CompareTo(right.Y1)); - void SortByY1(int start, int end) => intervals.AsSpan()[start..end].Sort(static (i0, i1) => i0.Y1.CompareTo(i1.Y1)); + // List capacity does not create count entries. Materialize and clear the row counters before incrementing them. + CollectionsMarshal.SetCount(this.numPatches, intervals[^1].Y1); + Span rowPatchCounts = CollectionsMarshal.AsSpan(this.numPatches); + rowPatchCounts.Clear(); - SortByY1(0, intervals.Length); - - // We have cleared the numPatches list at the start of this - // method. We have to assign a new one. - this.numPatches = new List(intervals[^1].Y1); - - // Finding frequency of each patch within any - // interval foreach (PatchInterval interval in intervals) { for (int y = interval.Y0; y < interval.Y1; y++) { - this.numPatches[y]++; + this.MaximumPatchesPerRow = Math.Max(this.MaximumPatchesPerRow, ++rowPatchCounts[y]); } } @@ -344,7 +353,7 @@ public void ComputePatchTree() int start = node.Start; int end = node.Start + node.Count; - SortByY0(start, end); + intervals[start..end].Sort(static (left, right) => left.Y0.CompareTo(right.Y0)); int middleIndex = start + (node.Count / 2); node.YCenter = intervals[middleIndex].Y0; @@ -358,7 +367,7 @@ public void ComputePatchTree() rightStart++; } - SortByY1(start, rightStart); + intervals[start..rightStart].Sort(static (left, right) => left.Y1.CompareTo(right.Y1)); int leftEnd = rightStart; while (leftEnd > start && intervals[leftEnd - 1].Y1 > node.YCenter) @@ -374,7 +383,7 @@ public void ComputePatchTree() this.sortedPatchesY1.Add(new SortedPatch(intervals[i].Y1, intervals[i].Index)); } - SortByY0(leftEnd, rightStart); + intervals[leftEnd..rightStart].Sort(static (left, right) => left.Y0.CompareTo(right.Y0)); for (int i = leftEnd; i < rightStart; i++) { @@ -389,8 +398,7 @@ public void ComputePatchTree() left.Start = start; left.Count = leftEnd - left.Start; - ref PatchTreeNode nextNode = ref CollectionsMarshal.AsSpan(this.patchTree)[next]; - nextNode.LeftChild = this.patchTree.Count; + node.LeftChild = this.patchTree.Count; this.patchTree.Add(left); } @@ -401,124 +409,119 @@ public void ComputePatchTree() right.Start = rightStart; right.Count = end - right.Start; - ref PatchTreeNode nextNode = ref CollectionsMarshal.AsSpan(this.patchTree)[next]; - nextNode.RightChild = this.patchTree.Count; + node.RightChild = this.patchTree.Count; this.patchTree.Add(right); } + // node is a value copy. Store its center, ranges, and child links after list growth has finished. + this.patchTree[next] = node; next++; } - - ArrayPool.Shared.Return(intervals); } - public IMemoryOwner GetPatchesForRow(Configuration configuration, int y) + /// + /// Collects intersecting patch indices in their encoded application order. + /// + /// The frame row. + /// Caller-owned storage with at least MaximumPatchesPerRow entries. + /// The written indices, or an empty span for a row without patches. + public Span GetPatchesForRow(int y, Span destination) { - IMemoryOwner? result = null; + if (y >= this.numPatches.Count || this.numPatches[y] == 0) + { + return destination[..0]; + } - Span sortedPatchesY0 = CollectionsMarshal.AsSpan(this.sortedPatchesY0); - Span sortedPatchesY1 = CollectionsMarshal.AsSpan(this.sortedPatchesY1); + ReadOnlySpan starts = CollectionsMarshal.AsSpan(this.sortedPatchesY0); + ReadOnlySpan ends = CollectionsMarshal.AsSpan(this.sortedPatchesY1); + ReadOnlySpan tree = CollectionsMarshal.AsSpan(this.patchTree); + int count = 0; - if (y < this.numPatches.Count && this.numPatches[y] > 0) + for (int nodeIndex = 0; nodeIndex != -1;) { - result = configuration.MemoryAllocator.Allocate(this.numPatches[y]); - Span spanResult = result.GetSpan(); - - // Instead of making result a List, we simply allocate - // a fixed-length memory, get a Span to it, and track the - // length separately. - int resultPointer = 0; + PatchTreeNode node = tree[nodeIndex]; - for (int treeIdx = 0; treeIdx != -1;) + if (y <= node.YCenter) { - // Index of the tree must be within the bounds of the patch tree - if (treeIdx >= this.patchTree.Count) + // Starts are ascending. Every remaining interval starts after this row and cannot intersect it. + for (int i = 0; i < node.Count; i++) { - throw new InvalidOperationException("Tree index out of bounds"); - } - - // Recomputing CollectionsMarshal.AsSpan is important as we'll - // be adding items to the backing list. Items should not be - // added or removed to the original list while the Span - // returned by CollectionsMarshal.AsSpan is in use. - Span patchTreeSpan = CollectionsMarshal.AsSpan(this.patchTree); + SortedPatch patch = starts[node.Start + i]; - ref PatchTreeNode node = ref patchTreeSpan[treeIdx]; - - if (y <= node.YCenter) - { - for (int i = 0; i < node.Count; ++i) + if (y < patch.First) { - ref SortedPatch p = ref sortedPatchesY0[node.Start + i]; - - if (y < p.First) - { - break; - } - - spanResult[resultPointer++] = p.Second; + break; } - treeIdx = y < node.YCenter ? (int)node.LeftChild : -1; + destination[count++] = patch.Second; } - else - { - for (int i = 0; i < node.Count; ++i) - { - ref SortedPatch p = ref sortedPatchesY1[node.Start + i]; - if (y >= p.First) - { - break; - } + nodeIndex = y < node.YCenter ? (int)node.LeftChild : -1; + } + else + { + // Ends are descending and exclusive. At the first expired interval, all remaining intervals have expired too. + for (int i = 0; i < node.Count; i++) + { + SortedPatch patch = ends[node.Start + i]; - spanResult[resultPointer++] = p.Second; + if (y >= patch.First) + { + break; } - treeIdx = (int)node.RightChild; + destination[count++] = patch.Second; } - } - // Once we collect patches, we must make sure - // that the resulting patches are sorted. - // - // Span.Sort() in theory should be faster - // and more lightweight than an array's sort. - spanResult.Sort(); + nodeIndex = (int)node.RightChild; + } } - return result - ?? throw new InvalidOperationException("Y offset out of bounds"); + // Overlapping patches must compose in encoded order. Tree traversal order is not application order. + Span result = destination[..count]; + result.Sort(); + return result; } + /// + /// Applies the ordered patches that intersect one row segment using caller-owned row storage. + /// + /// The row segment, indexed by color and extra channel. + /// The reusable table that receives borrowed reference rows. + /// The reusable blend results, with one row per channel. + /// Reusable storage for at least MaximumPatchesPerRow patch indices. + /// The frame row. + /// The frame column of the first sample in inOut. + /// The number of samples in the row segment. + /// The extra-channel types and alpha representations. + /// Whether each patch can be applied to the row segment. public bool AddOneRow( - Configuration configuration, Span> inOut, + Span> foreground, + JxlPlane scratch, + Span patchIndices, int y, - int x0, + int startX, int width, List extraChannelInfos) { - int numEc = extraChannelInfos.Count; // number of Extra Channels (ECs) + int extraChannelCount = extraChannelInfos.Count; - if (numEc + 1 > this.blendingsStride) + if (extraChannelCount + 1 > this.blendingsStride) { // out of bounds extra channel return false; } - using IMemoryOwner patches = this.GetPatchesForRow(configuration, y); - Span patchesSpan = patches.GetSpan(); - - Span> foreground = new Memory[3 + numEc]; + Span patches = this.GetPatchesForRow(y, patchIndices); Span patchPositions = CollectionsMarshal.AsSpan(this.positions); Span patchRefPositions = CollectionsMarshal.AsSpan(this.referencePositions); - for (int i = 0; i < patchesSpan.Length; i++) + for (int i = 0; i < patches.Length; i++) { - int posIdx = patchesSpan[i]; + int posIdx = patches[i]; int blendingIdx = posIdx * this.blendingsStride; ref JxlPatchPosition pos = ref patchPositions[posIdx]; @@ -536,34 +539,38 @@ public bool AddOneRow( int iy = y - by; int @ref = refPos.Ref; - if (bx >= x0 + width || bx + patchWidth < x0) + if (bx >= startX + width || bx + patchWidth < startX) { continue; } - int patchX0 = Math.Max(bx, x0); - int patchX1 = Math.Min(bx + patchWidth, x0 + width); + int patchX0 = Math.Max(bx, startX); + int patchX1 = Math.Min(bx + patchWidth, startX + width); + + // Slice at the actual overlap. The reference patch can start to the right of this row segment; + // computing a row origin before the reference allocation would be invalid even if a later offset cancels it. + int referenceStartX = refPos.X0 + patchX0 - bx; for (int c = 0; c < 3; c++) { - foreground[c] = this.referenceFrames[@ref].Frame.Color!.PlaneRowMemory(c, refPos.Y0 + iy)[(refPos.X0 + x0 - bx)..]; + foreground[c] = this.referenceFrames[@ref].Frame.Color!.PlaneRowMemory(c, refPos.Y0 + iy)[referenceStartX..]; } - for (int j = 0; j < 3; j++) + for (int j = 0; j < extraChannelCount; j++) { - foreground[3 + j] = this.referenceFrames[@ref].Frame.GetExtraChannel(j).GetRowMemory(refPos.Y0 + iy)[(refPos.X0 + x0 - bx)..]; + foreground[3 + j] = this.referenceFrames[@ref].Frame.GetExtraChannel(j).GetRowMemory(refPos.Y0 + iy)[referenceStartX..]; } JxlBlending.PerformBlending( - configuration, inOut, foreground, inOut, - patchX0 - x0, + patchX0 - startX, patchX1 - patchX0, this.blendings[blendingIdx], CollectionsMarshal.AsSpan(this.blendings)[(blendingIdx + 1)..], - extraChannelInfos); + extraChannelInfos, + scratch); } return true; diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/PatchDictionaryStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/PatchDictionaryStage.cs index 18660007fc..4d272678af 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/PatchDictionaryStage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/PatchDictionaryStage.cs @@ -1,39 +1,90 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Patch; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; +/// +/// Applies reference-frame patches to planar rows using reusable blend storage. +/// +/// The configuration that owns sample storage. +/// The decoded patch dictionary. +/// The extra-channel types and alpha representations. internal sealed class PatchDictionaryStage(Configuration configuration, JxlPatchDictionary patches, List extraChannelInfos) : RenderPipelineStageBase(configuration) { + // These tables contain borrowed memory, not owned sample buffers. Allocate them once for the fixed channel count. + private readonly Memory[] inputRowTable = new Memory[3 + extraChannelInfos.Count]; + private readonly Memory[] foregroundRowTable = new Memory[3 + extraChannelInfos.Count]; + + // One allocator-owned row per channel protects original alpha while results are prepared. + // The codec is sequential. Every patch and subsequent row can reuse these same rows. + private JxlPlane? blendStorage; + + // The interval index supplies an exact maximum. Rent the index buffer once for this sequential stage. + private IMemoryOwner? patchIndices; + /// public override string Name => "Patches"; /// - public override void ProcessRow(Buffer2D> inputRows, Buffer2D> outputRows, int xExtraLeft, int xExtraRight, int width, int xPos, int yPos) + public override void ProcessRow( + Buffer2D> inputRows, + Buffer2D> outputRows, + int xExtraLeft, + int xExtraRight, + int width, + int xPos, + int yPos) { - int channels = 3 + extraChannelInfos.Count; + if (patches.MaximumPatchesPerRow == 0) + { + return; + } - Span> rowPtrs = new Memory[channels]; + this.patchIndices ??= this.GetConfiguration().MemoryAllocator.Allocate(patches.MaximumPatchesPerRow); + Span indices = this.patchIndices.GetSpan(); + int rowWidth = width + xExtraLeft + xExtraRight; + JxlPlane? storage = this.blendStorage; - for (int i = 0; i < channels; i++) + if (storage is null || storage.XSize < rowWidth) { - rowPtrs[i] = this.GetInputRowMemory(inputRows, i, 0, xExtraLeft); + // Grow before releasing the previous allocation so allocation failure does not lose the owned buffer. + storage = new JxlPlane(this.GetConfiguration(), rowWidth, this.inputRowTable.Length); + this.blendStorage?.Dispose(); + this.blendStorage = storage; } - return patches.AddOneRow(rowPtrs, yPos, xPos - xExtraLeft, width + xExtraLeft + xExtraRight, extraChannelInfos); + for (int channel = 0; channel < this.inputRowTable.Length; channel++) + { + this.inputRowTable[channel] = this.GetInputRowMemory(inputRows, channel, 0, xExtraLeft); + } + + if (!patches.AddOneRow(this.inputRowTable, this.foregroundRowTable, storage, indices, yPos, xPos - xExtraLeft, rowWidth, extraChannelInfos)) + { + throw new InvalidImageContentException("Invalid JPEG XL patch row."); + } } /// public override RenderPipelineChannelMode GetChannelMode(int channel) + => channel < this.inputRowTable.Length ? RenderPipelineChannelMode.InPlace : RenderPipelineChannelMode.Ignored; + + /// + public override void Dispose() { - int numChannels = 3 + extraChannelInfos.Count; - return channel < numChannels - ? RenderPipelineChannelMode.InPlace - : RenderPipelineChannelMode.Ignored; + this.blendStorage?.Dispose(); + this.blendStorage = null; + this.patchIndices?.Dispose(); + this.patchIndices = null; + + // References belong to the pipeline and reference frames. Release the borrowed views without disposing their owners. + Array.Clear(this.inputRowTable); + Array.Clear(this.foregroundRowTable); } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineStageBase.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineStageBase.cs index 1f4dd1331b..ae354deb5c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineStageBase.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineStageBase.cs @@ -68,20 +68,57 @@ public virtual void SetInputSizes(Span inputSizes) { } - public Span GetInputRow(Buffer2D> inputRows, int c, int offset) - => inputRows[c, this.Settings.BorderY + offset].Span[RenderPipelineXOffset..]; + /// + /// Borrows an input row, including its remaining right-border storage. + /// + /// The input row table, indexed by channel and vertical position. + /// The channel index. + /// The vertical offset from the current row, in rows. + /// The row storage without the unused left prefix. + public Span GetInputRow(Buffer2D> inputRows, int channelIndex, int rowOffset) + => inputRows[channelIndex, this.Settings.BorderY + rowOffset].Span[RenderPipelineXOffset..]; - public Span GetInputRow(Buffer2D> inputRows, int c, int offset, int xExtraLeft) - => inputRows[c, this.Settings.BorderY + offset].Span[(RenderPipelineXOffset - xExtraLeft)..]; + /// + /// Borrows an input row, including its remaining right-border storage. + /// + /// The input row table, indexed by channel and vertical position. + /// The channel index. + /// The vertical offset from the current row, in rows. + /// The number of left-border samples to include. + /// The row storage without the unused left prefix. + public Span GetInputRow(Buffer2D> inputRows, int channelIndex, int rowOffset, int xExtraLeft) + => inputRows[channelIndex, this.Settings.BorderY + rowOffset].Span[(RenderPipelineXOffset - xExtraLeft)..]; - public Memory GetInputRowMemory(Buffer2D> inputRows, int c, int offset) - => inputRows[c, this.Settings.BorderY + offset][RenderPipelineXOffset..]; + /// + /// Borrows an input row, including its remaining right-border storage. + /// + /// The input row table, indexed by channel and vertical position. + /// The channel index. + /// The vertical offset from the current row, in rows. + /// The row storage without the unused left prefix. + public Memory GetInputRowMemory(Buffer2D> inputRows, int channelIndex, int rowOffset) + => inputRows[channelIndex, this.Settings.BorderY + rowOffset][RenderPipelineXOffset..]; - public Memory GetInputRowMemory(Buffer2D> inputRows, int c, int offset, int xExtraLeft) - => inputRows[c, this.Settings.BorderY + offset][(RenderPipelineXOffset - xExtraLeft)..]; + /// + /// Borrows an input row, including its remaining right-border storage. + /// + /// The input row table, indexed by channel and vertical position. + /// The channel index. + /// The vertical offset from the current row, in rows. + /// The number of left-border samples to include. + /// The row storage without the unused left prefix. + public Memory GetInputRowMemory(Buffer2D> inputRows, int channelIndex, int rowOffset, int xExtraLeft) + => inputRows[channelIndex, this.Settings.BorderY + rowOffset][(RenderPipelineXOffset - xExtraLeft)..]; - public static Span GetOutputRow(Buffer2D> outputRows, int c, int offset) - => outputRows[c, offset].Span[RenderPipelineXOffset..]; + /// + /// Borrows an output row without the unused left prefix. + /// + /// The output row table, indexed by channel and vertical position. + /// The channel index. + /// The row index within the output table. + /// The output samples and remaining right-border storage. + public static Span GetOutputRow(Buffer2D> outputRows, int channelIndex, int rowOffset) + => outputRows[channelIndex, rowOffset].Span[RenderPipelineXOffset..]; public virtual void GetImageDimensions(out int width, out int height, out Point frameOrigin) { diff --git a/src/ImageSharp/PixelFormats/PixelBlenders/PixelBlenders.cs b/src/ImageSharp/PixelFormats/PixelBlenders/PixelBlenders.cs new file mode 100644 index 0000000000..e35f0de282 --- /dev/null +++ b/src/ImageSharp/PixelFormats/PixelBlenders/PixelBlenders.cs @@ -0,0 +1,690 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.PixelFormats.PixelBlenders; + +/// +/// Applies existing blend operators to vector rows without pixel storage conversion. +/// +internal static class PixelBlenders +{ + // PixelBlender owns pixel conversion. These loops need only the operator, + // so codec-owned vectors can use the same SIMD traversal without selecting an unrelated pixel format. + + /// + /// Applies the selected blend operator to vector rows without pixel storage conversion. + /// + /// The existing color and alpha composition operator. + /// The output vectors. Its length selects the number of samples. + /// The background vectors. The row must cover the output length. + /// The source vectors. The row must cover the output length. + /// The source opacity, clamped to finite values in [0,1]. + /// Inputs must use the alpha representation required by the operator. + public static void Blend(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + where TOperator : struct, IPixelBlenderOperator + { + // Public entry points validate the row lengths, so all three references can advance in lockstep. + int scalarStart = 0; + amount = ColorNumerics.ClampToFinite(amount, 0, 1F); + + ref Vector4 destinationRef = ref MemoryMarshal.GetReference(destination); + ref Vector4 backgroundRef = ref MemoryMarshal.GetReference(background); + ref Vector4 sourceRef = ref MemoryMarshal.GetReference(source); + + if (Vector512.IsHardwareAccelerated && destination.Length >= 4) + { + // A 512-bit register holds four complete four-component vectors in [X,Y,Z,W] groups. + ref Vector512 destinationBase = ref Unsafe.As>(ref destinationRef); + ref Vector512 backgroundBase = ref Unsafe.As>(ref backgroundRef); + ref Vector512 sourceBase = ref Unsafe.As>(ref sourceRef); + int vectorCount = destination.Length / 4; + Vector512 amountVector = Vector512.Create(amount); + + for (nuint i = 0; i < (uint)vectorCount; i++) + { + Unsafe.Add(ref destinationBase, i) = TOperator.Invoke(Unsafe.Add(ref backgroundBase, i), Unsafe.Add(ref sourceBase, i), amountVector); + } + + scalarStart = vectorCount * 4; + } + else if (Vector256.IsHardwareAccelerated && destination.Length >= 2) + { + // A 256-bit register holds two complete four-component vectors in [X,Y,Z,W] groups. + ref Vector256 destinationBase = ref Unsafe.As>(ref destinationRef); + ref Vector256 backgroundBase = ref Unsafe.As>(ref backgroundRef); + ref Vector256 sourceBase = ref Unsafe.As>(ref sourceRef); + int vectorCount = destination.Length / 2; + Vector256 amountVector = Vector256.Create(amount); + + for (nuint i = 0; i < (uint)vectorCount; i++) + { + Unsafe.Add(ref destinationBase, i) = TOperator.Invoke(Unsafe.Add(ref backgroundBase, i), Unsafe.Add(ref sourceBase, i), amountVector); + } + + scalarStart = vectorCount * 2; + } + + // Vector4 is both the scalar pixel representation and the portable SIMD fallback. + for (int i = scalarStart; i < destination.Length; i++) + { + Unsafe.Add(ref destinationRef, (uint)i) = TOperator.Invoke(Unsafe.Add(ref backgroundRef, (uint)i), Unsafe.Add(ref sourceRef, (uint)i), amount); + } + } + + /// + /// Applies the selected blend operator to vector rows without pixel storage conversion. + /// + /// The existing color and alpha composition operator. + /// The output vectors. Its length selects the number of samples. + /// The background vectors. The row must cover the output length. + /// The source vector used for every output sample. + /// The source opacity, clamped to finite values in [0,1]. + /// Inputs must use the alpha representation required by the operator. + public static void Blend(Span destination, ReadOnlySpan background, Vector4 source, float amount) + where TOperator : struct, IPixelBlenderOperator + { + // Public entry points validate the row lengths, so the destination and background advance together. + int scalarStart = 0; + amount = ColorNumerics.ClampToFinite(amount, 0, 1F); + + ref Vector4 destinationRef = ref MemoryMarshal.GetReference(destination); + ref Vector4 backgroundRef = ref MemoryMarshal.GetReference(background); + + if (Vector512.IsHardwareAccelerated && destination.Length >= 4) + { + // Repeat one [X,Y,Z,W] source group four times to match the four background pixels. + ref Vector512 destinationBase = ref Unsafe.As>(ref destinationRef); + ref Vector512 backgroundBase = ref Unsafe.As>(ref backgroundRef); + int vectorCount = destination.Length / 4; + Vector512 sourceVector = CreateVector512(source); + Vector512 amountVector = Vector512.Create(amount); + + for (nuint i = 0; i < (uint)vectorCount; i++) + { + Unsafe.Add(ref destinationBase, i) = TOperator.Invoke(Unsafe.Add(ref backgroundBase, i), sourceVector, amountVector); + } + + scalarStart = vectorCount * 4; + } + else if (Vector256.IsHardwareAccelerated && destination.Length >= 2) + { + // Repeat one [X,Y,Z,W] source group twice to match the two background pixels. + ref Vector256 destinationBase = ref Unsafe.As>(ref destinationRef); + ref Vector256 backgroundBase = ref Unsafe.As>(ref backgroundRef); + int vectorCount = destination.Length / 2; + Vector256 sourceVector = CreateVector256(source); + Vector256 amountVector = Vector256.Create(amount); + + for (nuint i = 0; i < (uint)vectorCount; i++) + { + Unsafe.Add(ref destinationBase, i) = TOperator.Invoke(Unsafe.Add(ref backgroundBase, i), sourceVector, amountVector); + } + + scalarStart = vectorCount * 2; + } + + // The remaining pixel count is at most three after AVX-512 or one after AVX2. + for (int i = scalarStart; i < destination.Length; i++) + { + Unsafe.Add(ref destinationRef, (uint)i) = TOperator.Invoke(Unsafe.Add(ref backgroundRef, (uint)i), source, amount); + } + } + + /// + /// Applies the selected blend operator to vector rows without pixel storage conversion. + /// + /// The existing color and alpha composition operator. + /// The output vectors. Its length selects the number of samples. + /// The background vectors. The row must cover the output length. + /// The source vectors. The row must cover the output length. + /// The source opacity for each output sample, clamped to finite values in [0,1]. + /// Inputs must use the alpha representation required by the operator. + public static void Blend( + Span destination, + ReadOnlySpan background, + ReadOnlySpan source, + ReadOnlySpan amount) + where TOperator : struct, IPixelBlenderOperator + { + // Each amount belongs to one pixel and must be repeated across that pixel's four components. + int scalarStart = 0; + + ref Vector4 destinationRef = ref MemoryMarshal.GetReference(destination); + ref Vector4 backgroundRef = ref MemoryMarshal.GetReference(background); + ref Vector4 sourceRef = ref MemoryMarshal.GetReference(source); + ref float amountRef = ref MemoryMarshal.GetReference(amount); + + if (Vector512.IsHardwareAccelerated && destination.Length >= 4) + { + ref Vector512 destinationBase = ref Unsafe.As>(ref destinationRef); + ref Vector512 backgroundBase = ref Unsafe.As>(ref backgroundRef); + ref Vector512 sourceBase = ref Unsafe.As>(ref sourceRef); + int vectorCount = destination.Length / 4; + + for (nuint i = 0; i < (uint)vectorCount; i++) + { + // Four scalar amounts become four contiguous [a,a,a,a] groups before clamping. + ref float amountBase = ref Unsafe.Add(ref amountRef, i * 4); + Vector512 amountVector = CreateClampedVector512(ref amountBase); + + Unsafe.Add(ref destinationBase, i) = TOperator.Invoke(Unsafe.Add(ref backgroundBase, i), Unsafe.Add(ref sourceBase, i), amountVector); + } + + scalarStart = vectorCount * 4; + } + else if (Vector256.IsHardwareAccelerated && destination.Length >= 2) + { + ref Vector256 destinationBase = ref Unsafe.As>(ref destinationRef); + ref Vector256 backgroundBase = ref Unsafe.As>(ref backgroundRef); + ref Vector256 sourceBase = ref Unsafe.As>(ref sourceRef); + int vectorCount = destination.Length / 2; + + for (nuint i = 0; i < (uint)vectorCount; i++) + { + // Two scalar amounts become two contiguous [a,a,a,a] groups before clamping. + ref float amountBase = ref Unsafe.Add(ref amountRef, i * 2); + Vector256 amountVector = CreateClampedVector256(ref amountBase); + + Unsafe.Add(ref destinationBase, i) = TOperator.Invoke(Unsafe.Add(ref backgroundBase, i), Unsafe.Add(ref sourceBase, i), amountVector); + } + + scalarStart = vectorCount * 2; + } + + for (int i = scalarStart; i < destination.Length; i++) + { + Unsafe.Add(ref destinationRef, (uint)i) = TOperator.Invoke( + Unsafe.Add(ref backgroundRef, (uint)i), + Unsafe.Add(ref sourceRef, (uint)i), + ColorNumerics.ClampToFinite(Unsafe.Add(ref amountRef, (uint)i), 0, 1F)); + } + } + + /// + /// Applies the selected blend operator to vector rows without pixel storage conversion. + /// + /// The existing color and alpha composition operator. + /// The output vectors. Its length selects the number of samples. + /// The background vectors. The row must cover the output length. + /// The source vector used for every output sample. + /// The source opacity for each output sample, clamped to finite values in [0,1]. + /// Inputs must use the alpha representation required by the operator. + public static void Blend(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + where TOperator : struct, IPixelBlenderOperator + { + // The source is invariant, while each background pixel has its own independently clamped amount. + int scalarStart = 0; + + ref Vector4 destinationRef = ref MemoryMarshal.GetReference(destination); + ref Vector4 backgroundRef = ref MemoryMarshal.GetReference(background); + ref float amountRef = ref MemoryMarshal.GetReference(amount); + + if (Vector512.IsHardwareAccelerated && destination.Length >= 4) + { + ref Vector512 destinationBase = ref Unsafe.As>(ref destinationRef); + ref Vector512 backgroundBase = ref Unsafe.As>(ref backgroundRef); + int vectorCount = destination.Length / 4; + Vector512 sourceVector = CreateVector512(source); + + for (nuint i = 0; i < (uint)vectorCount; i++) + { + ref float amountBase = ref Unsafe.Add(ref amountRef, i * 4); + Vector512 amountVector = CreateClampedVector512(ref amountBase); + + Unsafe.Add(ref destinationBase, i) = TOperator.Invoke(Unsafe.Add(ref backgroundBase, i), sourceVector, amountVector); + } + + scalarStart = vectorCount * 4; + } + else if (Vector256.IsHardwareAccelerated && destination.Length >= 2) + { + ref Vector256 destinationBase = ref Unsafe.As>(ref destinationRef); + ref Vector256 backgroundBase = ref Unsafe.As>(ref backgroundRef); + int vectorCount = destination.Length / 2; + Vector256 sourceVector = CreateVector256(source); + + for (nuint i = 0; i < (uint)vectorCount; i++) + { + ref float amountBase = ref Unsafe.Add(ref amountRef, i * 2); + Vector256 amountVector = CreateClampedVector256(ref amountBase); + + Unsafe.Add(ref destinationBase, i) = TOperator.Invoke(Unsafe.Add(ref backgroundBase, i), sourceVector, amountVector); + } + + scalarStart = vectorCount * 2; + } + + for (int i = scalarStart; i < destination.Length; i++) + { + Unsafe.Add(ref destinationRef, (uint)i) = TOperator.Invoke( + Unsafe.Add(ref backgroundRef, (uint)i), + source, + ColorNumerics.ClampToFinite(Unsafe.Add(ref amountRef, (uint)i), 0, 1F)); + } + } + + /// + /// Applies the selected blend operator to vector rows without pixel storage conversion. + /// + /// The existing color and alpha composition operator. + /// The output vectors. Its length selects the number of samples. + /// The background vectors. The row must cover the output length. + /// The source vectors. The row must cover the output length. + /// The source opacity, clamped to finite values in [0,1]. + /// The coverage row, clamped to finite values in [0,1]. + /// Inputs must use the alpha representation required by the operator. + public static void BlendWithCoverage( + Span destination, + ReadOnlySpan background, + ReadOnlySpan source, + float amount, + ReadOnlySpan coverage) + where TOperator : struct, IPixelBlenderOperator + { + // Coverage mixes the composed result back toward the original background, so it is fused into this pass. + int scalarStart = 0; + amount = ColorNumerics.ClampToFinite(amount, 0, 1F); + + ref Vector4 destinationRef = ref MemoryMarshal.GetReference(destination); + ref Vector4 backgroundRef = ref MemoryMarshal.GetReference(background); + ref Vector4 sourceRef = ref MemoryMarshal.GetReference(source); + ref float coverageRef = ref MemoryMarshal.GetReference(coverage); + + if (Vector512.IsHardwareAccelerated && destination.Length >= 4) + { + ref Vector512 destinationBase = ref Unsafe.As>(ref destinationRef); + ref Vector512 backgroundBase = ref Unsafe.As>(ref backgroundRef); + ref Vector512 sourceBase = ref Unsafe.As>(ref sourceRef); + int vectorCount = destination.Length / 4; + Vector512 amountVector = Vector512.Create(amount); + + for (nuint i = 0; i < (uint)vectorCount; i++) + { + ref Vector512 backgroundVector = ref Unsafe.Add(ref backgroundBase, i); + ref float coverageBase = ref Unsafe.Add(ref coverageRef, i * 4); + Vector512 coverageVector = CreateClampedVector512(ref coverageBase); + Vector512 blended = TOperator.Invoke(backgroundVector, Unsafe.Add(ref sourceBase, i), amountVector); + + Unsafe.Add(ref destinationBase, i) = TOperator.IsAssociatedAlpha + ? AssociatedAlphaPorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector) + : PorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector); + } + + scalarStart = vectorCount * 4; + } + else if (Vector256.IsHardwareAccelerated && destination.Length >= 2) + { + ref Vector256 destinationBase = ref Unsafe.As>(ref destinationRef); + ref Vector256 backgroundBase = ref Unsafe.As>(ref backgroundRef); + ref Vector256 sourceBase = ref Unsafe.As>(ref sourceRef); + int vectorCount = destination.Length / 2; + Vector256 amountVector = Vector256.Create(amount); + + for (nuint i = 0; i < (uint)vectorCount; i++) + { + ref Vector256 backgroundVector = ref Unsafe.Add(ref backgroundBase, i); + ref float coverageBase = ref Unsafe.Add(ref coverageRef, i * 2); + Vector256 coverageVector = CreateClampedVector256(ref coverageBase); + Vector256 blended = TOperator.Invoke(backgroundVector, Unsafe.Add(ref sourceBase, i), amountVector); + + Unsafe.Add(ref destinationBase, i) = TOperator.IsAssociatedAlpha + ? AssociatedAlphaPorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector) + : PorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector); + } + + scalarStart = vectorCount * 2; + } + + for (int i = scalarStart; i < destination.Length; i++) + { + Vector4 backgroundPixel = Unsafe.Add(ref backgroundRef, (uint)i); + Vector4 blended = TOperator.Invoke(backgroundPixel, Unsafe.Add(ref sourceRef, (uint)i), amount); + + float clampedCoverage = ColorNumerics.ClampToFinite(Unsafe.Add(ref coverageRef, (uint)i), 0, 1F); + Unsafe.Add(ref destinationRef, (uint)i) = TOperator.IsAssociatedAlpha + ? AssociatedAlphaPorterDuffFunctions.BlendWithCoverage(backgroundPixel, blended, clampedCoverage) + : PorterDuffFunctions.BlendWithCoverage(backgroundPixel, blended, clampedCoverage); + } + } + + /// + /// Applies the selected blend operator to vector rows without pixel storage conversion. + /// + /// The existing color and alpha composition operator. + /// The output vectors. Its length selects the number of samples. + /// The background vectors. The row must cover the output length. + /// The source vector used for every output sample. + /// The source opacity, clamped to finite values in [0,1]. + /// The coverage row, clamped to finite values in [0,1]. + /// Inputs must use the alpha representation required by the operator. + public static void BlendWithCoverage( + Span destination, + ReadOnlySpan background, + Vector4 source, + float amount, + ReadOnlySpan coverage) + where TOperator : struct, IPixelBlenderOperator + { + // The constant source is expanded once per selected width and reused for the complete row. + int scalarStart = 0; + amount = ColorNumerics.ClampToFinite(amount, 0, 1F); + + ref Vector4 destinationRef = ref MemoryMarshal.GetReference(destination); + ref Vector4 backgroundRef = ref MemoryMarshal.GetReference(background); + ref float coverageRef = ref MemoryMarshal.GetReference(coverage); + + if (Vector512.IsHardwareAccelerated && destination.Length >= 4) + { + ref Vector512 destinationBase = ref Unsafe.As>(ref destinationRef); + ref Vector512 backgroundBase = ref Unsafe.As>(ref backgroundRef); + int vectorCount = destination.Length / 4; + Vector512 sourceVector = CreateVector512(source); + Vector512 amountVector = Vector512.Create(amount); + + for (nuint i = 0; i < (uint)vectorCount; i++) + { + ref Vector512 backgroundVector = ref Unsafe.Add(ref backgroundBase, i); + ref float coverageBase = ref Unsafe.Add(ref coverageRef, i * 4); + Vector512 coverageVector = CreateClampedVector512(ref coverageBase); + Vector512 blended = TOperator.Invoke(backgroundVector, sourceVector, amountVector); + + Unsafe.Add(ref destinationBase, i) = TOperator.IsAssociatedAlpha + ? AssociatedAlphaPorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector) + : PorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector); + } + + scalarStart = vectorCount * 4; + } + else if (Vector256.IsHardwareAccelerated && destination.Length >= 2) + { + ref Vector256 destinationBase = ref Unsafe.As>(ref destinationRef); + ref Vector256 backgroundBase = ref Unsafe.As>(ref backgroundRef); + int vectorCount = destination.Length / 2; + Vector256 sourceVector = CreateVector256(source); + Vector256 amountVector = Vector256.Create(amount); + + for (nuint i = 0; i < (uint)vectorCount; i++) + { + ref Vector256 backgroundVector = ref Unsafe.Add(ref backgroundBase, i); + ref float coverageBase = ref Unsafe.Add(ref coverageRef, i * 2); + Vector256 coverageVector = CreateClampedVector256(ref coverageBase); + Vector256 blended = TOperator.Invoke(backgroundVector, sourceVector, amountVector); + + Unsafe.Add(ref destinationBase, i) = TOperator.IsAssociatedAlpha + ? AssociatedAlphaPorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector) + : PorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector); + } + + scalarStart = vectorCount * 2; + } + + for (int i = scalarStart; i < destination.Length; i++) + { + Vector4 backgroundPixel = Unsafe.Add(ref backgroundRef, (uint)i); + Vector4 blended = TOperator.Invoke(backgroundPixel, source, amount); + + float clampedCoverage = ColorNumerics.ClampToFinite(Unsafe.Add(ref coverageRef, (uint)i), 0, 1F); + Unsafe.Add(ref destinationRef, (uint)i) = TOperator.IsAssociatedAlpha + ? AssociatedAlphaPorterDuffFunctions.BlendWithCoverage(backgroundPixel, blended, clampedCoverage) + : PorterDuffFunctions.BlendWithCoverage(backgroundPixel, blended, clampedCoverage); + } + } + + /// + /// Applies the selected blend operator to vector rows without pixel storage conversion. + /// + /// The existing color and alpha composition operator. + /// The output vectors. Its length selects the number of samples. + /// The background vectors. The row must cover the output length. + /// The source vectors. The row must cover the output length. + /// The source opacity for each output sample, clamped to finite values in [0,1]. + /// The coverage row, clamped to finite values in [0,1]. + /// Inputs must use the alpha representation required by the operator. + public static void BlendWithCoverage( + Span destination, + ReadOnlySpan background, + ReadOnlySpan source, + ReadOnlySpan amount, + ReadOnlySpan coverage) + where TOperator : struct, IPixelBlenderOperator + { + // Amount controls composition while coverage controls the final mix with the untouched background. + int scalarStart = 0; + + ref Vector4 destinationRef = ref MemoryMarshal.GetReference(destination); + ref Vector4 backgroundRef = ref MemoryMarshal.GetReference(background); + ref Vector4 sourceRef = ref MemoryMarshal.GetReference(source); + ref float amountRef = ref MemoryMarshal.GetReference(amount); + ref float coverageRef = ref MemoryMarshal.GetReference(coverage); + + if (Vector512.IsHardwareAccelerated && destination.Length >= 4) + { + ref Vector512 destinationBase = ref Unsafe.As>(ref destinationRef); + ref Vector512 backgroundBase = ref Unsafe.As>(ref backgroundRef); + ref Vector512 sourceBase = ref Unsafe.As>(ref sourceRef); + int vectorCount = destination.Length / 4; + + for (nuint i = 0; i < (uint)vectorCount; i++) + { + ref Vector512 backgroundVector = ref Unsafe.Add(ref backgroundBase, i); + ref float amountBase = ref Unsafe.Add(ref amountRef, i * 4); + ref float coverageBase = ref Unsafe.Add(ref coverageRef, i * 4); + Vector512 amountVector = CreateClampedVector512(ref amountBase); + Vector512 coverageVector = CreateClampedVector512(ref coverageBase); + Vector512 blended = TOperator.Invoke(backgroundVector, Unsafe.Add(ref sourceBase, i), amountVector); + + Unsafe.Add(ref destinationBase, i) = TOperator.IsAssociatedAlpha + ? AssociatedAlphaPorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector) + : PorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector); + } + + scalarStart = vectorCount * 4; + } + else if (Vector256.IsHardwareAccelerated && destination.Length >= 2) + { + ref Vector256 destinationBase = ref Unsafe.As>(ref destinationRef); + ref Vector256 backgroundBase = ref Unsafe.As>(ref backgroundRef); + ref Vector256 sourceBase = ref Unsafe.As>(ref sourceRef); + int vectorCount = destination.Length / 2; + + for (nuint i = 0; i < (uint)vectorCount; i++) + { + ref Vector256 backgroundVector = ref Unsafe.Add(ref backgroundBase, i); + ref float amountBase = ref Unsafe.Add(ref amountRef, i * 2); + ref float coverageBase = ref Unsafe.Add(ref coverageRef, i * 2); + Vector256 amountVector = CreateClampedVector256(ref amountBase); + Vector256 coverageVector = CreateClampedVector256(ref coverageBase); + Vector256 blended = TOperator.Invoke(backgroundVector, Unsafe.Add(ref sourceBase, i), amountVector); + + Unsafe.Add(ref destinationBase, i) = TOperator.IsAssociatedAlpha + ? AssociatedAlphaPorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector) + : PorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector); + } + + scalarStart = vectorCount * 2; + } + + for (int i = scalarStart; i < destination.Length; i++) + { + Vector4 backgroundPixel = Unsafe.Add(ref backgroundRef, (uint)i); + + Vector4 blended = TOperator.Invoke( + backgroundPixel, + Unsafe.Add(ref sourceRef, (uint)i), + ColorNumerics.ClampToFinite(Unsafe.Add(ref amountRef, (uint)i), 0, 1F)); + + float clampedCoverage = ColorNumerics.ClampToFinite(Unsafe.Add(ref coverageRef, (uint)i), 0, 1F); + Unsafe.Add(ref destinationRef, (uint)i) = TOperator.IsAssociatedAlpha + ? AssociatedAlphaPorterDuffFunctions.BlendWithCoverage(backgroundPixel, blended, clampedCoverage) + : PorterDuffFunctions.BlendWithCoverage(backgroundPixel, blended, clampedCoverage); + } + } + + /// + /// Applies the selected blend operator to vector rows without pixel storage conversion. + /// + /// The existing color and alpha composition operator. + /// The output vectors. Its length selects the number of samples. + /// The background vectors. The row must cover the output length. + /// The source vector used for every output sample. + /// The source opacity for each output sample, clamped to finite values in [0,1]. + /// The coverage row, clamped to finite values in [0,1]. + /// Inputs must use the alpha representation required by the operator. + public static void BlendWithCoverage( + Span destination, + ReadOnlySpan background, + Vector4 source, + ReadOnlySpan amount, + ReadOnlySpan coverage) + where TOperator : struct, IPixelBlenderOperator + { + // The invariant source is expanded once; only amount and coverage are gathered for each vector batch. + int scalarStart = 0; + + ref Vector4 destinationRef = ref MemoryMarshal.GetReference(destination); + ref Vector4 backgroundRef = ref MemoryMarshal.GetReference(background); + ref float amountRef = ref MemoryMarshal.GetReference(amount); + ref float coverageRef = ref MemoryMarshal.GetReference(coverage); + + if (Vector512.IsHardwareAccelerated && destination.Length >= 4) + { + ref Vector512 destinationBase = ref Unsafe.As>(ref destinationRef); + ref Vector512 backgroundBase = ref Unsafe.As>(ref backgroundRef); + int vectorCount = destination.Length / 4; + Vector512 sourceVector = CreateVector512(source); + + for (nuint i = 0; i < (uint)vectorCount; i++) + { + ref Vector512 backgroundVector = ref Unsafe.Add(ref backgroundBase, i); + ref float amountBase = ref Unsafe.Add(ref amountRef, i * 4); + ref float coverageBase = ref Unsafe.Add(ref coverageRef, i * 4); + Vector512 amountVector = CreateClampedVector512(ref amountBase); + Vector512 coverageVector = CreateClampedVector512(ref coverageBase); + Vector512 blended = TOperator.Invoke(backgroundVector, sourceVector, amountVector); + + Unsafe.Add(ref destinationBase, i) = TOperator.IsAssociatedAlpha + ? AssociatedAlphaPorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector) + : PorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector); + } + + scalarStart = vectorCount * 4; + } + else if (Vector256.IsHardwareAccelerated && destination.Length >= 2) + { + ref Vector256 destinationBase = ref Unsafe.As>(ref destinationRef); + ref Vector256 backgroundBase = ref Unsafe.As>(ref backgroundRef); + int vectorCount = destination.Length / 2; + Vector256 sourceVector = CreateVector256(source); + + for (nuint i = 0; i < (uint)vectorCount; i++) + { + ref Vector256 backgroundVector = ref Unsafe.Add(ref backgroundBase, i); + ref float amountBase = ref Unsafe.Add(ref amountRef, i * 2); + ref float coverageBase = ref Unsafe.Add(ref coverageRef, i * 2); + Vector256 amountVector = CreateClampedVector256(ref amountBase); + Vector256 coverageVector = CreateClampedVector256(ref coverageBase); + Vector256 blended = TOperator.Invoke(backgroundVector, sourceVector, amountVector); + + Unsafe.Add(ref destinationBase, i) = TOperator.IsAssociatedAlpha + ? AssociatedAlphaPorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector) + : PorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector); + } + + scalarStart = vectorCount * 2; + } + + for (int i = scalarStart; i < destination.Length; i++) + { + Vector4 backgroundPixel = Unsafe.Add(ref backgroundRef, (uint)i); + Vector4 blended = TOperator.Invoke(backgroundPixel, source, ColorNumerics.ClampToFinite(Unsafe.Add(ref amountRef, (uint)i), 0, 1F)); + + float clampedCoverage = ColorNumerics.ClampToFinite(Unsafe.Add(ref coverageRef, (uint)i), 0, 1F); + Unsafe.Add(ref destinationRef, (uint)i) = TOperator.IsAssociatedAlpha + ? AssociatedAlphaPorterDuffFunctions.BlendWithCoverage(backgroundPixel, blended, clampedCoverage) + : PorterDuffFunctions.BlendWithCoverage(backgroundPixel, blended, clampedCoverage); + } + } + + /// + /// Repeats one pixel vector twice to fill a 256-bit register. + /// + /// The pixel vector. + /// Two consecutive copies of the pixel. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 CreateVector256(Vector4 pixel) + => Vector256.Create(pixel.X, pixel.Y, pixel.Z, pixel.W, pixel.X, pixel.Y, pixel.Z, pixel.W); + + /// + /// Repeats one pixel vector four times to fill a 512-bit register. + /// + /// The pixel vector. + /// Four consecutive copies of the pixel. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 CreateVector512(Vector4 pixel) + => Vector512.Create( + pixel.X, + pixel.Y, + pixel.Z, + pixel.W, + pixel.X, + pixel.Y, + pixel.Z, + pixel.W, + pixel.X, + pixel.Y, + pixel.Z, + pixel.W, + pixel.X, + pixel.Y, + pixel.Z, + pixel.W); + + /// + /// Expands and clamps two per-pixel scalar values for two packed RGBA pixels. + /// + /// The first of two contiguous scalar values. + /// Two consecutive groups containing one clamped value in each group's four lanes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 CreateClampedVector256(ref float values) + { + Vector256 result = Vector256.Create(Vector128.Create(values), Vector128.Create(Unsafe.Add(ref values, 1))); + + // Use the scalar tail's rule: NaN becomes zero, and infinities become the corresponding opacity endpoints. + return ColorNumerics.ClampToFinite(result, Vector256.Zero, Vector256.One); + } + + /// + /// Expands and clamps four per-pixel scalar values for four packed RGBA pixels. + /// + /// The first of four contiguous scalar values. + /// Four consecutive groups containing one clamped value in each group's four lanes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 CreateClampedVector512(ref float values) + { + Vector512 result = Vector512.Create( + values, + values, + values, + values, + Unsafe.Add(ref values, 1), + Unsafe.Add(ref values, 1), + Unsafe.Add(ref values, 1), + Unsafe.Add(ref values, 1), + Unsafe.Add(ref values, 2), + Unsafe.Add(ref values, 2), + Unsafe.Add(ref values, 2), + Unsafe.Add(ref values, 2), + Unsafe.Add(ref values, 3), + Unsafe.Add(ref values, 3), + Unsafe.Add(ref values, 3), + Unsafe.Add(ref values, 3)); + + // Use the scalar tail's rule: NaN becomes zero, and infinities become the corresponding opacity endpoints. + return ColorNumerics.ClampToFinite(result, Vector512.Zero, Vector512.One); + } +} diff --git a/src/ImageSharp/PixelFormats/PixelBlenders/PixelBlender{TPixel,TOperator}.cs b/src/ImageSharp/PixelFormats/PixelBlenders/PixelBlender{TPixel,TOperator}.cs index 658f16ac33..ebb7108a8f 100644 --- a/src/ImageSharp/PixelFormats/PixelBlenders/PixelBlender{TPixel,TOperator}.cs +++ b/src/ImageSharp/PixelFormats/PixelBlenders/PixelBlender{TPixel,TOperator}.cs @@ -2,9 +2,6 @@ // Licensed under the Six Labors Split License. using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; namespace SixLabors.ImageSharp.PixelFormats.PixelBlenders; @@ -17,7 +14,7 @@ internal sealed class PixelBlender : PixelBlender where TPixel : unmanaged, IPixel where TOperator : struct, IPixelBlenderOperator { - // This type owns SIMD traversal and connects the operator to pixel conversion. The base class manages row buffers. + // This type connects pixel conversion to shared vector blending. The base class manages row buffers. // The operator owns composition arithmetic and declares its required alpha representation. // PixelOperations owns format-specific conversion and bulk optimizations. Pixel formats own rounding and storage layout. @@ -98,104 +95,11 @@ protected sealed override void FromBlendVector4( /// protected sealed override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - // Public entry points validate the row lengths, so all three references can advance in lockstep. - int scalarStart = 0; - amount = ColorNumerics.ClampToFinite(amount, 0, 1F); - - ref Vector4 destinationRef = ref MemoryMarshal.GetReference(destination); - ref Vector4 backgroundRef = ref MemoryMarshal.GetReference(background); - ref Vector4 sourceRef = ref MemoryMarshal.GetReference(source); - - if (Vector512.IsHardwareAccelerated && destination.Length >= 4) - { - // A 512-bit register holds four complete RGBA pixels in [R,G,B,A] groups. - ref Vector512 destinationBase = ref Unsafe.As>(ref destinationRef); - ref Vector512 backgroundBase = ref Unsafe.As>(ref backgroundRef); - ref Vector512 sourceBase = ref Unsafe.As>(ref sourceRef); - int vectorCount = destination.Length / 4; - Vector512 amountVector = Vector512.Create(amount); - - for (nuint i = 0; i < (uint)vectorCount; i++) - { - Unsafe.Add(ref destinationBase, i) = TOperator.Invoke(Unsafe.Add(ref backgroundBase, i), Unsafe.Add(ref sourceBase, i), amountVector); - } - - scalarStart = vectorCount * 4; - } - else if (Vector256.IsHardwareAccelerated && destination.Length >= 2) - { - // A 256-bit register holds two complete RGBA pixels in [R,G,B,A] groups. - ref Vector256 destinationBase = ref Unsafe.As>(ref destinationRef); - ref Vector256 backgroundBase = ref Unsafe.As>(ref backgroundRef); - ref Vector256 sourceBase = ref Unsafe.As>(ref sourceRef); - int vectorCount = destination.Length / 2; - Vector256 amountVector = Vector256.Create(amount); - - for (nuint i = 0; i < (uint)vectorCount; i++) - { - Unsafe.Add(ref destinationBase, i) = TOperator.Invoke(Unsafe.Add(ref backgroundBase, i), Unsafe.Add(ref sourceBase, i), amountVector); - } - - scalarStart = vectorCount * 2; - } - - // Vector4 is both the scalar pixel representation and the portable SIMD fallback. - for (int i = scalarStart; i < destination.Length; i++) - { - Unsafe.Add(ref destinationRef, (uint)i) = TOperator.Invoke(Unsafe.Add(ref backgroundRef, (uint)i), Unsafe.Add(ref sourceRef, (uint)i), amount); - } - } + => PixelBlenders.Blend(destination, background, source, amount); /// protected sealed override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) - { - // Public entry points validate the row lengths, so the destination and background advance together. - int scalarStart = 0; - amount = ColorNumerics.ClampToFinite(amount, 0, 1F); - - ref Vector4 destinationRef = ref MemoryMarshal.GetReference(destination); - ref Vector4 backgroundRef = ref MemoryMarshal.GetReference(background); - - if (Vector512.IsHardwareAccelerated && destination.Length >= 4) - { - // Repeat one [R,G,B,A] source group four times to match the four background pixels. - ref Vector512 destinationBase = ref Unsafe.As>(ref destinationRef); - ref Vector512 backgroundBase = ref Unsafe.As>(ref backgroundRef); - int vectorCount = destination.Length / 4; - Vector512 sourceVector = CreateVector512(source); - Vector512 amountVector = Vector512.Create(amount); - - for (nuint i = 0; i < (uint)vectorCount; i++) - { - Unsafe.Add(ref destinationBase, i) = TOperator.Invoke(Unsafe.Add(ref backgroundBase, i), sourceVector, amountVector); - } - - scalarStart = vectorCount * 4; - } - else if (Vector256.IsHardwareAccelerated && destination.Length >= 2) - { - // Repeat one [R,G,B,A] source group twice to match the two background pixels. - ref Vector256 destinationBase = ref Unsafe.As>(ref destinationRef); - ref Vector256 backgroundBase = ref Unsafe.As>(ref backgroundRef); - int vectorCount = destination.Length / 2; - Vector256 sourceVector = CreateVector256(source); - Vector256 amountVector = Vector256.Create(amount); - - for (nuint i = 0; i < (uint)vectorCount; i++) - { - Unsafe.Add(ref destinationBase, i) = TOperator.Invoke(Unsafe.Add(ref backgroundBase, i), sourceVector, amountVector); - } - - scalarStart = vectorCount * 2; - } - - // The remaining pixel count is at most three after AVX-512 or one after AVX2. - for (int i = scalarStart; i < destination.Length; i++) - { - Unsafe.Add(ref destinationRef, (uint)i) = TOperator.Invoke(Unsafe.Add(ref backgroundRef, (uint)i), source, amount); - } - } + => PixelBlenders.Blend(destination, background, source, amount); /// protected sealed override void BlendFunction( @@ -203,114 +107,11 @@ protected sealed override void BlendFunction( ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - // Each amount belongs to one pixel and must be repeated across that pixel's four components. - int scalarStart = 0; - - ref Vector4 destinationRef = ref MemoryMarshal.GetReference(destination); - ref Vector4 backgroundRef = ref MemoryMarshal.GetReference(background); - ref Vector4 sourceRef = ref MemoryMarshal.GetReference(source); - ref float amountRef = ref MemoryMarshal.GetReference(amount); - - if (Vector512.IsHardwareAccelerated && destination.Length >= 4) - { - ref Vector512 destinationBase = ref Unsafe.As>(ref destinationRef); - ref Vector512 backgroundBase = ref Unsafe.As>(ref backgroundRef); - ref Vector512 sourceBase = ref Unsafe.As>(ref sourceRef); - int vectorCount = destination.Length / 4; - - for (nuint i = 0; i < (uint)vectorCount; i++) - { - // Four scalar amounts become four contiguous [a,a,a,a] groups before clamping. - ref float amountBase = ref Unsafe.Add(ref amountRef, i * 4); - Vector512 amountVector = CreateClampedVector512(ref amountBase); - - Unsafe.Add(ref destinationBase, i) = TOperator.Invoke(Unsafe.Add(ref backgroundBase, i), Unsafe.Add(ref sourceBase, i), amountVector); - } - - scalarStart = vectorCount * 4; - } - else if (Vector256.IsHardwareAccelerated && destination.Length >= 2) - { - ref Vector256 destinationBase = ref Unsafe.As>(ref destinationRef); - ref Vector256 backgroundBase = ref Unsafe.As>(ref backgroundRef); - ref Vector256 sourceBase = ref Unsafe.As>(ref sourceRef); - int vectorCount = destination.Length / 2; - - for (nuint i = 0; i < (uint)vectorCount; i++) - { - // Two scalar amounts become two contiguous [a,a,a,a] groups before clamping. - ref float amountBase = ref Unsafe.Add(ref amountRef, i * 2); - Vector256 amountVector = CreateClampedVector256(ref amountBase); - - Unsafe.Add(ref destinationBase, i) = TOperator.Invoke(Unsafe.Add(ref backgroundBase, i), Unsafe.Add(ref sourceBase, i), amountVector); - } - - scalarStart = vectorCount * 2; - } - - for (int i = scalarStart; i < destination.Length; i++) - { - Unsafe.Add(ref destinationRef, (uint)i) = TOperator.Invoke( - Unsafe.Add(ref backgroundRef, (uint)i), - Unsafe.Add(ref sourceRef, (uint)i), - ColorNumerics.ClampToFinite(Unsafe.Add(ref amountRef, (uint)i), 0, 1F)); - } - } + => PixelBlenders.Blend(destination, background, source, amount); /// protected sealed override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) - { - // The source is invariant, while each background pixel has its own independently clamped amount. - int scalarStart = 0; - - ref Vector4 destinationRef = ref MemoryMarshal.GetReference(destination); - ref Vector4 backgroundRef = ref MemoryMarshal.GetReference(background); - ref float amountRef = ref MemoryMarshal.GetReference(amount); - - if (Vector512.IsHardwareAccelerated && destination.Length >= 4) - { - ref Vector512 destinationBase = ref Unsafe.As>(ref destinationRef); - ref Vector512 backgroundBase = ref Unsafe.As>(ref backgroundRef); - int vectorCount = destination.Length / 4; - Vector512 sourceVector = CreateVector512(source); - - for (nuint i = 0; i < (uint)vectorCount; i++) - { - ref float amountBase = ref Unsafe.Add(ref amountRef, i * 4); - Vector512 amountVector = CreateClampedVector512(ref amountBase); - - Unsafe.Add(ref destinationBase, i) = TOperator.Invoke(Unsafe.Add(ref backgroundBase, i), sourceVector, amountVector); - } - - scalarStart = vectorCount * 4; - } - else if (Vector256.IsHardwareAccelerated && destination.Length >= 2) - { - ref Vector256 destinationBase = ref Unsafe.As>(ref destinationRef); - ref Vector256 backgroundBase = ref Unsafe.As>(ref backgroundRef); - int vectorCount = destination.Length / 2; - Vector256 sourceVector = CreateVector256(source); - - for (nuint i = 0; i < (uint)vectorCount; i++) - { - ref float amountBase = ref Unsafe.Add(ref amountRef, i * 2); - Vector256 amountVector = CreateClampedVector256(ref amountBase); - - Unsafe.Add(ref destinationBase, i) = TOperator.Invoke(Unsafe.Add(ref backgroundBase, i), sourceVector, amountVector); - } - - scalarStart = vectorCount * 2; - } - - for (int i = scalarStart; i < destination.Length; i++) - { - Unsafe.Add(ref destinationRef, (uint)i) = TOperator.Invoke( - Unsafe.Add(ref backgroundRef, (uint)i), - source, - ColorNumerics.ClampToFinite(Unsafe.Add(ref amountRef, (uint)i), 0, 1F)); - } - } + => PixelBlenders.Blend(destination, background, source, amount); /// protected sealed override void BlendWithCoverageFunction( @@ -319,72 +120,7 @@ protected sealed override void BlendWithCoverageFunction( ReadOnlySpan source, float amount, ReadOnlySpan coverage) - { - // Coverage mixes the composed result back toward the original background, so it is fused into this pass. - int scalarStart = 0; - amount = ColorNumerics.ClampToFinite(amount, 0, 1F); - - ref Vector4 destinationRef = ref MemoryMarshal.GetReference(destination); - ref Vector4 backgroundRef = ref MemoryMarshal.GetReference(background); - ref Vector4 sourceRef = ref MemoryMarshal.GetReference(source); - ref float coverageRef = ref MemoryMarshal.GetReference(coverage); - - if (Vector512.IsHardwareAccelerated && destination.Length >= 4) - { - ref Vector512 destinationBase = ref Unsafe.As>(ref destinationRef); - ref Vector512 backgroundBase = ref Unsafe.As>(ref backgroundRef); - ref Vector512 sourceBase = ref Unsafe.As>(ref sourceRef); - int vectorCount = destination.Length / 4; - Vector512 amountVector = Vector512.Create(amount); - - for (nuint i = 0; i < (uint)vectorCount; i++) - { - ref Vector512 backgroundVector = ref Unsafe.Add(ref backgroundBase, i); - ref float coverageBase = ref Unsafe.Add(ref coverageRef, i * 4); - Vector512 coverageVector = CreateClampedVector512(ref coverageBase); - Vector512 blended = TOperator.Invoke(backgroundVector, Unsafe.Add(ref sourceBase, i), amountVector); - - Unsafe.Add(ref destinationBase, i) = TOperator.IsAssociatedAlpha - ? AssociatedAlphaPorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector) - : PorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector); - } - - scalarStart = vectorCount * 4; - } - else if (Vector256.IsHardwareAccelerated && destination.Length >= 2) - { - ref Vector256 destinationBase = ref Unsafe.As>(ref destinationRef); - ref Vector256 backgroundBase = ref Unsafe.As>(ref backgroundRef); - ref Vector256 sourceBase = ref Unsafe.As>(ref sourceRef); - int vectorCount = destination.Length / 2; - Vector256 amountVector = Vector256.Create(amount); - - for (nuint i = 0; i < (uint)vectorCount; i++) - { - ref Vector256 backgroundVector = ref Unsafe.Add(ref backgroundBase, i); - ref float coverageBase = ref Unsafe.Add(ref coverageRef, i * 2); - Vector256 coverageVector = CreateClampedVector256(ref coverageBase); - Vector256 blended = TOperator.Invoke(backgroundVector, Unsafe.Add(ref sourceBase, i), amountVector); - - Unsafe.Add(ref destinationBase, i) = TOperator.IsAssociatedAlpha - ? AssociatedAlphaPorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector) - : PorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector); - } - - scalarStart = vectorCount * 2; - } - - for (int i = scalarStart; i < destination.Length; i++) - { - Vector4 backgroundPixel = Unsafe.Add(ref backgroundRef, (uint)i); - Vector4 blended = TOperator.Invoke(backgroundPixel, Unsafe.Add(ref sourceRef, (uint)i), amount); - - float clampedCoverage = ColorNumerics.ClampToFinite(Unsafe.Add(ref coverageRef, (uint)i), 0, 1F); - Unsafe.Add(ref destinationRef, (uint)i) = TOperator.IsAssociatedAlpha - ? AssociatedAlphaPorterDuffFunctions.BlendWithCoverage(backgroundPixel, blended, clampedCoverage) - : PorterDuffFunctions.BlendWithCoverage(backgroundPixel, blended, clampedCoverage); - } - } + => PixelBlenders.BlendWithCoverage(destination, background, source, amount, coverage); /// protected sealed override void BlendWithCoverageFunction( @@ -393,71 +129,7 @@ protected sealed override void BlendWithCoverageFunction( Vector4 source, float amount, ReadOnlySpan coverage) - { - // The constant source is expanded once per selected width and reused for the complete row. - int scalarStart = 0; - amount = ColorNumerics.ClampToFinite(amount, 0, 1F); - - ref Vector4 destinationRef = ref MemoryMarshal.GetReference(destination); - ref Vector4 backgroundRef = ref MemoryMarshal.GetReference(background); - ref float coverageRef = ref MemoryMarshal.GetReference(coverage); - - if (Vector512.IsHardwareAccelerated && destination.Length >= 4) - { - ref Vector512 destinationBase = ref Unsafe.As>(ref destinationRef); - ref Vector512 backgroundBase = ref Unsafe.As>(ref backgroundRef); - int vectorCount = destination.Length / 4; - Vector512 sourceVector = CreateVector512(source); - Vector512 amountVector = Vector512.Create(amount); - - for (nuint i = 0; i < (uint)vectorCount; i++) - { - ref Vector512 backgroundVector = ref Unsafe.Add(ref backgroundBase, i); - ref float coverageBase = ref Unsafe.Add(ref coverageRef, i * 4); - Vector512 coverageVector = CreateClampedVector512(ref coverageBase); - Vector512 blended = TOperator.Invoke(backgroundVector, sourceVector, amountVector); - - Unsafe.Add(ref destinationBase, i) = TOperator.IsAssociatedAlpha - ? AssociatedAlphaPorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector) - : PorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector); - } - - scalarStart = vectorCount * 4; - } - else if (Vector256.IsHardwareAccelerated && destination.Length >= 2) - { - ref Vector256 destinationBase = ref Unsafe.As>(ref destinationRef); - ref Vector256 backgroundBase = ref Unsafe.As>(ref backgroundRef); - int vectorCount = destination.Length / 2; - Vector256 sourceVector = CreateVector256(source); - Vector256 amountVector = Vector256.Create(amount); - - for (nuint i = 0; i < (uint)vectorCount; i++) - { - ref Vector256 backgroundVector = ref Unsafe.Add(ref backgroundBase, i); - ref float coverageBase = ref Unsafe.Add(ref coverageRef, i * 2); - Vector256 coverageVector = CreateClampedVector256(ref coverageBase); - Vector256 blended = TOperator.Invoke(backgroundVector, sourceVector, amountVector); - - Unsafe.Add(ref destinationBase, i) = TOperator.IsAssociatedAlpha - ? AssociatedAlphaPorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector) - : PorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector); - } - - scalarStart = vectorCount * 2; - } - - for (int i = scalarStart; i < destination.Length; i++) - { - Vector4 backgroundPixel = Unsafe.Add(ref backgroundRef, (uint)i); - Vector4 blended = TOperator.Invoke(backgroundPixel, source, amount); - - float clampedCoverage = ColorNumerics.ClampToFinite(Unsafe.Add(ref coverageRef, (uint)i), 0, 1F); - Unsafe.Add(ref destinationRef, (uint)i) = TOperator.IsAssociatedAlpha - ? AssociatedAlphaPorterDuffFunctions.BlendWithCoverage(backgroundPixel, blended, clampedCoverage) - : PorterDuffFunctions.BlendWithCoverage(backgroundPixel, blended, clampedCoverage); - } - } + => PixelBlenders.BlendWithCoverage(destination, background, source, amount, coverage); /// protected sealed override void BlendWithCoverageFunction( @@ -466,78 +138,7 @@ protected sealed override void BlendWithCoverageFunction( ReadOnlySpan source, ReadOnlySpan amount, ReadOnlySpan coverage) - { - // Amount controls composition while coverage controls the final mix with the untouched background. - int scalarStart = 0; - - ref Vector4 destinationRef = ref MemoryMarshal.GetReference(destination); - ref Vector4 backgroundRef = ref MemoryMarshal.GetReference(background); - ref Vector4 sourceRef = ref MemoryMarshal.GetReference(source); - ref float amountRef = ref MemoryMarshal.GetReference(amount); - ref float coverageRef = ref MemoryMarshal.GetReference(coverage); - - if (Vector512.IsHardwareAccelerated && destination.Length >= 4) - { - ref Vector512 destinationBase = ref Unsafe.As>(ref destinationRef); - ref Vector512 backgroundBase = ref Unsafe.As>(ref backgroundRef); - ref Vector512 sourceBase = ref Unsafe.As>(ref sourceRef); - int vectorCount = destination.Length / 4; - - for (nuint i = 0; i < (uint)vectorCount; i++) - { - ref Vector512 backgroundVector = ref Unsafe.Add(ref backgroundBase, i); - ref float amountBase = ref Unsafe.Add(ref amountRef, i * 4); - ref float coverageBase = ref Unsafe.Add(ref coverageRef, i * 4); - Vector512 amountVector = CreateClampedVector512(ref amountBase); - Vector512 coverageVector = CreateClampedVector512(ref coverageBase); - Vector512 blended = TOperator.Invoke(backgroundVector, Unsafe.Add(ref sourceBase, i), amountVector); - - Unsafe.Add(ref destinationBase, i) = TOperator.IsAssociatedAlpha - ? AssociatedAlphaPorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector) - : PorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector); - } - - scalarStart = vectorCount * 4; - } - else if (Vector256.IsHardwareAccelerated && destination.Length >= 2) - { - ref Vector256 destinationBase = ref Unsafe.As>(ref destinationRef); - ref Vector256 backgroundBase = ref Unsafe.As>(ref backgroundRef); - ref Vector256 sourceBase = ref Unsafe.As>(ref sourceRef); - int vectorCount = destination.Length / 2; - - for (nuint i = 0; i < (uint)vectorCount; i++) - { - ref Vector256 backgroundVector = ref Unsafe.Add(ref backgroundBase, i); - ref float amountBase = ref Unsafe.Add(ref amountRef, i * 2); - ref float coverageBase = ref Unsafe.Add(ref coverageRef, i * 2); - Vector256 amountVector = CreateClampedVector256(ref amountBase); - Vector256 coverageVector = CreateClampedVector256(ref coverageBase); - Vector256 blended = TOperator.Invoke(backgroundVector, Unsafe.Add(ref sourceBase, i), amountVector); - - Unsafe.Add(ref destinationBase, i) = TOperator.IsAssociatedAlpha - ? AssociatedAlphaPorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector) - : PorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector); - } - - scalarStart = vectorCount * 2; - } - - for (int i = scalarStart; i < destination.Length; i++) - { - Vector4 backgroundPixel = Unsafe.Add(ref backgroundRef, (uint)i); - - Vector4 blended = TOperator.Invoke( - backgroundPixel, - Unsafe.Add(ref sourceRef, (uint)i), - ColorNumerics.ClampToFinite(Unsafe.Add(ref amountRef, (uint)i), 0, 1F)); - - float clampedCoverage = ColorNumerics.ClampToFinite(Unsafe.Add(ref coverageRef, (uint)i), 0, 1F); - Unsafe.Add(ref destinationRef, (uint)i) = TOperator.IsAssociatedAlpha - ? AssociatedAlphaPorterDuffFunctions.BlendWithCoverage(backgroundPixel, blended, clampedCoverage) - : PorterDuffFunctions.BlendWithCoverage(backgroundPixel, blended, clampedCoverage); - } - } + => PixelBlenders.BlendWithCoverage(destination, background, source, amount, coverage); /// protected sealed override void BlendWithCoverageFunction( @@ -546,149 +147,5 @@ protected sealed override void BlendWithCoverageFunction( Vector4 source, ReadOnlySpan amount, ReadOnlySpan coverage) - { - // The invariant source is expanded once; only amount and coverage are gathered for each vector batch. - int scalarStart = 0; - - ref Vector4 destinationRef = ref MemoryMarshal.GetReference(destination); - ref Vector4 backgroundRef = ref MemoryMarshal.GetReference(background); - ref float amountRef = ref MemoryMarshal.GetReference(amount); - ref float coverageRef = ref MemoryMarshal.GetReference(coverage); - - if (Vector512.IsHardwareAccelerated && destination.Length >= 4) - { - ref Vector512 destinationBase = ref Unsafe.As>(ref destinationRef); - ref Vector512 backgroundBase = ref Unsafe.As>(ref backgroundRef); - int vectorCount = destination.Length / 4; - Vector512 sourceVector = CreateVector512(source); - - for (nuint i = 0; i < (uint)vectorCount; i++) - { - ref Vector512 backgroundVector = ref Unsafe.Add(ref backgroundBase, i); - ref float amountBase = ref Unsafe.Add(ref amountRef, i * 4); - ref float coverageBase = ref Unsafe.Add(ref coverageRef, i * 4); - Vector512 amountVector = CreateClampedVector512(ref amountBase); - Vector512 coverageVector = CreateClampedVector512(ref coverageBase); - Vector512 blended = TOperator.Invoke(backgroundVector, sourceVector, amountVector); - - Unsafe.Add(ref destinationBase, i) = TOperator.IsAssociatedAlpha - ? AssociatedAlphaPorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector) - : PorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector); - } - - scalarStart = vectorCount * 4; - } - else if (Vector256.IsHardwareAccelerated && destination.Length >= 2) - { - ref Vector256 destinationBase = ref Unsafe.As>(ref destinationRef); - ref Vector256 backgroundBase = ref Unsafe.As>(ref backgroundRef); - int vectorCount = destination.Length / 2; - Vector256 sourceVector = CreateVector256(source); - - for (nuint i = 0; i < (uint)vectorCount; i++) - { - ref Vector256 backgroundVector = ref Unsafe.Add(ref backgroundBase, i); - ref float amountBase = ref Unsafe.Add(ref amountRef, i * 2); - ref float coverageBase = ref Unsafe.Add(ref coverageRef, i * 2); - Vector256 amountVector = CreateClampedVector256(ref amountBase); - Vector256 coverageVector = CreateClampedVector256(ref coverageBase); - Vector256 blended = TOperator.Invoke(backgroundVector, sourceVector, amountVector); - - Unsafe.Add(ref destinationBase, i) = TOperator.IsAssociatedAlpha - ? AssociatedAlphaPorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector) - : PorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector); - } - - scalarStart = vectorCount * 2; - } - - for (int i = scalarStart; i < destination.Length; i++) - { - Vector4 backgroundPixel = Unsafe.Add(ref backgroundRef, (uint)i); - Vector4 blended = TOperator.Invoke(backgroundPixel, source, ColorNumerics.ClampToFinite(Unsafe.Add(ref amountRef, (uint)i), 0, 1F)); - - float clampedCoverage = ColorNumerics.ClampToFinite(Unsafe.Add(ref coverageRef, (uint)i), 0, 1F); - Unsafe.Add(ref destinationRef, (uint)i) = TOperator.IsAssociatedAlpha - ? AssociatedAlphaPorterDuffFunctions.BlendWithCoverage(backgroundPixel, blended, clampedCoverage) - : PorterDuffFunctions.BlendWithCoverage(backgroundPixel, blended, clampedCoverage); - } - } - - /// - /// Repeats one pixel vector twice to fill a 256-bit register. - /// - /// The pixel vector. - /// Two consecutive copies of the pixel. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector256 CreateVector256(Vector4 pixel) - => Vector256.Create(pixel.X, pixel.Y, pixel.Z, pixel.W, pixel.X, pixel.Y, pixel.Z, pixel.W); - - /// - /// Repeats one pixel vector four times to fill a 512-bit register. - /// - /// The pixel vector. - /// Four consecutive copies of the pixel. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector512 CreateVector512(Vector4 pixel) - => Vector512.Create( - pixel.X, - pixel.Y, - pixel.Z, - pixel.W, - pixel.X, - pixel.Y, - pixel.Z, - pixel.W, - pixel.X, - pixel.Y, - pixel.Z, - pixel.W, - pixel.X, - pixel.Y, - pixel.Z, - pixel.W); - - /// - /// Expands and clamps two per-pixel scalar values for two packed RGBA pixels. - /// - /// The first of two contiguous scalar values. - /// Two consecutive groups containing one clamped value in each group's four lanes. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector256 CreateClampedVector256(ref float values) - { - Vector256 result = Vector256.Create(Vector128.Create(values), Vector128.Create(Unsafe.Add(ref values, 1))); - - // Use the scalar tail's rule: NaN becomes zero, and infinities become the corresponding opacity endpoints. - return ColorNumerics.ClampToFinite(result, Vector256.Zero, Vector256.One); - } - - /// - /// Expands and clamps four per-pixel scalar values for four packed RGBA pixels. - /// - /// The first of four contiguous scalar values. - /// Four consecutive groups containing one clamped value in each group's four lanes. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector512 CreateClampedVector512(ref float values) - { - Vector512 result = Vector512.Create( - values, - values, - values, - values, - Unsafe.Add(ref values, 1), - Unsafe.Add(ref values, 1), - Unsafe.Add(ref values, 1), - Unsafe.Add(ref values, 1), - Unsafe.Add(ref values, 2), - Unsafe.Add(ref values, 2), - Unsafe.Add(ref values, 2), - Unsafe.Add(ref values, 2), - Unsafe.Add(ref values, 3), - Unsafe.Add(ref values, 3), - Unsafe.Add(ref values, 3), - Unsafe.Add(ref values, 3)); - - // Use the scalar tail's rule: NaN becomes zero, and infinities become the corresponding opacity endpoints. - return ColorNumerics.ClampToFinite(result, Vector512.Zero, Vector512.One); - } + => PixelBlenders.BlendWithCoverage(destination, background, source, amount, coverage); } diff --git a/tests/ImageSharp.Tests/Common/SimdUtilsTests.FloatPlanes.cs b/tests/ImageSharp.Tests/Common/SimdUtilsTests.FloatPlanes.cs index ff34f9b3c0..4aaa77d75e 100644 --- a/tests/ImageSharp.Tests/Common/SimdUtilsTests.FloatPlanes.cs +++ b/tests/ImageSharp.Tests/Common/SimdUtilsTests.FloatPlanes.cs @@ -70,6 +70,24 @@ private static void AssertFloatPlanes(int count) } } + // A native extra channel occupies X and uses W for its selected weight. Missing Y/Z planes must supply zero + // without allocating dummy rows. Check packing before unpacking so inverse mistakes cannot cancel. + SimdUtils.InterleaveFloatPlanes(components[0], default, default, components[3], vectors); + for (int i = 0; i < count; i++) + { + Assert.Equal(BitConverter.SingleToInt32Bits(components[0][i]), BitConverter.SingleToInt32Bits(vectors[i].X)); + Assert.Equal(0, BitConverter.SingleToInt32Bits(vectors[i].Y)); + Assert.Equal(0, BitConverter.SingleToInt32Bits(vectors[i].Z)); + Assert.Equal(BitConverter.SingleToInt32Bits(components[3][i]), BitConverter.SingleToInt32Bits(vectors[i].W)); + } + + SimdUtils.DeinterleaveFloatPlanes(vectors, output[0], default, default, output[3]); + for (int i = 0; i < count; i++) + { + Assert.Equal(BitConverter.SingleToInt32Bits(components[0][i]), BitConverter.SingleToInt32Bits(output[0][i])); + Assert.Equal(BitConverter.SingleToInt32Bits(components[3][i]), BitConverter.SingleToInt32Bits(output[3][i])); + } + // An absent fourth plane must supply opaque values in both full registers and tails. SimdUtils.InterleaveFloatPlanes(components[0], components[1], components[2], ReadOnlySpan.Empty, vectors); for (int i = 0; i < count; i++) diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/AlphaBlendingTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/AlphaBlendingTests.cs index 57e3a8a629..827f134c5d 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/AlphaBlendingTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/AlphaBlendingTests.cs @@ -2,7 +2,10 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Blending; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Patch; namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; @@ -27,37 +30,13 @@ public void BlendingWithNonPremultiplied() float[] outB = [0]; float[] outAlpha = [0]; - JxlAlphaBlendingInputLayer inputBg = new() - { - R = [bgRgb.R], - G = [bgRgb.G], - B = [bgRgb.B], - A = [bgA], - }; + JxlBlendInput inputBg = new([bgRgb.R], [bgRgb.G], [bgRgb.B], [bgA]); - JxlAlphaBlendingInputLayer inputFg = new() - { - R = [fgRgb.R], - G = [fgRgb.G], - B = [fgRgb.B], - A = [fgA], - }; + JxlBlendInput inputFg = new([fgRgb.R], [fgRgb.G], [fgRgb.B], [fgA]); - JxlAlphaBlendingOutput output = new() - { - R = outR, - G = outG, - B = outB, - A = outAlpha, - }; + JxlBlendOutput output = new(outR, outG, outB, outAlpha); - JxlAlphaHelper.PerformAlphaBlending( - inputBg, - inputFg, - output, - 1, - alphaIsPremultiplied: false, - clamp: false); + JxlBlending.SourceOver(inputBg, inputFg, output, associated: false, clampWeight: false); Assert.Equal( Expected, @@ -66,21 +45,9 @@ public void BlendingWithNonPremultiplied() Assert.True(MathF.Abs(outAlpha[0] - (3174f / 4095)) <= 1e-5f); - inputFg = new() - { - R = [fgRgb.R], - G = [fgRgb.G], - B = [fgRgb.B], - A = [fgA2], - }; + inputFg = new([fgRgb.R], [fgRgb.G], [fgRgb.B], [fgA2]); - JxlAlphaHelper.PerformAlphaBlending( - inputBg, - inputFg, - output, - 1, - alphaIsPremultiplied: false, - clamp: true); + JxlBlending.SourceOver(inputBg, inputFg, output, associated: false, clampWeight: true); Assert.Equal( RgbToArray(fgRgb), @@ -105,37 +72,13 @@ public void BlendingWithPremultiplied() float[] outB = [0]; float[] outAlpha = [0]; - JxlAlphaBlendingInputLayer inputBg = new() - { - R = [bgRgb.R], - G = [bgRgb.G], - B = [bgRgb.B], - A = [bgA], - }; + JxlBlendInput inputBg = new([bgRgb.R], [bgRgb.G], [bgRgb.B], [bgA]); - JxlAlphaBlendingInputLayer inputFg = new() - { - R = [fgRgb.R], - G = [fgRgb.G], - B = [fgRgb.B], - A = [fgA], - }; + JxlBlendInput inputFg = new([fgRgb.R], [fgRgb.G], [fgRgb.B], [fgA]); - JxlAlphaBlendingOutput output = new() - { - R = outR, - G = outG, - B = outB, - A = outAlpha, - }; + JxlBlendOutput output = new(outR, outG, outB, outAlpha); - JxlAlphaHelper.PerformAlphaBlending( - inputBg, - inputFg, - output, - 1, - alphaIsPremultiplied: true, - clamp: false); + JxlBlending.SourceOver(inputBg, inputFg, output, associated: true, clampWeight: false); Assert.Equal( new float[] { 101.5f, 105.1f, 114.8f }, @@ -144,21 +87,9 @@ public void BlendingWithPremultiplied() Assert.True(MathF.Abs(outAlpha[0] - (3174f / 4095)) <= 1e-5f); - inputFg = new() - { - R = [fgRgb.R], - G = [fgRgb.G], - B = [fgRgb.B], - A = [fgA2], - }; + inputFg = new([fgRgb.R], [fgRgb.G], [fgRgb.B], [fgA2]); - JxlAlphaHelper.PerformAlphaBlending( - inputBg, - inputFg, - output, - 1, - alphaIsPremultiplied: true, - clamp: true); + JxlBlending.SourceOver(inputBg, inputFg, output, associated: true, clampWeight: true); Assert.Equal( RgbToArray(fgRgb), @@ -175,25 +106,74 @@ public void Mul() Span fg = [25]; Span output = [0]; - JxlAlphaHelper.PerformMultiplyBlending( + JxlBlending.Multiply( bg, fg, output, - 1, - clamp: false); + clampSource: false); Assert.True(MathF.Abs(output[0] - (fg[0] * bg[0])) <= 0.05f); - JxlAlphaHelper.PerformMultiplyBlending( + JxlBlending.Multiply( bg, fg, output, - 1, - clamp: true); + clampSource: true); Assert.True(MathF.Abs(output[0] - bg[0]) <= 0.05f); } + /// + /// Checks the native alpha floor, including NaN, through complete SIMD blocks and the scalar tail. + /// + [Fact] + public void AlphaRepresentation_PreservesColorsWithNativeFloor() + { + static void RunTest() + { + // The native floor is 2^-26. Its power-of-two factors make these expected products and reciprocals exact. + ReadOnlySpan pattern = [0F, float.NaN, 0.5F, 1F]; + ReadOnlySpan factors = [1F / (1 << 26), 1F / (1 << 26), 0.5F, 1F]; + float[] alpha = new float[37]; + float[] red = new float[alpha.Length]; + float[] green = new float[alpha.Length]; + float[] blue = new float[alpha.Length]; + + for (int i = 0; i < alpha.Length; i++) + { + alpha[i] = pattern[i % pattern.Length]; + } + + red.AsSpan().Fill(2F); + green.AsSpan().Fill(3F); + blue.AsSpan().Fill(4F); + JxlAlphaHelper.PremultiplyAlpha(red, green, blue, alpha, alpha.Length); + + for (int i = 0; i < alpha.Length; i++) + { + float factor = factors[i % factors.Length]; + Assert.Equal(2F * factor, red[i]); + Assert.Equal(3F * factor, green[i]); + Assert.Equal(4F * factor, blue[i]); + } + + JxlAlphaHelper.UnpremultiplyAlpha(red, green, blue, alpha, alpha.Length); + + for (int i = 0; i < alpha.Length; i++) + { + Assert.Equal(2F, red[i]); + Assert.Equal(3F, green[i]); + Assert.Equal(4F, blue[i]); + } + + Assert.True(float.IsNaN(alpha[1])); + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } + [Fact] public void PremultiplyAndUnpremultiply() { @@ -329,4 +309,216 @@ public void UnpremultiplyAndPremultiply() b, new ApproximateFloatComparer(1e-4f)); } + + /// + /// Checks all planar traversal widths and scalar tails against fixed source-over and weighted-add values. + /// + [Fact] + public void PlanarCompositionMatchesKnownValuesAcrossHardwareWidths() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + VerifyPlanarComposition, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + + /// + /// Checks known rational values, optional alpha clipping, alpha-only composition, and in-place channel writes. + /// + private static void VerifyPlanarComposition() + { + // Eight units of background at alpha 1/4 and two source units at alpha 1/2 produce 16/5 at alpha 5/8. + // Counts straddle every float SIMD width. Each mode also runs with all hardware intrinsics disabled. + foreach (int count in new[] { 0, 1, 3, 4, 5, 7, 8, 9, 15, 16, 17, 33 }) + { + float[] background = new float[count]; + float[] source = new float[count]; + float[] backgroundAlpha = new float[count]; + float[] sourceAlpha = new float[count]; + float[] destination = new float[count]; + Array.Fill(background, 8F); + Array.Fill(source, 2F); + Array.Fill(backgroundAlpha, .25F); + Array.Fill(sourceAlpha, .5F); + + JxlBlending.SourceOver(background, backgroundAlpha, source, sourceAlpha, destination, false, false); + Assert.All(destination, value => Assert.Equal(3.2F, value)); + + JxlBlending.SourceOver(background, backgroundAlpha, source, sourceAlpha, destination, true, false); + Assert.All(destination, value => Assert.Equal(6F, value)); + + JxlBlending.SourceOver(backgroundAlpha, backgroundAlpha, sourceAlpha, sourceAlpha, destination, false, false); + Assert.All(destination, value => Assert.Equal(.625F, value)); + + JxlBlending.AddWeighted(background, source, sourceAlpha, destination, false); + Assert.All(destination, value => Assert.Equal(9F, value)); + + // Adding an alpha channel weighted by itself must retain the background, including its first sample. + JxlBlending.AddWeighted(background, sourceAlpha, sourceAlpha, destination, true); + Assert.All(destination, value => Assert.Equal(8F, value)); + + Array.Fill(sourceAlpha, 2F); + JxlBlending.SourceOver(background, backgroundAlpha, source, sourceAlpha, destination, false, true); + Assert.All(destination, value => Assert.Equal(2F, value)); + + // With clipping disabled, alpha 2 is retained: the output is (4 - 2) / 1.75, not the clipped source value. + JxlBlending.SourceOver(background, backgroundAlpha, source, sourceAlpha, destination, false, false); + Assert.All(destination, value => Assert.Equal(8F / 7F, value)); + + // The encoded clamp uses the shared finite rules: NaN and negative infinity map to zero; positive infinity maps to one. + // Check all native operations so one mode cannot silently return to a different clamp implementation. + foreach (float weight in new[] { float.NaN, float.NegativeInfinity, float.PositiveInfinity }) + { + Array.Fill(sourceAlpha, weight); + float boundedWeight = float.IsPositiveInfinity(weight) ? 1F : 0F; + JxlBlending.SourceOver(background, backgroundAlpha, source, sourceAlpha, destination, false, true); + Assert.All(destination, value => Assert.Equal(boundedWeight == 1F ? 2F : 8F, value)); + + JxlBlending.AddWeighted(background, source, sourceAlpha, destination, true); + Assert.All(destination, value => Assert.Equal(8F + (2F * boundedWeight), value)); + + JxlBlending.Multiply(background, sourceAlpha, destination, true); + Assert.All(destination, value => Assert.Equal(8F * boundedWeight, value)); + } + + Array.Fill(sourceAlpha, 2F); + Array.Fill(backgroundAlpha, 2F); + float[] result0 = new float[count]; + float[] result1 = new float[count]; + float[] result2 = new float[count]; + float[] resultWeight = new float[count]; + JxlBlendInput bottom = new(background, background, background, backgroundAlpha); + JxlBlendInput top = new(source, source, source, sourceAlpha); + JxlBlendOutput result = new(result0, result1, result2, resultWeight); + + // Native weights 2 and 2 cancel to output weight zero despite a nonzero associated numerator. + // Reconstruction must use a zero reciprocal here. Valid pixel alpha remains bounded to [0,1]. + JxlBlending.SourceOver(bottom, top, result, false, false); + Assert.All(result0, value => Assert.Equal(0F, value)); + Assert.All(result1, value => Assert.Equal(0F, value)); + Assert.All(result2, value => Assert.Equal(0F, value)); + Assert.All(resultWeight, value => Assert.Equal(0F, value)); + + Array.Fill(backgroundAlpha, 3F); + JxlBlending.SourceOver(bottom, top, result, false, false); + Assert.All(result0, value => Assert.Equal(0F, value)); + Assert.All(resultWeight, value => Assert.Equal(-1F, value)); + + // Load-before-store permits exact in-place writes without a row copy. + JxlBlending.AddWeighted(background, source, sourceAlpha, background, false); + Assert.All(background, value => Assert.Equal(12F, value)); + + JxlBlending.Multiply(background, source, destination, true); + Assert.All(destination, value => Assert.Equal(12F, value)); + } + } + + /// + /// Checks row offsets, reversed blends, selected alpha, and extra-channel writes through the codec's blend entry point. + /// + [Fact] + public void RowBlendingPreservesOriginalAlphaAndAllChannels() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + VerifyRowBlending, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + + /// + /// Checks native blend-mode results with aliased background/output rows and two independently selected alpha channels. + /// + private static void VerifyRowBlending() + { + const int startX = 2; + const int count = 33; + const int rowLength = startX + count + 2; + List channels = + [ + new() { Type = JxlExtraChannel.Depth }, + new() { Type = JxlExtraChannel.Alpha }, + new() { Type = JxlExtraChannel.SpotColor }, + new() { Type = JxlExtraChannel.Alpha } + ]; + + JxlPatchBlending[] extraBlending = + [ + new() { Mode = JxlPatchBlendMode.Multiply }, + new() { Mode = JxlPatchBlendMode.None }, + new() { Mode = JxlPatchBlendMode.BlendBelow, AlphaChannel = 3 }, + new() { Mode = JxlPatchBlendMode.AlphaWeightedAddAbove, AlphaChannel = 3 } + ]; + + // These values come from the native blend equations, not from another call to the implementation under test. + (JxlPatchBlendMode Mode, float Red, float Green, float Blue, float Alpha)[] expected = + [ + (JxlPatchBlendMode.None, 8F, 16F, 24F, .25F), + (JxlPatchBlendMode.Replace, 2F, 4F, 6F, .25F), + (JxlPatchBlendMode.Add, 10F, 20F, 30F, .25F), + (JxlPatchBlendMode.Multiply, 16F, 64F, 144F, .25F), + (JxlPatchBlendMode.BlendAbove, 3.2F, 6.4F, 9.6F, .625F), + (JxlPatchBlendMode.BlendBelow, 4.4F, 8.8F, 13.2F, .625F), + (JxlPatchBlendMode.AlphaWeightedAddAbove, 9F, 18F, 27F, .25F), + (JxlPatchBlendMode.AlphaWeightedAddBelow, 4F, 8F, 12F, .25F) + ]; + + using JxlPlane scratch = new(Configuration.Default, count, 7); + + foreach (bool associated in new[] { false, true }) + { + channels[1].AlphaAssociated = associated; + + foreach (var result in expected) + { + float[] backgroundValues = [8F, 16F, 24F, 12F, .25F, 20F, .75F]; + float[] foregroundValues = [2F, 4F, 6F, .5F, .5F, 4F, .5F]; + Memory[] background = new Memory[7]; + Memory[] foreground = new Memory[7]; + + for (int channel = 0; channel < background.Length; channel++) + { + float[] backgroundRow = new float[rowLength]; + float[] foregroundRow = new float[count]; + Array.Fill(backgroundRow, backgroundValues[channel]); + Array.Fill(foregroundRow, foregroundValues[channel]); + background[channel] = backgroundRow; + foreground[channel] = foregroundRow; + } + + JxlBlending.PerformBlending( + background, + foreground, + background, + startX, + count, + new JxlPatchBlending { Mode = result.Mode, AlphaChannel = 1 }, + extraBlending, + channels, + scratch); + + float[] expectedChannels = [result.Red, result.Green, result.Blue, 6F, result.Alpha, 124F / 7F, .75F]; + + if (associated && result.Mode == JxlPatchBlendMode.BlendAbove) + { + // Associated source-over is source + background * (1 - sourceWeight). + expectedChannels[0] = 6F; + expectedChannels[1] = 12F; + expectedChannels[2] = 18F; + } + else if (associated && result.Mode == JxlPatchBlendMode.BlendBelow) + { + expectedChannels[0] = 9.5F; + expectedChannels[1] = 19F; + expectedChannels[2] = 28.5F; + } + + for (int channel = 0; channel < background.Length; channel++) + { + Span row = background[channel].Span; + + for (int x = 0; x < rowLength; x++) + { + float expectedValue = x >= startX && x < startX + count ? expectedChannels[channel] : backgroundValues[channel]; + + // The native equation multiplies by reciprocal alpha. Its rounding can differ from direct rational division by one float step. + Assert.True(MathF.Abs(expectedValue - row[x]) <= 1e-5F, $"Mode {result.Mode}, channel {channel}, column {x}."); + } + } + } + } + } } From 7e22125dfc04d48eedded9d106d489eeb329fb01 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 22:58:39 +1000 Subject: [PATCH 276/328] Use shared SIMD interleaving and correct JPEG XL integer sequences --- .../Common/Helpers/SimdUtils.ColorPacking.cs | 100 ++++++ ...JxlSimdUtils.StoreInterleaved.Generated.cs | 223 ------------- .../JxlSimdUtils.StoreInterleaved.tt | 45 --- .../Formats/Jxl/Processing/JxlSimdUtils.cs | 294 +++--------------- .../Modular/Transforms/JxlSqueeze.cs | 5 +- .../HorizontalChromaUpsamplingStage.cs | 2 +- src/ImageSharp/ImageSharp.csproj | 14 - .../Common/SimdUtilsTests.ColorPacking.cs | 93 ++++++ .../Formats/Jxl/Processing/SimdUtilsTests.cs | 89 +++--- 9 files changed, 284 insertions(+), 581 deletions(-) delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.Generated.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.tt diff --git a/src/ImageSharp/Common/Helpers/SimdUtils.ColorPacking.cs b/src/ImageSharp/Common/Helpers/SimdUtils.ColorPacking.cs index 980c2ef944..1aa1fd2504 100644 --- a/src/ImageSharp/Common/Helpers/SimdUtils.ColorPacking.cs +++ b/src/ImageSharp/Common/Helpers/SimdUtils.ColorPacking.cs @@ -12,6 +12,106 @@ namespace SixLabors.ImageSharp; internal static partial class SimdUtils { + /// + /// Interleaves four integer pairs without changing their stored bits. + /// + /// The first value of each pair. + /// The second value of each pair. + /// The first of eight destination integers. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Interleave2(Vector128 left, Vector128 right, ref int destination) + { + // Unpack covers a complete 128-bit register: the first store writes pairs 0/1, and the second writes pairs 2/3. + Vector128_.UnpackLow(left, right).StoreUnsafe(ref destination); + Vector128_.UnpackHigh(left, right).StoreUnsafe(ref destination, 4U); + } + + /// + /// Interleaves eight integer pairs without changing their stored bits. + /// + /// The first value of each pair. + /// The second value of each pair. + /// The first of sixteen destination integers. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Interleave2(Vector256 left, Vector256 right, ref int destination) + { + // Native unpack works within each 128-bit lane. Join matching low/high lanes so the stores contain pairs 0..3, then 4..7. + Vector256 low = Vector256_.UnpackLow(left, right); + Vector256 high = Vector256_.UnpackHigh(left, right); + Vector256.Create(low.GetLower(), high.GetLower()).StoreUnsafe(ref destination); + Vector256.Create(low.GetUpper(), high.GetUpper()).StoreUnsafe(ref destination, 8U); + } + + /// + /// Interleaves sixteen integer pairs without changing their stored bits. + /// + /// The first value of each pair. + /// The second value of each pair. + /// The first of thirty-two destination integers. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Interleave2(Vector512 left, Vector512 right, ref int destination) + { + // Indices 0..15 select left; 16..31 select right. Two complete-register permutations retain pair order across 128-bit lanes. + Vector512 lower = Vector512.Create(0, 16, 1, 17, 2, 18, 3, 19, 4, 20, 5, 21, 6, 22, 7, 23); + Vector512 upper = Vector512.Create(8, 24, 9, 25, 10, 26, 11, 27, 12, 28, 13, 29, 14, 30, 15, 31); + Vector512_.PermuteVar16x32x2(left, lower, right).StoreUnsafe(ref destination); + Vector512_.PermuteVar16x32x2(left, upper, right).StoreUnsafe(ref destination, 16U); + } + + /// + /// Interleaves four floating-point pairs without changing their stored bits. + /// + /// The first value of each pair. + /// The second value of each pair. + /// The first of eight destination floats. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Interleave2(Vector128 left, Vector128 right, ref float destination) + => Interleave2(left.AsInt32(), right.AsInt32(), ref Unsafe.As(ref destination)); + + /// + /// Interleaves eight floating-point pairs without changing their stored bits. + /// + /// The first value of each pair. + /// The second value of each pair. + /// The first of sixteen destination floats. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Interleave2(Vector256 left, Vector256 right, ref float destination) + => Interleave2(left.AsInt32(), right.AsInt32(), ref Unsafe.As(ref destination)); + + /// + /// Interleaves sixteen floating-point pairs without changing their stored bits. + /// + /// The first value of each pair. + /// The second value of each pair. + /// The first of thirty-two destination floats. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Interleave2(Vector512 left, Vector512 right, ref float destination) + => Interleave2(left.AsInt32(), right.AsInt32(), ref Unsafe.As(ref destination)); + + /// + /// Interleaves two floating-point registers into consecutive pairs. + /// + /// The first value of each pair. + /// The second value of each pair. + /// The first of twice destination floats. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Interleave2(Vector left, Vector right, ref float destination) + { + // Vector's register width is fixed by the runtime. These constant size checks leave only the matching store after compilation. + if (Vector.Count == Vector512.Count) + { + Interleave2(left.AsVector512(), right.AsVector512(), ref destination); + } + else if (Vector.Count == Vector256.Count) + { + Interleave2(left.AsVector256(), right.AsVector256(), ref destination); + } + else + { + Interleave2(left.AsVector128(), right.AsVector128(), ref destination); + } + } + /// /// Interleaves 32 values from four byte planes into consecutive quadruples. /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.Generated.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.Generated.cs deleted file mode 100644 index 227af2d8ec..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.Generated.cs +++ /dev/null @@ -1,223 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; - -internal static partial class JxlSimdUtils -{ - public static unsafe void StoreInterleaved(Vector v1, Vector v2, ref T memory) - where T : unmanaged - { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); - } - - public static unsafe void StoreInterleaved(Vector v1, Vector v2, Vector v3, ref T memory) - where T : unmanaged - { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); - } - - public static unsafe void StoreInterleaved(Vector v1, Vector v2, Vector v3, Vector v4, ref T memory) - where T : unmanaged - { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); - v4.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 3)); - } - - public static unsafe void StoreInterleaved(Vector v1, Vector v2, Vector v3, Vector v4, Vector v5, ref T memory) - where T : unmanaged - { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); - v4.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 3)); - v5.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 4)); - } - - public static unsafe void StoreInterleaved(Vector v1, Vector v2, Vector v3, Vector v4, Vector v5, Vector v6, ref T memory) - where T : unmanaged - { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); - v4.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 3)); - v5.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 4)); - v6.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 5)); - } - - public static unsafe void StoreInterleaved(Vector v1, Vector v2, Vector v3, Vector v4, Vector v5, Vector v6, Vector v7, ref T memory) - where T : unmanaged - { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); - v4.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 3)); - v5.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 4)); - v6.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 5)); - v7.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 6)); - } - - public static unsafe void StoreInterleaved(Vector v1, Vector v2, Vector v3, Vector v4, Vector v5, Vector v6, Vector v7, Vector v8, ref T memory) - where T : unmanaged - { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); - v4.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 3)); - v5.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 4)); - v6.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 5)); - v7.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 6)); - v8.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 7)); - } - - public static unsafe void StoreInterleaved(Vector128 v1, Vector128 v2, ref T memory) - where T : unmanaged - { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); - } - - public static unsafe void StoreInterleaved(Vector128 v1, Vector128 v2, Vector128 v3, ref T memory) - where T : unmanaged - { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); - } - - public static unsafe void StoreInterleaved(Vector128 v1, Vector128 v2, Vector128 v3, Vector128 v4, ref T memory) - where T : unmanaged - { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); - v4.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 3)); - } - - public static unsafe void StoreInterleaved(Vector128 v1, Vector128 v2, Vector128 v3, Vector128 v4, Vector128 v5, ref T memory) - where T : unmanaged - { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); - v4.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 3)); - v5.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 4)); - } - - public static unsafe void StoreInterleaved(Vector128 v1, Vector128 v2, Vector128 v3, Vector128 v4, Vector128 v5, Vector128 v6, ref T memory) - where T : unmanaged - { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); - v4.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 3)); - v5.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 4)); - v6.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 5)); - } - - public static unsafe void StoreInterleaved(Vector128 v1, Vector128 v2, Vector128 v3, Vector128 v4, Vector128 v5, Vector128 v6, Vector128 v7, ref T memory) - where T : unmanaged - { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); - v4.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 3)); - v5.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 4)); - v6.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 5)); - v7.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 6)); - } - - public static unsafe void StoreInterleaved(Vector128 v1, Vector128 v2, Vector128 v3, Vector128 v4, Vector128 v5, Vector128 v6, Vector128 v7, Vector128 v8, ref T memory) - where T : unmanaged - { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); - v4.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 3)); - v5.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 4)); - v6.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 5)); - v7.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 6)); - v8.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 7)); - } - - public static unsafe void StoreInterleaved(Vector256 v1, Vector256 v2, ref T memory) - where T : unmanaged - { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); - } - - public static unsafe void StoreInterleaved(Vector256 v1, Vector256 v2, Vector256 v3, ref T memory) - where T : unmanaged - { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); - } - - public static unsafe void StoreInterleaved(Vector256 v1, Vector256 v2, Vector256 v3, Vector256 v4, ref T memory) - where T : unmanaged - { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); - v4.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 3)); - } - - public static unsafe void StoreInterleaved(Vector256 v1, Vector256 v2, Vector256 v3, Vector256 v4, Vector256 v5, ref T memory) - where T : unmanaged - { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); - v4.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 3)); - v5.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 4)); - } - - public static unsafe void StoreInterleaved(Vector256 v1, Vector256 v2, Vector256 v3, Vector256 v4, Vector256 v5, Vector256 v6, ref T memory) - where T : unmanaged - { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); - v4.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 3)); - v5.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 4)); - v6.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 5)); - } - - public static unsafe void StoreInterleaved(Vector256 v1, Vector256 v2, Vector256 v3, Vector256 v4, Vector256 v5, Vector256 v6, Vector256 v7, ref T memory) - where T : unmanaged - { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); - v4.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 3)); - v5.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 4)); - v6.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 5)); - v7.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 6)); - } - - public static unsafe void StoreInterleaved(Vector256 v1, Vector256 v2, Vector256 v3, Vector256 v4, Vector256 v5, Vector256 v6, Vector256 v7, Vector256 v8, ref T memory) - where T : unmanaged - { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); - v4.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 3)); - v5.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 4)); - v6.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 5)); - v7.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 6)); - v8.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 7)); - } - -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.tt b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.tt deleted file mode 100644 index b97deb0086..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.tt +++ /dev/null @@ -1,45 +0,0 @@ -<#@ template debug="false" hostspecific="false" language="C#" #> -<#@ assembly name="System.Core" #> -<#@ import namespace="System.Linq" #> -<#@ import namespace="System.Text" #> -<#@ import namespace="System.Collections.Generic" #> -<#@ output extension=".Generated.cs" #> -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; - -internal static partial class JxlSimdUtils -{ -<# - string[] vectorTypes = [ - "Vector", - "Vector128", - "Vector256" - ]; - - const int maxVectorSize = 8; - - foreach (string vect in vectorTypes) { - for (int i = 2; i <= maxVectorSize; i++) { - List vectorParameters = []; - for (int j = 0; j < i; j++) { - vectorParameters.Add($"{vect} v{j + 1}"); - } - string inlineParameters = string.Join(", ", vectorParameters) + ", "; -#> - public static unsafe void StoreInterleaved(<#= inlineParameters #>ref T memory) - where T : unmanaged - { -<# for (int j = 0; j < i; j++) { #> - v<#= j + 1 #>.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * <#= j #>)); -<# } #> - } - -<# } } #> -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs index 931d717add..d242431c13 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs @@ -108,249 +108,80 @@ public static void Transpose8x8Block(Span fromSpan, Span toSpan, int s } /// - /// - /// Fills the span so its first value is equal to - /// and subsequent values increment by one. For example, with start=5, - /// the span's values will be: - /// - /// { start, start+1, start+2, start+3, start+4, ... to the end of the span } - /// - /// - /// - /// or, more precisely: - /// - /// { 5, 6, 7, 8, 9, 10, 11, ... to the end of the span } - /// - /// - /// + /// Fills consecutive integers, starting with the supplied value. /// - /// The span where the values are filled. - /// Initial value. - public static void Iota(Span span, int start) + /// The integer component type. + /// The destination. An empty span is unchanged. + /// The first value. Arithmetic wraps within the component type. + public static void Iota(Span span, T start) + where T : unmanaged, IBinaryInteger { - ref int spanRef = ref MemoryMarshal.GetReference(span); + ref T destination = ref MemoryMarshal.GetReference(span); + int i = 0; + T value = start; - // Using fixed-size vectors so we can construct an - // incrementMask more easily. - if (Vector512.IsHardwareAccelerated) + // Runtime indices supply 0..N-1. Every subsequent block adds N to every lane, not a different increment to each lane. + // Integer vector addition and the scalar tail both wrap within T. A complete store never touches the tail or an empty span. + if (Vector512.IsHardwareAccelerated && Vector512.IsSupported && span.Length >= Vector512.Count) { - Vector512 incrementMask = Vector512.Create(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16); - Vector512 v = Vector512.Create(start) + (incrementMask - Vector512.One); + int count = Vector512.Count; + nuint vectorCount = span.Length.Vector512Count(); + Vector512 current = Vector512.Create(value) + Vector512.Indices; + Vector512 increment = Vector512.Create(T.CreateTruncating(count)); - if ((span.Length % Vector512.Count) == 0) + for (nuint block = 0; block < vectorCount; block++, i += count) { - // Aligned length - for (int i = 0; i < span.Length; i += Vector512.Count) - { - v.StoreUnsafe(ref Unsafe.Add(ref spanRef, i)); - v += incrementMask; - } + current.StoreUnsafe(ref destination, (uint)i); + current += increment; } - else - { - // We will need a scalar remainder - int vectorLength = span.Length - (span.Length % Vector512.Count); - int i; - for (i = 0; i < vectorLength; i += Vector512.Count) - { - v.StoreUnsafe(ref Unsafe.Add(ref spanRef, i)); - v += incrementMask; - } - - int val = v.ToScalar(); - for (; i < span.Length; i++) - { - Unsafe.Add(ref spanRef, i) = val++; - } - } + value = current.ToScalar(); } - else if (Vector256.IsHardwareAccelerated) + else if (Vector256.IsHardwareAccelerated && Vector256.IsSupported && span.Length >= Vector256.Count) { - Vector256 incrementMask = Vector256.Create(1, 2, 3, 4, 5, 6, 7, 8); - Vector256 v = Vector256.Create(start) + (incrementMask - Vector256.One); + int count = Vector256.Count; + nuint vectorCount = span.Length.Vector256Count(); + Vector256 current = Vector256.Create(value) + Vector256.Indices; + Vector256 increment = Vector256.Create(T.CreateTruncating(count)); - if ((span.Length % Vector256.Count) == 0) + for (nuint block = 0; block < vectorCount; block++, i += count) { - // Aligned length - for (int i = 0; i < span.Length; i += Vector256.Count) - { - v.StoreUnsafe(ref Unsafe.Add(ref spanRef, i)); - v += incrementMask; - } + current.StoreUnsafe(ref destination, (uint)i); + current += increment; } - else - { - // We will need a scalar remainder - int vectorLength = span.Length - (span.Length % Vector256.Count); - - int i; - for (i = 0; i < vectorLength; i += Vector256.Count) - { - v.StoreUnsafe(ref Unsafe.Add(ref spanRef, i)); - v += incrementMask; - } - int val = v.ToScalar(); - for (; i < span.Length; i++) - { - Unsafe.Add(ref spanRef, i) = val++; - } - } + value = current.ToScalar(); } - else if (Vector128.IsHardwareAccelerated) + else if (Vector128.IsHardwareAccelerated && Vector128.IsSupported && span.Length >= Vector128.Count) { - Vector128 incrementMask = Vector128.Create(1, 2, 3, 4); - Vector128 v = Vector128.Create(start) + (incrementMask - Vector128.One); + int count = Vector128.Count; + nuint vectorCount = span.Length.Vector128Count(); + Vector128 current = Vector128.Create(value) + Vector128.Indices; + Vector128 increment = Vector128.Create(T.CreateTruncating(count)); - if ((span.Length % Vector128.Count) == 0) + for (nuint block = 0; block < vectorCount; block++, i += count) { - // Aligned length - for (int i = 0; i < span.Length; i += Vector128.Count) - { - v.StoreUnsafe(ref Unsafe.Add(ref spanRef, i)); - v += incrementMask; - } + current.StoreUnsafe(ref destination, (uint)i); + current += increment; } - else - { - // We will need a scalar remainder - int vectorLength = span.Length - (span.Length % Vector128.Count); - - int i; - for (i = 0; i < vectorLength; i += Vector128.Count) - { - v.StoreUnsafe(ref Unsafe.Add(ref spanRef, i)); - v += incrementMask; - } - int val = v.ToScalar(); - for (; i < span.Length; i++) - { - Unsafe.Add(ref spanRef, i) = val++; - } - } + value = current.ToScalar(); } - else if (Vector64.IsHardwareAccelerated) - { - Vector64 incrementMask = Vector64.Create(1, 2); - Vector64 v = Vector64.Create(start) + (incrementMask - Vector64.One); - if ((span.Length % Vector64.Count) == 0) - { - // Aligned length - for (int i = 0; i < span.Length; i += Vector64.Count) - { - v.StoreUnsafe(ref Unsafe.Add(ref spanRef, i)); - v += incrementMask; - } - } - else - { - // We will need a scalar remainder - int vectorLength = span.Length - (span.Length % Vector64.Count); - - int i; - for (i = 0; i < vectorLength; i += Vector64.Count) - { - v.StoreUnsafe(ref Unsafe.Add(ref spanRef, i)); - v += incrementMask; - } - - int val = v.ToScalar(); - for (; i < span.Length; i++) - { - Unsafe.Add(ref spanRef, i) = val++; - } - } - } - else + for (; i < span.Length; i++) { - // No SIMD - int value = start; - spanRef = value; - value++; - for (int i = 1; i < span.Length; i++) - { - Unsafe.Add(ref spanRef, i) = value++; - } + Unsafe.Add(ref destination, i) = value; + value = unchecked(value + T.One); } } /// - /// - /// Fills the span so its first value is equal to - /// and subsequent values increment by one. For example, with start=5, - /// the span's values will be: - /// - /// { start, start+1, start+2, start+3, start+4, ... to the end of the span } - /// - /// - /// - /// or, more precisely: - /// - /// { 5, 6, 7, 8, 9, 10, 11, ... to the end of the span } - /// - /// - /// + /// Creates consecutive unsigned integers, starting with the supplied value. /// - /// The span where the values are filled. - /// Initial value. - public static void Iota(Span span, T start) - where T : unmanaged, INumber - { - // Slightly slower than the int variant - ref T spanRef = ref MemoryMarshal.GetReference(span); - - if (Vector.IsSupported && Vector.IsHardwareAccelerated) - { - Vector incrementMask = IotaMask.IncrementMask; - Vector v = Vector.Create(start) + (incrementMask - Vector.One); - - if ((span.Length % Vector.Count) == 0) - { - // Aligned length - for (int i = 0; i < span.Length; i += Vector.Count) - { - v.StoreUnsafe(ref Unsafe.Add(ref spanRef, i)); - v += incrementMask; - } - } - else - { - // Remainder needed - int vectorLength = span.Length - (span.Length % Vector.Count); - - int i; - for (i = 0; i < vectorLength; i += Vector.Count) - { - v.StoreUnsafe(ref Unsafe.Add(ref spanRef, i)); - v += incrementMask; - } - - T val = v.ToScalar(); - for (; i < span.Length; i++) - { - Unsafe.Add(ref spanRef, i) = val++; - } - } - } - else - { - // Scalar (slow) - T value = start; - spanRef = value; - value++; - for (int i = 1; i < span.Length; i++) - { - Unsafe.Add(ref spanRef, i) = value++; - } - } - } - - public static Vector Iota(T start) - where T : unmanaged, INumber - => IotaMask.BaseMask + Vector.Create(start); + /// The first value. Arithmetic wraps within UInt32. + /// One register containing start, start + 1, and the following integers. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector Iota(uint start) => Vector.Indices + Vector.Create(start); /// /// Vectorized floating-point error function (precise approximate). @@ -548,37 +379,4 @@ public static Vector LoadDuplicate128(Vector128 vec) return new Vector(value); } } - - /// - /// Incrementing values to compute the Iota function. - /// - /// - /// Creating a Vector<T> incrementing values would be - /// slow as Vector<T> is not a fixed-size vector, leaving - /// no other option but a slow loop. This class caches these - /// vectors for significantly better performance, though still - /// not as fast as an int variant. - /// - /// Type of the vector. - private static class IotaMask - where T : unmanaged, INumber - { - public static readonly Vector IncrementMask; - public static readonly Vector BaseMask; - - static IotaMask() - { - Span values = stackalloc T[Vector.Count]; - Span baseValues = stackalloc T[Vector.Count]; - - for (int i = 0; i < Vector.Count; i++) - { - values[i] = T.CreateSaturating(i + 1); - baseValues[i] = T.CreateSaturating(i); - } - - IncrementMask = Vector.Create(values); - BaseMask = Vector.Create(baseValues); - } - } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs index 87135e7533..8bf1ee97b9 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs @@ -310,10 +310,7 @@ public static void InverseHorizontalSqueeze(Configuration configuration, JxlModu Vector256 even = Vector256.LoadUnsafe(ref Unsafe.Add(ref bpOutEvenTRef, offset)); Vector256 odd = Vector256.LoadUnsafe(ref Unsafe.Add(ref bpOutOddTRef, offset)); - JxlSimdUtils.StoreInterleaved( - even, - odd, - ref Unsafe.Add(ref pOutRef, ((x + i) << 1) + (onerow_out * y))); + SimdUtils.Interleave2(even, odd, ref Unsafe.Add(ref pOutRef, ((x + i) << 1) + (onerow_out * y))); } } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/HorizontalChromaUpsamplingStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/HorizontalChromaUpsamplingStage.cs index 7a4796370d..595be096a1 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/HorizontalChromaUpsamplingStage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/HorizontalChromaUpsamplingStage.cs @@ -43,7 +43,7 @@ public override void ProcessRow(Buffer2D> inputRows, Buffer2D left = (oneFour * prev) + current; Vector right = (oneFour * next) + current; - JxlSimdUtils.StoreInterleaved(left, right, ref Unsafe.Add(ref rRowOut, x * 2)); + SimdUtils.Interleave2(left, right, ref Unsafe.Add(ref rRowOut, x * 2)); } } diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index 60232f43cb..adb03fedfa 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -44,11 +44,6 @@ - - True - True - JxlSimdUtils.StoreInterleaved.tt - @@ -72,11 +67,6 @@ True InlineArray.tt - - True - True - JxlSimdUtils.StoreInterleaved.tt - True True @@ -207,10 +197,6 @@ TextTemplatingFileGenerator InlineArray.cs - - TextTemplatingFileGenerator - JxlSimdUtils.StoreInterleaved.Generated.cs - ImageMetadataExtensions.cs TextTemplatingFileGenerator diff --git a/tests/ImageSharp.Tests/Common/SimdUtilsTests.ColorPacking.cs b/tests/ImageSharp.Tests/Common/SimdUtilsTests.ColorPacking.cs index 4cd6245c40..718dbea6fa 100644 --- a/tests/ImageSharp.Tests/Common/SimdUtilsTests.ColorPacking.cs +++ b/tests/ImageSharp.Tests/Common/SimdUtilsTests.ColorPacking.cs @@ -14,6 +14,99 @@ namespace SixLabors.ImageSharp.Tests.Common; /// public partial class SimdUtilsTests { + /// + /// Checks two-stream alternation, complete-register boundaries and exact floating-point bits with each hardware configuration. + /// + [Fact] + public void Interleave2_PreservesPairsAndDestinationGuards() => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateInterleave2, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + + /// + /// Checks every stored lane against independent alternating inputs for all fixed register widths and the runtime-sized register. + /// + private static void ValidateInterleave2() + { + int[] left = new int[Vector512.Count]; + int[] right = new int[Vector512.Count]; + for (int i = 0; i < left.Length; i++) + { + left[i] = int.MinValue + (i * 12345); + right[i] = int.MaxValue - (i * 54321); + } + + // Treat these words as both integers and floats. Interleaving must preserve NaN payloads, infinities and negative zero. + left[0] = unchecked((int)0x7FC01234); + left[1] = unchecked((int)0x80000000); + right[0] = 0x7F800000; + right[1] = unchecked((int)0xFF800000); + ref int leftRef = ref MemoryMarshal.GetArrayDataReference(left); + ref int rightRef = ref MemoryMarshal.GetArrayDataReference(right); + + foreach (int count in new[] { Vector128.Count, Vector256.Count, Vector512.Count }) + { + int[] integers = new int[(2 * count) + 2]; + float[] floats = new float[integers.Length]; + integers.AsSpan().Fill(123); + MemoryMarshal.Cast(floats).Fill(123); + ref int intDestination = ref integers[1]; + ref float floatDestination = ref floats[1]; + + // Call each fixed-width overload even without acceleration. Its shared helper must retain a correct portable implementation. + if (count == Vector512.Count) + { + Vector512 a = Vector512.LoadUnsafe(ref leftRef); + Vector512 b = Vector512.LoadUnsafe(ref rightRef); + SimdUtils.Interleave2(a, b, ref intDestination); + SimdUtils.Interleave2(a.AsSingle(), b.AsSingle(), ref floatDestination); + } + else if (count == Vector256.Count) + { + Vector256 a = Vector256.LoadUnsafe(ref leftRef); + Vector256 b = Vector256.LoadUnsafe(ref rightRef); + SimdUtils.Interleave2(a, b, ref intDestination); + SimdUtils.Interleave2(a.AsSingle(), b.AsSingle(), ref floatDestination); + } + else + { + Vector128 a = Vector128.LoadUnsafe(ref leftRef); + Vector128 b = Vector128.LoadUnsafe(ref rightRef); + SimdUtils.Interleave2(a, b, ref intDestination); + SimdUtils.Interleave2(a.AsSingle(), b.AsSingle(), ref floatDestination); + } + + ReadOnlySpan floatBits = MemoryMarshal.Cast(floats); + for (int i = 0; i < count; i++) + { + Assert.Equal(left[i], integers[1 + (2 * i)]); + Assert.Equal(right[i], integers[2 + (2 * i)]); + Assert.Equal(left[i], floatBits[1 + (2 * i)]); + Assert.Equal(right[i], floatBits[2 + (2 * i)]); + } + + Assert.Equal(123, integers[0]); + Assert.Equal(123, integers[^1]); + Assert.Equal(123, floatBits[0]); + Assert.Equal(123, floatBits[^1]); + } + + float[] runtime = new float[(2 * Vector.Count) + 2]; + MemoryMarshal.Cast(runtime).Fill(123); + Vector runtimeLeft = Vector.AsVectorSingle(new Vector(left)); + Vector runtimeRight = Vector.AsVectorSingle(new Vector(right)); + SimdUtils.Interleave2(runtimeLeft, runtimeRight, ref runtime[1]); + + ReadOnlySpan runtimeBits = MemoryMarshal.Cast(runtime); + for (int i = 0; i < Vector.Count; i++) + { + Assert.Equal(left[i], runtimeBits[1 + (2 * i)]); + Assert.Equal(right[i], runtimeBits[2 + (2 * i)]); + } + + Assert.Equal(123, runtimeBits[0]); + Assert.Equal(123, runtimeBits[^1]); + } + private static readonly int[] Lengths = [0, 1, 2, 3, 4, 5, 7, 8, 15, 16, 17, 31, 32, 33, 129]; /// diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/SimdUtilsTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/SimdUtilsTests.cs index 1000c4ac2d..8087ded830 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/SimdUtilsTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/SimdUtilsTests.cs @@ -1,68 +1,65 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Buffers; using System.Numerics; -using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.Processing; +using SixLabors.ImageSharp.Tests.TestUtilities; namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; +/// +/// Tests JPEG XL integer sequence generation. +/// public class SimdUtilsTests { + /// + /// Checks consecutive values, integer wraparound, scalar tails and destination guards across all supported SIMD widths. + /// [Fact] - public void TestInterleave2() - { - Span mem = [0.0f, 0.0f]; - - Vector vec1 = JxlSimdUtils.Iota(0f); - Vector vec2 = JxlSimdUtils.Iota(128.0f); - - JxlSimdUtils.StoreInterleaved(vec1, vec2, ref MemoryMarshal.GetReference(mem)); - - for (int j = 0; j < 2; j++) - { - Assert.Equal(mem[j], j * 128.0f); - } - } + public void Iota_FillsOnlyTheDestination() => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateIota, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); - [Fact] - public void TestInterleave4() + /// + /// Checks empty, complete-register and partial-register spans for each production integer type. + /// + private static void ValidateIota() { - Span mem = [0.0f, 0.0f, 0.0f, 0.0f]; - - Vector vec1 = JxlSimdUtils.Iota(0f); - Vector vec2 = JxlSimdUtils.Iota(128.0f); - Vector vec3 = JxlSimdUtils.Iota(256.0f); - Vector vec4 = JxlSimdUtils.Iota(384.0f); - - JxlSimdUtils.StoreInterleaved(vec1, vec2, vec3, vec4, ref MemoryMarshal.GetReference(mem)); - - for (int j = 0; j < 4; j++) + ReadOnlySpan lengths = [0, 1, 3, 4, 7, 8, 15, 16, 17, 31, 32, 33, 63, 64, 65, 129]; + foreach (int length in lengths) { - Assert.Equal(mem[j], j * 128.0f); - } - } + byte[] bytes = new byte[length + 2]; + int[] integers = new int[length + 2]; + uint[] unsigned = new uint[length + 2]; + bytes.AsSpan().Fill(123); + integers.AsSpan().Fill(123); + unsigned.AsSpan().Fill(123); - [Fact] - public void TestInterleave8() - { - Span mem = [0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f]; + JxlSimdUtils.Iota(bytes.AsSpan(1, length), (byte)250); + JxlSimdUtils.Iota(integers.AsSpan(1, length), int.MaxValue - 9); + JxlSimdUtils.Iota(unsigned.AsSpan(1, length), uint.MaxValue - 9); - Vector vec1 = JxlSimdUtils.Iota(0f); - Vector vec2 = JxlSimdUtils.Iota(1 * 128.0f); - Vector vec3 = JxlSimdUtils.Iota(2 * 128.0f); - Vector vec4 = JxlSimdUtils.Iota(3 * 128.0f); - Vector vec5 = JxlSimdUtils.Iota(4 * 128.0f); - Vector vec6 = JxlSimdUtils.Iota(5 * 128.0f); - Vector vec7 = JxlSimdUtils.Iota(6 * 128.0f); - Vector vec8 = JxlSimdUtils.Iota(7 * 128.0f); + // Check every value against scalar integer arithmetic. Guard values also detect writes to an empty destination. + for (int i = 0; i < length; i++) + { + Assert.Equal(unchecked((byte)(250 + i)), bytes[i + 1]); + Assert.Equal(unchecked(int.MaxValue - 9 + i), integers[i + 1]); + Assert.Equal(unchecked(uint.MaxValue - 9 + (uint)i), unsigned[i + 1]); + } - JxlSimdUtils.StoreInterleaved(vec1, vec2, vec3, vec4, vec5, vec6, vec7, vec8, ref MemoryMarshal.GetReference(mem)); + Assert.Equal((byte)123, bytes[0]); + Assert.Equal((byte)123, bytes[^1]); + Assert.Equal(123, integers[0]); + Assert.Equal(123, integers[^1]); + Assert.Equal(123U, unsigned[0]); + Assert.Equal(123U, unsigned[^1]); + } - for (int j = 0; j < 8; j++) + // The modular encoder uses the register-returning form. Check its full register, including unsigned wraparound. + Vector vector = JxlSimdUtils.Iota(uint.MaxValue - 3); + for (int i = 0; i < Vector.Count; i++) { - Assert.Equal(mem[j], j * 128.0f); + Assert.Equal(unchecked(uint.MaxValue - 3 + (uint)i), vector[i]); } } } From 6a6f1cb10690c17c9dff766628ab7273d4f7a8c4 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 22:58:59 +1000 Subject: [PATCH 277/328] Port JPEG XL palette search to reusable owned sample storage --- .../Modular/Transforms/JxlPalette.cs | 1052 ++++++++++------- 1 file changed, 632 insertions(+), 420 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs index dc99a3a13b..6bd1ea5d2a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs @@ -1,11 +1,14 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.Common.Helpers; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; @@ -14,14 +17,18 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; /// internal static class JxlPalette { + /// + /// Compares stored palette arrays and borrowed color records with the same value-based contract. + /// + private static readonly PaletteColorComparer ColorKeyComparer = new(); + /// /// Represents maximum number of colors in a palette. /// private const int MaxPaletteLookupTableSize = 1 << 16; /// - /// Represents number of channels for RGB. This is 3 because RGB - /// has three channels: R (Red), G (Green), B (Blue). + /// The first three components defined by implicit palette colors and retained RGB delta statistics. /// private const int RgbChannels = 3; @@ -36,9 +43,9 @@ internal static class JxlPalette private const int SmallCube = 4; /// - /// Number of bits required to represent a small cube. + /// The wire-index bit count for one component of the four-position implicit cube. /// - private const int SmallCubeBits = 2; // 2 bits gives us 0..3 inclusive, so it's perfect to represent a small cube + private const int SmallCubeBits = 2; /// /// Cube of SmallCube @@ -47,77 +54,75 @@ internal static class JxlPalette private const int ImplicitPaletteSize = LargeCubeOffset + (LargeCube * LargeCube * LargeCube); - private const bool EncodeToHighQualityImplicitPalette = true; - /// - /// Minimum index required to use the implicit palette. + /// The number of fixed RGB delta entries defined by the format. /// - private const int MinimumImplicitPaletteIndex = -((2 * 72) - 1); + private const int DeltaPaletteEntries = 72; /// - /// Backing array used to construct data for the matrix. + /// The first index in one complete signed implicit-delta period. /// - private static readonly int[,] DefaultOffsetsData = - { - { 1, 2 }, - { 0, 3 }, - { 0, 4 }, - { 1, 1 }, - { 1, 3 }, - { 2, 2 }, - { 1, 0 }, - { 1, 4 }, - { 2, 1 }, - { 2, 3 }, - { 2, 0 }, - { 2, 4 } - }; + private const int MinimumImplicitPaletteIndex = -((2 * DeltaPaletteEntries) - 1); /// - /// Used by the palette encoder. + /// Gets native diffusion offsets as adjacent [error row, padded column offset] pairs. /// - private static readonly DenseMatrix DefaultOffsets = new(DefaultOffsetsData); + private static ReadOnlySpan DefaultOffsets => + [ + 1, 2, 0, 3, 0, 4, 1, 1, 1, 3, 2, 2, 1, 0, 1, 4, 2, 1, 2, 3, 2, 0, 2, 4 + ]; /// - /// Static delta palette used by . + /// Static delta palette used by . /// - private static readonly int[][] DeltaPalette = + private static ReadOnlySpan DeltaPalette => [ - [0, 0, 0], [4, 4, 4], [11, 0, 0], - [0, 0, -13], [0, -12, 0], [-10, -10, -10], - [-18, -18, -18], [-27, -27, -27], [-18, -18, 0], - [0, 0, -32], [-32, 0, 0], [-37, -37, -37], - [0, -32, -32], [24, 24, 45], [50, 50, 50], - [-45, -24, -24], [-24, -45, -45], [0, -24, -24], - [-34, -34, 0], [-24, 0, -24], [-45, -45, -24], - [64, 64, 64], [-32, 0, -32], [0, -32, 0], - [-32, 0, 32], [-24, -45, -24], [45, 24, 45], - [24, -24, -45], [-45, -24, 24], [80, 80, 80], - [64, 0, 0], [0, 0, -64], [0, -64, -64], - [-24, -24, 45], [96, 96, 96], [64, 64, 0], - [45, -24, -24], [34, -34, 0], [112, 112, 112], - [24, -45, -45], [45, 45, -24], [0, -32, 32], - [24, -24, 45], [0, 96, 96], [45, -24, 24], - [24, -45, -24], [-24, -45, 24], [0, -64, 0], - [96, 0, 0], [128, 128, 128], [64, 0, 64], - [144, 144, 144], [96, 96, 0], [-36, -36, 36], - [45, -24, -45], [45, -45, -24], [0, 0, -96], - [0, 128, 128], [0, 96, 0], [45, 24, -45], - [-128, 0, 0], [24, -45, 24], [-45, 24, -45], - [64, 0, -64], [64, -64, -64], [96, 0, 96], - [45, -45, 24], [24, 45, -45], [64, 64, -64], - [128, 128, 0], [0, 0, -128], [-24, 45, -45] + 0, 0, 0, 4, 4, 4, 11, 0, 0, + 0, 0, -13, 0, -12, 0, -10, -10, -10, + -18, -18, -18, -27, -27, -27, -18, -18, 0, + 0, 0, -32, -32, 0, 0, -37, -37, -37, + 0, -32, -32, 24, 24, 45, 50, 50, 50, + -45, -24, -24, -24, -45, -45, 0, -24, -24, + -34, -34, 0, -24, 0, -24, -45, -45, -24, + 64, 64, 64, -32, 0, -32, 0, -32, 0, + -32, 0, 32, -24, -45, -24, 45, 24, 45, + 24, -24, -45, -45, -24, 24, 80, 80, 80, + 64, 0, 0, 0, 0, -64, 0, -64, -64, + -24, -24, 45, 96, 96, 96, 64, 64, 0, + 45, -24, -24, 34, -34, 0, 112, 112, 112, + 24, -45, -45, 45, 45, -24, 0, -32, 32, + 24, -24, 45, 0, 96, 96, 45, -24, 24, + 24, -45, -24, -24, -45, 24, 0, -64, 0, + 96, 0, 0, 128, 128, 128, 64, 0, 64, + 144, 144, 144, 96, 96, 0, -36, -36, 36, + 45, -24, -45, 45, -45, -24, 0, 0, -96, + 0, 128, 128, 0, 96, 0, 45, 24, -45, + -128, 0, 0, 24, -45, 24, -45, 24, -45, + 64, 0, -64, 64, -64, -64, 96, 0, 96, + 45, -45, 24, 24, 45, -45, 64, 64, -64, + 128, 128, 0, 0, 0, -128, -24, 45, -45 ]; + /// + /// Scales one implicit cube component with the format's fixed denominator of four. + /// + /// The component position within the cube. + /// The source sample precision. + /// The integer component. Widened multiplication preserves the full supported integer range. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int Scale(int denominator, int value, int bitDepth) - { - DebugGuard.IsTrue(denominator == 4, "Denominator is defined as 4"); - - return (value * ((1 << bitDepth) - 1)) >> 2; - } + private static int Scale(int value, int bitDepth) => (int)(((long)value * ((1L << bitDepth) - 1)) >> 2); - public static int GetPaletteValue(Span palette, int index, int c, int oneRow, int bitDepth) + /// + /// Resolves an explicit palette entry or a format-defined implicit color or delta. + /// + /// The complete borrowed palette storage, including row padding. + /// The explicit, implicit or negative delta index. + /// The component index. + /// The logical number of explicit entries, excluding row padding and other component rows. + /// The physical stride between component rows. + /// The sample precision used by implicit entries. + /// The selected component. Callers add prediction when this entry is a predictive delta. + public static int GetPaletteValue(ReadOnlySpan palette, int index, int c, int paletteSize, int oneRow, int bitDepth) { if (index < 0) { @@ -127,18 +132,11 @@ public static int GetPaletteValue(Span palette, int index, int c, int oneRo } index = -(index + 1); - index %= 1 + (2 * (DeltaPalette.Length - 1)); - - // JPEG XL reference uses: - // static constexpr int kMultiplier[] = {-1, 1}; - // kMultiplier[index & 1] - // - // we use: - // ((index & 1) != 0 ? 1 : -1) - // - // The latter avoids multiplication and can make use - // of CPU registers instead of memory access (if the JIT allows it). - int result = DeltaPalette[(index + 1) >> 1][c] * ((index & 1) != 0 ? 1 : -1); + index %= 1 + (2 * (DeltaPaletteEntries - 1)); + + // Entry zero has one index. Other entries have opposite-sign pairs, selected by index parity. + int entry = (index + 1) >> 1; + int result = DeltaPalette[(entry * RgbChannels) + c] * ((index & 1) != 0 ? 1 : -1); if (bitDepth > 8) { @@ -147,24 +145,26 @@ public static int GetPaletteValue(Span palette, int index, int c, int oneRo return result; } - else if (palette.Length <= index && index < palette.Length + LargeCubeOffset) + else if (paletteSize <= index && index < paletteSize + LargeCubeOffset) { if (c >= RgbChannels) { return 0; } - index -= palette.Length; + index -= paletteSize; index >>= c * SmallCubeBits; - return Scale(SmallCube, index % SmallCube, bitDepth) + (1 << Math.Max(0, bitDepth - 3)); + return Scale(index % SmallCube, bitDepth) + (1 << Math.Max(0, bitDepth - 3)); } - else if (palette.Length + LargeCubeOffset <= index) + else if (paletteSize + LargeCubeOffset <= index) { if (c >= RgbChannels) { return 0; } + index -= paletteSize + LargeCubeOffset; + switch (c) { case 1: @@ -179,7 +179,7 @@ public static int GetPaletteValue(Span palette, int index, int c, int oneRo break; } - return Scale(LargeCube - 1, index % LargeCube, bitDepth); + return Scale(index % LargeCube, bitDepth); } return palette[(c * oneRow) + index]; @@ -304,7 +304,7 @@ public static void InversePalette( { int index = Math.Clamp(p[x], 0, palette.Width - 1); - p[x] = GetPaletteValue(paletteData, index, 0, oneRow, bitDepth); + p[x] = GetPaletteValue(paletteData, index, 0, palette.Width, oneRow, bitDepth); } } } @@ -325,8 +325,8 @@ public static void InversePalette( int index0 = Math.Clamp(p0[x], 0, palette.Width - 1); int index1 = Math.Clamp(p1[x], 0, palette.Width - 1); - p0[x] = GetPaletteValue(paletteData, index0, 0, oneRow, bitDepth); - p1[x] = GetPaletteValue(paletteData, index1, 0, oneRow, bitDepth); + p0[x] = GetPaletteValue(paletteData, index0, 0, palette.Width, oneRow, bitDepth); + p1[x] = GetPaletteValue(paletteData, index1, 0, palette.Width, oneRow, bitDepth); } } } @@ -351,9 +351,9 @@ public static void InversePalette( int index1 = Math.Clamp(p1[x], 0, palette.Width - 1); int index2 = Math.Clamp(p2[x], 0, palette.Width - 1); - p0[x] = GetPaletteValue(paletteData, index0, 0, oneRow, bitDepth); - p1[x] = GetPaletteValue(paletteData, index1, 0, oneRow, bitDepth); - p2[x] = GetPaletteValue(paletteData, index2, 0, oneRow, bitDepth); + p0[x] = GetPaletteValue(paletteData, index0, 0, palette.Width, oneRow, bitDepth); + p1[x] = GetPaletteValue(paletteData, index1, 0, palette.Width, oneRow, bitDepth); + p2[x] = GetPaletteValue(paletteData, index2, 0, palette.Width, oneRow, bitDepth); } } } @@ -382,10 +382,10 @@ public static void InversePalette( int index2 = Math.Clamp(p2[x], 0, palette.Width - 1); int index3 = Math.Clamp(p3[x], 0, palette.Width - 1); - p0[x] = GetPaletteValue(paletteData, index0, 0, oneRow, bitDepth); - p1[x] = GetPaletteValue(paletteData, index1, 0, oneRow, bitDepth); - p2[x] = GetPaletteValue(paletteData, index2, 0, oneRow, bitDepth); - p3[x] = GetPaletteValue(paletteData, index3, 0, oneRow, bitDepth); + p0[x] = GetPaletteValue(paletteData, index0, 0, palette.Width, oneRow, bitDepth); + p1[x] = GetPaletteValue(paletteData, index1, 0, palette.Width, oneRow, bitDepth); + p2[x] = GetPaletteValue(paletteData, index2, 0, palette.Width, oneRow, bitDepth); + p3[x] = GetPaletteValue(paletteData, index3, 0, palette.Width, oneRow, bitDepth); } } } @@ -418,11 +418,11 @@ public static void InversePalette( int index3 = Math.Clamp(p3[x], 0, palette.Width - 1); int index4 = Math.Clamp(p4[x], 0, palette.Width - 1); - p0[x] = GetPaletteValue(paletteData, index0, 0, oneRow, bitDepth); - p1[x] = GetPaletteValue(paletteData, index1, 0, oneRow, bitDepth); - p2[x] = GetPaletteValue(paletteData, index2, 0, oneRow, bitDepth); - p3[x] = GetPaletteValue(paletteData, index3, 0, oneRow, bitDepth); - p4[x] = GetPaletteValue(paletteData, index4, 0, oneRow, bitDepth); + p0[x] = GetPaletteValue(paletteData, index0, 0, palette.Width, oneRow, bitDepth); + p1[x] = GetPaletteValue(paletteData, index1, 0, palette.Width, oneRow, bitDepth); + p2[x] = GetPaletteValue(paletteData, index2, 0, palette.Width, oneRow, bitDepth); + p3[x] = GetPaletteValue(paletteData, index3, 0, palette.Width, oneRow, bitDepth); + p4[x] = GetPaletteValue(paletteData, index4, 0, palette.Width, oneRow, bitDepth); } } } @@ -464,7 +464,7 @@ public static void InversePalette( { int index = idx[x]; int value = 0; - int paletteEntry = GetPaletteValue(paletteData, index, c, oneRow, bitDepth); + int paletteEntry = GetPaletteValue(paletteData, index, c, palette.Width, oneRow, bitDepth); JxlPredictionResult pred = JxlContextPrediction.PredictNoTreeWeightedPrediction( outputChannel.Width, ref p[x], @@ -508,7 +508,7 @@ public static void InversePalette( { int index = idx[x]; int value = 0; - int paletteEntry = GetPaletteValue(paletteData, index, c, oneRow, bitDepth); + int paletteEntry = GetPaletteValue(paletteData, index, c, palette.Width, oneRow, bitDepth); if (index < nbDeltas) { @@ -559,28 +559,20 @@ public static void InversePalette( input.Channels.RemoveAt(0); } - private static float ColorDistance(Span a, Span b) - { - InlineArray3 array = default; - array[0] = b[0]; - array[1] = b[1]; - array[2] = b[2]; - return ColorDistance(a, array); - } - - private static float ColorDistance(Span a, InlineArray3 b) + /// + /// Calculates the native palette-search distance for matching component vectors, including extra channels. + /// + /// The source components with diffusion error applied. + /// The candidate reconstructed components. + /// The weighted component and luminance error used to rank palette candidates. + private static float ColorDistance(ReadOnlySpan a, ReadOnlySpan b) { - if (a.Length != 3) - { - throw new InvalidOperationException("Length mismatch"); - } - float distance = 0; float ave3 = 0; - if (a.Length >= 3) + if (a.Length >= RgbChannels) { - ave3 = ((a[0] + b[0]) + (a[1] + b[1]) + (a[2] + b[2])) * (1.21f / 3.0f); + ave3 = (a[0] + b[0] + a[1] + b[1] + a[2] + b[2]) * (1.21F / 3F); } float sumA = 0; @@ -589,34 +581,40 @@ private static float ColorDistance(Span a, InlineArray3 b) for (int c = 0; c < a.Length; c++) { float diff = a[c] - b[c]; - float weight = c == 0 ? 3f : c == 1 ? 5f : 2f; + float weight = c == 0 ? 3F : c == 1 ? 5F : 2F; - if (c < 3 && (a[c] + b[c] >= ave3)) + if (c < RgbChannels && a[c] + b[c] >= ave3) { - weight += c == 2 ? 1.12f : 1.15f; + weight += c == 2 ? 1.12F : 1.15F; - if (c == 2 && ((a[2] + b[2]) < 1.22f * ave3)) + if (c == 2 && a[2] + b[2] < 1.22D * ave3) { - weight -= 0.5f; + weight -= 0.5F; } } distance += diff * diff * weight * weight; int sumWeight = c == 0 ? 3 : c == 1 ? 5 : 1; - sumA += a[c] * sumWeight; sumB += b[c] * sumWeight; } distance *= 4; float sumDiff = sumA - sumB; - distance += sumDiff * sumDiff; - return distance; + return distance + (sumDiff * sumDiff); } + /// + /// Selects the nearest implicit cube entry using the native integer quantization formula. + /// + /// The bounded source components. + /// The number of explicit entries preceding the two implicit cubes. + /// The source precision used to quantize cube positions. + /// Whether to select the larger five-position cube instead of the offset four-position cube. + /// The implicit index after the explicit palette entries. private static int QuantizeColorToImplicitPaletteIndex(Span color, int paletteSize, int bitDepth, bool highQuality) { - int index = 1; + int index = 0; int quant = (1 << bitDepth) - 1; int half = bitDepth > 1 ? (1 << (bitDepth - 1)) : 0; @@ -632,7 +630,7 @@ private static int QuantizeColorToImplicitPaletteIndex(Span color, int pale multiplier *= LargeCube; } - return index + (paletteSize * LargeCubeOffset); + return index + paletteSize + LargeCubeOffset; } else { @@ -656,17 +654,19 @@ private static int QuantizeColorToImplicitPaletteIndex(Span color, int pale } } + /// + /// Rounds an integer residual to the nearest bucket with symmetric handling of negative residuals. + /// + /// The signed residual. + /// The positive bucket size. + /// The signed nearest bucket index. Midpoints round away from zero. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int RoundInteger(int value, int div) { - if (value < 0) - { - return (-value + (div / 2)) / div; - } - else - { - return (value + (div / 2)) / div; - } + // Widen before negation or midpoint addition. Restore the sign before narrowing so Int32.MinValue remains representable. + long magnitude = value < 0 ? -(long)value : value; + long rounded = (magnitude + (div / 2)) / div; + return (int)(value < 0 ? -rounded : rounded); } /// @@ -694,11 +694,18 @@ public static void ForwardPalette( ref JxlPredictor predictor, JxlModularHeader wpHeader) { - PaletteIterationData paletteIterationData = new(); + // Only the lossy preprocessing pass retains per-pixel delta statistics. The final pass reuses the same tracked buffers. + // Statistics measure RGB residuals; one- and two-component palettes search directly without an RGB preprocessing pass. + bool preprocess = lossy && input.BitDepth >= 8 && endC - beginC + 1 >= RgbChannels; + int pixelCapacity = preprocess + ? checked(input.Channels[beginC].Width * input.Channels[beginC].Height) + : 0; + + using PaletteIterationData paletteIterationData = new(configuration, pixelCapacity); int originalNumberOfColors = numberOfColors; int originalNumberOfDeltas = numberOfDeltas; - if (lossy && input.BitDepth >= 8) + if (preprocess) { ForwardPaletteIteration( configuration, @@ -714,7 +721,7 @@ public static void ForwardPalette( paletteIterationData); } - paletteIterationData.IsFinalRun = false; + paletteIterationData.IsFinalRun = true; ForwardPaletteIteration( configuration, input, @@ -760,11 +767,6 @@ private static void ForwardPaletteIteration( DebugGuard.MustBeGreaterThanOrEqualTo(beginC, input.MetaChannels, nameof(beginC)); int nb = endC - beginC + 1; // inclusive number of channels - // TODO: if this assert below triggers, increase nb to 6 and update - // the stack allocation for the 'tmp' variable below - // so the size is big enough. - DebugGuard.MustBeLessThanOrEqualTo(nb, 5, nameof(nb)); - JxlModularChannel beginCChannel = input.Channels[beginC]; int w = beginCChannel.Width; int h = beginCChannel.Height; @@ -781,7 +783,7 @@ private static void ForwardPaletteIteration( if (!lossy && numberOfColors < 2) { - throw new InvalidOperationException("Lossless palette transform needs at least 3 channels"); + throw new InvalidOperationException("A lossless palette needs at least two colors."); } int idx = 0; @@ -794,11 +796,11 @@ private static void ForwardPaletteIteration( } JxlImageOperations.ImageMinMax(beginCChannel.Plane, out int minValue, out int maxValue); - int lookupTableSize = maxValue - minValue + 1; + long lookupTableSize = (long)maxValue - minValue + 1; - if (lookupTableSize < MaxPaletteLookupTableSize) + if (lookupTableSize > MaxPaletteLookupTableSize) { - HashSet chPalette = []; + SortedSet chPalette = []; for (int y = 0; y < h; y++) { @@ -854,7 +856,8 @@ private static void ForwardPaletteIteration( return; } - Span lookup = stackalloc int[lookupTableSize]; + using IMemoryOwner lookupOwner = configuration.MemoryAllocator.Allocate((int)lookupTableSize, AllocationOptions.Clean); + Span lookup = lookupOwner.GetSpan()[..(int)lookupTableSize]; idx = 0; for (int y = 0; y < h; y++) @@ -907,78 +910,84 @@ private static void ForwardPaletteIteration( return; } - // Only the lossy path needs quantized sample channels. Both paths use the real image dimensions and one scoped owner. - using JxlModularImage quantizedInput = JxlModularImage.Create(configuration, w, h, input.BitDepth, lossy ? nb : 0); + // Native palette search retains a reconstructed copy. One contiguous allocation covers every selected channel and four candidate vectors. + // A current pixel still contains its original sample until the winning candidate is stored. Earlier pixels contain reconstructed history. + int channelLength = checked(w * h); + int quantizedLength = lossy ? checked(nb * channelLength) : 0; + using IMemoryOwner? quantizedOwner = lossy + ? configuration.MemoryAllocator.Allocate(checked(quantizedLength + (4 * nb))) + : null; + + Span quantizedStorage = quantizedOwner is null ? [] : quantizedOwner.GetSpan()[..(quantizedLength + (4 * nb))]; + Span quantizedSamples = lossy ? quantizedStorage[..quantizedLength] : []; + Span candidateStorage = lossy ? quantizedStorage[quantizedLength..] : []; + + // Lossless search needs complete color records, not reconstructed history. Reuse one interleaved row across both scans. + using IMemoryOwner? colorRowOwner = lossy ? null : configuration.MemoryAllocator.Allocate(checked(nb * w)); + Span colorRow = colorRowOwner is null ? [] : colorRowOwner.GetSpan()[..(nb * w)]; if (lossy) { for (int c = 0; c < nb; c++) { - if (!JxlImageOperations.CopyImage(input.Channels[beginC + c].Plane, quantizedInput.Channels[c].Plane)) + JxlModularChannel sourceChannel = input.Channels[beginC + c]; + Span original = sourceChannel.GetRow(0); + int originalStride = sourceChannel.Plane.PixelsPerRow; + Span reconstructed = quantizedSamples.Slice(c * channelLength, channelLength); + + for (int y = 0; y < h; y++) { - throw new InvalidOperationException("Copying failed"); + original.Slice(y * originalStride, w).CopyTo(reconstructed.Slice(y * w, w)); } } } + Span deltaSamples = paletteIterationData.DeltaSamples; + Span deltaDistances = paletteIterationData.DeltaDistances; + Span frequentDeltas = paletteIterationData.FrequentDeltas; numberOfDeltas = 0; bool deltaUsed = false; - List candidatePalette = []; + HashSet candidatePalette = new(ColorKeyComparer); + HashSet.AlternateLookup> candidateLookup = candidatePalette.GetAlternateLookup>(); List candidatePaletteImageOrder = []; - Dictionary inversePalette = []; + Dictionary inversePalette = new(ColorKeyComparer); + Dictionary.AlternateLookup> inverseLookup = inversePalette.GetAlternateLookup>(); - // Don't use stackalloc for color so we can store it as a - // dictionary member in colorFrequencyMap (see below) + // This mutable array supplies lookup scratch. Stored collection keys are cloned only when a new color is inserted. int[] color = new int[nb]; Span colorSpan = color.AsSpan(); - Span colorWithError = stackalloc float[nb]; if (lossy) { paletteIterationData.FindFrequentColorDeltas(w * h, input.BitDepth); - numberOfDeltas = paletteIterationData.FrequentDeltas[0].Count; - Dictionary colorFrequencyMap = []; - - DenseMatrix offsets = new(4, 2); + numberOfDeltas = paletteIterationData.NumberOfFrequentDeltas; + SortedDictionary colorFrequencyMap = new(ColorKeyComparer); + // All cross and frequency scans finish before reconstructed samples replace originals in the workspace. for (int y = 1; y + 1 < h; y++) { for (int x = 1; x + 1 < w; x++) { + bool makesCross = true; for (int c = 0; c < nb; c++) { - colorSpan[c] = input.Channels[beginC + c].GetRow(y)[x]; + int position = (c * channelLength) + (y * w) + x; + int value = quantizedSamples[position]; + color[c] = value; + makesCross &= quantizedSamples[position - 1] == value && quantizedSamples[position + 1] == value && + quantizedSamples[position - w] == value && quantizedSamples[position + w] == value; } - // Defaults - offsets[0, 0] = 1; - offsets[0, 0] = 0; - offsets[1, 0] = -1; - offsets[1, 1] = 0; - offsets[2, 0] = 0; - offsets[2, 1] = 1; - offsets[3, 0] = 0; - offsets[3, 1] = -1; - - bool makesCross = true; - - for (int i = 0; i < 4 && makesCross; ++i) + if (makesCross) { - int dx = offsets[i, 0]; - int dy = offsets[i, 1]; - - for (int c = 0; c < nb && makesCross; c++) + if (colorFrequencyMap.TryGetValue(color, out int frequency)) { - if (input.Channels[beginC + c].GetRow(y + dy)[x + dx] != colorSpan[c]) - { - makesCross = false; - } + colorFrequencyMap[color] = frequency + 1; + } + else + { + colorFrequencyMap.Add(colorSpan.ToArray(), 1); } - } - - if (makesCross) - { - colorFrequencyMap[color]++; } } } @@ -990,30 +999,36 @@ private static void ForwardPaletteIteration( { if (colorFreq.Value > colorFrequencyLowerBound) { - candidatePalette.Insert(0, colorFreq.Key); + candidatePalette.Add(colorFreq.Key); candidatePaletteImageOrder.Add(colorFreq.Key); } } } - Dictionary implicitColor = []; + HashSet implicitColor = new(ColorKeyComparer); int[][] implicitColors = new int[ImplicitPaletteSize][]; for (int k = 0; k < ImplicitPaletteSize; k++) { - for (int i = 0; i < nb; i++) + int[] implicitValue = new int[nb]; + for (int c = 0; c < nb; c++) { - color[i] = GetPaletteValue([], k, i, 0, input.BitDepth); + implicitValue[c] = GetPaletteValue([], k, c, 0, 0, input.BitDepth); } - implicitColor[color] = true; - implicitColors[k] = color; + implicitColor.Add(implicitValue); + implicitColors[k] = implicitValue; } int implicitColorsUsed = 0; - Dictionary colorFreqMap = []; + Dictionary colorFreqMap = new(ColorKeyComparer); for (int y = 0; y < h; y++) { + if (!lossy) + { + PrepareColorRow(input, beginC, nb, y, colorRow); + } + for (int x = 0; x < w; x++) { if (lossy && candidatePalette.Count >= numberOfColors) @@ -1021,21 +1036,34 @@ private static void ForwardPaletteIteration( break; } - for (int c = 0; c < nb; c++) + ReadOnlySpan record = colorRow; + if (lossy) { - colorSpan[c] = input.Channels[beginC + c].GetRow(y)[x]; + for (int c = 0; c < nb; c++) + { + color[c] = quantizedSamples[(c * channelLength) + (y * w) + x]; + } + + record = colorSpan; + } + else + { + record = colorRow.Slice(x * nb, nb); } - const bool new_color = candidatePalette.Add(color).second; - if (new_color) + if (!candidateLookup.TryGetValue(record, out int[]? key)) { - if (implicitColor[color]) + // Hash keys describe values. Retain a new immutable key only when this complete color is first encountered. + key = record.ToArray(); + candidatePalette.Add(key); + + if (implicitColor.Contains(key)) { implicitColorsUsed++; } else { - candidatePaletteImageOrder.Add(color); + candidatePaletteImageOrder.Add(key); if (candidatePaletteImageOrder.Count > numberOfColors) { throw new InvalidOperationException("Too many colors for palette/indexed"); @@ -1043,7 +1071,8 @@ private static void ForwardPaletteIteration( } } - colorFreqMap[color]++; + ref int frequency = ref CollectionsMarshal.GetValueRefOrAddDefault(colorFreqMap, key, out _); + frequency++; } } @@ -1056,18 +1085,18 @@ private static void ForwardPaletteIteration( for (int k = 0; k < ImplicitPaletteSize; k++) { - color = implicitColors[k]; - if (colorFreqMap[color] > 10) + int[] implicitValue = implicitColors[k]; + colorFreqMap.TryGetValue(implicitValue, out int frequency); + if (frequency > 10) { numberOfColors++; - candidatePaletteImageOrder.Add(color); + candidatePaletteImageOrder.Add(implicitValue); } } for (int k = 0; k < ImplicitPaletteSize; k++) { - color = implicitColors[k]; - inversePalette[color] = numberOfColors + k; + inversePalette[implicitColors[k]] = numberOfColors + k; } // This owner transfers to input only when the final encoding pass completes. @@ -1078,16 +1107,24 @@ private static void ForwardPaletteIteration( { Span palette = newChannel.GetRow(0); int oneRow = newChannel.Plane.PixelsPerRow; - int oneRowImage = beginCChannel.Plane.PixelsPerRow; int bitDepth = Math.Min(input.BitDepth, 24); // max. 24 bits, cannot be greater + long maximumSample = (1L << input.BitDepth) - 1; + double distanceScale = 32D / (1L << Math.Max(0, 2 * (bitDepth - 8))); if (lossy) { + // The encoder measures frequent deltas for RGB only. Zero extra-component deltas retain each component's predicted value. + // Explicit palette entries have no implicit zero rule; initialize only these otherwise unwritten delta columns. + for (int c = RgbChannels; c < nb; c++) + { + palette.Slice(c * oneRow, numberOfDeltas).Clear(); + } + for (int i = 0; i < numberOfDeltas; i++) { for (int c = 0; c < 3; c++) { - palette[(c * oneRow) + i] = paletteIterationData.FrequentDeltas[c][i]; + palette[(c * oneRow) + i] = frequentDeltas[(c * PaletteIterationData.MaxDeltas) + i]; } } } @@ -1099,22 +1136,24 @@ private static void ForwardPaletteIteration( { candidatePaletteImageOrder.Sort((ap, bp) => { - float ay = (0.299f * ap[0]) + (0.587f * ap[1]) + (0.114f * ap[2]) + 0.1f; + float ay = (0.299F * ap[0]) + (0.587F * ap[1]) + (0.114F * ap[2]) + 0.1F; if (ap.Length > 3) { - ay *= 1f + ap[3]; + ay *= 1F + ap[3]; } - float by = (0.299f * bp[0]) + (0.587f * bp[1]) + (0.114f * bp[2]) + 0.1f; + float by = (0.299F * bp[0]) + (0.587F * bp[1]) + (0.114F * bp[2]) + 0.1F; if (bp.Length > 3) { - by *= 1f + bp[3]; + by *= 1F + bp[3]; } - ay = colorFreqMap[ap] > frequencyThreshold ? -ay : ay; - by = colorFreqMap[bp] > frequencyThreshold ? -by : by; + colorFreqMap.TryGetValue(ap, out int frequencyA); + colorFreqMap.TryGetValue(bp, out int frequencyB); + ay = frequencyA > frequencyThreshold ? -ay : ay; + by = frequencyB > frequencyThreshold ? -by : by; return ay.CompareTo(by); }); @@ -1136,138 +1175,116 @@ private static void ForwardPaletteIteration( using JxlModularState? wpState = lossy ? new(configuration, wpHeader, w, nb) : null; Span weightedStorage = wpState is not null ? wpState.Reset(wpHeader, w) : default; - InlineArray3> errorRow = default; - - if (lossy) - { - errorRow[0] = new(nb, w + 4); - errorRow[1] = new(nb, w + 4); - errorRow[2] = new(nb, w + 4); - } - - Span tmp = stackalloc int[32]; // Power of 2 - Span bestValue = tmp.Slice(0 * nb, nb); - Span idealResidual = tmp.Slice(1 * nb, nb); - Span quantizedValue = tmp.Slice(2 * nb, nb); - Span predictions = tmp.Slice(30 * nb, nb); - - // This is a temporary buffer, values are copied here. - // It is so we can swap spans. Since spans are just a view - // of memory, using CopyTo as a swap means we need a - // separate buffer like this for the swapping value. - Span tempBuffer = stackalloc float[w + 4]; + // Three planar error rows cover y, y+1 and y+2. Each channel has two padding positions at both ends. + // Rotate these borrowed spans after a row; no row copies or per-pixel owners are needed. + int errorStride = checked(w + 4); + int errorPlaneLength = checked(nb * errorStride); + using IMemoryOwner? errorOwner = lossy + ? configuration.MemoryAllocator.Allocate(checked((3 * errorPlaneLength) + nb), AllocationOptions.Clean) + : null; + + Span errors = errorOwner is null ? [] : errorOwner.GetSpan()[..((3 * errorPlaneLength) + nb)]; + Span error0 = lossy ? errors[..errorPlaneLength] : []; + Span error1 = lossy ? errors.Slice(errorPlaneLength, errorPlaneLength) : []; + Span error2 = lossy ? errors.Slice(2 * errorPlaneLength, errorPlaneLength) : []; + Span colorWithError = lossy ? errors[(3 * errorPlaneLength)..] : []; + ReadOnlySpan errorOffsets = DefaultOffsets; + Span bestValue = lossy ? candidateStorage[..nb] : []; + Span idealResidual = lossy ? candidateStorage.Slice(nb, nb) : []; + Span quantizedValue = lossy ? candidateStorage.Slice(2 * nb, nb) : []; + Span predictions = lossy ? candidateStorage.Slice(3 * nb, nb) : []; + ReadOnlySpan diffusionMultipliers = [0.55, 0.75]; + using JxlPlane? indexPlane = paletteIterationData.IsFinalRun ? new(configuration, w, h) : null; + Span indexSamples = indexPlane is null ? [] : indexPlane.GetRow(0); + int indexStride = indexPlane is null ? 0 : indexPlane.PixelsPerRow; for (int y = 0; y < h; y++) { - for (int c = 0; c < nb; c++) + if (!lossy) { - p_in[c] = input.channel[beginC + c].Row(y); - if (lossy) - p_quant[c] = quantized_input.channel[c].Row(y); + PrepareColorRow(input, beginC, nb, y, colorRow); } - Span p = sourceSamples[(y * sourceStride)..]; + Span p = indexSamples[(y * indexStride)..]; for (int x = 0; x < w; x++) { int index; if (wpState is null) { - for (int c = 0; c < nb; c++) - { - color[c] = p_in[c][x]; - } - - index = inversePalette[color]; + index = inverseLookup[colorRow.Slice(x * nb, nb)]; } else { - int best_index = 0; - bool best_is_delta = false; - float best_distance = float.PositiveInfinity; + int bestIndex = 0; + bool bestIsDelta = false; + float bestDistance = float.PositiveInfinity; - tmp.Clear(); + candidateStorage.Clear(); - foreach (double diffusion_multiplier in (Span)[0.55, 0.75]) + // Both diffusion trials use the same reconstructed neighbors and error histories. + // Predict once; histories change only after the winning candidate is selected. + for (int c = 0; c < nb; c++) { - for (int c = 0; c < nb; c++) - { - colorWithError[c] = - p_in[c][x] + ((paletteIterationData.IsFinalRun ? 1 : 0) * - diffusion_multiplier * errorRow[0][c, x + 2]); - color[c] = (int)Math.Clamp(MathF.Round(colorWithError[c]), 0, (1 << input.BitDepth) - 1); - } + predictions[c] = JxlContextPrediction.PredictNoTreeWeightedPrediction( + w, + ref quantizedSamples[(c * channelLength) + (y * w) + x], + w, + x, + y, + predictor, + wpState, + weightedStorage, + c).Guess; + } + foreach (double diffusionMultiplier in diffusionMultipliers) + { for (int c = 0; c < nb; c++) { - predictions[c] = JxlContextPrediction.PredictNoTreeWeightedPrediction( - w, - ref p_quant[c][x], - oneRowImage, - x, - y, - predictor, - wpState, - weightedStorage, - c).Guess; + colorWithError[c] = (float)(quantizedSamples[(c * channelLength) + (y * w) + x] + + ((paletteIterationData.IsFinalRun ? 1 : 0) * diffusionMultiplier * error0[(c * errorStride) + x + 2])); + + long rounded = (long)MathF.Round(colorWithError[c], MidpointRounding.AwayFromZero); + color[c] = (int)Math.Clamp(rounded, 0L, maximumSample); } - void TryIndex( - int index, - Span predictions, - Span idealResidual, - Span colorWithError, - ref Span bestValue, - ref Span quantizedValue, - Span palette, - ref int numberOfColors, - ref int numberOfDeltas) + // Visit the explicit/negative entries, then the two nearest implicit cube candidates in native order. + // This is one scoring body; no delegate captures spans or allocates a per-pixel candidate operation. + for (int candidate = MinimumImplicitPaletteIndex; candidate < numberOfColors + 2; candidate++) { + int candidateIndex = candidate < numberOfColors + ? candidate + : QuantizeColorToImplicitPaletteIndex(color, numberOfColors, bitDepth, candidate != numberOfColors); + + for (int c = 0; c < nb; c++) { - quantizedValue[c] = GetPaletteValue(palette, index, c, oneRow, bitDepth); - if (index < numberOfDeltas) + quantizedValue[c] = GetPaletteValue(palette, candidateIndex, c, numberOfColors, oneRow, bitDepth); + if (candidateIndex < numberOfDeltas) { quantizedValue[c] += predictions[c]; } } - float color_distance = 32.0f / (1 << Math.Max(0, 2 * (bitDepth - 8))) * ColorDistance(colorWithError, quantizedValue); - - float indexPenalty = 0; - if (index == -1) - { - indexPenalty = -124; - } - else if (index < 0) + // Native code evaluates the scale in double, then stores the weighted distance as float. + float colorDistance = (float)(distanceScale * ColorDistance(colorWithError, quantizedValue)); + float indexPenalty = candidateIndex == -1 ? -124F + : candidateIndex < 0 ? -2F * candidateIndex + : candidateIndex < numberOfDeltas ? 250F + : candidateIndex < numberOfColors ? 150F + : candidateIndex < numberOfColors + LargeCubeOffset ? 70F + : 256F; + float distance = colorDistance + indexPenalty; + + if (distance < bestDistance) { - indexPenalty = -2 * index; - } - else if (index < numberOfDeltas) - { - indexPenalty = 250; - } - else if (index < numberOfColors) - { - indexPenalty = 150; - } - else if (index < numberOfColors + LargeCubeOffset) - { - indexPenalty = 70; - } - else - { - indexPenalty = 256; - } - - float distance = color_distance + indexPenalty; - if (distance < best_distance) - { - best_distance = distance; - best_index = index; - best_is_delta = index < numberOfDeltas; - - RuntimeUtility.Swap(ref bestValue, ref quantizedValue); + bestDistance = distance; + bestIndex = candidateIndex; + bestIsDelta = candidateIndex < numberOfDeltas; + Span previousBest = bestValue; + bestValue = quantizedValue; + quantizedValue = previousBest; for (int c = 0; c < nb; c++) { @@ -1275,99 +1292,87 @@ void TryIndex( } } } - - for (index = MinimumImplicitPaletteIndex; index < numberOfColors; index++) - { - TryIndex(index); - } - - TryIndex(QuantizeColorToImplicitPaletteIndex(color, numberOfColors, bitDepth, false)); - - if (EncodeToHighQualityImplicitPalette) - { - TryIndex(QuantizeColorToImplicitPaletteIndex(color, numberOfColors, bitDepth, true)); - } } - index = best_index; - deltaUsed |= best_is_delta; + index = bestIndex; + deltaUsed |= bestIsDelta; if (!paletteIterationData.IsFinalRun) { for (int c = 0; c < 3; c++) { - paletteIterationData.Deltas[c].Add(idealResidual[c]); + deltaSamples[(c * paletteIterationData.PixelCapacity) + paletteIterationData.SampleCount] = idealResidual[c]; } - paletteIterationData.DeltaDistances.Add(best_distance); + deltaDistances[paletteIterationData.SampleCount++] = bestDistance; } for (int c = 0; c < nb; c++) { wpState.UpdatePredictionErrors(bestValue[c], x, y, weightedStorage, c); - p_quant[c][x] = bestValue[c]; + quantizedSamples[(c * channelLength) + (y * w) + x] = bestValue[c]; } - float len_error = 0; + float errorLength = 0; for (int c = 0; c < nb; c++) { float local_error = colorWithError[c] - bestValue[c]; - len_error += local_error * local_error; + errorLength += local_error * local_error; } - len_error = MathF.Sqrt(len_error); + errorLength = MathF.Sqrt(errorLength); float modulate = 1f; - long len_limit = 38 << Math.Max(0, bitDepth - 8); - if (len_error > len_limit) + int errorLimit = 38 << Math.Max(0, bitDepth - 8); + if (errorLength > errorLimit) { - modulate *= len_limit / len_error; + modulate *= errorLimit / errorLength; } - DenseMatrix offsets = new(12, 2); - for (int c = 0; c < nb; c++) { - float total_error = colorWithError[c] - bestValue[c]; - - DefaultOffsets.Data.AsSpan().CopyTo(offsets.Data); + float totalError = colorWithError[c] - bestValue[c]; - float total_available = 0; + float totalAvailable = 0; for (int i = 0; i < 11; i++) { - int row = offsets[i, 0]; - int col = offsets[i, 1]; + int row = errorOffsets[2 * i]; + int col = errorOffsets[(2 * i) + 1]; + Span rowErrors = row == 0 ? error0 : row == 1 ? error1 : error2; + ref float neighborError = ref rowErrors[(c * errorStride) + x + col]; - if (Math.Sign(errorRow[row][c, x + col]) != Math.Sign(total_error)) + if (float.IsNegative(neighborError) != float.IsNegative(totalError)) { - total_available += errorRow[row][c, x + col]; + totalAvailable += neighborError; } } - float weight = MathF.Abs(total_error) / (MathF.Abs(total_available) + 1e-3f); + float weight = (float)(MathF.Abs(totalError) / (MathF.Abs(totalAvailable) + 1E-3D)); weight = MathF.Min(weight, 1.0f); for (int i = 0; i < 11; ++i) { - int row = offsets[i, 0]; - int col = offsets[i, 1]; + int row = errorOffsets[2 * i]; + int col = errorOffsets[(2 * i) + 1]; + Span rowErrors = row == 0 ? error0 : row == 1 ? error1 : error2; + ref float neighborError = ref rowErrors[(c * errorStride) + x + col]; - if (Math.Sign(errorRow[row][c, x + col]) != Math.Sign(total_error)) + if (float.IsNegative(neighborError) != float.IsNegative(totalError)) { - total_error += weight * errorRow[row][c, x + col]; - errorRow[row][c, x + col] *= 1 - weight; + totalError += weight * neighborError; + neighborError *= 1 - weight; } } - total_error *= modulate; - float remaining_error = (1.0f / 14f) * total_error; - errorRow[0][c, x + 3] += 2 * remaining_error; - errorRow[0][c, x + 4] += remaining_error; - errorRow[1][c, x + 0] += remaining_error; + totalError *= modulate; + float remainingError = (float)((1F / 14D) * totalError); + error0[(c * errorStride) + x + 3] += 2 * remainingError; + error0[(c * errorStride) + x + 4] += remainingError; + error1[(c * errorStride) + x + 0] += remainingError; for (int i = 0; i < 5; ++i) { - errorRow[1][c, x + i] += remaining_error; - errorRow[2][c, x + i] += remaining_error; + error1[(c * errorStride) + x + i] += remainingError; + error2[(c * errorStride) + x + i] += remainingError; } } } @@ -1380,25 +1385,11 @@ void TryIndex( if (lossy) { - for (int c = 0; c < nb; c++) - { - // Variables for swapping - Span pos0 = errorRow[0].Data.AsSpan(c, w + 4); - Span pos1 = errorRow[1].Data.AsSpan(c, w + 4); - Span pos2 = errorRow[2].Data.AsSpan(c, w + 4); - - // we need to swap: - // error_row[0][c].swap(error_row[1][c]); - pos0.CopyTo(tempBuffer); - pos1.CopyTo(pos0); - tempBuffer.CopyTo(pos1); - - // swap old1, old2 - pos1.CopyTo(tempBuffer); - pos2.CopyTo(pos1); - - pos2.Clear(); - } + Span recycled = error0; + error0 = error1; + error1 = error2; + error2 = recycled; + error2.Clear(); } } @@ -1407,8 +1398,10 @@ void TryIndex( predictor = JxlPredictor.Zero; } - if (paletteIterationData.IsFinalRun) + if (indexPlane is not null) { + // Publish the completed index allocation without copying. The scoped plane receives and releases the original samples. + beginCChannel.Plane.Swap(indexPlane); input.MetaChannels++; // The first channel now contains indices. Release the other selected source channels before removing their references. @@ -1434,82 +1427,301 @@ void TryIndex( } /// - /// For palette encoding. + /// Prepares one interleaved color row for allocation-free span lookup in palette collections. /// - internal sealed class PaletteIterationData + /// The source channel owner. Every selected channel has the same logical dimensions. + /// The first selected channel. + /// The number of components in each complete color record. + /// The source row. + /// The reusable row of width*channelCount integer components. + private static void PrepareColorRow(JxlModularImage input, int firstChannel, int channelCount, int y, Span records) + { + int width = input.Channels[firstChannel].Width; + if (channelCount == 1) + { + input.Channels[firstChannel].GetRow(y)[..width].CopyTo(records); + return; + } + + if (channelCount == 3 || channelCount == 4) + { + ReadOnlySpan plane0 = input.Channels[firstChannel].GetRow(y); + ReadOnlySpan plane1 = input.Channels[firstChannel + 1].GetRow(y); + ReadOnlySpan plane2 = input.Channels[firstChannel + 2].GetRow(y); + ReadOnlySpan plane3 = channelCount == 4 ? input.Channels[firstChannel + 3].GetRow(y) : []; + ref int first0 = ref MemoryMarshal.GetReference(plane0); + ref int first1 = ref MemoryMarshal.GetReference(plane1); + ref int first2 = ref MemoryMarshal.GetReference(plane2); + ref int first3 = ref MemoryMarshal.GetReference(plane3); + ref float destination = ref Unsafe.As(ref MemoryMarshal.GetReference(records)); + int x = 0; + + // Registers contain consecutive components from each integer plane. The shared shuffles store complete three- or four-component records. + // AsSingle reinterprets bits only: no integer-to-float conversion or floating-point arithmetic changes the component values. + if (Vector512.IsHardwareAccelerated) + { + nuint count = (width - x).Vector512Count(); + for (nuint block = 0; block < count; block++, x += Vector512.Count) + { + Vector512 c0 = Vector512.LoadUnsafe(ref first0, (uint)x).AsSingle(); + Vector512 c1 = Vector512.LoadUnsafe(ref first1, (uint)x).AsSingle(); + Vector512 c2 = Vector512.LoadUnsafe(ref first2, (uint)x).AsSingle(); + + if (channelCount == 3) + { + SimdUtils.Interleave3(c0, c1, c2, ref Unsafe.Add(ref destination, x * 3)); + } + else + { + Vector512 c3 = Vector512.LoadUnsafe(ref first3, (uint)x).AsSingle(); + SimdUtils.Interleave4(c0, c1, c2, c3, ref Unsafe.Add(ref destination, x * 4)); + } + } + } + + if (Vector256.IsHardwareAccelerated) + { + nuint count = (width - x).Vector256Count(); + for (nuint block = 0; block < count; block++, x += Vector256.Count) + { + Vector256 c0 = Vector256.LoadUnsafe(ref first0, (uint)x).AsSingle(); + Vector256 c1 = Vector256.LoadUnsafe(ref first1, (uint)x).AsSingle(); + Vector256 c2 = Vector256.LoadUnsafe(ref first2, (uint)x).AsSingle(); + + if (channelCount == 3) + { + SimdUtils.Interleave3(c0, c1, c2, ref Unsafe.Add(ref destination, x * 3)); + } + else + { + Vector256 c3 = Vector256.LoadUnsafe(ref first3, (uint)x).AsSingle(); + SimdUtils.Interleave4(c0, c1, c2, c3, ref Unsafe.Add(ref destination, x * 4)); + } + } + } + + if (Vector128.IsHardwareAccelerated) + { + nuint count = (width - x).Vector128Count(); + for (nuint block = 0; block < count; block++, x += Vector128.Count) + { + Vector128 c0 = Vector128.LoadUnsafe(ref first0, (uint)x).AsSingle(); + Vector128 c1 = Vector128.LoadUnsafe(ref first1, (uint)x).AsSingle(); + Vector128 c2 = Vector128.LoadUnsafe(ref first2, (uint)x).AsSingle(); + + if (channelCount == 3) + { + SimdUtils.Interleave3(c0, c1, c2, ref Unsafe.Add(ref destination, x * 3)); + } + else + { + Vector128 c3 = Vector128.LoadUnsafe(ref first3, (uint)x).AsSingle(); + SimdUtils.Interleave4(c0, c1, c2, c3, ref Unsafe.Add(ref destination, x * 4)); + } + } + } + + // Every vector stores an exact number of complete records. The tail uses identical component order and touches no row padding. + for (; x < width; x++) + { + int offset = x * channelCount; + records[offset] = plane0[x]; + records[offset + 1] = plane1[x]; + records[offset + 2] = plane2[x]; + if (channelCount == 4) + { + records[offset + 3] = plane3[x]; + } + } + + return; + } + + // Arbitrary channel counts use the same record layout. Borrow each channel once at row preparation, outside the sample loop. + for (int c = 0; c < channelCount; c++) + { + ReadOnlySpan source = input.Channels[firstChannel + c].GetRow(y); + for (int x = 0; x < width; x++) + { + records[(x * channelCount) + c] = source[x]; + } + } + } + + /// + /// Defines value equality between immutable stored color arrays and borrowed interleaved color records. + /// + private sealed class PaletteColorComparer : IEqualityComparer, IAlternateEqualityComparer, int[]>, IComparer + { + /// + public bool Equals(int[]? left, int[]? right) => this.Equals(left.AsSpan(), right); + + /// + public bool Equals(ReadOnlySpan alternate, int[]? other) => alternate.SequenceEqual(other); + + /// + public int GetHashCode(int[] value) => this.GetHashCode(value.AsSpan()); + + /// + public int GetHashCode(ReadOnlySpan alternate) + { + // Both lookup forms hash the same raw Int32 words. Signed values and every extra channel participate in the key. + HashCode hash = default; + hash.AddBytes(MemoryMarshal.AsBytes(alternate)); + return hash.ToHashCode(); + } + + /// + public int[] Create(ReadOnlySpan alternate) => alternate.ToArray(); + + /// + public int Compare(int[]? left, int[]? right) => left.AsSpan().SequenceCompareTo(right); + } + + /// + /// Owns the fixed-capacity delta statistics retained between palette search passes. + /// + /// + /// Three integer planes retain RGB residuals. A separate double plane retains candidate distances. + /// The integer allocation also holds the selected delta palette. Callers borrow spans before their sample loops. + /// + internal sealed class PaletteIterationData : IDisposable { /// - /// Maximum number of deltas. + /// The native limit for selected predictive delta colors. /// - private const int MaxDeltas = 128; - - private InlineArray3> deltas; + public const int MaxDeltas = 128; - public bool IsFinalRun { get; set; } + private readonly IMemoryOwner? deltaOwner; + private readonly IMemoryOwner? distanceOwner; - public InlineArray3> Deltas + /// + /// Prepares retained statistics once for an optional preprocessing pass. + /// + /// The configuration that owns numeric storage. + /// The exact preprocessing sample count, or zero when no preprocessing pass is used. + public PaletteIterationData(Configuration configuration, int pixelCapacity) { - get => this.deltas; - set => this.deltas = value; + this.PixelCapacity = pixelCapacity; + if (pixelCapacity == 0) + { + return; + } + + // Attach the second owner only after allocation succeeds. A failed constructor must return the first buffer. + this.deltaOwner = configuration.MemoryAllocator.Allocate(checked(RgbChannels * (pixelCapacity + MaxDeltas))); + try + { + this.distanceOwner = configuration.MemoryAllocator.Allocate(pixelCapacity); + } + catch + { + this.deltaOwner.Dispose(); + throw; + } } - public List DeltaDistances { get; set; } = []; + /// + /// Gets or sets whether search writes final indices instead of collecting preprocessing statistics. + /// + public bool IsFinalRun { get; set; } - public List[] FrequentDeltas { get; set; } = new List[3]; + /// + /// Gets the prepared per-component residual capacity. + /// + public int PixelCapacity { get; } + + /// + /// Gets or sets the number of retained preprocessing samples. + /// + public int SampleCount { get; set; } + /// + /// Gets the number of selected RGB delta entries. + /// + public int NumberOfFrequentDeltas { get; private set; } + + /// + /// Gets the three planar residual arrays. Borrow once before a sample loop. + /// + public Span DeltaSamples => this.deltaOwner is null ? [] : this.deltaOwner.GetSpan()[..(RgbChannels * this.PixelCapacity)]; + + /// + /// Gets the retained native double-precision candidate distances. Borrow once before a sample loop. + /// + public Span DeltaDistances => this.distanceOwner is null ? [] : this.distanceOwner.GetSpan()[..this.PixelCapacity]; + + /// + /// Gets three selected-delta component planes, each with MaxDeltas entries. Only NumberOfFrequentDeltas entries are initialized. + /// + public Span FrequentDeltas => this.deltaOwner is null + ? [] + : this.deltaOwner.GetSpan().Slice(RgbChannels * this.PixelCapacity, RgbChannels * MaxDeltas); + + /// + public void Dispose() + { + this.distanceOwner?.Dispose(); + this.deltaOwner?.Dispose(); + } + + /// + /// Selects up to MaxDeltas residual colors using the native frequency and distance weighting. + /// + /// The image sample count used to normalize frequency. + /// The source precision used to cluster residuals. public void FindFrequentColorDeltas(int numPixels, int bitDepth) { - Dictionary, double> deltaFrequencyMap = []; - int bucketSize = 3 << Math.Max(0, bitDepth - 3); - for (int i = 0; i < this.Deltas[0].Count; i++) + Span deltas = this.DeltaSamples; + Span distances = this.DeltaDistances; + Span frequent = this.FrequentDeltas; + SortedDictionary<(int Red, int Green, int Blue), double> frequencies = []; + int bucketSize = 3 << Math.Max(0, bitDepth - 8); + + for (int i = 0; i < this.SampleCount; i++) { - InlineArray3 delta = default; - delta[0] = RoundInteger(this.Deltas[0][i], bucketSize); - delta[1] = RoundInteger(this.Deltas[1][i], bucketSize); - delta[2] = RoundInteger(this.Deltas[2][i], bucketSize); + (int Red, int Green, int Blue) delta = ( + RoundInteger(deltas[i], bucketSize), + RoundInteger(deltas[this.PixelCapacity + i], bucketSize), + RoundInteger(deltas[(2 * this.PixelCapacity) + i], bucketSize)); - // Condition equivalent to delta[0] == 0 && delta[1] == 0 && delta[2] == 0 - if ((delta[0] | delta[1] | delta[2]) == 0) + if ((delta.Red | delta.Green | delta.Blue) == 0) { continue; } - deltaFrequencyMap[delta] += Math.Sqrt(Math.Sqrt(this.DeltaDistances[i])); + frequencies.TryGetValue(delta, out double frequency); + frequencies[delta] = frequency + Math.Sqrt(Math.Sqrt(distances[i])); } - float deltaDistanceMultiplier = 1f / numPixels; - Span allZero = [0, 0, 0]; + float multiplier = 1F / numPixels; + ReadOnlySpan zero = [0, 0, 0]; + List> ranked = new(frequencies.Count); - foreach (KeyValuePair, double> deltaFrequency in deltaFrequencyMap) + foreach (KeyValuePair<(int Red, int Green, int Blue), double> entry in frequencies) { - float deltaDistance = MathF.Sqrt(ColorDistance(allZero, deltaFrequency.Key)) + 1f; - double second = deltaFrequency.Value * deltaDistance * deltaDistanceMultiplier; - deltaFrequencyMap[deltaFrequency.Key] = second; + InlineArray3 color = default; + color[0] = entry.Key.Red; + color[1] = entry.Key.Green; + color[2] = entry.Key.Blue; + float distance = MathF.Sqrt(ColorDistance(zero, color)) + 1F; + ranked.Add(new(entry.Key, entry.Value * ((double)distance * multiplier))); } - Dictionary, double> sorted = deltaFrequencyMap.ToDictionary( - entry => entry.Key, - entry => entry.Value); + ranked.Sort(static (left, right) => right.Value.CompareTo(left.Value)); + this.NumberOfFrequentDeltas = 0; - IOrderedEnumerable, double>> sortedEnumerator = sorted.OrderBy( - x => x.Value); - - foreach (KeyValuePair, double> deltaFrequency in sortedEnumerator) + foreach (KeyValuePair<(int Red, int Green, int Blue), double> entry in ranked) { - if (this.FrequentDeltas[0].Count >= MaxDeltas) + if (this.NumberOfFrequentDeltas == MaxDeltas || entry.Value < 17) { break; } - if (deltaFrequency.Value < 17) - { - break; - } - - for (int c = 0; c < 3; c++) - { - this.FrequentDeltas[c].Add(deltaFrequency.Key[c] * bucketSize); - } + int index = this.NumberOfFrequentDeltas++; + frequent[index] = entry.Key.Red * bucketSize; + frequent[MaxDeltas + index] = entry.Key.Green * bucketSize; + frequent[(2 * MaxDeltas) + index] = entry.Key.Blue * bucketSize; } } } From b3db7f812ecada60bf7974860a1e23c9e785e4bf Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 23:11:16 +1000 Subject: [PATCH 278/328] Store JPEG XL hybrid integer settings as immutable values --- .../Entropy/JxlAnsHybridUIntConfiguration.cs | 72 ++++++++++++++----- 1 file changed, 56 insertions(+), 16 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHybridUIntConfiguration.cs b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHybridUIntConfiguration.cs index 9ff4306522..571911d979 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHybridUIntConfiguration.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHybridUIntConfiguration.cs @@ -1,36 +1,78 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.Processing; namespace SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; -internal sealed class JxlAnsHybridUIntConfiguration : IJxlFields +/// +/// Specifies how an unsigned integer is divided between an entropy token and its remaining bit payload. +/// +/// +/// The entropy reader prepares every configuration before processing tokens. Configuration values remain fixed for that entropy stream. +/// This value stores the four native UInt32 fields together, without a separate object for each histogram or token calculation. +/// +internal readonly struct JxlAnsHybridUIntConfiguration { - public JxlAnsHybridUIntConfiguration(uint splitExponent = 4, uint msbInToken = 2, uint lsbInToken = 0) + /// + /// Initializes the default split exponent of four, with two high token bits and no low token bits. + /// + public JxlAnsHybridUIntConfiguration() + : this(4, 2, 0) { - this.SplitExponent = splitExponent; - this.SplitToken = 1u << (int)splitExponent; - this.MsbInToken = msbInToken; - this.LsbInToken = lsbInToken; + } + /// + /// Initializes the token split and its derived direct-token threshold. + /// + /// The base-two exponent of the direct-token threshold. + /// The number of high value bits stored in the token. + /// The number of low value bits stored in the token. + public JxlAnsHybridUIntConfiguration(uint splitExponent, uint msbInToken, uint lsbInToken) + { if (splitExponent < msbInToken + lsbInToken) { - throw new InvalidOperationException("Split exponent should be < msbInToken + lsbInToken"); + throw new InvalidOperationException("Split exponent must be at least the sum of the high and low token bit counts."); } + + this.SplitExponent = splitExponent; + this.SplitToken = 1U << (int)splitExponent; + this.MsbInToken = msbInToken; + this.LsbInToken = lsbInToken; } - public uint SplitExponent { get; set; } + /// + /// Gets the base-two exponent of the direct-token threshold. + /// + public uint SplitExponent { get; } - public uint SplitToken { get; set; } + /// + /// Gets the direct-token threshold. Smaller input values are represented directly by their token. + /// + public uint SplitToken { get; } - public uint MsbInToken { get; set; } // Most significant bit + /// + /// Gets the number of high value bits stored in a nondirect token. + /// + public uint MsbInToken { get; } - public uint LsbInToken { get; set; } // Least significant bit + /// + /// Gets the number of low value bits stored in a nondirect token. + /// + public uint LsbInToken { get; } + /// + /// Gets the mask for the low value bits stored in the token. + /// public uint LsbMask => (1u << (int)this.LsbInToken) - 1; + /// + /// Divides an unsigned value into its entropy token and remaining bit payload. + /// + /// The value to encode. + /// The symbol to encode through the selected entropy table. + /// The number of payload bits to write after the token. + /// The payload bits in least-significant-bit-first order. public void Encode(uint value, out uint token, out uint bitCount, out uint bits) { if (value < this.SplitToken) @@ -46,8 +88,8 @@ public void Encode(uint value, out uint token, out uint bitCount, out uint bits) unchecked { - // The following expression is quite complex. - // See https://github.com/libjxl/libjxl/blob/main/lib/jxl/dec_ans.h#L83C16-L86C47. + // Values below SplitToken are direct. Larger values remove their leading one, then place the exponent + // and selected high/low value bits in the token. Only the intervening bits remain in the payload. token = this.SplitToken + (uint)(((n - this.SplitExponent) << (int)(this.MsbInToken + this.LsbInToken)) + ((m >> (int)(n - this.MsbInToken)) << (int)this.LsbInToken) + @@ -58,6 +100,4 @@ public void Encode(uint value, out uint token, out uint bitCount, out uint bits) } } } - - public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); } From 9605e5d61ada4b353b3f7f5b67b72be46343cdfe Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 23:13:36 +1000 Subject: [PATCH 279/328] Retain JPEG XL color owners while transferring sample allocations --- .../Decoder/Patch/JxlPatchDictionary.cs | 2 +- .../Encoder/Comparator/JxlComparator.cs | 6 +- .../JxlComparatorScoreComputation.cs | 2 +- .../Encoder/JxlAdaptiveQuantization.cs | 6 +- .../Encoder/JxlExternalImageEncoder.cs | 34 ++++++---- .../Encoder/JxlImageBundleEncoder.cs | 9 +-- .../Jxl/Processing/Image/JxlImageBundle.cs | 59 ++++++---------- .../RenderPipeline/WriteToImageBundleStage.cs | 5 +- .../Formats/Jxl/IO/ImageBundleTests.cs | 67 +++++++++++++++++++ .../Processing/Decoder/JxlDecoderCoreTests.cs | 4 +- 10 files changed, 125 insertions(+), 69 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchDictionary.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchDictionary.cs index f93cd6e4b0..fda307d2a3 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchDictionary.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchDictionary.cs @@ -553,7 +553,7 @@ public bool AddOneRow( for (int c = 0; c < 3; c++) { - foreground[c] = this.referenceFrames[@ref].Frame.Color!.PlaneRowMemory(c, refPos.Y0 + iy)[referenceStartX..]; + foreground[c] = this.referenceFrames[@ref].Frame.Color.PlaneRowMemory(c, refPos.Y0 + iy)[referenceStartX..]; } for (int j = 0; j < extraChannelCount; j++) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparator.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparator.cs index 0fa78e60c3..c3acaa6c33 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparator.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparator.cs @@ -64,7 +64,7 @@ public void CompareWith(Configuration configuration, JxlImageBundle actual, JxlP ? new(configuration, this.width, this.height) : null; - JxlImage3 scaledActualLinearSRgb = scaledStorage ?? actualLinearSRgb.Color!; + JxlImage3 scaledActualLinearSRgb = scaledStorage ?? actualLinearSRgb.Color; if (scaleIntensity) { @@ -72,7 +72,7 @@ public void CompareWith(Configuration configuration, JxlImageBundle actual, JxlP float scale = actual.Metadata.IntensityTarget / this.intensityTarget; for (int c = 0; c < 3; ++c) { - JxlPlane source = actualLinearSRgb.Color!.Plane(c); + JxlPlane source = actualLinearSRgb.Color.Plane(c); JxlPlane destination = scaledActualLinearSRgb.Plane(c); ReadOnlySpan sourceSamples = source.GetRow(0); Span destinationSamples = destination.GetRow(0); @@ -112,7 +112,7 @@ public void SetReferenceImage(Configuration configuration, JxlImageBundle refere } // Build the replacement before releasing the previous reference. A failed conversion or allocation leaves it usable. - ButteraugliComparator replacement = ButteraugliComparator.Make(configuration, refLinearSRgb.Color!, parameters); + ButteraugliComparator replacement = ButteraugliComparator.Make(configuration, refLinearSRgb.Color, parameters); this.comparator?.Dispose(); this.comparator = replacement; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparatorScoreComputation.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparatorScoreComputation.cs index b5abad27f0..2fbc509c83 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparatorScoreComputation.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparatorScoreComputation.cs @@ -75,7 +75,7 @@ private static void AlphaBlend(float backgroundLinear, JxlImageBundle image) for (int c = 0; c < 3; c++) { - AlphaBlend(image.Color!, c, backgroundLinear, image.Alpha!, image.Color!); + AlphaBlend(image.Color, c, backgroundLinear, image.Alpha!, image.Color); } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs index 971d38e412..89da5432f2 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs @@ -762,10 +762,10 @@ private static JxlImageBundle RoundtripImage( try { - JxlImage3 tmp = new(configuration, opsin.XSize, opsin.YSize); + using JxlImage3 tmp = new(configuration, opsin.XSize, opsin.YSize); + if (!decoded.SetFromImage(tmp, decState.OutputEncodingInfo.ColorEncoding)) { - tmp.Dispose(); throw new InvalidOperationException("Could not set image bundle"); } @@ -1185,7 +1185,7 @@ public static bool FindBestQuantizationMaxError( } using JxlImageBundle decoded = RoundtripImage(configuration, frameHeader, opsin, encState, cms); - JxlImage3 reconstructed = decoded.Color!; + JxlImage3 reconstructed = decoded.Color; ReadOnlySpan decoded0 = reconstructed.Plane(0).GetRow(0); ReadOnlySpan decoded1 = reconstructed.Plane(1).GetRow(0); ReadOnlySpan decoded2 = reconstructed.Plane(2).GetRow(0); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlExternalImageEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlExternalImageEncoder.cs index caed1a447b..7ab76dd1db 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlExternalImageEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlExternalImageEncoder.cs @@ -169,7 +169,7 @@ public static bool ConvertFromExternal( throw new InvalidOperationException($"Expected {colorChannels} color channels, received only {format.Channels} channels."); } - JxlImage3 color = new(configuration, xSize, ySize); + using JxlImage3 color = new(configuration, xSize, ySize); for (int c = 0; c < colorChannels; c++) { @@ -190,25 +190,35 @@ public static bool ConvertFromExternal( throw new InvalidOperationException("Could not set image bundle"); } + // The bundle now owns the copied samples; the local image wrapper is empty. The supplied byte memory remains borrowed. if (setAlpha) { - JxlPlane alpha = new(configuration, xSize, ySize); + JxlPlane? alpha = new(configuration, xSize, ySize); - if (hasAlpha) + try { - if (!ConvertFromExternal(configuration, bytes, xSize, ySize, bitsPerSample, format, format.Channels - 1, alpha)) + if (hasAlpha) { - return false; + if (!ConvertFromExternal(configuration, bytes, xSize, ySize, bitsPerSample, format, format.Channels - 1, alpha)) + { + return false; + } + } + else + { + JxlImageOperations.FillImage(1.0F, alpha); } - } - else - { - JxlImageOperations.FillImage(1.0F, alpha); - } - if (!imageBundle.TrySetAlpha(alpha)) + if (!imageBundle.TrySetAlpha(alpha)) + { + throw new InvalidOperationException("Failed to set alpha channel"); + } + + alpha = null; + } + finally { - throw new InvalidOperationException("Failed to set alpha channel"); + alpha?.Dispose(); } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlImageBundleEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlImageBundleEncoder.cs index 74583a7cba..29bb3a96e0 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlImageBundleEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlImageBundleEncoder.cs @@ -144,7 +144,7 @@ public static bool CopyToT( configuration, imageBundle.CurrentColorEncoding, metadata.IntensityTarget, - imageBundle.Color!, + imageBundle.Color, imageBundle.Black!, rectangle, desiredEncoding, @@ -175,12 +175,12 @@ public static bool TransformIfNeeded( return true; } - JxlImage3? color = new(configuration, input.Color!.XSize, input.Color.YSize); + using JxlImage3 color = new(configuration, input.Color.XSize, input.Color.YSize); List>? extraChannels = null; try { - JxlImageOperations.ForceCopyImage(input.Color!, color); + JxlImageOperations.ForceCopyImage(input.Color, color); // The destination owns its previous planes. Clear its old channels before replacing a differently sized frame. // The input remains borrowed throughout this operation, including when the output selects it without conversion. @@ -190,8 +190,6 @@ public static bool TransformIfNeeded( return false; } - // SetFromImage took ownership. Do not dispose its image through the local cleanup path. - color = null; if (input.ContainsExtraChannels()) { // Reserve every list slot before allocating its plane. Ownership cannot be stranded by a later list-growth failure. @@ -221,7 +219,6 @@ public static bool TransformIfNeeded( } finally { - color?.Dispose(); if (extraChannels is not null) { foreach (JxlPlane copy in extraChannels) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs index 45d5d00db2..ad40d8fa52 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs @@ -14,8 +14,9 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Image; /// Stores frame sample planes, the original image metadata, and the current sample encoding. /// /// -/// The bundle owns assigned color and extra-channel planes. Metadata and row views are borrowed. -/// Successful setters transfer plane ownership; failed setters leave ownership with the caller. +/// The bundle retains one color-image wrapper and owns its sample allocations and assigned extra-channel planes. +/// Metadata and row views are borrowed. Replacing samples invalidates earlier row views. +/// SetFromImage moves allocations from a caller-owned image into the retained wrapper. The caller keeps an empty source wrapper. /// internal sealed class JxlImageBundle : IDisposable { @@ -137,9 +138,9 @@ public JxlImageBundle(JxlImageMetadata metadata) public JxlImageMetadata Metadata { get; } /// - /// Gets the color data. + /// Gets the retained color-image owner. Its planes are empty until color samples are assigned. /// - public JxlImage3? Color { get; private set; } + public JxlImage3 Color { get; } = new(); /// /// Gets the number of owned extra-channel planes. @@ -149,7 +150,7 @@ public JxlImageBundle(JxlImageMetadata metadata) /// /// Gets a value indicating whether the color data is present and usable. /// - public bool HasColor => this.Color is not null && this.Color.XSize != 0; + public bool HasColor => this.Color.XSize != 0; /// /// Gets the width. @@ -165,7 +166,7 @@ public int XSize if (this.HasColor) { - return this.Color!.XSize; + return this.Color.XSize; } return this.extraChannels.Count == 0 ? 0 : this.extraChannels[0].XSize; @@ -186,7 +187,7 @@ public int YSize if (this.HasColor) { - return this.Color!.YSize; + return this.Color.YSize; } return this.extraChannels.Count == 0 ? 0 : this.extraChannels[0].YSize; @@ -284,29 +285,9 @@ public bool VerifySizes() public void OverrideProfile(JxlColorEncoding encoding) => this.CurrentColorEncoding = encoding; /// - /// Borrows the color image without transferring its ownership. + /// Releases the color sample allocations and resets the retained image to its empty state. /// - /// The borrowed color image, or null when none is present. - /// Whether the bundle contains a color image. - public bool TryGetColor(out JxlImage3? color) - { - color = null; - if (this.Color is not null) - { - color = this.Color; - } - - return color is not null; - } - - /// - /// Releases the owned color planes and removes the color image. - /// - public void RemoveColor() - { - this.Color?.Dispose(); - this.Color = null; - } + public void RemoveColor() => this.Color.Dispose(); /// /// Releases and removes every owned extra-channel plane. @@ -462,9 +443,9 @@ public bool VerifyMetadata() } /// - /// Replaces the color image and takes ownership when its encoding and dimensions are valid. + /// Replaces the color sample allocations when the supplied encoding and dimensions are valid. /// - /// The caller-owned color image. On success, the caller must not dispose it or assign it to another bundle. + /// The caller-owned image. Success moves its sample allocations into this bundle and leaves this source wrapper empty. /// Current color encoding. /// Whether ownership transferred. On failure, this bundle and the supplied image remain unchanged. public bool SetFromImage(JxlImage3 color, JxlColorEncoding current) @@ -488,9 +469,9 @@ public bool SetFromImage(JxlImage3 color, JxlColorEncoding current) } } - // Every production caller supplies newly owned storage. Existing row views remain borrows and cannot become a second image owner. + // The wrapper belongs to this bundle for its whole lifetime. Only the allocations move; callers can dispose their emptied source wrapper. this.RemoveColor(); - this.Color = color; + this.Color.Swap(color); this.CurrentColorEncoding = current; return true; @@ -513,7 +494,7 @@ public bool ShrinkTo(int width, int height) { if (this.HasColor) { - if (this.Color?.ShrinkTo(width, height) != true) + if (!this.Color.ShrinkTo(width, height)) { return false; } @@ -549,10 +530,12 @@ public JxlImageBundle Copy(Configuration configuration) { // Reserve list slots before allocating planes, so adding an owned plane cannot fail while growing the list. copy.extraChannels.EnsureCapacity(this.extraChannels.Count); - if (this.Color is not null) + + if (this.HasColor) { - copy.Color = new JxlImage3(configuration, this.Color.XSize, this.Color.YSize); - JxlImageOperations.ForceCopyImage(this.Color, copy.Color); + using JxlImage3 color = new(configuration, this.Color.XSize, this.Color.YSize); + JxlImageOperations.ForceCopyImage(this.Color, color); + copy.Color.Swap(color); } // Metadata and JPEG coefficient data remain shared managed objects. Only the sample planes transfer or copy ownership. @@ -598,7 +581,7 @@ public bool TransformTo( JxlColorEncoding desiredEncoding, JxlCmsInterface cms) { - if (!this.CopyTo(configuration, this.Color!.GetRectangle(), desiredEncoding, cms, this.Color!)) + if (!this.CopyTo(configuration, this.Color.GetRectangle(), desiredEncoding, cms, this.Color)) { return false; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToImageBundleStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToImageBundleStage.cs index 0eccbc2e8e..62df6f0a97 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToImageBundleStage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToImageBundleStage.cs @@ -29,14 +29,13 @@ public override void SetInputSizes(Span inputSizes) } } - JxlImage3 tmp = new(this.GetConfiguration(), inputSizes[0].Width, inputSizes[0].Height); + using JxlImage3 tmp = new(this.GetConfiguration(), inputSizes[0].Width, inputSizes[0].Height); // The bundle releases its previous channels. New planes transfer to it as each setter succeeds. bundle.ClearExtraChannels(); if (!bundle.SetFromImage(tmp, colorEncoding)) { - tmp.Dispose(); throw new InvalidOperationException("Could not set the image of an image bundle"); } @@ -64,7 +63,7 @@ public override void ProcessRow( for (int c = 0; c < 3; c++) { this.GetInputRow(inputRows, c, 0)[..width] - .CopyTo(bundle.Color!.PlaneRow(c, yPos)[xPos..]); + .CopyTo(bundle.Color.PlaneRow(c, yPos)[xPos..]); } int ecIndex = 0; diff --git a/tests/ImageSharp.Tests/Formats/Jxl/IO/ImageBundleTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/IO/ImageBundleTests.cs index ddf5377113..9b9311464a 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/IO/ImageBundleTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/IO/ImageBundleTests.cs @@ -12,6 +12,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Tests.ColorProfiles.Icc; @@ -19,6 +20,72 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jxl.IO; public class ImageBundleTests { + /// + /// Checks that color transfers and removal retain the bundle's wrapper while moving or releasing sample allocations. + /// + [Fact] + public void ColorTransfer_KeepsWrapperAcrossReplacementAndRemoval() + { + using JxlImageBundle bundle = new(new JxlImageMetadata()); + JxlImage3 retained = bundle.Color; + JxlColorEncoding encoding = new(); + using JxlImage3 first = new(TestEnvironment.Configuration, 1, 1); + JxlImageOperations.FillImage(7F, first); + + Assert.True(bundle.SetFromImage(first, encoding)); + Assert.Same(retained, bundle.Color); + Assert.Equal(0, first.XSize); + Assert.Equal(7F, bundle.Color.PlaneRow(0, 0)[0]); + + // The input wrappers stay caller-owned. Replacing the bundle moves the next allocation into the same retained wrapper. + using JxlImage3 replacement = new(TestEnvironment.Configuration, 2, 1); + JxlImageOperations.FillImage(5F, replacement); + Assert.True(bundle.SetFromImage(replacement, encoding)); + Assert.Same(retained, bundle.Color); + Assert.Equal(0, replacement.XSize); + Assert.Equal(2, bundle.Color.XSize); + Assert.Equal(5F, bundle.Color.PlaneRow(0, 0)[0]); + + bundle.RemoveColor(); + + Assert.Same(retained, bundle.Color); + Assert.False(bundle.HasColor); + Assert.Same(encoding, bundle.CurrentColorEncoding); + + for (int c = 0; c < 3; c++) + { + Assert.True(bundle.Color.PlaneRowMemory(c, 0).IsEmpty); + } + } + + /// + /// Checks that rejecting a gray/RGB model change leaves both the bundle and caller-owned samples intact. + /// + [Fact] + public void ColorTransfer_IncompatibleEncodingLeavesBothOwnersUnchanged() + { + using JxlImageBundle bundle = new(new JxlImageMetadata()); + JxlColorEncoding encoding = new(); + using JxlImage3 first = new(TestEnvironment.Configuration, 1, 1); + JxlImageOperations.FillImage(3F, first); + Assert.True(bundle.SetFromImage(first, encoding)); + + using JxlImage3 rejected = new(TestEnvironment.Configuration, 2, 1); + JxlImageOperations.FillImage(9F, rejected); + JxlColorEncoding gray = new() { ColorSpace = JxlColorSpace.Gray }; + + Assert.False(bundle.SetFromImage(rejected, gray)); + Assert.Equal(1, bundle.Color.XSize); + Assert.Equal(2, rejected.XSize); + Assert.Same(encoding, bundle.CurrentColorEncoding); + + for (int c = 0; c < 3; c++) + { + Assert.Equal(3F, bundle.Color.PlaneRow(c, 0)[0]); + Assert.Equal(9F, rejected.PlaneRow(c, 0)[0]); + } + } + /// /// Checks that released plane storage cannot be reused through its previous dimensions. /// diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/Decoder/JxlDecoderCoreTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Decoder/JxlDecoderCoreTests.cs index d8e17ce826..f181bac56d 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/Decoder/JxlDecoderCoreTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Decoder/JxlDecoderCoreTests.cs @@ -300,11 +300,11 @@ static void Upsample7(JxlPlane input, JxlPlane output) } } - JxlImage3 preview = new(configuration, ib.XSize * 7, ib.YSize * 7); + using JxlImage3 preview = new(configuration, ib.XSize * 7, ib.YSize * 7); for (int c = 0; c < 3; c++) { - Upsample7(ib.Color!.Plane(c), preview.Plane(c)); + Upsample7(ib.Color.Plane(c), preview.Plane(c)); } List> extraChannels = []; From 4efeece497a5ca1e7ebb3baa543e158d3c1435c7 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 23:24:17 +1000 Subject: [PATCH 280/328] Correct JPEG XL color correlation midpoint rounding --- .../JxlChromaFromLumaEncoder.cs | 13 +++++++- .../Encoder/ChromaFromLumaEncoderTests.cs | 32 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/ChromaFromLumaEncoderTests.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlChromaFromLumaEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlChromaFromLumaEncoder.cs index b2fb99d575..90a350f16e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlChromaFromLumaEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlChromaFromLumaEncoder.cs @@ -31,6 +31,16 @@ public static JxlPlane InitializeDcStorage(Configuration configuration, i return dcValues; } + /// + /// Selects the signed luma-to-chroma multiplier adjustment from observed coefficient pairs. + /// + /// The luma-derived multipliers for the observations. + /// The chroma values for the observations. + /// The number of observations, excluding storage padding. + /// The base luma-to-chroma multiplier before the encoded adjustment. + /// The regularization strength for the encoded adjustment. + /// Whether to use the quadratic estimate instead of the iterative loss. + /// The adjustment in [-128,127]. Midpoints round away from zero. public static int FindBestMultiplier(Span valuesM, Span valuesS, int num, float baseValue, float distanceMul, bool fast) { if (num == 0) @@ -123,7 +133,8 @@ public static int FindBestMultiplier(Span valuesM, Span valuesS, i x = 0; } - return (int)Math.Clamp(MathF.Round(x), -128.0f, 127.0f); + // The encoded adjustment uses native std::round midpoint rules. Ties-to-even would choose a different correlation table entry. + return (int)Math.Clamp(MathF.Round(x, MidpointRounding.AwayFromZero), -128.0f, 127.0f); } /// diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/ChromaFromLumaEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/ChromaFromLumaEncoderTests.cs new file mode 100644 index 0000000000..7d35f5ee8d --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/ChromaFromLumaEncoderTests.cs @@ -0,0 +1,32 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Processing; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.ChromaFromLuma; + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing.Encoder; + +public class ChromaFromLumaEncoderTests +{ + /// + /// Checks the native rounding rule where an estimated correlation reaches an exact midpoint after the zero bias. + /// + /// The unregularized multiplier estimate. + /// The encoded integer adjustment. + [Theory] + [InlineData(3.1F, 1)] + [InlineData(-3.1F, -1)] + [InlineData(5.1F, 3)] + [InlineData(-5.1F, -3)] + public void FindBestMultiplier_RoundsMidpointsAwayFromZero(float estimate, int expected) + { + // A luma coefficient equal to the default color factor makes the normalized quadratic coefficient one. + // With no base or regularization, the estimate equals chroma. Subtracting the 2.6 bias leaves an exact half-integer. + float[] luma = [JxlChromaFromLuma.DefaultColorFactor]; + float[] chroma = [estimate]; + + int actual = JxlChromaFromLumaEncoder.FindBestMultiplier(luma, chroma, 1, 0F, 0F, true); + + Assert.Equal(expected, actual); + } +} From bee86797642115392df9521741532e9ef36e58e1 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 23:37:23 +1000 Subject: [PATCH 281/328] Reconstruct JPEG XL palettes with shared SIMD gathers --- .../Modular/Transforms/JxlPalette.cs | 585 ++++++++++-------- .../Formats/Jxl/Processing/PaletteTests.cs | 299 +++++++++ 2 files changed, 636 insertions(+), 248 deletions(-) create mode 100644 tests/ImageSharp.Tests/Formats/Jxl/Processing/PaletteTests.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs index 6bd1ea5d2a..ae5861e030 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs @@ -2,9 +2,11 @@ // Licensed under the Six Labors Split License. using System.Buffers; +using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; @@ -228,17 +230,15 @@ public static void MetaPalette(Configuration configuration, JxlModularImage inpu } /// - /// Decodes palette/indexed images. + /// Restores palette components from the retained index channel. /// - /// The configuration used for restored channel samples. - /// Input & out images. - /// Offset of output channel. - /// Number of colors. - /// Number of deltas. - /// Kind of predictor mode to use. - /// For weighted prediction. - /// Thrown when the input for prediction is invalid. - /// Thrown when there are too many channels. + /// The configuration that owns restored channel samples and weighted-prediction storage. + /// The modular image whose palette and index channel are consumed. + /// The first restored component, excluding the leading palette metachannel. + /// The number of absolute colors declared by the transform. The palette width also includes delta entries. + /// The explicit delta count. Every smaller index, including a negative index, receives prediction. + /// The prediction method applied to delta entries. + /// The settings used when predictor is Weighted. public static void InversePalette( Configuration configuration, JxlModularImage input, @@ -253,310 +253,399 @@ public static void InversePalette( throw new InvalidOperationException("A palette transform was invoked without a palette"); } - int nb = input.Channels[0].Height; - int c0 = beginC + 1; - - if (c0 >= input.Channels.Count) + int components = input.Channels[0].Height; + int firstChannel = beginC + 1; + if (firstChannel >= input.Channels.Count) { throw new InvalidOperationException("Channel is out of range"); } - JxlModularChannel channel = input.Channels[c0]; - int w = channel.Width; - int h = channel.Height; - - if (nb < 1) + if (components < 1) { throw new InvalidOperationException("Transforms are corrupted"); } - // Each new channel belongs to input as soon as its samples are allocated. A later failure leaves no unattached owner. - input.Channels.EnsureCapacity(input.Channels.Count + nb - 1); + JxlModularChannel indexChannel = input.Channels[firstChannel]; + int width = indexChannel.Width; + int height = indexChannel.Height; - for (int i = 1; i < nb; i++) + // Reserve references before allocating samples. Each completed channel immediately belongs to input, including on a later failure. + input.Channels.EnsureCapacity(input.Channels.Count + components - 1); + for (int component = 1; component < components; component++) { - JxlModularChannel newChannel = new(configuration, w, h, channel.HorizontalShift, channel.VerticalShift); - input.Channels.Insert(c0 + 1, newChannel); + JxlModularChannel restored = new(configuration, width, height, indexChannel.HorizontalShift, indexChannel.VerticalShift); + input.Channels.Insert(firstChannel + 1, restored); } JxlModularChannel palette = input.Channels[0]; + if (width != 0) + { + ReadOnlySpan paletteSamples = palette.GetRow(0); + ReadOnlySpan indices = indexChannel.GetRow(0); + int paletteStride = palette.Plane.PixelsPerRow; + int indexStride = indexChannel.Plane.PixelsPerRow; + int bitDepth = Math.Min(input.BitDepth, 24); - int oneRow = palette.Plane.PixelsPerRow; - int bitDepth = Math.Min(input.BitDepth, 24); - Span paletteData = palette.GetRow(0); + // Retain the native one-component shortcut when an explicit table exists. An empty table has no endpoint to clamp to; + // its entries use the general implicit-color rules. Multi-component and predictive transforms retain their complete signed indices. + bool clampIndices = components == 1 && nbDeltas == 0 && predictor == JxlPredictor.Zero && palette.Width != 0; - if (w == 0) - { - // Channel is empty. Don't do anything. - } - else if (nbDeltas == 0 && predictor == JxlPredictor.Zero) - { - if (nb == 1) + if (predictor == JxlPredictor.Weighted) { - Span channel0Samples = channel.GetRow(0); - int channel0Stride = channel.Plane.PixelsPerRow; + using JxlModularState weightedState = new(configuration, weightedHeader, width, 1); - for (int y = 0; y < h; y++) + // Components have independent prediction histories. Reconstruct component zero last so every other component + // finishes reading the original index plane before its samples are replaced. No separate index image is needed. + for (int component = components - 1; component >= 0; component--) { - Span p = channel0Samples[(y * channel0Stride)..]; + JxlModularChannel output = input.Channels[firstChannel + component]; + Span outputSamples = output.GetRow(0); + int outputStride = output.Plane.PixelsPerRow; + Span weightedStorage = weightedState.Reset(weightedHeader, width); - for (int x = 0; x < w; x++) + for (int y = 0; y < height; y++) { - int index = Math.Clamp(p[x], 0, palette.Width - 1); + ReadOnlySpan indexRow = indices.Slice(y * indexStride, width); + Span outputRow = outputSamples.Slice(y * outputStride, width); - p[x] = GetPaletteValue(paletteData, index, 0, palette.Width, oneRow, bitDepth); - } - } - } - else if (nb == 2) - { - Span channel0Samples = channel.GetRow(0); - int channel0Stride = channel.Plane.PixelsPerRow; - Span channel1Samples = input.Channels[c0 + 1].GetRow(0); - int channel1Stride = input.Channels[c0 + 1].Plane.PixelsPerRow; + for (int x = 0; x < width;) + { + int count = ExpandPaletteBlock( + indexRow[x..], + outputRow[x..], + paletteSamples, + component, + palette.Width, + paletteStride, + bitDepth, + nbDeltas, + clampIndices, + out uint deltaMask); + + // Gather independent palette values first. Prediction then runs left to right because each sample uses restored neighbors. + // The mask retains the original index test even when component zero has overwritten that block of indices. + for (int lane = 0; lane < count; lane++) + { + int sampleX = x + lane; + JxlPredictionResult prediction = JxlContextPrediction.PredictNoTreeWeightedPrediction( + width, ref outputRow[sampleX], outputStride, sampleX, y, predictor, weightedState, weightedStorage, 0); - for (int y = 0; y < h; y++) - { - Span p0 = channel0Samples[(y * channel0Stride)..]; - Span p1 = channel1Samples[(y * channel1Stride)..]; + if ((deltaMask & (1U << lane)) != 0) + { + outputRow[sampleX] = unchecked(outputRow[sampleX] + prediction.Guess); + } - for (int x = 0; x < w; x++) - { - int index0 = Math.Clamp(p0[x], 0, palette.Width - 1); - int index1 = Math.Clamp(p1[x], 0, palette.Width - 1); + // Absolute palette entries also update the weighted history. Skipping them changes later delta predictions. + weightedState.UpdatePredictionErrors(outputRow[sampleX], sampleX, y, weightedStorage, 0); + } - p0[x] = GetPaletteValue(paletteData, index0, 0, palette.Width, oneRow, bitDepth); - p1[x] = GetPaletteValue(paletteData, index1, 0, palette.Width, oneRow, bitDepth); + x += count; + } } } } - else if (nb == 3) + else { - Span channel0Samples = channel.GetRow(0); - int channel0Stride = channel.Plane.PixelsPerRow; - Span channel1Samples = input.Channels[c0 + 1].GetRow(0); - int channel1Stride = input.Channels[c0 + 1].Plane.PixelsPerRow; - Span channel2Samples = input.Channels[c0 + 2].GetRow(0); - int channel2Stride = input.Channels[c0 + 2].Plane.PixelsPerRow; - - for (int y = 0; y < h; y++) + // Use the same descending ownership order for zero and fixed prediction. Component zero consumes each index before replacing it. + for (int component = components - 1; component >= 0; component--) { - Span p0 = channel0Samples[(y * channel0Stride)..]; - Span p1 = channel1Samples[(y * channel1Stride)..]; - Span p2 = channel2Samples[(y * channel2Stride)..]; + JxlModularChannel output = input.Channels[firstChannel + component]; + Span outputSamples = output.GetRow(0); + int outputStride = output.Plane.PixelsPerRow; - for (int x = 0; x < w; x++) + for (int y = 0; y < height; y++) { - int index0 = Math.Clamp(p0[x], 0, palette.Width - 1); - int index1 = Math.Clamp(p1[x], 0, palette.Width - 1); - int index2 = Math.Clamp(p2[x], 0, palette.Width - 1); + ReadOnlySpan indexRow = indices.Slice(y * indexStride, width); + Span outputRow = outputSamples.Slice(y * outputStride, width); - p0[x] = GetPaletteValue(paletteData, index0, 0, palette.Width, oneRow, bitDepth); - p1[x] = GetPaletteValue(paletteData, index1, 0, palette.Width, oneRow, bitDepth); - p2[x] = GetPaletteValue(paletteData, index2, 0, palette.Width, oneRow, bitDepth); - } - } - } - else if (nb == 4) - { - Span channel0Samples = channel.GetRow(0); - int channel0Stride = channel.Plane.PixelsPerRow; - Span channel1Samples = input.Channels[c0 + 1].GetRow(0); - int channel1Stride = input.Channels[c0 + 1].Plane.PixelsPerRow; - Span channel2Samples = input.Channels[c0 + 2].GetRow(0); - int channel2Stride = input.Channels[c0 + 2].Plane.PixelsPerRow; - Span channel3Samples = input.Channels[c0 + 3].GetRow(0); - int channel3Stride = input.Channels[c0 + 3].Plane.PixelsPerRow; + for (int x = 0; x < width;) + { + int count = ExpandPaletteBlock( + indexRow[x..], + outputRow[x..], + paletteSamples, + component, + palette.Width, + paletteStride, + bitDepth, + nbDeltas, + clampIndices, + out uint deltaMask); + + if (predictor != JxlPredictor.Zero) + { + for (int lane = 0; lane < count; lane++) + { + if ((deltaMask & (1U << lane)) != 0) + { + int sampleX = x + lane; + JxlPredictionResult prediction = JxlContextPrediction.PredictNoTreeNoWeightedPrediction( + width, ref outputRow[sampleX], outputStride, sampleX, y, predictor); - for (int y = 0; y < h; y++) - { - Span p0 = channel0Samples[(y * channel0Stride)..]; - Span p1 = channel1Samples[(y * channel1Stride)..]; - Span p2 = channel2Samples[(y * channel2Stride)..]; - Span p3 = channel3Samples[(y * channel3Stride)..]; + outputRow[sampleX] = unchecked(outputRow[sampleX] + prediction.Guess); + } + } + } - for (int x = 0; x < w; x++) - { - int index0 = Math.Clamp(p0[x], 0, palette.Width - 1); - int index1 = Math.Clamp(p1[x], 0, palette.Width - 1); - int index2 = Math.Clamp(p2[x], 0, palette.Width - 1); - int index3 = Math.Clamp(p3[x], 0, palette.Width - 1); - - p0[x] = GetPaletteValue(paletteData, index0, 0, palette.Width, oneRow, bitDepth); - p1[x] = GetPaletteValue(paletteData, index1, 0, palette.Width, oneRow, bitDepth); - p2[x] = GetPaletteValue(paletteData, index2, 0, palette.Width, oneRow, bitDepth); - p3[x] = GetPaletteValue(paletteData, index3, 0, palette.Width, oneRow, bitDepth); + x += count; + } } } } - else if (nb == 5) - { - Span channel0Samples = channel.GetRow(0); - int channel0Stride = channel.Plane.PixelsPerRow; - Span channel1Samples = input.Channels[c0 + 1].GetRow(0); - int channel1Stride = input.Channels[c0 + 1].Plane.PixelsPerRow; - Span channel2Samples = input.Channels[c0 + 2].GetRow(0); - int channel2Stride = input.Channels[c0 + 2].Plane.PixelsPerRow; - Span channel3Samples = input.Channels[c0 + 3].GetRow(0); - int channel3Stride = input.Channels[c0 + 3].Plane.PixelsPerRow; - Span channel4Samples = input.Channels[c0 + 4].GetRow(0); - int channel4Stride = input.Channels[c0 + 4].Plane.PixelsPerRow; - - for (int y = 0; y < h; y++) - { - Span p0 = channel0Samples[(y * channel0Stride)..]; - Span p1 = channel1Samples[(y * channel1Stride)..]; - Span p2 = channel2Samples[(y * channel2Stride)..]; - Span p3 = channel3Samples[(y * channel3Stride)..]; - Span p4 = channel4Samples[(y * channel4Stride)..]; + } - for (int x = 0; x < w; x++) - { - int index0 = Math.Clamp(p0[x], 0, palette.Width - 1); - int index1 = Math.Clamp(p1[x], 0, palette.Width - 1); - int index2 = Math.Clamp(p2[x], 0, palette.Width - 1); - int index3 = Math.Clamp(p3[x], 0, palette.Width - 1); - int index4 = Math.Clamp(p4[x], 0, palette.Width - 1); - - p0[x] = GetPaletteValue(paletteData, index0, 0, palette.Width, oneRow, bitDepth); - p1[x] = GetPaletteValue(paletteData, index1, 0, palette.Width, oneRow, bitDepth); - p2[x] = GetPaletteValue(paletteData, index2, 0, palette.Width, oneRow, bitDepth); - p3[x] = GetPaletteValue(paletteData, index3, 0, palette.Width, oneRow, bitDepth); - p4[x] = GetPaletteValue(paletteData, index4, 0, palette.Width, oneRow, bitDepth); - } - } + if (firstChannel >= input.MetaChannels) + { + input.MetaChannels--; + } + else + { + if (input.MetaChannels < 2 - components) + { + throw new InvalidOperationException("Too many meta channels"); } - else + + input.MetaChannels -= 2 - components; + if (beginC + components - 1 >= input.MetaChannels) { - throw new NotImplementedException($"Too many channels for palette compressed images: {nb}"); + throw new InvalidOperationException("Too many meta channels"); } } - else + + // The palette remains owned and readable until every restored component has consumed it. + palette.Dispose(); + input.Channels.RemoveAt(0); + } + + /// + /// Expands one complete SIMD block or one scalar remainder and retains its original predictive-delta decisions. + /// + /// The remaining index samples in the row. + /// The equally positioned output, which can begin at the same address as indices. + /// The complete palette storage, including component rows and padding. + /// The palette component to restore. + /// The logical explicit table width, including explicit delta entries. + /// The distance between component rows, in integers. + /// The implicit-color precision, capped to 24 bits by the caller. + /// The threshold below which entries receive prediction. + /// Whether to retain the nonempty one-component compatibility shortcut. + /// One bit per written sample whose original index is below deltaCount. + /// The number of expanded samples. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int ExpandPaletteBlock( + ReadOnlySpan indices, + Span destination, + ReadOnlySpan palette, + int component, + int paletteSize, + int paletteStride, + int bitDepth, + int deltaCount, + bool clampIndices, + out uint deltaMask) + { + ref int input = ref MemoryMarshal.GetReference(indices); + ref int output = ref MemoryMarshal.GetReference(destination); + + // Load the complete index register and its delta mask before storing. This permits exact in-place expansion of component zero. + if (Vector512.IsHardwareAccelerated && indices.Length >= Vector512.Count) { - JxlPlane plane = input.Channels[c0].Plane; - using JxlPlane indices = new(configuration, plane.XSize, plane.YSize); + Vector512 index = Vector512.LoadUnsafe(ref input); + deltaMask = (uint)Vector512.ExtractMostSignificantBits(Vector512.LessThan(index, Vector512.Create(deltaCount))); + if (clampIndices) + { + index = Vector512.Clamp(index, Vector512.Zero, Vector512.Create(paletteSize - 1)); + } - // Keep the original index samples separate from reconstructed output. The channel retains its plane wrapper; - // the scoped temporary receives the old index allocation and releases it after every output channel has read it. - plane.Swap(indices); + GetPaletteValue(palette, index, component, paletteSize, paletteStride, bitDepth).StoreUnsafe(ref output); + return Vector512.Count; + } + + if (Vector256.IsHardwareAccelerated && indices.Length >= Vector256.Count) + { + Vector256 index = Vector256.LoadUnsafe(ref input); + deltaMask = Vector256.ExtractMostSignificantBits(Vector256.LessThan(index, Vector256.Create(deltaCount))); + if (clampIndices) + { + index = Vector256.Clamp(index, Vector256.Zero, Vector256.Create(paletteSize - 1)); + } - Span indexSamples = indices.GetRow(0); - int indexStride = indices.PixelsPerRow; + GetPaletteValue(palette, index, component, paletteSize, paletteStride, bitDepth).StoreUnsafe(ref output); + return Vector256.Count; + } - // Read each output stride after the swap. The original indices can retain wider storage from a metadata shrink. - if (predictor == JxlPredictor.Weighted) + if (Vector128.IsHardwareAccelerated && indices.Length >= Vector128.Count) + { + Vector128 index = Vector128.LoadUnsafe(ref input); + deltaMask = Vector128.ExtractMostSignificantBits(Vector128.LessThan(index, Vector128.Create(deltaCount))); + if (clampIndices) { - using JxlModularState wpState = new(configuration, weightedHeader, indices.XSize, 1); + index = Vector128.Clamp(index, Vector128.Zero, Vector128.Create(paletteSize - 1)); + } - for (int c = 0; c < nb; c++) - { - JxlModularChannel outputChannel = input.Channels[c0 + c]; - int oneRowImage = outputChannel.Plane.PixelsPerRow; - Span outputSamples = outputChannel.GetRow(0); - Span weightedStorage = wpState.Reset(weightedHeader, outputChannel.Width); + GetPaletteValue(palette, index, component, paletteSize, paletteStride, bitDepth).StoreUnsafe(ref output); + return Vector128.Count; + } - for (int y = 0; y < outputChannel.Height; y++) - { - Span p = outputSamples[(y * oneRowImage)..]; - Span idx = indexSamples[(y * indexStride)..]; + int scalarIndex = input; + deltaMask = scalarIndex < deltaCount ? 1U : 0U; + if (clampIndices) + { + scalarIndex = Math.Clamp(scalarIndex, 0, paletteSize - 1); + } - for (int x = 0; x < outputChannel.Width; x++) - { - int index = idx[x]; - int value = 0; - int paletteEntry = GetPaletteValue(paletteData, index, c, palette.Width, oneRow, bitDepth); - JxlPredictionResult pred = JxlContextPrediction.PredictNoTreeWeightedPrediction( - outputChannel.Width, - ref p[x], - oneRowImage, - x, - y, - predictor, - wpState, - weightedStorage, - 0); + output = GetPaletteValue(palette, scalarIndex, component, paletteSize, paletteStride, bitDepth); + return 1; + } - if (index < nbDeltas) - { - value = pred.Guess + paletteEntry; - } - else - { - value = paletteEntry; - } + /// + /// Resolves 4 explicit or implicit palette values without changing lane order. + /// + /// The complete borrowed palette storage. + /// The original signed palette indices. + /// The component to restore. + /// The explicit table width, excluding padding. + /// The distance between palette component rows, in integers. + /// The implicit-color precision, capped to 24 bits. + /// The resolved values. Negative indices and implicit cubes use the scalar format rules. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 GetPaletteValue( + ReadOnlySpan palette, + Vector128 indices, + int component, + int paletteSize, + int paletteStride, + int bitDepth) + { + // An unsigned comparison excludes negative indices as well as the implicit cubes beyond the explicit table. + Vector128 explicitMask = Vector128.LessThan(indices.AsUInt32(), Vector128.Create((uint)paletteSize)).AsInt32(); + Vector128 values = Vector128.Zero; + if (paletteSize != 0) + { + ref int table = ref Unsafe.Add(ref MemoryMarshal.GetReference(palette), component * paletteStride); - p[x] = value; - wpState.UpdatePredictionErrors(p[x], x, y, weightedStorage, 0); - } - } - } - } - else - { - for (int c = 0; c < nb; c++) - { - JxlModularChannel outputChannel = input.Channels[c0 + c]; - int oneRowImage = outputChannel.Plane.PixelsPerRow; - Span outputSamples = outputChannel.GetRow(0); + // Every gather address must be valid, including lanes later replaced by implicit values. Entry zero exists only in a nonempty table. + Vector128 safeIndices = Vector128.ConditionalSelect(explicitMask, indices, Vector128.Zero); + values = Vector128_.Gather(ref table, safeIndices); + } - for (int y = 0; y < outputChannel.Height; y++) - { - Span p = outputSamples[(y * oneRowImage)..]; - Span idx = indexSamples[(y * indexStride)..]; + if (component >= RgbChannels) + { + // Implicit colors and deltas define only three components. Extra components keep explicit entries and otherwise become zero. + return Vector128.ConditionalSelect(explicitMask, values, Vector128.Zero); + } - for (int x = 0; x < outputChannel.Width; x++) - { - int index = idx[x]; - int value = 0; - int paletteEntry = GetPaletteValue(paletteData, index, c, palette.Width, oneRow, bitDepth); + uint implicitMask = ~Vector128.ExtractMostSignificantBits(explicitMask) & ((1U << Vector128.Count) - 1); + while (implicitMask != 0) + { + // Resolve only exceptional lanes. Keep their original indices; the safe gather indices are not color-clipping decisions. + int lane = BitOperations.TrailingZeroCount(implicitMask); + int value = GetPaletteValue(palette, indices.GetElement(lane), component, paletteSize, paletteStride, bitDepth); + values = values.WithElement(lane, value); + implicitMask &= implicitMask - 1; + } - if (index < nbDeltas) - { - JxlPredictionResult pred = JxlContextPrediction.PredictNoTreeNoWeightedPrediction( - outputChannel.Width, - p[x..], - oneRowImage, - x, - y, - predictor); - - value = pred.Guess + paletteEntry; - } - else - { - value = paletteEntry; - } + return values; + } - p[x] = value; - } - } - } - } + /// + /// Resolves 8 explicit or implicit palette values without changing lane order. + /// + /// The complete borrowed palette storage. + /// The original signed palette indices. + /// The component to restore. + /// The explicit table width, excluding padding. + /// The distance between palette component rows, in integers. + /// The implicit-color precision, capped to 24 bits. + /// The resolved values. Negative indices and implicit cubes use the scalar format rules. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 GetPaletteValue( + ReadOnlySpan palette, + Vector256 indices, + int component, + int paletteSize, + int paletteStride, + int bitDepth) + { + // An unsigned comparison excludes negative indices as well as the implicit cubes beyond the explicit table. + Vector256 explicitMask = Vector256.LessThan(indices.AsUInt32(), Vector256.Create((uint)paletteSize)).AsInt32(); + Vector256 values = Vector256.Zero; + if (paletteSize != 0) + { + ref int table = ref Unsafe.Add(ref MemoryMarshal.GetReference(palette), component * paletteStride); + + // Every gather address must be valid, including lanes later replaced by implicit values. Entry zero exists only in a nonempty table. + Vector256 safeIndices = Vector256.ConditionalSelect(explicitMask, indices, Vector256.Zero); + values = Vector256_.Gather(ref table, safeIndices); } - if (c0 >= input.MetaChannels) + if (component >= RgbChannels) { - input.MetaChannels--; + // Implicit colors and deltas define only three components. Extra components keep explicit entries and otherwise become zero. + return Vector256.ConditionalSelect(explicitMask, values, Vector256.Zero); } - else + + uint implicitMask = ~Vector256.ExtractMostSignificantBits(explicitMask) & ((1U << Vector256.Count) - 1); + while (implicitMask != 0) { - if (input.MetaChannels < 2 - nb) - { - throw new InvalidOperationException("Too many meta channels"); - } + // Resolve only exceptional lanes. Keep their original indices; the safe gather indices are not color-clipping decisions. + int lane = BitOperations.TrailingZeroCount(implicitMask); + int value = GetPaletteValue(palette, indices.GetElement(lane), component, paletteSize, paletteStride, bitDepth); + values = values.WithElement(lane, value); + implicitMask &= implicitMask - 1; + } - input.MetaChannels -= 2 - nb; + return values; + } - if (beginC + nb - 1 >= input.MetaChannels) - { - throw new InvalidOperationException("Too many meta channels"); - } + /// + /// Resolves 16 explicit or implicit palette values without changing lane order. + /// + /// The complete borrowed palette storage. + /// The original signed palette indices. + /// The component to restore. + /// The explicit table width, excluding padding. + /// The distance between palette component rows, in integers. + /// The implicit-color precision, capped to 24 bits. + /// The resolved values. Negative indices and implicit cubes use the scalar format rules. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 GetPaletteValue( + ReadOnlySpan palette, + Vector512 indices, + int component, + int paletteSize, + int paletteStride, + int bitDepth) + { + // An unsigned comparison excludes negative indices as well as the implicit cubes beyond the explicit table. + Vector512 explicitMask = Vector512.LessThan(indices.AsUInt32(), Vector512.Create((uint)paletteSize)).AsInt32(); + Vector512 values = Vector512.Zero; + if (paletteSize != 0) + { + ref int table = ref Unsafe.Add(ref MemoryMarshal.GetReference(palette), component * paletteStride); + // Every gather address must be valid, including lanes later replaced by implicit values. Entry zero exists only in a nonempty table. + Vector512 safeIndices = Vector512.ConditionalSelect(explicitMask, indices, Vector512.Zero); + values = Vector512_.Gather(ref table, safeIndices); } - // Both normal-channel and metachannel palettes consume the leading palette after its final read. - palette.Dispose(); - input.Channels.RemoveAt(0); + if (component >= RgbChannels) + { + // Implicit colors and deltas define only three components. Extra components keep explicit entries and otherwise become zero. + return Vector512.ConditionalSelect(explicitMask, values, Vector512.Zero); + } + + uint implicitMask = ~(uint)Vector512.ExtractMostSignificantBits(explicitMask) & ((1U << Vector512.Count) - 1); + while (implicitMask != 0) + { + // Resolve only exceptional lanes. Keep their original indices; the safe gather indices are not color-clipping decisions. + int lane = BitOperations.TrailingZeroCount(implicitMask); + int value = GetPaletteValue(palette, indices.GetElement(lane), component, paletteSize, paletteStride, bitDepth); + values = values.WithElement(lane, value); + implicitMask &= implicitMask - 1; + } + + return values; } /// diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/PaletteTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/PaletteTests.cs new file mode 100644 index 0000000000..16187c4e73 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/PaletteTests.cs @@ -0,0 +1,299 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; +using SixLabors.ImageSharp.Tests.Memory; +using SixLabors.ImageSharp.Tests.TestUtilities; + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; + +/// +/// Tests inverse palette sample reconstruction and its channel ownership. +/// +public class PaletteTests +{ + /// + /// Checks component selection, retained index strides, channel placement and allocation ownership at every SIMD width. + /// + [Fact] + public void InversePalette_RestoresComponentsAndRetainsIndexStorage() + { + static void RunTest() + { + ReadOnlySpan componentCounts = [1, 3, 6]; + foreach (int components in componentCounts) + { + TestMemoryAllocator allocator = new(173); + allocator.EnableNonThreadSafeLogging(); + Configuration configuration = TestEnvironment.Configuration.Clone(); + configuration.MemoryAllocator = allocator; + + using (JxlModularImage image = JxlModularImage.Create(configuration, 65, 2, 8, components + 2)) + { + // A channel before and after the selected range must survive. Retained indices keep an 80-sample stride; + // newly restored 33-sample channels use a 48-sample stride. Every component must read the same logical index row. + image.Channels[0].GetRow(0).Fill(-777); + JxlModularChannel prefix = image.Channels[0]; + JxlModularChannel suffix = image.Channels[^1]; + for (int component = 0; component < components; component++) + { + image.Channels[1 + component].Shrink(33, 2); + } + + JxlPalette.MetaPalette(configuration, image, 1, components, 3, 0); + JxlModularChannel indices = image.Channels[2]; + int indexStride = indices.Plane.PixelsPerRow; + Span indexSamples = indices.GetRow(0); + for (int y = 0; y < 2; y++) + { + for (int x = 0; x < 33; x++) + { + indexSamples[(y * indexStride) + x] = (x + y) % 3; + } + + indexSamples.Slice((y * indexStride) + 33, indexStride - 33).Fill(int.MinValue); + } + + JxlModularChannel palette = image.Channels[0]; + for (int component = 0; component < components; component++) + { + Span entries = palette.GetRow(component); + entries[0] = (component * 100) + 11; + entries[1] = (component * 100) + 22; + entries[2] = (component * 100) + 33; + } + + int allocationCount = allocator.AllocationLog.Count; + JxlPalette.InversePalette(configuration, image, 1, 3, 0, JxlPredictor.Zero, new JxlModularHeader()); + + // Only the missing output components need allocations. A second full index plane would add another allocation here. + Assert.Equal(allocationCount + components - 1, allocator.AllocationLog.Count); + Assert.Equal(components + 2, image.Channels.Count); + Assert.Equal(0, image.MetaChannels); + Assert.Same(prefix, image.Channels[0]); + Assert.Same(indices, image.Channels[1]); + Assert.Same(suffix, image.Channels[^1]); + Assert.Equal(indexStride, indices.Plane.PixelsPerRow); + Assert.Equal(-777, prefix.GetRow(0)[0]); + + for (int component = 0; component < components; component++) + { + JxlModularChannel restored = image.Channels[1 + component]; + Span samples = restored.GetRow(0); + int stride = restored.Plane.PixelsPerRow; + for (int y = 0; y < 2; y++) + { + for (int x = 0; x < 33; x++) + { + int expected = (component * 100) + (11 * (((x + y) % 3) + 1)); + Assert.Equal(expected, samples[(y * stride) + x]); + } + } + } + + // No output tail may overwrite retained index padding, even when its original stride is wider than other outputs. + for (int y = 0; y < 2; y++) + { + ReadOnlySpan padding = indexSamples.Slice((y * indexStride) + 33, indexStride - 33); + for (int x = 0; x < padding.Length; x++) + { + Assert.Equal(int.MinValue, padding[x]); + } + } + } + + Assert.Equal(0, allocator.LiveBufferBytes); + } + + // Preserve the native complete-table shortcut for one component with no prediction. + using JxlModularImage clamped = JxlModularImage.Create(TestEnvironment.Configuration, 33, 1, 8, 1); + JxlPalette.MetaPalette(TestEnvironment.Configuration, clamped, 0, 0, 2, 0); + clamped.Channels[0].GetRow(0)[0] = 11; + clamped.Channels[0].GetRow(0)[1] = 22; + ReadOnlySpan codes = [-4, 0, 1, 2, int.MaxValue]; + Span clampedIndices = clamped.Channels[1].GetRow(0); + for (int x = 0; x < 33; x++) + { + clampedIndices[x] = codes[x % codes.Length]; + } + + JxlPalette.InversePalette(TestEnvironment.Configuration, clamped, 0, 2, 0, JxlPredictor.Zero, new JxlModularHeader()); + ReadOnlySpan expectedClamped = [11, 11, 22, 22, 22]; + ReadOnlySpan clampedSamples = clamped.Channels[0].GetRow(0); + for (int x = 0; x < 33; x++) + { + Assert.Equal(expectedClamped[x % expectedClamped.Length], clampedSamples[x]); + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } + + /// + /// Checks negative deltas, both implicit cubes, signed index extremes and extra components against fixed integer expectations. + /// + [Fact] + public void InversePalette_PreservesImplicitIndices() + { + static void RunTest() + { + ReadOnlySpan precisions = [8, 24]; + ReadOnlySpan codes = [-1, -2, -3, -144, int.MinValue, 2, 65, 66, 190, 191, int.MaxValue, 0, 1]; + foreach (int precision in precisions) + { + using JxlModularImage image = JxlModularImage.Create(TestEnvironment.Configuration, 33, 1, precision, 6); + JxlPalette.MetaPalette(TestEnvironment.Configuration, image, 0, 5, 2, 0); + for (int component = 0; component < 6; component++) + { + Span entries = image.Channels[0].GetRow(component); + entries[0] = (component * 100) + 10; + entries[1] = (component * 100) + 20; + } + + Span indices = image.Channels[1].GetRow(0); + for (int x = 0; x < 33; x++) + { + indices[x] = codes[x % codes.Length]; + } + + // The negative period has 143 entries: Int32.MinValue selects signed delta [0,-32,-32]. + // Int32.MaxValue selects large-cube digits [1,1,3]. At 24 bits, scale those digits with the format's integer rounding. + int[][] expected = precision == 8 + ? + [ + [0, 4, -4, 0, 0, 32, 223, 0, 255, 0, 63, 10, 20], + [0, 4, -4, 0, -32, 32, 223, 0, 255, 0, 63, 110, 120], + [0, 4, -4, 0, -32, 32, 223, 0, 255, 0, 191, 210, 220] + ] + : + [ + [0, 262144, -262144, 0, 0, 2097152, 14680063, 0, 16777215, 0, 4194303, 10, 20], + [0, 262144, -262144, 0, -2097152, 2097152, 14680063, 0, 16777215, 0, 4194303, 110, 120], + [0, 262144, -262144, 0, -2097152, 2097152, 14680063, 0, 16777215, 0, 12582911, 210, 220] + ]; + + JxlPalette.InversePalette(TestEnvironment.Configuration, image, 0, 2, 0, JxlPredictor.Zero, new JxlModularHeader()); + for (int component = 0; component < 6; component++) + { + ReadOnlySpan samples = image.Channels[component].GetRow(0); + for (int x = 0; x < 33; x++) + { + int slot = x % codes.Length; + int value = component < 3 ? expected[component][slot] + : codes[slot] == 0 ? (component * 100) + 10 + : codes[slot] == 1 ? (component * 100) + 20 + : 0; + + Assert.Equal(value, samples[x]); + } + } + } + + // A zero-width palette is valid metadata. No explicit entry exists, so even one-component expansion uses implicit colors. + using JxlModularImage emptyTable = JxlModularImage.Create(TestEnvironment.Configuration, 33, 1, 8, 1); + JxlPalette.MetaPalette(TestEnvironment.Configuration, emptyTable, 0, 0, 0, 0); + ReadOnlySpan emptyCodes = [-1, 0, 63, 64, 188, 189]; + ReadOnlySpan emptyExpected = [0, 32, 223, 0, 255, 0]; + Span emptyIndices = emptyTable.Channels[1].GetRow(0); + for (int x = 0; x < 33; x++) + { + emptyIndices[x] = emptyCodes[x % emptyCodes.Length]; + } + + JxlPalette.InversePalette(TestEnvironment.Configuration, emptyTable, 0, 0, 0, JxlPredictor.Zero, new JxlModularHeader()); + ReadOnlySpan emptySamples = emptyTable.Channels[0].GetRow(0); + for (int x = 0; x < 33; x++) + { + Assert.Equal(emptyExpected[x % emptyExpected.Length], emptySamples[x]); + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } + + /// + /// Checks causal delta reconstruction and independent weighted histories without allocating a copy of the index image. + /// + [Fact] + public void InversePalette_ReconstructsPredictedDeltas() + { + static void RunTest() + { + ReadOnlySpan predictors = [JxlPredictor.Left, JxlPredictor.Weighted]; + foreach (JxlPredictor predictor in predictors) + { + TestMemoryAllocator allocator = new(173); + allocator.EnableNonThreadSafeLogging(); + Configuration configuration = TestEnvironment.Configuration.Clone(); + configuration.MemoryAllocator = allocator; + + using (JxlModularImage image = JxlModularImage.Create(configuration, 33, 2, 8, 6)) + { + JxlPalette.MetaPalette(configuration, image, 0, 5, 1, 1); + for (int component = 0; component < 6; component++) + { + Span entries = image.Channels[0].GetRow(component); + entries[0] = predictor == JxlPredictor.Left ? (component + 1) * 2 : 0; + entries[1] = (component + 1) * 10; + } + + JxlModularChannel indexChannel = image.Channels[1]; + Span indices = indexChannel.GetRow(0); + int indexStride = indexChannel.Plane.PixelsPerRow; + ReadOnlySpan leftCodes = [1, 0, -2, 1]; + for (int y = 0; y < 2; y++) + { + for (int x = 0; x < 33; x++) + { + indices[(y * indexStride) + x] = predictor == JxlPredictor.Left ? leftCodes[x % 4] : x == 0 ? 1 : 0; + } + } + + int allocations = allocator.AllocationLog.Count; + JxlPalette.InversePalette(configuration, image, 0, 1, 1, predictor, new JxlModularHeader()); + Assert.Same(indexChannel, image.Channels[0]); + Assert.Equal(allocations + 5 + (predictor == JxlPredictor.Weighted ? 1 : 0), allocator.AllocationLog.Count); + + for (int component = 0; component < 6; component++) + { + JxlModularChannel output = image.Channels[component]; + ReadOnlySpan samples = output.GetRow(0); + int stride = output.Plane.PixelsPerRow; + int absolute = (component + 1) * 10; + int delta = (component + 1) * 2; + + for (int y = 0; y < 2; y++) + { + for (int x = 0; x < 33; x++) + { + // Left adds each delta to its restored predecessor, then the next absolute entry resets the run. + // Weighted prediction of a constant neighborhood retains that constant, including the absolute first sample. + int expected = predictor == JxlPredictor.Weighted ? absolute : (x % 4) switch + { + 1 => absolute + delta, + 2 => absolute + delta + (component < 3 ? 4 : 0), + _ => absolute + }; + + Assert.Equal(expected, samples[(y * stride) + x]); + } + } + } + } + + Assert.Equal(0, allocator.LiveBufferBytes); + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } +} + From c18cca2d7dde1aa19267122198b04b89a6be1d65 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 23:43:56 +1000 Subject: [PATCH 282/328] Evaluate JPEG XL correlation loss through every SIMD width --- .../Encoder/ChromaFromLuma/JxlCflFunction.cs | 388 ++++++++++++++---- .../Encoder/ChromaFromLumaEncoderTests.cs | 54 +++ .../Formats/Jxl/Processing/PaletteTests.cs | 1 - 3 files changed, 361 insertions(+), 82 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlCflFunction.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlCflFunction.cs index 15d0f97ce6..16c2c90d26 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlCflFunction.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlCflFunction.cs @@ -1,101 +1,327 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.ChromaFromLuma; +/// +/// Evaluates the regularized correlation loss and its neighboring derivatives from borrowed coefficient pairs. +/// internal readonly ref struct JxlCflFunction { - private const float Coefficient = 1.0f / 3f; - private const float Threshold = 100f; - private const float InverseColorFactor = 1.0f / JxlChromaFromLuma.DefaultColorFactor; - - private readonly Span valuesM; - private readonly Span valuesS; - private readonly int num; - private readonly float @base; + /// The derivative scale for the one-third residual loss. + private const float DerivativeScale = 2F / 3F; + + /// The residual magnitude at which an observation stops contributing to the loss. + private const float ResidualThreshold = 100F; + + /// Converts the encoded correlation adjustment to a multiplier. + private const float InverseColorFactor = 1F / JxlChromaFromLuma.DefaultColorFactor; + + private readonly ReadOnlySpan luma; + private readonly ReadOnlySpan chroma; + private readonly float baseMultiplier; private readonly float distanceMultiplier; - public JxlCflFunction( - Span valuesM, - Span valuesS, - int num, - float @base, - float distanceMultiplier) + /// + /// Borrows the logical observations used by one correlation search. Padding does not belong to the loss. + /// + /// The luma coefficients. + /// The matching chroma coefficients. + /// The number of logical coefficient pairs. + /// The base correlation multiplier before the encoded adjustment. + /// The regularization strength for the encoded adjustment. + public JxlCflFunction(ReadOnlySpan valuesM, ReadOnlySpan valuesS, int num, float baseValue, float distanceMultiplier) { - this.valuesM = valuesM; - this.valuesS = valuesS; - this.num = num; - this.@base = @base; + this.luma = valuesM[..num]; + this.chroma = valuesS[..num]; + this.baseMultiplier = baseValue; this.distanceMultiplier = distanceMultiplier; } - // Returns f'(x), where f is 1/3 * sum ((|color residual| + 1)^2-1) + - // distance_mul * x^2 * num. - public readonly float Compute(float x, float eps, ref float fpeps, ref float fmeps) + /// + /// Computes the derivative at an adjustment and at its two finite-difference neighbors. + /// + /// The current encoded-adjustment estimate. + /// The distance from the estimate to each neighboring derivative. + /// The derivative at x + eps. + /// The derivative at x - eps. + /// The derivative at x, including the quadratic regularization. + public float Compute(float x, float eps, ref float fpeps, ref float fmeps) + { + // Each coefficient pair contributes independently to three sums. Keep all three in registers in a single pass; + // separate tensor reductions would reload the coefficients or require materialized residual buffers. + DerivativeOperator operation = new(this.baseMultiplier, x, eps); + float derivative = 2F * this.distanceMultiplier * this.luma.Length * x; + fpeps = 2F * this.distanceMultiplier * this.luma.Length * (x + eps); + fmeps = 2F * this.distanceMultiplier * this.luma.Length * (x - eps); + ref float luma = ref MemoryMarshal.GetReference(this.luma); + ref float chroma = ref MemoryMarshal.GetReference(this.chroma); + nuint length = (nuint)this.luma.Length; + nuint i = 0; + + if (Vector512.IsHardwareAccelerated) + { + Vector512 sum = Vector512.Zero; + Vector512 sumPlus = Vector512.Zero; + Vector512 sumMinus = Vector512.Zero; + nuint end = this.luma.Vector512Count() * (nuint)Vector512.Count; + + for (; i < end; i += (nuint)Vector512.Count) + { + operation.Invoke( + Vector512.LoadUnsafe(ref luma, i), + Vector512.LoadUnsafe(ref chroma, i), + out Vector512 value, + out Vector512 plus, + out Vector512 minus); + + sum += value; + sumPlus += plus; + sumMinus += minus; + } + + derivative += Vector512.Sum(sum); + fpeps += Vector512.Sum(sumPlus); + fmeps += Vector512.Sum(sumMinus); + } + + if (Vector256.IsHardwareAccelerated) + { + Vector256 sum = Vector256.Zero; + Vector256 sumPlus = Vector256.Zero; + Vector256 sumMinus = Vector256.Zero; + nuint end = this.luma.Vector256Count() * (nuint)Vector256.Count; + + for (; i < end; i += (nuint)Vector256.Count) + { + operation.Invoke( + Vector256.LoadUnsafe(ref luma, i), + Vector256.LoadUnsafe(ref chroma, i), + out Vector256 value, + out Vector256 plus, + out Vector256 minus); + + sum += value; + sumPlus += plus; + sumMinus += minus; + } + + derivative += Vector256.Sum(sum); + fpeps += Vector256.Sum(sumPlus); + fmeps += Vector256.Sum(sumMinus); + } + + if (Vector128.IsHardwareAccelerated) + { + Vector128 sum = Vector128.Zero; + Vector128 sumPlus = Vector128.Zero; + Vector128 sumMinus = Vector128.Zero; + nuint end = this.luma.Vector128Count() * (nuint)Vector128.Count; + + for (; i < end; i += (nuint)Vector128.Count) + { + operation.Invoke( + Vector128.LoadUnsafe(ref luma, i), + Vector128.LoadUnsafe(ref chroma, i), + out Vector128 value, + out Vector128 plus, + out Vector128 minus); + + sum += value; + sumPlus += plus; + sumMinus += minus; + } + + derivative += Vector128.Sum(sum); + fpeps += Vector128.Sum(sumPlus); + fmeps += Vector128.Sum(sumMinus); + } + + // Smaller hardware widths consume the remainder after wider blocks. The scalar path uses the same three residuals + // and the same central-residual cutoff; it also handles every observation when hardware intrinsics are disabled. + for (; i < length; i++) + { + operation.Invoke(Unsafe.Add(ref luma, i), Unsafe.Add(ref chroma, i), out float value, out float plus, out float minus); + derivative += value; + fpeps += plus; + fmeps += minus; + } + + return derivative; + } + + /// + /// Evaluates the three derivatives for corresponding coefficient lanes without storage or traversal. + /// + private readonly struct DerivativeOperator { - float firstDerivative = 2 * this.distanceMultiplier * this.num * x; - float firstDerivativePeps = 2 * this.distanceMultiplier * this.num * (x + eps); - float firstDerivativeMeps = 2 * this.distanceMultiplier * this.num * (x - eps); - - Vector inverseColorFactor = Vector.Create(InverseColorFactor); - Vector thres = Vector.Create(Threshold); - Vector coeffx2 = Vector.Create(Coefficient * 2.0f); - Vector one = Vector.One; - Vector zero = Vector.Zero; - Vector baseV = Vector.Create(this.@base); - Vector xV = Vector.Create(x); - Vector xpeV = Vector.Create(x + eps); - Vector xmeV = Vector.Create(x - eps); - Vector fdV = Vector.Zero; - Vector fdpeV = Vector.Zero; - Vector fdmeV = Vector.Zero; - - for (int i = 0; i <= this.num - Vector.Count; i += Vector.Count) + private readonly float baseMultiplier; + private readonly float x; + private readonly float xPlus; + private readonly float xMinus; + + /// + /// Prepares the adjustment and its two neighbors once for a coefficient pass. + /// + /// The base correlation multiplier. + /// The encoded-adjustment estimate. + /// The finite-difference distance. + public DerivativeOperator(float baseMultiplier, float x, float eps) + { + this.baseMultiplier = baseMultiplier; + this.x = x; + this.xPlus = x + eps; + this.xMinus = x - eps; + } + + /// + /// Evaluates one coefficient pair using the same fused residual arithmetic as the SIMD paths. + /// + /// The luma coefficient. + /// The chroma coefficient. + /// The derivative at x. + /// The derivative at x + eps. + /// The derivative at x - eps. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Invoke(float luma, float chroma, out float derivative, out float plus, out float minus) + { + float a = InverseColorFactor * luma; + float b = (this.baseMultiplier * luma) - chroma; + float residual = MathF.FusedMultiplyAdd(a, this.x, b); + float residualPlus = MathF.FusedMultiplyAdd(a, this.xPlus, b); + float residualMinus = MathF.FusedMultiplyAdd(a, this.xMinus, b); + float magnitude = MathF.Abs(residual); + float scale = DerivativeScale * a; + derivative = scale * (magnitude + 1F); + plus = scale * (MathF.Abs(residualPlus) + 1F); + minus = scale * (MathF.Abs(residualMinus) + 1F); + derivative = residual < 0F ? 0F - derivative : derivative; + plus = residualPlus < 0F ? 0F - plus : plus; + minus = residualMinus < 0F ? 0F - minus : minus; + + // The native loss excludes all three derivatives according to the central residual, not each displaced residual. + if (magnitude >= ResidualThreshold) + { + derivative = 0F; + plus = 0F; + minus = 0F; + } + } + + /// + /// Evaluates independent coefficient pairs in 4 lanes, with no horizontal arithmetic until the caller reduces each sum. + /// + /// The luma coefficients in their original order. + /// The matching chroma coefficients. + /// The derivatives at x. + /// The derivatives at x + eps. + /// The derivatives at x - eps. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Invoke( + Vector128 luma, + Vector128 chroma, + out Vector128 derivative, + out Vector128 plus, + out Vector128 minus) { - // color residual = ax + b - Vector a = inverseColorFactor * Vector.Create(this.valuesM[i..]); - Vector b = (baseV * Vector.Create(this.valuesM[i..])) - Vector.Create(this.valuesS[i..]); - - Vector v = Vector.FusedMultiplyAdd(a, xV, b); - Vector vpe = Vector.FusedMultiplyAdd(a, xpeV, b); - Vector vme = Vector.FusedMultiplyAdd(a, xmeV, b); - - // Abs of three above vectors - Vector av = Vector.Abs(v); - Vector avpe = Vector.Abs(vpe); - Vector avme = Vector.Abs(vme); - - Vector acoeffx2 = coeffx2 * a; - Vector d = acoeffx2 * (av + one); - Vector dpe = acoeffx2 * (avpe + one); - Vector dme = acoeffx2 * (avme + one); - - // if (V < 0) { D = -D } else { D } - d = Vector.ConditionalSelect( - Vector.LessThan(v, zero), - zero - d, - d); - dpe = Vector.ConditionalSelect( - Vector.LessThan(vpe, zero), - zero - dpe, - dpe); - dme = Vector.ConditionalSelect( - Vector.LessThan(vme, zero), - zero - dme, - dme); - - Vector above = Vector.GreaterThanOrEqual(av, thres); - - // F += above ? 0 : D - fdV += Vector.ConditionalSelect(above, zero, d); - fdpeV += Vector.ConditionalSelect(above, zero, dpe); - fdmeV += Vector.ConditionalSelect(above, zero, dme); + Vector128 a = Vector128.Create(InverseColorFactor) * luma; + Vector128 b = (Vector128.Create(this.baseMultiplier) * luma) - chroma; + Vector128 residual = Vector128.FusedMultiplyAdd(a, Vector128.Create(this.x), b); + Vector128 residualPlus = Vector128.FusedMultiplyAdd(a, Vector128.Create(this.xPlus), b); + Vector128 residualMinus = Vector128.FusedMultiplyAdd(a, Vector128.Create(this.xMinus), b); + Vector128 magnitude = Vector128.Abs(residual); + Vector128 scale = Vector128.Create(DerivativeScale) * a; + derivative = scale * (magnitude + Vector128.One); + plus = scale * (Vector128.Abs(residualPlus) + Vector128.One); + minus = scale * (Vector128.Abs(residualMinus) + Vector128.One); + derivative = Vector128.ConditionalSelect(Vector128.LessThan(residual, Vector128.Zero), Vector128.Zero - derivative, derivative); + plus = Vector128.ConditionalSelect(Vector128.LessThan(residualPlus, Vector128.Zero), Vector128.Zero - plus, plus); + minus = Vector128.ConditionalSelect(Vector128.LessThan(residualMinus, Vector128.Zero), Vector128.Zero - minus, minus); + + // Share the central-residual exclusion across the three estimates, exactly as in the scalar operation. + Vector128 excluded = Vector128.GreaterThanOrEqual(magnitude, Vector128.Create(ResidualThreshold)); + derivative = Vector128.ConditionalSelect(excluded, Vector128.Zero, derivative); + plus = Vector128.ConditionalSelect(excluded, Vector128.Zero, plus); + minus = Vector128.ConditionalSelect(excluded, Vector128.Zero, minus); } - fpeps = firstDerivativePeps + Vector.Sum(fdpeV); - fmeps = firstDerivativeMeps + Vector.Sum(fdmeV); - return firstDerivative + Vector.Sum(fdV); + /// + /// Evaluates independent coefficient pairs in 8 lanes, with no horizontal arithmetic until the caller reduces each sum. + /// + /// The luma coefficients in their original order. + /// The matching chroma coefficients. + /// The derivatives at x. + /// The derivatives at x + eps. + /// The derivatives at x - eps. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Invoke( + Vector256 luma, + Vector256 chroma, + out Vector256 derivative, + out Vector256 plus, + out Vector256 minus) + { + Vector256 a = Vector256.Create(InverseColorFactor) * luma; + Vector256 b = (Vector256.Create(this.baseMultiplier) * luma) - chroma; + Vector256 residual = Vector256.FusedMultiplyAdd(a, Vector256.Create(this.x), b); + Vector256 residualPlus = Vector256.FusedMultiplyAdd(a, Vector256.Create(this.xPlus), b); + Vector256 residualMinus = Vector256.FusedMultiplyAdd(a, Vector256.Create(this.xMinus), b); + Vector256 magnitude = Vector256.Abs(residual); + Vector256 scale = Vector256.Create(DerivativeScale) * a; + derivative = scale * (magnitude + Vector256.One); + plus = scale * (Vector256.Abs(residualPlus) + Vector256.One); + minus = scale * (Vector256.Abs(residualMinus) + Vector256.One); + derivative = Vector256.ConditionalSelect(Vector256.LessThan(residual, Vector256.Zero), Vector256.Zero - derivative, derivative); + plus = Vector256.ConditionalSelect(Vector256.LessThan(residualPlus, Vector256.Zero), Vector256.Zero - plus, plus); + minus = Vector256.ConditionalSelect(Vector256.LessThan(residualMinus, Vector256.Zero), Vector256.Zero - minus, minus); + + // Share the central-residual exclusion across the three estimates, exactly as in the scalar operation. + Vector256 excluded = Vector256.GreaterThanOrEqual(magnitude, Vector256.Create(ResidualThreshold)); + derivative = Vector256.ConditionalSelect(excluded, Vector256.Zero, derivative); + plus = Vector256.ConditionalSelect(excluded, Vector256.Zero, plus); + minus = Vector256.ConditionalSelect(excluded, Vector256.Zero, minus); + } + + /// + /// Evaluates independent coefficient pairs in 16 lanes, with no horizontal arithmetic until the caller reduces each sum. + /// + /// The luma coefficients in their original order. + /// The matching chroma coefficients. + /// The derivatives at x. + /// The derivatives at x + eps. + /// The derivatives at x - eps. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Invoke( + Vector512 luma, + Vector512 chroma, + out Vector512 derivative, + out Vector512 plus, + out Vector512 minus) + { + Vector512 a = Vector512.Create(InverseColorFactor) * luma; + Vector512 b = (Vector512.Create(this.baseMultiplier) * luma) - chroma; + Vector512 residual = Vector512.FusedMultiplyAdd(a, Vector512.Create(this.x), b); + Vector512 residualPlus = Vector512.FusedMultiplyAdd(a, Vector512.Create(this.xPlus), b); + Vector512 residualMinus = Vector512.FusedMultiplyAdd(a, Vector512.Create(this.xMinus), b); + Vector512 magnitude = Vector512.Abs(residual); + Vector512 scale = Vector512.Create(DerivativeScale) * a; + derivative = scale * (magnitude + Vector512.One); + plus = scale * (Vector512.Abs(residualPlus) + Vector512.One); + minus = scale * (Vector512.Abs(residualMinus) + Vector512.One); + derivative = Vector512.ConditionalSelect(Vector512.LessThan(residual, Vector512.Zero), Vector512.Zero - derivative, derivative); + plus = Vector512.ConditionalSelect(Vector512.LessThan(residualPlus, Vector512.Zero), Vector512.Zero - plus, plus); + minus = Vector512.ConditionalSelect(Vector512.LessThan(residualMinus, Vector512.Zero), Vector512.Zero - minus, minus); + + // Share the central-residual exclusion across the three estimates, exactly as in the scalar operation. + Vector512 excluded = Vector512.GreaterThanOrEqual(magnitude, Vector512.Create(ResidualThreshold)); + derivative = Vector512.ConditionalSelect(excluded, Vector512.Zero, derivative); + plus = Vector512.ConditionalSelect(excluded, Vector512.Zero, plus); + minus = Vector512.ConditionalSelect(excluded, Vector512.Zero, minus); + } } } diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/ChromaFromLumaEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/ChromaFromLumaEncoderTests.cs index 7d35f5ee8d..d92e818e9c 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/ChromaFromLumaEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/ChromaFromLumaEncoderTests.cs @@ -3,11 +3,65 @@ using SixLabors.ImageSharp.Formats.Jxl.Processing; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.ChromaFromLuma; +using SixLabors.ImageSharp.Tests.TestUtilities; namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing.Encoder; public class ChromaFromLumaEncoderTests { + /// + /// Checks logical counts, signed derivatives and scalar tails at every supported vector width. + /// + [Fact] + public void CorrelationLoss_UsesEveryLogicalObservation() + { + static void RunTest() + { + ReadOnlySpan lengths = [0, 1, 3, 4, 7, 8, 15, 16, 17, 31, 32, 33, 65]; + ReadOnlySpan signs = [-1, 1]; + foreach (int length in lengths) + { + float[] luma = new float[length + 1]; + float[] chroma = new float[length + 1]; + luma.AsSpan(0, length).Fill(JxlChromaFromLuma.DefaultColorFactor); + + // The extra pair is storage padding, not an observation. Reading it would introduce NaN into the loss. + luma[length] = float.MaxValue; + chroma[length] = float.NaN; + + foreach (int sign in signs) + { + // Normalized luma is one. A residual of +/-0.5 has derivative +/-1 under the one-third loss. + // With eps = 0, all three sums must equal the signed number of observations, without rounding tolerance. + JxlCflFunction function = new(luma, chroma, length, 0F, 0F); + float plus = float.NaN; + float minus = float.NaN; + float actual = function.Compute(sign * 0.5F, 0F, ref plus, ref minus); + Assert.Equal((float)(sign * length), actual); + Assert.Equal((float)(sign * length), plus); + Assert.Equal((float)(sign * length), minus); + } + } + + // At the cutoff, the native loss excludes all three terms according to the central residual. + // The displaced residuals straddle the cutoff, so applying three independent cutoff tests would fail here. + float[] cutoffLuma = new float[33]; + float[] cutoffChroma = new float[33]; + cutoffLuma.AsSpan().Fill(JxlChromaFromLuma.DefaultColorFactor); + cutoffChroma.AsSpan().Fill(-99.5F); + JxlCflFunction cutoffFunction = new(cutoffLuma, cutoffChroma, cutoffLuma.Length, 0F, 0F); + float cutoffPlus = float.NaN; + float cutoffMinus = float.NaN; + Assert.Equal(0F, cutoffFunction.Compute(0.5F, 1F, ref cutoffPlus, ref cutoffMinus)); + Assert.Equal(0F, cutoffPlus); + Assert.Equal(0F, cutoffMinus); + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } + /// /// Checks the native rounding rule where an estimated correlation reaches an exact midpoint after the zero bias. /// diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/PaletteTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/PaletteTests.cs index 16187c4e73..467df133c0 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/PaletteTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/PaletteTests.cs @@ -296,4 +296,3 @@ static void RunTest() HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); } } - From 7f2e1f6bfb768b2609f74a35a9d17c5405ed2603 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 Oct 2026 23:51:24 +1000 Subject: [PATCH 283/328] Reuse JPEG XL noise workspace and complete correlation SIMD traversal --- .../Encoder/ChromaFromLuma/JxlCflFunction.cs | 91 +++ .../JxlChromaFromLumaEncoder.cs | 42 +- .../Encoder/Noise/JxlNoiseEncoder.cs | 559 +++++++++++++----- .../Encoder/ChromaFromLumaEncoderTests.cs | 14 +- .../Encoder/Noise/NoiseEncoderTests.cs | 2 +- 5 files changed, 525 insertions(+), 183 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlCflFunction.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlCflFunction.cs index 16c2c90d26..d5f5382184 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlCflFunction.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlCflFunction.cs @@ -43,6 +43,97 @@ public JxlCflFunction(ReadOnlySpan valuesM, ReadOnlySpan valuesS, this.distanceMultiplier = distanceMultiplier; } + /// + /// Finds the minimum of the quadratic correlation estimate from one pass over nonempty coefficient pairs. + /// + /// The unrounded adjustment before the search's bias toward zero. + public float EstimateMinimum() + { + ref float luma = ref MemoryMarshal.GetReference(this.luma); + ref float chroma = ref MemoryMarshal.GetReference(this.chroma); + float sumSquares = 0F; + float sumProducts = 0F; + nuint i = 0; + nuint length = (nuint)this.luma.Length; + + // The residual is a*x+b, where a=luma/colorFactor and b=base*luma-chroma. Accumulate a*a and a*b + // together so coefficient storage is read once. No transformed arrays or separate dot-product passes are needed. + if (Vector512.IsHardwareAccelerated) + { + Vector512 squares = Vector512.Zero; + Vector512 products = Vector512.Zero; + Vector512 inverseFactor = Vector512.Create(InverseColorFactor); + Vector512 baseValue = Vector512.Create(this.baseMultiplier); + nuint end = this.luma.Vector512Count() * (nuint)Vector512.Count; + + for (; i < end; i += (nuint)Vector512.Count) + { + Vector512 value = Vector512.LoadUnsafe(ref luma, i); + Vector512 a = inverseFactor * value; + Vector512 b = (baseValue * value) - Vector512.LoadUnsafe(ref chroma, i); + squares += a * a; + products += a * b; + } + + sumSquares += Vector512.Sum(squares); + sumProducts += Vector512.Sum(products); + } + + if (Vector256.IsHardwareAccelerated) + { + Vector256 squares = Vector256.Zero; + Vector256 products = Vector256.Zero; + Vector256 inverseFactor = Vector256.Create(InverseColorFactor); + Vector256 baseValue = Vector256.Create(this.baseMultiplier); + nuint end = this.luma.Vector256Count() * (nuint)Vector256.Count; + + for (; i < end; i += (nuint)Vector256.Count) + { + Vector256 value = Vector256.LoadUnsafe(ref luma, i); + Vector256 a = inverseFactor * value; + Vector256 b = (baseValue * value) - Vector256.LoadUnsafe(ref chroma, i); + squares += a * a; + products += a * b; + } + + sumSquares += Vector256.Sum(squares); + sumProducts += Vector256.Sum(products); + } + + if (Vector128.IsHardwareAccelerated) + { + Vector128 squares = Vector128.Zero; + Vector128 products = Vector128.Zero; + Vector128 inverseFactor = Vector128.Create(InverseColorFactor); + Vector128 baseValue = Vector128.Create(this.baseMultiplier); + nuint end = this.luma.Vector128Count() * (nuint)Vector128.Count; + + for (; i < end; i += (nuint)Vector128.Count) + { + Vector128 value = Vector128.LoadUnsafe(ref luma, i); + Vector128 a = inverseFactor * value; + Vector128 b = (baseValue * value) - Vector128.LoadUnsafe(ref chroma, i); + squares += a * a; + products += a * b; + } + + sumSquares += Vector128.Sum(squares); + sumProducts += Vector128.Sum(products); + } + + for (; i < length; i++) + { + float value = Unsafe.Add(ref luma, i); + float a = InverseColorFactor * value; + float b = (this.baseMultiplier * value) - Unsafe.Add(ref chroma, i); + sumSquares += a * a; + sumProducts += a * b; + } + + // The fast estimate uses half the per-observation regularization weight in its denominator. + return -sumProducts / (sumSquares + (this.luma.Length * this.distanceMultiplier * 0.5F)); + } + /// /// Computes the derivative at an adjustment and at its two finite-difference neighbors. /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlChromaFromLumaEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlChromaFromLumaEncoder.cs index 90a350f16e..8eb9e9f25a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlChromaFromLumaEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlChromaFromLumaEncoder.cs @@ -48,61 +48,25 @@ public static int FindBestMultiplier(Span valuesM, Span valuesS, i return 0; } + JxlCflFunction function = new(valuesM, valuesS, num, baseValue, distanceMul); float x; if (fast) { - const float inverseColorFactor = 1.0f / JxlChromaFromLuma.DefaultColorFactor; - - Vector ca = Vector.Zero; - Vector cb = Vector.Zero; - Vector invColorFactor = new(inverseColorFactor); - Vector baseV = new(baseValue); - - int i = 0; - int lanes = Vector.Count; - - for (; i + lanes <= num; i += lanes) - { - Vector m = new(valuesM[i..]); - Vector s = new(valuesS[i..]); - - Vector a = invColorFactor * m; - Vector b = (baseV * m) - s; - - ca += a * a; - cb += a * b; - } - - float sumCa = Vector.Sum(ca); - float sumCb = Vector.Sum(cb); - - for (; i < num; i++) - { - float m = valuesM[i]; - float s = valuesS[i]; - float a = inverseColorFactor * m; - float b = (baseValue * m) - s; - - sumCa += a * a; - sumCb += a * b; - } - - x = -sumCb / (sumCa + (num * distanceMul * 0.5f)); + x = function.EstimateMinimum(); } else { const float eps = 100.0f; const float clamp = 20.0f; - JxlCflFunction fn = new(valuesM, valuesS, num, baseValue, distanceMul); x = 0; for (int i = 0; i < 20; i++) { float dFPeps = 0; float dFMeps = 0; - float dF = fn.Compute(x, eps, ref dFPeps, ref dFMeps); + float dF = function.Compute(x, eps, ref dFPeps, ref dFMeps); float ddF = (dFPeps - dFMeps) / (2.0f * eps); const float experimentalInsignificantStabilizer = 0.85f; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseEncoder.cs index 2add2f6794..1f501f59c5 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseEncoder.cs @@ -3,11 +3,15 @@ using System.Buffers; using System.Numerics.Tensors; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; using SixLabors.ImageSharp.Formats.Jxl.Processing.Noise; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Processing.Processors.Convolution; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Noise; @@ -16,87 +20,230 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Noise; /// internal static class JxlNoiseEncoder { - private static readonly float[][] LaplacianFilter = + /// + /// The width and height of each patch used to estimate noise. + /// + private const int PatchSize = 8; + + /// + /// The width of each window compared with the central window inside a patch. + /// + private const int ComparisonWidth = 3; + + /// + /// The height of each window compared with the central window inside a patch. + /// + private const int ComparisonHeight = 4; + + /// + /// Gets the native row-major Laplacian coefficients for an eight-by-eight noise patch. + /// + private static ReadOnlySpan LaplacianFilter => [ - [-0.25f, -1.0f, -0.25f], - [-1.0f, 5.0f, -1.0f], - [-0.25f, -1.0f, -0.25f], + -0.25F, -1F, -0.25F, + -1F, 5F, -1F, + -0.25F, -1F, -0.25F ]; - public static float GetScoreSumsOfAbsoluteDifferences(JxlImage3 opsin, int x, int y, int blockSize) + /// + /// Prepares one contiguous intensity patch from source spans borrowed before image traversal. + /// + /// The complete X-plane allocation from its logical origin. + /// The complete Y-plane allocation from its logical origin. + /// The physical X-plane row stride. + /// The physical Y-plane row stride. + /// The complete patch's left column. + /// The complete patch's top row. + /// The reused eight-by-eight row-major intensity storage. + private static void PrepareIntensityPatch( + ReadOnlySpan planeX, + ReadOnlySpan planeY, + int strideX, + int strideY, + int x, + int y, + Span patch) { - const int smallBlockSizeX = 3; - const int smallBlockSizeY = 4; - - int numSAD = (blockSize - smallBlockSizeX) * (blockSize - smallBlockSizeY); - int counter = 0; - const int offset = 2; - - float[]? pooled = null; + for (int row = 0; row < PatchSize; row++) + { + // Native intensity is (Y+X)*0.5. Preserve that operation order and use the shared binary SIMD traversal. + TensorPrimitives_.Invoke( + planeY.Slice(((y + row) * strideY) + x, PatchSize), + planeX.Slice(((y + row) * strideX) + x, PatchSize), + patch.Slice(row * PatchSize, PatchSize), + default(MeanIntensityOperator)); + } + } - Span sad = numSAD <= 128 - ? stackalloc float[128].Slice(0, numSAD) - : pooled = ArrayPool.Shared.Rent(numSAD); + /// + /// Measures the mean of the ten smallest window differences inside one prepared intensity patch. + /// + /// The complete eight-by-eight row-major intensity patch. + /// Four eight-slot score rows. Only the first five positions in each row contribute. + /// The least-textured half's mean absolute difference. + private static float GetScoreSumsOfAbsoluteDifferences(ReadOnlySpan patch, Span workspace) + { + const int comparisonRows = PatchSize - ComparisonHeight; + const int comparisonColumns = PatchSize - ComparisonWidth; + const int centerOffset = 2; + ref float first = ref MemoryMarshal.GetReference(patch); + ref float output = ref MemoryMarshal.GetReference(workspace); + + // Native uses strict window-end bounds: four rows of five windows, with the central window starting at (2,2). + // A lane is an independent comparison window. Every lane accumulates the same twelve terms in native cy/cx order. + // The eight-slot row stride matches the patch. Vector512 covers two comparison rows; Vector256 covers one. + if (Vector512.IsHardwareAccelerated) + { + for (int row = 0; row < comparisonRows; row += 2) + { + Vector512 sum = Vector512.Zero; + for (int cy = 0; cy < ComparisonHeight; cy++) + { + for (int cx = 0; cx < ComparisonWidth; cx++) + { + float center = patch[((centerOffset + cy) * PatchSize) + centerOffset + cx]; + Vector512 windows = Vector512.LoadUnsafe(ref first, (uint)(((row + cy) * PatchSize) + cx)); + sum += Vector512.Abs(Vector512.Create(center) - windows); + } + } - for (int yBl = 0; yBl + smallBlockSizeY < blockSize; ++yBl) + sum.StoreUnsafe(ref output, (uint)(row * PatchSize)); + } + } + else if (Vector256.IsHardwareAccelerated) { - for (int xBl = 0; xBl + smallBlockSizeX < blockSize; ++xBl) + for (int row = 0; row < comparisonRows; row++) { - float sadSum = 0; + Vector256 sum = Vector256.Zero; + for (int cy = 0; cy < ComparisonHeight; cy++) + { + for (int cx = 0; cx < ComparisonWidth; cx++) + { + float center = patch[((centerOffset + cy) * PatchSize) + centerOffset + cx]; + Vector256 windows = Vector256.LoadUnsafe(ref first, (uint)(((row + cy) * PatchSize) + cx)); + sum += Vector256.Abs(Vector256.Create(center) - windows); + } + } - for (int cy = 0; cy < smallBlockSizeY; ++cy) + sum.StoreUnsafe(ref output, (uint)(row * PatchSize)); + } + } + else if (Vector128.IsHardwareAccelerated) + { + // Four lanes cover the first four windows. The fifth uses the same scalar terms without reading a padding lane. + for (int row = 0; row < comparisonRows; row++) + { + Vector128 sum = Vector128.Zero; + float last = 0; + for (int cy = 0; cy < ComparisonHeight; cy++) { - for (int cx = 0; cx < smallBlockSizeX; ++cx) + for (int cx = 0; cx < ComparisonWidth; cx++) { - float wnd = 0.5f * (opsin.PlaneRow(1, y + yBl + cy)[x + xBl + cx] + opsin.PlaneRow(0, y + yBl + cy)[x + xBl + cx]); - float center = 0.5f * (opsin.PlaneRow(1, y + offset + cy)[x + offset + cx] + opsin.PlaneRow(0, y + offset + cy)[x + offset + cx]); - sadSum += MathF.Abs(center - wnd); + float center = patch[((centerOffset + cy) * PatchSize) + centerOffset + cx]; + int source = ((row + cy) * PatchSize) + cx; + sum += Vector128.Abs(Vector128.Create(center) - Vector128.LoadUnsafe(ref first, (uint)source)); + last += MathF.Abs(center - patch[source + comparisonColumns - 1]); } } - sad[counter++] = sadSum; + sum.StoreUnsafe(ref output, (uint)(row * PatchSize)); + workspace[(row * PatchSize) + comparisonColumns - 1] = last; + } + } + else + { + for (int row = 0; row < comparisonRows; row++) + { + for (int column = 0; column < comparisonColumns; column++) + { + float sum = 0; + for (int cy = 0; cy < ComparisonHeight; cy++) + { + for (int cx = 0; cx < ComparisonWidth; cx++) + { + float center = patch[((centerOffset + cy) * PatchSize) + centerOffset + cx]; + float window = patch[((row + cy) * PatchSize) + column + cx]; + sum += MathF.Abs(center - window); + } + } + + workspace[(row * PatchSize) + column] = sum; + } } } - int samples = numSAD / 2; + // Wider registers compute unused lanes, but all source loads stay within the initialized 64-value patch (last index 57). + // Compact only the five real windows from each row. Never include the three spare slots in selection or reduction. + for (int row = 1; row < comparisonRows; row++) + { + workspace.Slice(row * PatchSize, comparisonColumns).CopyTo(workspace.Slice(row * comparisonColumns, comparisonColumns)); + } + + Span sad = workspace[..(comparisonRows * comparisonColumns)]; + int samples = sad.Length / 2; - // As with ROAD (rank order absolute distance), we keep the smallest half of - // the values in SAD (we use here the more robust patch SAD instead of - // absolute single-pixel differences). + // Native sorts these twenty scores and sums the first ten in order. Keep that reduction order after SIMD window evaluation. sad.Sort(); + float total = 0; - float totalSadSum = TensorPrimitives.Sum(sad); - - if (pooled is not null) + for (int i = 0; i < samples; i++) { - ArrayPool.Shared.Return(pooled); + total += sad[i]; } - return totalSadSum / samples; + return total / samples; } - public static IMemoryOwner GetSadScoresForPatches( + /// + /// Allocates patch scores and fills them with source spans and bounded workspace borrowed by the operation owner. + /// + /// The allocator configuration for the returned scores. + /// The source image dimensions and physical row strides. + /// The borrowed complete X-plane storage. + /// The borrowed complete Y-plane storage. + /// The reused intensity and score workspace. + /// The scale used before histogram indexing. + /// The histogram updated by completed patches. + /// The completed scores. The caller owns and disposes their allocation. + private static IMemoryOwner GetSadScoresForPatches( Configuration configuration, JxlImage3 opsin, - int blockSize, + ReadOnlySpan planeX, + ReadOnlySpan planeY, + Span workspace, int binCount, JxlNoiseHistogram sadHistogram) { - IMemoryOwner sadScores = configuration.MemoryAllocator.Allocate((opsin.YSize / blockSize) * (opsin.XSize / blockSize)); - Span sadScoresData = sadScores.GetSpan(); - int blockIndex = 0; + int count = (opsin.YSize / PatchSize) * (opsin.XSize / PatchSize); + IMemoryOwner sadScores = configuration.MemoryAllocator.Allocate(count); - for (int y = 0; y <= opsin.YSize - blockSize; y += blockSize) + try { - for (int x = 0; x <= opsin.XSize - blockSize; x += blockSize) + Span scores = sadScores.GetSpan()[..count]; + Span patch = workspace[..(PatchSize * PatchSize)]; + Span sad = workspace.Slice(PatchSize * PatchSize, (PatchSize - ComparisonHeight) * PatchSize); + int strideX = opsin.Plane(0).PixelsPerRow; + int strideY = opsin.Plane(1).PixelsPerRow; + int blockIndex = 0; + + for (int y = 0; y <= opsin.YSize - PatchSize; y += PatchSize) { - float score = GetScoreSumsOfAbsoluteDifferences(opsin, x, y, blockSize); - sadScoresData[blockIndex++] = score; - sadHistogram.Increment(score * binCount); + for (int x = 0; x <= opsin.XSize - PatchSize; x += PatchSize) + { + PrepareIntensityPatch(planeX, planeY, strideX, strideY, x, y, patch); + float score = GetScoreSumsOfAbsoluteDifferences(patch, sad); + scores[blockIndex++] = score; + sadHistogram.Increment(score * binCount); + } } - } - return sadScores; + return sadScores; + } + catch + { + sadScores.Dispose(); + throw; + } } /// @@ -105,26 +252,25 @@ public static IMemoryOwner GetSadScoresForPatches( /// The measurements selected by the patch threshold. /// The lookup values to replace, or clear when the fit error is too large. /// The strength applied to the fitted lookup values. - public static void OptimizeNoiseParameters(List noiseLevel, JxlNoiseParameters noiseParameters, float multiplier) + public static void OptimizeNoiseParameters(ReadOnlySpan noiseLevel, JxlNoiseParameters noiseParameters, float multiplier) { const double maxError = 1e-3; - ReadOnlySpan noiseLevelSpan = CollectionsMarshal.AsSpan(noiseLevel); // Start with the measured mean at every lookup point. Patch selection guarantees at least one measurement. float avg = 0; - for (int i = 0; i < noiseLevelSpan.Length; i++) + for (int i = 0; i < noiseLevel.Length; i++) { - avg += noiseLevelSpan[i].NoiseLevel; + avg += noiseLevel[i].NoiseLevel; } - avg /= noiseLevelSpan.Length; + avg /= noiseLevel.Length; // The curve always has eight points. Its numeric workspace stays on the stack and owns no pooled memory. InlineArray8 parameterStorage = default; Span parameters = parameterStorage; parameters.Fill(avg); - OptimizeNoiseCurve(noiseLevelSpan, parameters); + OptimizeNoiseCurve(noiseLevel, parameters); // The lookup is stored as floats. Apply that rounding before measuring the final fit against the codestream limits. for (int i = 0; i < parameters.Length; i++) @@ -133,7 +279,7 @@ public static void OptimizeNoiseParameters(List noiseLevel, JxlNo } InlineArray8 unusedGradient = default; - float loss = (float)(ComputeNoiseLoss(noiseLevelSpan, parameters, unusedGradient, false) / noiseLevelSpan.Length); + float loss = (float)(ComputeNoiseLoss(noiseLevel, parameters, unusedGradient, false) / noiseLevel.Length); if (loss > maxError) { @@ -324,112 +470,231 @@ private static double ComputeNoiseLoss( return loss; } - public static List GetNoiseLevel( - JxlImage3 opsin, - Span textureStrength, - float threshold, - int blockSize) + /// + /// Applies the native Laplacian to one intensity patch with shared Bounce sampling offsets. + /// + /// The complete eight-by-eight intensity patch. + /// The reused eight-by-eight filtered output. + /// Three shared sampling offsets for each output row. + /// Three shared sampling offsets for each output column. + /// Valid left-neighbor indices for two eight-value rows. + /// Valid right-neighbor indices for two eight-value rows. + private static void FilterIntensityPatch( + ReadOnlySpan patch, + Span filtered, + ReadOnlySpan rows, + ReadOnlySpan columns, + in Vector512 leftIndices, + in Vector512 rightIndices) { - List noiseLevelPerIntensity = []; - const int filterSize = 1; - int patchIndex = 0; + ReadOnlySpan coefficients = LaplacianFilter; + ref float first = ref MemoryMarshal.GetReference(patch); + ref float output = ref MemoryMarshal.GetReference(filtered); - for (int y = 0; y + blockSize <= opsin.YSize; y += blockSize) + // Every lane is one output position. Add all nine products in native row-major tap order; do not group rows or use FMA. + // Shared Bounce offsets select row 1 above row 0 and row 6 below row 7. The patch boundary is independent of the image boundary. + if (Vector512.IsHardwareAccelerated) { - for (int x = 0; x + blockSize <= opsin.XSize; x += blockSize) + for (int y = 0; y < PatchSize; y += 2) { - if (textureStrength[patchIndex] <= threshold) + Vector512 sum = Vector512.Zero; + for (int tapRow = 0; tapRow < 3; tapRow++) + { + Vector256 lower = Vector256.LoadUnsafe(ref first, (uint)(rows[(y * 3) + tapRow] * PatchSize)); + Vector256 upper = Vector256.LoadUnsafe(ref first, (uint)(rows[((y + 1) * 3) + tapRow] * PatchSize)); + Vector512 center = Vector512.Create(lower, upper); + + // Lower/upper halves hold complete eight-value rows. Indices remain within their own row and are all valid. + // The native index-vector shuffle therefore matches Shuffle without adding out-of-range zeroing. + Vector512 left = Vector512.ShuffleNative(center, leftIndices); + Vector512 right = Vector512.ShuffleNative(center, rightIndices); + sum += left * Vector512.Create(coefficients[tapRow * 3]); + sum += center * Vector512.Create(coefficients[(tapRow * 3) + 1]); + sum += right * Vector512.Create(coefficients[(tapRow * 3) + 2]); + } + + sum.StoreUnsafe(ref output, (uint)(y * PatchSize)); + } + } + else if (Vector256.IsHardwareAccelerated) + { + Vector256 leftMask = leftIndices.GetLower(); + Vector256 rightMask = rightIndices.GetLower(); + + for (int y = 0; y < PatchSize; y++) + { + Vector256 sum = Vector256.Zero; + for (int tapRow = 0; tapRow < 3; tapRow++) { - float meanInt = 0; + Vector256 center = Vector256.LoadUnsafe(ref first, (uint)(rows[(y * 3) + tapRow] * PatchSize)); + Vector256 left = Vector256.ShuffleNative(center, leftMask); + Vector256 right = Vector256.ShuffleNative(center, rightMask); + sum += left * Vector256.Create(coefficients[tapRow * 3]); + sum += center * Vector256.Create(coefficients[(tapRow * 3) + 1]); + sum += right * Vector256.Create(coefficients[(tapRow * 3) + 2]); + } - for (int yBlock = 0; yBlock < blockSize; ++yBlock) + sum.StoreUnsafe(ref output, (uint)(y * PatchSize)); + } + } + else if (Vector128.IsHardwareAccelerated) + { + Vector128 leftMask = leftIndices.GetLower().GetLower(); + Vector128 rightMask = rightIndices.GetLower().GetUpper() - Vector128.Create(4); + + for (int y = 0; y < PatchSize; y++) + { + Vector128 sum0 = Vector128.Zero; + Vector128 sum1 = Vector128.Zero; + for (int tapRow = 0; tapRow < 3; tapRow++) + { + uint source = (uint)(rows[(y * 3) + tapRow] * PatchSize); + Vector128 center0 = Vector128.LoadUnsafe(ref first, source); + Vector128 center1 = Vector128.LoadUnsafe(ref first, source + 4); + Vector128 left0 = Vector128.ShuffleNative(center0, leftMask); + Vector128 right0 = Vector128.LoadUnsafe(ref first, source + 1); + Vector128 left1 = Vector128.LoadUnsafe(ref first, source + 3); + Vector128 right1 = Vector128.ShuffleNative(center1, rightMask); + Vector128 coefficient0 = Vector128.Create(coefficients[tapRow * 3]); + Vector128 coefficient1 = Vector128.Create(coefficients[(tapRow * 3) + 1]); + Vector128 coefficient2 = Vector128.Create(coefficients[(tapRow * 3) + 2]); + + // Loads across the middle use real columns 1..4 and 3..6. Only the actual outer borders use Bounce shuffles. + sum0 += left0 * coefficient0; + sum0 += center0 * coefficient1; + sum0 += right0 * coefficient2; + sum1 += left1 * coefficient0; + sum1 += center1 * coefficient1; + sum1 += right1 * coefficient2; + } + + sum0.StoreUnsafe(ref output, (uint)(y * PatchSize)); + sum1.StoreUnsafe(ref output, (uint)((y * PatchSize) + 4)); + } + } + else + { + for (int y = 0; y < PatchSize; y++) + { + for (int x = 0; x < PatchSize; x++) + { + float sum = 0; + for (int tapRow = 0; tapRow < 3; tapRow++) { - for (int xBlock = 0; xBlock < blockSize; ++xBlock) + for (int tapColumn = 0; tapColumn < 3; tapColumn++) { - meanInt += 0.5f * (opsin.PlaneRow(1, y + yBlock)[x + xBlock] + - opsin.PlaneRow(0, y + yBlock)[x + xBlock]); + sum += patch[(rows[(y * 3) + tapRow] * PatchSize) + columns[(x * 3) + tapColumn]] * coefficients[(tapRow * 3) + tapColumn]; } } - meanInt /= blockSize * blockSize; + filtered[(y * PatchSize) + x] = sum; + } + } + } + } + /// + /// Measures noise in selected complete patches using cached source spans and one reused intensity/filter workspace. + /// + /// The configuration used for the shared boundary sampling map. + /// The source image dimensions and physical strides. + /// The complete borrowed X-plane storage. + /// The complete borrowed Y-plane storage. + /// The logical row-major patch scores. + /// The largest score selected for noise measurement. + /// The reused intensity and filtered patch storage. + /// Caller-owned storage with capacity for every complete patch. + /// The number of measurements written in patch order. + private static int GetNoiseLevel( + Configuration configuration, + JxlImage3 opsin, + ReadOnlySpan planeX, + ReadOnlySpan planeY, + ReadOnlySpan textureStrength, + float threshold, + Span workspace, + Span measurements) + { + int measurementCount = 0; + Span patch = workspace[..(PatchSize * PatchSize)]; + Span filtered = workspace.Slice(PatchSize * PatchSize, PatchSize * PatchSize); + int strideX = opsin.Plane(0).PixelsPerRow; + int strideY = opsin.Plane(1).PixelsPerRow; + int patchIndex = 0; + + // The same fixed patch bounds apply everywhere. Build and borrow the existing Bounce map once, outside all patch loops. + using KernelSamplingMap sampling = new(configuration.MemoryAllocator); + sampling.BuildSamplingOffsetMap(3, 3, new Rectangle(0, 0, PatchSize, PatchSize), BorderWrappingMode.Bounce, BorderWrappingMode.Bounce); + ReadOnlySpan rows = sampling.GetRowOffsetSpan(); + ReadOnlySpan columns = sampling.GetColumnOffsetSpan(); + InlineArray16 leftStorage = default; + InlineArray16 rightStorage = default; + Span left = leftStorage; + Span right = rightStorage; + + for (int i = 0; i < left.Length; i++) + { + int column = i % PatchSize; + int rowBase = (i / PatchSize) * PatchSize; + left[i] = rowBase + columns[column * 3]; + right[i] = rowBase + columns[(column * 3) + 2]; + } + + Vector512 leftIndices = Vector512.LoadUnsafe(ref MemoryMarshal.GetReference(left)); + Vector512 rightIndices = Vector512.LoadUnsafe(ref MemoryMarshal.GetReference(right)); + + for (int y = 0; y + PatchSize <= opsin.YSize; y += PatchSize) + { + for (int x = 0; x + PatchSize <= opsin.XSize; x += PatchSize) + { + if (textureStrength[patchIndex] <= threshold) + { + PrepareIntensityPatch(planeX, planeY, strideX, strideY, x, y, patch); + FilterIntensityPatch(patch, filtered, rows, columns, in leftIndices, in rightIndices); + float meanIntensity = 0; float noiseLevel = 0; - int count = 0; - for (int yBlock = 0; yBlock < blockSize; ++yBlock) + // SIMD changes independent output positions, not these native row-major reductions. + for (int i = 0; i < patch.Length; i++) { - for (int xBlock = 0; xBlock < blockSize; ++xBlock) - { - float filteredValue = 0; - - for (int yFilter = -1 * filterSize; yFilter <= filterSize; ++yFilter) - { - if (yBlock + yFilter >= 0 && (yBlock + yFilter) < blockSize) - { - for (int xFilter = -1 * filterSize; xFilter <= filterSize; ++xFilter) - { - if ((xBlock + xFilter) >= 0 && (xBlock + xFilter) < blockSize) - { - filteredValue += - 0.5f * - (opsin.PlaneRow(1, y + yBlock + yFilter)[x + xBlock + xFilter] + - opsin.PlaneRow(0, y + yBlock + yFilter)[x + xBlock + xFilter]) * - LaplacianFilter[yFilter + filterSize][xFilter + filterSize]; - } - else - { - filteredValue += - 0.5f * - (opsin.PlaneRow(1, y + yBlock + yFilter)[x + xBlock - xFilter] + - opsin.PlaneRow(0, y + yBlock + yFilter)[x + xBlock - xFilter]) * - LaplacianFilter[yFilter + filterSize][xFilter + filterSize]; - } - } - } - else - { - for (int xFilter = -1 * filterSize; xFilter <= filterSize; ++xFilter) - { - if ((xBlock + xFilter) >= 0 && - (xBlock + xFilter) < blockSize) - { - filteredValue += - 0.5f * - (opsin.PlaneRow(1, y + yBlock - yFilter)[x + xBlock + xFilter] + - opsin.PlaneRow(0, y + yBlock - yFilter)[x + xBlock + xFilter]) * - LaplacianFilter[yFilter + filterSize][xFilter + filterSize]; - } - else - { - filteredValue += - 0.5f * - (opsin.PlaneRow(1, y + yBlock - yFilter)[x + xBlock - xFilter] + - opsin.PlaneRow(0, y + yBlock - yFilter)[x + xBlock - xFilter]) * - LaplacianFilter[yFilter + filterSize][xFilter + filterSize]; - } - } - } - } - - noiseLevel += MathF.Abs(filteredValue); - count++; - } + meanIntensity += patch[i]; + noiseLevel += MathF.Abs(filtered[i]); } - noiseLevel /= count; - - JxlNoiseLevel nl = default; - nl.Intensity = meanInt; - nl.NoiseLevel = noiseLevel; - - noiseLevelPerIntensity.Add(nl); + measurements[measurementCount++] = new JxlNoiseLevel + { + Intensity = meanIntensity / (PatchSize * PatchSize), + NoiseLevel = noiseLevel / (PatchSize * PatchSize) + }; } patchIndex++; } } - return noiseLevelPerIntensity; + return measurementCount; + } + + /// + /// Calculates the native intensity mean without changing addition order or fusing operations. + /// + private readonly struct MeanIntensityOperator : TensorPrimitives_.IBinaryOperator + { + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float Invoke(float first, float second) => (first + second) * 0.5F; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector128 Invoke(Vector128 first, Vector128 second) => (first + second) * Vector128.Create(0.5F); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector256 Invoke(Vector256 first, Vector256 second) => (first + second) * Vector256.Create(0.5F); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector512 Invoke(Vector512 first, Vector512 second) => (first + second) * Vector512.Create(0.5F); } public static float GetSadThreshold(JxlNoiseHistogram histogram) => (float)histogram.Mode / JxlNoiseHistogram.Bins; @@ -454,18 +719,22 @@ public static void EncodeFloatParameter(float value, float precision, JxlBitWrit /// /// Estimates a noise lookup from selected patches and clears it when no usable fit is available. /// - /// The allocator configuration for patch scores. + /// The allocator configuration for patch scores, measurements, and sampling offsets. /// The borrowed image planes used to measure patch noise. /// The lookup values to replace or clear. /// The strength used to scale the fitted noise. /// Whether the fitted lookup contains a nonzero noise level. public static bool GetNoiseParameter(Configuration configuration, JxlImage3 opsin, JxlNoiseParameters noiseParams, float qualityCoefficient) { - const int blockSize = 8; const int binCount = 256; + // One bounded stack workspace serves both passes: 64 intensity values followed by 32 SAD slots or 64 filtered values. + InlineArray128 workspaceStorage = default; + Span workspace = workspaceStorage; + ReadOnlySpan planeX = opsin.PlaneRow(0, 0); + ReadOnlySpan planeY = opsin.PlaneRow(1, 0); JxlNoiseHistogram sadHistogram = new(); - using IMemoryOwner sadScores = GetSadScoresForPatches(configuration, opsin, blockSize, binCount, sadHistogram); + using IMemoryOwner sadScores = GetSadScoresForPatches(configuration, opsin, planeX, planeY, workspace, binCount, sadHistogram); float sadThreshold = GetSadThreshold(sadHistogram); if (sadThreshold is > 0.15f or <= 0.0f) @@ -474,16 +743,22 @@ public static bool GetNoiseParameter(Configuration configuration, JxlImage3 nl = GetNoiseLevel(opsin, sadScores.GetSpan(), sadThreshold, blockSize); + int patchCount = (opsin.YSize / PatchSize) * (opsin.XSize / PatchSize); + ReadOnlySpan scores = sadScores.GetSpan()[..patchCount]; + + // Each complete patch contributes at most one measurement. Allocate once after the threshold accepts this image. + using IMemoryOwner measurementOwner = configuration.MemoryAllocator.Allocate(patchCount); + Span measurements = measurementOwner.GetSpan()[..patchCount]; + int measurementCount = GetNoiseLevel(configuration, opsin, planeX, planeY, scores, sadThreshold, workspace, measurements); // The histogram threshold can exclude every patch. There is then no measured curve to average or fit. - if (nl.Count == 0) + if (measurementCount == 0) { noiseParams.Clear(); return false; } - OptimizeNoiseParameters(nl, noiseParams, qualityCoefficient * 1.4f); + OptimizeNoiseParameters(measurements[..measurementCount], noiseParams, qualityCoefficient * 1.4f); return noiseParams.ContainsAny; } diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/ChromaFromLumaEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/ChromaFromLumaEncoderTests.cs index d92e818e9c..db31cf33be 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/ChromaFromLumaEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/ChromaFromLumaEncoderTests.cs @@ -13,7 +13,7 @@ public class ChromaFromLumaEncoderTests /// Checks logical counts, signed derivatives and scalar tails at every supported vector width. /// [Fact] - public void CorrelationLoss_UsesEveryLogicalObservation() + public void CorrelationSearch_UsesEveryLogicalObservation() { static void RunTest() { @@ -41,6 +41,18 @@ static void RunTest() Assert.Equal((float)(sign * length), plus); Assert.Equal((float)(sign * length), minus); } + + if (length > 0) + { + chroma.AsSpan(0, length).Fill(2F); + + // For a=1 and distance=2, the denominator is 2*num. With base zero, b=-2 gives adjustment 1. + // With base 0.5, b=(0.5*84)-2=40 gives adjustment -20. Both cases have exact sums at these lengths. + JxlCflFunction zeroBase = new(luma, chroma, length, 0F, 2F); + JxlCflFunction nonzeroBase = new(luma, chroma, length, 0.5F, 2F); + Assert.Equal(1F, zeroBase.EstimateMinimum()); + Assert.Equal(-20F, nonzeroBase.EstimateMinimum()); + } } // At the cutoff, the native loss excludes all three terms according to the central residual. diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/Noise/NoiseEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/Noise/NoiseEncoderTests.cs index 6cb53362d8..2a40ef8d0b 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/Noise/NoiseEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/Noise/NoiseEncoderTests.cs @@ -22,7 +22,7 @@ public void OptimizeNoiseParameters_ConstantMeasurements_PreservesFlatCurve(floa { // These intensities produce exact lookup positions or half-way fractions. A flat curve has zero fitting and smoothing error. // Its gradient is zero before the first search step, so the optimizer must not divide by the direction's zero length. - List measurements = + ReadOnlySpan measurements = [ new(noiseLevel, 0F), new(noiseLevel, 0.25F), From 394f9c74757817c4ebb5eae88952cb397c62e627 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Oct 2026 00:03:45 +1000 Subject: [PATCH 284/328] Restore JPEG XL noise sequences with SIMD state updates --- .../Jxl/Processing/Decoder/JxlNoiseDecoder.cs | 100 ++++++++++++----- .../Jxl/Processing/Primitives/JxlXorShift.cs | 104 +++++++++++++++--- .../Processing/Primitives/XorShiftTests.cs | 88 ++++++++++++++- 3 files changed, 242 insertions(+), 50 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs index dc20fb3002..df796bd9cc 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs @@ -1,9 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Noise; @@ -13,14 +13,62 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; internal static class JxlNoiseDecoder { + /// + /// Converts random words to floats in [1,2) without changing values outside the destination. + /// + /// One random word per output sample. + /// The logical output samples. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void BitsToFloatingPoint(ReadOnlySpan randomBits, Span floats) { - Vector bits = new(randomBits); - Vector rand12 = ((bits >> 9) | new Vector(0x3F800000u)).As(); - rand12.StoreUnsafe(ref MemoryMarshal.GetReference(floats)); + const int mantissaShift = 9; + const uint unitExponentBits = 0x3F800000U; + ref uint input = ref MemoryMarshal.GetReference(randomBits); + ref float output = ref MemoryMarshal.GetReference(floats); + uint i = 0; + + // Each UInt32 supplies 23 random mantissa bits. The fixed exponent produces [1,2), as in native noise reconstruction. + // The later noise filter has zero coefficient sum, so this constant offset contributes nothing to its output. + if (Vector512.IsHardwareAccelerated) + { + for (; i + Vector512.Count <= floats.Length; i += (uint)Vector512.Count) + { + Vector512 bits = Vector512.LoadUnsafe(ref input, i); + ((bits >> mantissaShift) | Vector512.Create(unitExponentBits)).AsSingle().StoreUnsafe(ref output, i); + } + } + + if (Vector256.IsHardwareAccelerated) + { + for (; i + Vector256.Count <= floats.Length; i += (uint)Vector256.Count) + { + Vector256 bits = Vector256.LoadUnsafe(ref input, i); + ((bits >> mantissaShift) | Vector256.Create(unitExponentBits)).AsSingle().StoreUnsafe(ref output, i); + } + } + + if (Vector128.IsHardwareAccelerated) + { + for (; i + Vector128.Count <= floats.Length; i += (uint)Vector128.Count) + { + Vector128 bits = Vector128.LoadUnsafe(ref input, i); + ((bits >> mantissaShift) | Vector128.Create(unitExponentBits)).AsSingle().StoreUnsafe(ref output, i); + } + } + + // Store only real samples. The scalar tail uses the identical bit pattern and does not write row padding or adjacent rectangles. + for (; i < floats.Length; i++) + { + Unsafe.Add(ref output, i) = BitConverter.UInt32BitsToSingle((Unsafe.Add(ref input, i) >> mantissaShift) | unitExponentBits); + } } + /// + /// Fills one rectangle with the native random sequence while preserving samples outside it. + /// + /// The generator advanced by this rectangle. + /// The logical output rectangle. + /// The plane that owns the output storage. public static void GenerateRandomImage(JxlXorShift rng, Rectangle rectangle, JxlPlane noise) { const int floatsPerBatch = JxlXorShift.Generators * sizeof(ulong) / sizeof(float); @@ -28,42 +76,40 @@ public static void GenerateRandomImage(JxlXorShift rng, Rectangle rectangle, Jxl int xSize = rectangle.Width; int ySize = rectangle.Height; - Span batch64 = stackalloc ulong[JxlXorShift.Generators]; - Span batch32 = stackalloc uint[JxlXorShift.Generators * 2]; - - // stackalloc doesn't zero-initialize, so clear values - batch64.Clear(); - batch32.Clear(); - - int n = Vector.Count; + InlineArray8 batchStorage = default; + Span batch64 = batchStorage; + ReadOnlySpan batch32 = MemoryMarshal.Cast(batch64); + Span storage = noise.GetRow(0); + int stride = noise.PixelsPerRow; for (int y = 0; y < ySize; y++) { - Span row = noise.GetRow(rectangle, y); + Span row = storage.Slice(((rectangle.Y + y) * stride) + rectangle.X, xSize); int x = 0; + for (; x + floatsPerBatch < xSize; x += floatsPerBatch) { rng.Fill(batch64); - MemoryMarshal.Cast(batch32).CopyTo(batch64); - for (int i = 0; i < floatsPerBatch; i += n) - { - BitsToFloatingPoint(batch32[i..], row[(x + i)..]); - } + BitsToFloatingPoint(batch32, row.Slice(x, floatsPerBatch)); } + // Native consumes one final batch per row, including an exactly full batch or an empty row. + // The UInt32 view borrows the freshly filled words directly. No copied or uninitialized batch is read. rng.Fill(batch64); - MemoryMarshal.Cast(batch32).CopyTo(batch64); - - int batchPos = 0; - - for (; x < xSize; x += n) - { - BitsToFloatingPoint(batch32[batchPos..], row[x..]); - batchPos += n; - } + BitsToFloatingPoint(batch32, row[x..]); } } + /// + /// Fills three noise rectangles with one continuous frame-and-position random sequence. + /// + /// The visible frame seed. + /// The non-visible frame seed. + /// The group's horizontal seed coordinate. + /// The group's vertical seed coordinate. + /// The first output plane and rectangle. + /// The second output plane and rectangle. + /// The third output plane and rectangle. public static void Random3Planes( int visibleFrameIndex, int nonVisibleFrameIndex, diff --git a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlXorShift.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlXorShift.cs index 8df7f47999..b0370cd772 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlXorShift.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlXorShift.cs @@ -2,6 +2,8 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; @@ -21,47 +23,115 @@ internal sealed class JxlXorShift /// public const int Generators = 8; - private readonly ulong[] s0 = new ulong[8]; - private readonly ulong[] s1 = new ulong[8]; + /// The SplitMix64 seed increment used by both native seed conventions. + private const ulong SeedIncrement = 0x9E3779B97F4A7C15UL; + // Each lane has its own two-word state. Inline storage keeps all eight generators in this owner without separate arrays. + private InlineArray8 s0; + private InlineArray8 s1; + + /// + /// Initializes eight independent generators from one seed using the native SplitMix64 sequence. + /// + /// The initial seed. public JxlXorShift(ulong seed) { - this.s0[0] = SplitMix64(seed + 0x9E3779B97F4A7C15L); + this.s0[0] = SplitMix64(seed + SeedIncrement); this.s1[0] = SplitMix64(this.s0[0]); - for (int i = 1; i < 8; ++i) + + for (int i = 1; i < Generators; ++i) { this.s0[i] = SplitMix64(this.s1[i - 1]); this.s1[i] = SplitMix64(this.s0[i]); } } + /// + /// Initializes the native frame-and-position sequence from four seed words. + /// + /// The high word of the first seed. + /// The low word of the first seed. + /// The high word of the second seed. + /// The low word of the second seed. public JxlXorShift(uint seed1, uint seed2, uint seed3, uint seed4) { - this.s0[0] = SplitMix64((((ulong)seed1 << 32) + seed2) + 0x9E3779B97F4A7C15uL); - this.s1[0] = SplitMix64((((ulong)seed3 << 32) + seed4) + 0x9E3779B97F4A7C15uL); - for (int i = 1; i < 8; ++i) + this.s0[0] = SplitMix64((((ulong)seed1 << 32) + seed2) + SeedIncrement); + this.s1[0] = SplitMix64((((ulong)seed3 << 32) + seed4) + SeedIncrement); + + for (int i = 1; i < Generators; ++i) { this.s0[i] = SplitMix64(this.s0[i - 1]); this.s1[i] = SplitMix64(this.s1[i - 1]); } } - // TODO: SIMD + /// + /// Advances every generator once and writes its previous state sum in lane order. + /// + /// Storage for at least eight output words. Remaining values are unchanged. public void Fill(Span randomBits) { - for (int i = 0; i < 8; ++i) + Span state0 = this.s0; + Span state1 = this.s1; + ref ulong first0 = ref MemoryMarshal.GetReference(state0); + ref ulong first1 = ref MemoryMarshal.GetReference(state1); + ref ulong output = ref MemoryMarshal.GetReference(randomBits); + + // The eight states are independent lanes. Every width applies the native XOR/shift recurrence without cross-lane operations. + // Unsigned addition wraps modulo 2^64. The output uses the old words, before either state is advanced. + if (Vector512.IsHardwareAccelerated) + { + Vector512 previous0 = Vector512.LoadUnsafe(ref first0); + Vector512 previous1 = Vector512.LoadUnsafe(ref first1); + (previous0 + previous1).StoreUnsafe(ref output); + previous1.StoreUnsafe(ref first0); + previous0 ^= previous0 << 23; + (previous0 ^ previous1 ^ (previous0 >> 18) ^ (previous1 >> 5)).StoreUnsafe(ref first1); + } + else if (Vector256.IsHardwareAccelerated) + { + for (uint i = 0; i < Generators; i += (uint)Vector256.Count) + { + Vector256 previous0 = Vector256.LoadUnsafe(ref first0, i); + Vector256 previous1 = Vector256.LoadUnsafe(ref first1, i); + (previous0 + previous1).StoreUnsafe(ref output, i); + previous1.StoreUnsafe(ref first0, i); + previous0 ^= previous0 << 23; + (previous0 ^ previous1 ^ (previous0 >> 18) ^ (previous1 >> 5)).StoreUnsafe(ref first1, i); + } + } + else if (Vector128.IsHardwareAccelerated) { - ulong s1 = this.s0[i]; - ulong s0 = this.s1[i]; - ulong bits = s1 + s0; - this.s0[i] = s0; - s1 ^= s1 << 23; - randomBits[i] = bits; - s1 ^= s0 ^ (s1 >> 18) ^ (s0 >> 5); - this.s1[i] = s1; + for (uint i = 0; i < Generators; i += (uint)Vector128.Count) + { + Vector128 previous0 = Vector128.LoadUnsafe(ref first0, i); + Vector128 previous1 = Vector128.LoadUnsafe(ref first1, i); + (previous0 + previous1).StoreUnsafe(ref output, i); + previous1.StoreUnsafe(ref first0, i); + previous0 ^= previous0 << 23; + (previous0 ^ previous1 ^ (previous0 >> 18) ^ (previous1 >> 5)).StoreUnsafe(ref first1, i); + } + } + else + { + // Eight is divisible by every supported ulong lane count, so only the scalar feature mode reaches this path. + for (int i = 0; i < Generators; ++i) + { + ulong previous0 = state0[i]; + ulong previous1 = state1[i]; + randomBits[i] = unchecked(previous0 + previous1); + state0[i] = previous1; + previous0 ^= previous0 << 23; + state1[i] = previous0 ^ previous1 ^ (previous0 >> 18) ^ (previous1 >> 5); + } } } + /// + /// Mixes a seed word with the native unsigned SplitMix64 recurrence. + /// + /// The seed word to mix. + /// The mixed state word. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static ulong SplitMix64(ulong z) { diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/Primitives/XorShiftTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Primitives/XorShiftTests.cs index 756ed5668e..3037a8ac2b 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/Primitives/XorShiftTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Primitives/XorShiftTests.cs @@ -4,7 +4,10 @@ using System.Diagnostics; using System.Numerics; using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.Memory; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using SixLabors.ImageSharp.Tests.TestUtilities; using static SixLabors.ImageSharp.Tests.TestImages; namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing.Primitives; @@ -271,21 +274,94 @@ public class XorShiftTests 0xA6511C10AEE742B6uL, 0x48FB48F2CACCB43EuL] ]; + /// + /// Checks every native reference word across scalar and all SIMD paths. + /// [Fact] public void TestGolden() { - JxlXorShift rng = new(12345); - Span lanes = stackalloc ulong[JxlXorShift.Generators]; + static void RunTest() + { + // libjxl's xorshift128plus_test.cc supplies these exact 64 batches for seed 12345. + JxlXorShift rng = new(12345); + Span lanes = stackalloc ulong[JxlXorShift.Generators + 1]; + lanes[^1] = ulong.MaxValue; + + for (int vector = 0; vector < Vectors; vector++) + { + rng.Fill(lanes); + + for (int i = 0; i < JxlXorShift.Generators; i++) + { + Assert.Equal(ExpectedVectors[vector][i], lanes[i]); + } + + Assert.Equal(ulong.MaxValue, lanes[^1]); + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } - for (ulong vector = 0; vector < Vectors; vector++) + /// + /// Checks native random samples, row strides, rectangle guards, short tails, and generator advancement at every SIMD width. + /// + [Fact] + public void GenerateRandomImage_PreservesNativeSequenceAndRectangle() + { + static void RunTest() { - rng.Fill(lanes); + ReadOnlySpan widths = [0, 1, 3, 4, 7, 8, 15, 16, 17, 31, 32, 33]; + Span next = stackalloc ulong[JxlXorShift.Generators]; - for (int i = 0; i < JxlXorShift.Generators; i++) + foreach (int width in widths) { - Assert.Equal(ExpectedVectors[(int)vector][i], lanes[i]); + using JxlPlane plane = new(TestEnvironment.Configuration, width + 7, 4, 2); + Span allocation = plane.GetRowMinus(0, 2); + allocation.Fill(-7F); + Span samples = plane.GetRow(0); + JxlXorShift rng = new(12345); + JxlNoiseDecoder.GenerateRandomImage(rng, new Rectangle(3, 1, width, 2), plane); + int batchesPerRow = Math.Max(1, (width + 15) / 16); + + // Each native batch supplies sixteen UInt32 words. Read the fixed reference words in native memory order. + // The integer bit formula has no rounding and remains independent of the production conversion code. + for (int y = 0; y < plane.YSize; y++) + { + for (int x = 0; x < plane.PixelsPerRow; x++) + { + float expected = -7F; + if (y >= 1 && y < 3 && x >= 3 && x < width + 3) + { + int position = x - 3; + int batch = ((y - 1) * batchesPerRow) + (position / 16); + int word = (position % 16) / 2; + int shift = (BitConverter.IsLittleEndian ? position & 1 : 1 - (position & 1)) * 32; + uint random = (uint)(ExpectedVectors[batch][word] >> shift); + expected = BitConverter.UInt32BitsToSingle((random >> 9) | 0x3F800000U); + } + + Assert.Equal(expected, samples[(y * plane.PixelsPerRow) + x]); + } + } + + Assert.Equal(-7F, allocation[0]); + Assert.Equal(-7F, allocation[1]); + + // Native consumes one final batch even for an empty row. A tail must not advance the generator a second time. + rng.Fill(next); + for (int i = 0; i < next.Length; i++) + { + Assert.Equal(ExpectedVectors[2 * batchesPerRow][i], next[i]); + } } } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); } [Fact] From 570a05e5971823d525ed875888ec302d87940b94 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Oct 2026 00:11:51 +1000 Subject: [PATCH 285/328] Share strided SIMD transposes for JPEG XL coefficient blocks --- .../Common/Helpers/SimdUtils.Transpose.cs | 187 ++++++++++++++++++ .../Formats/Jxl/Processing/Dct/JxlDct.cs | 12 +- .../Encoder/ChromaFromLuma/JxlCflFunction.cs | 1 - .../Formats/Jxl/Processing/JxlTranspose.cs | 135 ------------- .../Common/SimdUtilsTests.Transpose.cs | 83 ++++++++ 5 files changed, 276 insertions(+), 142 deletions(-) create mode 100644 src/ImageSharp/Common/Helpers/SimdUtils.Transpose.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlTranspose.cs create mode 100644 tests/ImageSharp.Tests/Common/SimdUtilsTests.Transpose.cs diff --git a/src/ImageSharp/Common/Helpers/SimdUtils.Transpose.cs b/src/ImageSharp/Common/Helpers/SimdUtils.Transpose.cs new file mode 100644 index 0000000000..4f71304488 --- /dev/null +++ b/src/ImageSharp/Common/Helpers/SimdUtils.Transpose.cs @@ -0,0 +1,187 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp; + +/// +/// Provides SIMD operations for strided sample matrices. +/// +internal static partial class SimdUtils +{ + /// + /// Transposes the logical rectangle of a floating-point matrix without changing sample bits. + /// + /// The source storage from its logical origin. It must not overlap destination. + /// The source row stride, in samples. + /// The destination storage from its logical origin. + /// The destination row stride, in samples. + /// The number of source rows and destination columns. + /// The number of source columns and destination rows. + public static void Transpose(ReadOnlySpan source, int sourceStride, Span destination, int destinationStride, int rows, int columns) + // The integer operation only moves 32-bit words. It preserves NaN payloads, signed zero and every other floating-point bit. + => Transpose(MemoryMarshal.Cast(source), sourceStride, MemoryMarshal.Cast(destination), destinationStride, rows, columns); + + /// + /// Transposes a logical integer rectangle with independent source and destination strides. + /// + /// The source storage from its logical origin. It must not overlap destination. + /// The source row stride, in integers. + /// The destination storage from its logical origin. Padding remains unchanged. + /// The destination row stride, in integers. + /// The number of source rows and destination columns. + /// The number of source columns and destination rows. + public static void Transpose(ReadOnlySpan source, int sourceStride, Span destination, int destinationStride, int rows, int columns) + { + ref int input = ref MemoryMarshal.GetReference(source); + ref int output = ref MemoryMarshal.GetReference(destination); + + // A tile spans sixteen rows and columns, the largest supported 32-bit register width. + // Complete each tile before moving on so widely strided destination columns do not displace the whole matrix from cache. + int tileSize = Vector512.Count; + for (int rowBase = 0; rowBase < rows; rowBase += tileSize) + { + int rowEnd = Math.Min(rowBase + tileSize, rows); + for (int columnBase = 0; columnBase < columns; columnBase += tileSize) + { + int columnEnd = Math.Min(columnBase + tileSize, columns); + int row = rowBase; + + // Transpose4 treats each 128-bit group as a separate four-by-four matrix. + // Wider loads handle adjacent matrices together; the stores place each four-word column at its true destination stride. + for (; row + 4 <= rowEnd; row += 4) + { + int column = columnBase; + if (Vector512.IsHardwareAccelerated) + { + for (; column + Vector512.Count <= columnEnd; column += Vector512.Count) + { + Vector512 row0 = Vector512.LoadUnsafe(ref input, (nuint)((row * sourceStride) + column)).AsSingle(); + Vector512 row1 = Vector512.LoadUnsafe(ref input, (nuint)(((row + 1) * sourceStride) + column)).AsSingle(); + Vector512 row2 = Vector512.LoadUnsafe(ref input, (nuint)(((row + 2) * sourceStride) + column)).AsSingle(); + Vector512 row3 = Vector512.LoadUnsafe(ref input, (nuint)(((row + 3) * sourceStride) + column)).AsSingle(); + Transpose4( + row0, + row1, + row2, + row3, + out Vector512 column0, + out Vector512 column1, + out Vector512 column2, + out Vector512 column3); + + StoreTransposedColumns(column0.AsInt32(), ref output, destinationStride, row, column + 0); + StoreTransposedColumns(column1.AsInt32(), ref output, destinationStride, row, column + 1); + StoreTransposedColumns(column2.AsInt32(), ref output, destinationStride, row, column + 2); + StoreTransposedColumns(column3.AsInt32(), ref output, destinationStride, row, column + 3); + } + } + + if (Vector256.IsHardwareAccelerated) + { + for (; column + Vector256.Count <= columnEnd; column += Vector256.Count) + { + Vector256 row0 = Vector256.LoadUnsafe(ref input, (nuint)((row * sourceStride) + column)).AsSingle(); + Vector256 row1 = Vector256.LoadUnsafe(ref input, (nuint)(((row + 1) * sourceStride) + column)).AsSingle(); + Vector256 row2 = Vector256.LoadUnsafe(ref input, (nuint)(((row + 2) * sourceStride) + column)).AsSingle(); + Vector256 row3 = Vector256.LoadUnsafe(ref input, (nuint)(((row + 3) * sourceStride) + column)).AsSingle(); + Transpose4( + row0, + row1, + row2, + row3, + out Vector256 column0, + out Vector256 column1, + out Vector256 column2, + out Vector256 column3); + + StoreTransposedColumns(column0.AsInt32(), ref output, destinationStride, row, column + 0); + StoreTransposedColumns(column1.AsInt32(), ref output, destinationStride, row, column + 1); + StoreTransposedColumns(column2.AsInt32(), ref output, destinationStride, row, column + 2); + StoreTransposedColumns(column3.AsInt32(), ref output, destinationStride, row, column + 3); + } + } + + if (Vector128.IsHardwareAccelerated) + { + for (; column + Vector128.Count <= columnEnd; column += Vector128.Count) + { + Vector128 row0 = Vector128.LoadUnsafe(ref input, (nuint)((row * sourceStride) + column)).AsSingle(); + Vector128 row1 = Vector128.LoadUnsafe(ref input, (nuint)(((row + 1) * sourceStride) + column)).AsSingle(); + Vector128 row2 = Vector128.LoadUnsafe(ref input, (nuint)(((row + 2) * sourceStride) + column)).AsSingle(); + Vector128 row3 = Vector128.LoadUnsafe(ref input, (nuint)(((row + 3) * sourceStride) + column)).AsSingle(); + Transpose4( + row0, + row1, + row2, + row3, + out Vector128 column0, + out Vector128 column1, + out Vector128 column2, + out Vector128 column3); + + column0.AsInt32().StoreUnsafe(ref output, (nuint)(((column + 0) * destinationStride) + row)); + column1.AsInt32().StoreUnsafe(ref output, (nuint)(((column + 1) * destinationStride) + row)); + column2.AsInt32().StoreUnsafe(ref output, (nuint)(((column + 2) * destinationStride) + row)); + column3.AsInt32().StoreUnsafe(ref output, (nuint)(((column + 3) * destinationStride) + row)); + } + } + + // The column remainder has fewer than four elements. Move four independent rows without reading padding. + for (; column < columnEnd; column++) + { + for (int lane = 0; lane < 4; lane++) + { + Unsafe.Add(ref output, (column * destinationStride) + row + lane) = + Unsafe.Add(ref input, ((row + lane) * sourceStride) + column); + } + } + } + + // Up to three rows remain at the bottom of this tile. The same sample equation also covers scalar-only hardware. + for (; row < rowEnd; row++) + { + for (int column = columnBase; column < columnEnd; column++) + { + Unsafe.Add(ref output, (column * destinationStride) + row) = Unsafe.Add(ref input, (row * sourceStride) + column); + } + } + } + } + } + + /// + /// Places two independent four-word columns from a 256-bit transpose at their strided matrix positions. + /// + /// Four source rows from column c followed by four source rows from column c + 4. + /// The first destination sample. + /// The destination row stride, in integers. + /// The first source row, which becomes a destination column. + /// The first source column, which becomes a destination row. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void StoreTransposedColumns(Vector256 columns, ref int destination, int stride, int row, int column) + { + columns.GetLower().StoreUnsafe(ref destination, (nuint)((column * stride) + row)); + columns.GetUpper().StoreUnsafe(ref destination, (nuint)(((column + 4) * stride) + row)); + } + + /// + /// Places four independent four-word columns from a 512-bit transpose at their strided matrix positions. + /// + /// Four source rows from each of columns c, c + 4, c + 8 and c + 12, in that order. + /// The first destination sample. + /// The destination row stride, in integers. + /// The first source row, which becomes a destination column. + /// The first source column, which becomes a destination row. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void StoreTransposedColumns(Vector512 columns, ref int destination, int stride, int row, int column) + { + columns.GetLower().GetLower().StoreUnsafe(ref destination, (nuint)((column * stride) + row)); + columns.GetLower().GetUpper().StoreUnsafe(ref destination, (nuint)(((column + 4) * stride) + row)); + columns.GetUpper().GetLower().StoreUnsafe(ref destination, (nuint)(((column + 8) * stride) + row)); + columns.GetUpper().GetUpper().StoreUnsafe(ref destination, (nuint)(((column + 12) * stride) + row)); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDct.cs b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDct.cs index c498c3a6d7..a99ab81e8d 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDct.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDct.cs @@ -135,14 +135,14 @@ public static void ComputeScaledDct(int rows, int columns, JxlDctSource from, Sp if (rows < columns) { Dct1D(rows, columns, from, new JxlDctOutput(block, columns), tmp); - JxlTranspose.Transpose(rows, columns, new JxlDctSource(block, columns), new JxlDctOutput(to, rows)); + SimdUtils.Transpose(block, columns, to, rows, rows, columns); Dct1D(columns, rows, new JxlDctSource(to, rows), new JxlDctOutput(block, rows), tmp); - JxlTranspose.Transpose(columns, rows, new JxlDctSource(block, rows), new JxlDctOutput(to, columns)); + SimdUtils.Transpose(block, rows, to, columns, columns, rows); } else { Dct1D(rows, columns, from, new JxlDctOutput(to, columns), tmp); - JxlTranspose.Transpose(rows, columns, new JxlDctSource(to, columns), new JxlDctOutput(block, rows)); + SimdUtils.Transpose(to, columns, block, rows, rows, columns); Dct1D(columns, rows, new JxlDctSource(block, rows), new JxlDctOutput(to, rows), tmp); } } @@ -154,15 +154,15 @@ public static void ComputeScaledInverseDct(int rows, int columns, Span fr if (rows < columns) { - JxlTranspose.Transpose(rows, columns, new JxlDctSource(from, columns), new JxlDctOutput(block, rows)); + SimdUtils.Transpose(from, columns, block, rows, rows, columns); InverseDct1D(columns, rows, new JxlDctSource(block, rows), new JxlDctOutput(from, rows), tmp); - JxlTranspose.Transpose(columns, rows, new JxlDctSource(from, rows), new JxlDctOutput(block, columns)); + SimdUtils.Transpose(from, rows, block, columns, columns, rows); InverseDct1D(rows, columns, new JxlDctSource(block, columns), to, tmp); } else { InverseDct1D(columns, rows, new JxlDctSource(from, rows), new JxlDctOutput(block, rows), tmp); - JxlTranspose.Transpose(columns, rows, new JxlDctSource(block, rows), new JxlDctOutput(from, columns)); + SimdUtils.Transpose(block, rows, from, columns, columns, rows); InverseDct1D(rows, columns, new JxlDctSource(from, columns), to, tmp); } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlCflFunction.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlCflFunction.cs index d5f5382184..736894c338 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlCflFunction.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlCflFunction.cs @@ -4,7 +4,6 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; -using SixLabors.ImageSharp.Common.Helpers; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.ChromaFromLuma; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlTranspose.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlTranspose.cs deleted file mode 100644 index 5797553f54..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlTranspose.cs +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.Intrinsics; -using SixLabors.ImageSharp.Common.Helpers; -using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; - -/// -/// Performs transpose on JPEG XL DCT blocks. -/// -internal static class JxlTranspose -{ - public static void Transpose(int r, int c, JxlDctSource from, JxlDctOutput to) - { - if (Vector256.IsHardwareAccelerated) - { - if (((r | c) & 7) == 0) // equivalent to (r % 8 == 0 && c % 8 == 0); micro-optimization, reduces one branch - { - // we can use SIMD - TransposeSimd256(r, c, from, to, r, c); - return; - } - } - else if (Vector128.IsHardwareAccelerated) - { - if (((r | c) & 3) == 0) // equivalent to (r % 4 == 0 && c % 4 == 0); micro-optimization, reduces one branch - { - // we can use SIMD - TransposeSimd128(r, c, from, to, r, c); - return; - } - } - - // fallback: can't use SIMD (block size isn't aligned or - // there's no v128/v256 support) - for (int n = 0; n < r; n++) - { - for (int m = 0; m < c; m++) - { - to.Write(from.Read(n, m), m, n); - } - } - } - - private static void TransposeSimd256(int rowsOr0, int colsOr0, JxlDctSource from, JxlDctOutput to, int rowsP, int colsP) - { - int rows = rowsOr0 == 0 ? rowsP : rowsOr0; - int cols = colsOr0 == 0 ? colsP : colsOr0; - - for (int n = 0; n < rows; n += 8) - { - for (int m = 0; m < cols; m += 8) - { - Vector256 i0 = from.LoadPart256(n, m); - Vector256 i1 = from.LoadPart256(n + 1, m); - Vector256 i2 = from.LoadPart256(n + 2, m); - Vector256 i3 = from.LoadPart256(n + 3, m); - Vector256 i4 = from.LoadPart256(n + 4, m); - Vector256 i5 = from.LoadPart256(n + 5, m); - Vector256 i6 = from.LoadPart256(n + 6, m); - Vector256 i7 = from.LoadPart256(n + 7, m); - - Vector256 q0 = Vector256_.InterleaveLower(i0, i2); - Vector256 q1 = Vector256_.InterleaveLower(i1, i3); - Vector256 q2 = Vector256_.InterleaveUpper(i0, i2); - Vector256 q3 = Vector256_.InterleaveUpper(i1, i3); - Vector256 q4 = Vector256_.InterleaveLower(i4, i6); - Vector256 q5 = Vector256_.InterleaveLower(i5, i7); - Vector256 q6 = Vector256_.InterleaveUpper(i4, i6); - Vector256 q7 = Vector256_.InterleaveUpper(i5, i7); - - Vector256 r0 = Vector256_.InterleaveLower(q0, q1); - Vector256 r1 = Vector256_.InterleaveUpper(q0, q1); - Vector256 r2 = Vector256_.InterleaveLower(q2, q3); - Vector256 r3 = Vector256_.InterleaveUpper(q2, q3); - Vector256 r4 = Vector256_.InterleaveLower(q4, q5); - Vector256 r5 = Vector256_.InterleaveUpper(q4, q5); - Vector256 r6 = Vector256_.InterleaveLower(q6, q7); - Vector256 r7 = Vector256_.InterleaveUpper(q6, q7); - - i0 = JxlSimdUtils.ConcatLowerLower(r4, r0); - i1 = JxlSimdUtils.ConcatLowerLower(r5, r1); - i2 = JxlSimdUtils.ConcatLowerLower(r6, r2); - i3 = JxlSimdUtils.ConcatLowerLower(r7, r3); - i4 = JxlSimdUtils.ConcatUpperUpper(r4, r0); - i5 = JxlSimdUtils.ConcatUpperUpper(r5, r1); - i6 = JxlSimdUtils.ConcatUpperUpper(r6, r2); - i7 = JxlSimdUtils.ConcatUpperUpper(r7, r3); - - to.StorePart256(i0, m, n); - to.StorePart256(i1, m + 1, n); - to.StorePart256(i2, m + 2, n); - to.StorePart256(i3, m + 3, n); - to.StorePart256(i4, m + 4, n); - to.StorePart256(i5, m + 5, n); - to.StorePart256(i6, m + 6, n); - to.StorePart256(i7, m + 7, n); - } - } - } - - private static void TransposeSimd128(int rowsOr0, int colsOr0, JxlDctSource from, JxlDctOutput to, int rowsP, int colsP) - { - int rows = rowsOr0 == 0 ? rowsP : rowsOr0; - int cols = colsOr0 == 0 ? colsP : colsOr0; - - for (int n = 0; n < rows; n += 4) - { - for (int m = 0; m < cols; m += 4) - { - Vector128 p0 = from.LoadPart128(n, m); - Vector128 p1 = from.LoadPart128(n + 1, m); - Vector128 p2 = from.LoadPart128(n + 2, m); - Vector128 p3 = from.LoadPart128(n + 3, m); - - Vector128 q0 = Vector128_.InterleaveLower(p0, p2); - Vector128 q1 = Vector128_.InterleaveLower(p1, p3); - Vector128 q2 = Vector128_.InterleaveUpper(p0, p2); - Vector128 q3 = Vector128_.InterleaveUpper(p1, p3); - - Vector128 r0 = Vector128_.InterleaveLower(q0, q1); - Vector128 r1 = Vector128_.InterleaveUpper(q0, q1); - Vector128 r2 = Vector128_.InterleaveLower(q2, q3); - Vector128 r3 = Vector128_.InterleaveUpper(q2, q3); - - to.StorePart128(r0, m, n); - to.StorePart128(r1, m + 1, n); - to.StorePart128(r2, m + 2, n); - to.StorePart128(r3, m + 3, n); - } - } - } -} diff --git a/tests/ImageSharp.Tests/Common/SimdUtilsTests.Transpose.cs b/tests/ImageSharp.Tests/Common/SimdUtilsTests.Transpose.cs new file mode 100644 index 0000000000..88e32f3717 --- /dev/null +++ b/tests/ImageSharp.Tests/Common/SimdUtilsTests.Transpose.cs @@ -0,0 +1,83 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Tests.TestUtilities; + +namespace SixLabors.ImageSharp.Tests.Common; + +public partial class SimdUtilsTests +{ + /// + /// Checks rectangular coordinates, raw floating-point bits, strided padding and scalar tails with each hardware configuration. + /// + [Fact] + public void Transpose_PreservesCoordinatesAndPadding() + { + static void RunTest() + { + ReadOnlySpan<(int Rows, int Columns)> sizes = [(0, 5), (5, 0), (1, 1), (3, 7), (4, 4), (8, 8), (16, 16), (17, 19), (33, 65)]; + ReadOnlySpan floatingPointViews = [false, true]; + ReadOnlySpan payloads = [0x7FC12345, int.MinValue, 0x7F800000, unchecked((int)0xFF800000), 0x3F800000, 0]; + const int prefix = 3; + const int sentinel = 0x12345678; + + foreach ((int rows, int columns) in sizes) + { + int sourceStride = columns + 5; + int destinationStride = rows + 7; + int[] source = new int[prefix + (rows * sourceStride) + 5]; + int[] destination = new int[prefix + (columns * destinationStride) + 5]; + source.AsSpan().Fill(sentinel); + + for (int row = 0; row < rows; row++) + { + for (int column = 0; column < columns; column++) + { + // The first row includes NaN, signed zero and infinities. Other values identify their exact source coordinates. + source[prefix + (row * sourceStride) + column] = row == 0 && column < payloads.Length + ? payloads[column] + : (row * 131071) + column; + } + } + + foreach (bool floatingPoint in floatingPointViews) + { + destination.AsSpan().Fill(sentinel); + if (floatingPoint) + { + SimdUtils.Transpose( + MemoryMarshal.Cast(source.AsSpan(prefix)), + sourceStride, + MemoryMarshal.Cast(destination.AsSpan(prefix)), + destinationStride, + rows, + columns); + } + else + { + SimdUtils.Transpose(source.AsSpan(prefix), sourceStride, destination.AsSpan(prefix), destinationStride, rows, columns); + } + + for (int i = 0; i < destination.Length; i++) + { + int relative = i - prefix; + int row = relative / destinationStride; + int column = relative % destinationStride; + + // A transpose maps destination[row,column] to source[column,row]. All other words remain untouched. + int expected = relative >= 0 && row < columns && column < rows + ? source[prefix + (column * sourceStride) + row] + : sentinel; + + Assert.Equal(expected, destination[i]); + } + } + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } +} From aa5dbef7a1a40224f5fb6b2a97caa10dabdbdedf Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 29 Oct 2026 18:25:30 +0400 Subject: [PATCH 286/328] Implement all render pipeline stages --- src/ImageSharp/Common/InlineArray.cs | 9 + src/ImageSharp/Common/InlineArray.tt | 2 +- .../Formats/Jxl/Memory/JxlMemoryHelpers.cs | 94 +++++- .../Decoder/FrameDecoder/JxlFrameDecoder.cs | 6 +- .../BlockLoader/JxlBitStreamBlockLoader.cs | 2 +- .../Decoder/Group/JxlGroupDecoder.cs | 6 +- .../Processing/Decoder/JxlDecoderInternal.cs | 2 +- .../Jxl/Processing/Decoder/JxlNoiseDecoder.cs | 2 +- ...derState.cs => JxlPassesDecoderContext.cs} | 11 +- .../Encoder/JxlAdaptiveQuantization.cs | 4 +- .../Jxl/Processing/Image/JxlImageBundle.cs | 6 + .../Formats/Jxl/Processing/JxlLoopFilter.cs | 2 +- ...aredState.cs => JxlPassesCommonContext.cs} | 10 +- .../RenderPipeline/BlendingStage.cs | 270 ++++++++++++++++++ .../Processing/RenderPipeline/Epf0Stage.cs | 3 +- .../Processing/RenderPipeline/Epf1Stage.cs | 3 +- .../Processing/RenderPipeline/Epf2Stage.cs | 3 +- .../Jxl/Processing/RenderPipeline/EpfUtils.cs | 2 +- .../RenderPipeline/UpsamplingStage.cs | 218 +++++++++++++- .../Jxl/Processing/RenderPipeline/XybStage.cs | 110 +++++++ 20 files changed, 727 insertions(+), 38 deletions(-) rename src/ImageSharp/Formats/Jxl/Processing/Decoder/{JxlPassesDecoderState.cs => JxlPassesDecoderContext.cs} (90%) rename src/ImageSharp/Formats/Jxl/Processing/{JxlPassesSharedState.cs => JxlPassesCommonContext.cs} (91%) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/BlendingStage.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/XybStage.cs diff --git a/src/ImageSharp/Common/InlineArray.cs b/src/ImageSharp/Common/InlineArray.cs index a72e3f5394..542ba742b7 100644 --- a/src/ImageSharp/Common/InlineArray.cs +++ b/src/ImageSharp/Common/InlineArray.cs @@ -71,6 +71,15 @@ internal struct InlineArray19 private T t; } +/// +/// Represents a safe, fixed sized buffer of 25 elements. +/// +[InlineArray(25)] +internal struct InlineArray25 +{ + private T t; +} + /// /// Represents a safe, fixed sized buffer of 26 elements. /// diff --git a/src/ImageSharp/Common/InlineArray.tt b/src/ImageSharp/Common/InlineArray.tt index d15650592a..a7110d543f 100644 --- a/src/ImageSharp/Common/InlineArray.tt +++ b/src/ImageSharp/Common/InlineArray.tt @@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp; <#GenerateInlineArrays();#> <#+ -private static int[] Lengths = [4, 8, 14, 16, 17, 18, 19, 26, 32, 33, 36, 256]; +private static int[] Lengths = [4, 8, 14, 16, 17, 18, 19, 25, 26, 32, 33, 36, 256]; void GenerateInlineArrays() { diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlMemoryHelpers.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlMemoryHelpers.cs index 54c7bd7560..d8d33921e1 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/JxlMemoryHelpers.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlMemoryHelpers.cs @@ -3,7 +3,6 @@ using System.Buffers; using System.Runtime.InteropServices; -using System.Security.AccessControl; namespace SixLabors.ImageSharp.Formats.Jxl.Memory; @@ -12,6 +11,15 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Memory; /// internal static class JxlMemoryHelpers { + /// + /// Reinterprets a . This method is highly similar to + /// , which is suited + /// for only. + /// + /// Type of the input memory. + /// Type of the output memory. + /// Input memory that will be reinterpreted. + /// The reinterpreted memory. public static Memory CastMemory(Memory memory) where TSourceType : unmanaged where TOutputType : unmanaged @@ -20,6 +28,22 @@ public static Memory CastMemory(Memory + /// Creates a wrapper over the input + /// data. While returns a + /// wrapper over list data, this method returns a + /// wrapper over list data. + /// + /// The type of the input list. + /// List that will have a Memory wrapper. + /// + /// A . Writing values of this memory will directly affect + /// values for the underlying List. + /// + /// + /// ⚠️ Using the Memory wrapper after the list's items have been removed or added + /// is unsafe. + /// public static Memory MemoryFromList(List list) where T : unmanaged { @@ -27,38 +51,90 @@ public static Memory MemoryFromList(List list) return lf.Memory; } + /// + /// Returns an empty . + /// + /// Kind of the memory owner. + /// A memory owner whose is empty. + public static IMemoryOwner GetEmptyMemoryOwner() => EmptyMemoryOwner.Instance; + + /// + /// Memory manager used to reinterpret a from one type + /// to another. Similar to + /// but suited for . + /// + /// Source type of the input memory. + /// Destination type of the output memory. + /// + /// Input memory of type , to be reinterpreted + /// to . + /// private sealed class CastingMemoryManager(Memory memory) : MemoryManager where TSourceType : unmanaged where TOutputType : unmanaged { + /// public override Span GetSpan() => MemoryMarshal.Cast(memory.Span); - // We don't use these. - public override MemoryHandle Pin(int elementIndex = 0) => throw new NotImplementedException(); + /// + public override MemoryHandle Pin(int elementIndex = 0) => default; - public override void Unpin() => throw new NotImplementedException(); + /// + public override void Unpin() + { + } + /// protected override void Dispose(bool disposing) { - // blank } } + /// + /// Memory manager used to create a wrapper over a + /// data. See . + /// + /// The kind of the list and the memory. + /// The input list whose wrapper will be created. private sealed class ListFactoryMemoryManager(List list) : MemoryManager where T : unmanaged { + /// public override Span GetSpan() => CollectionsMarshal.AsSpan(list); - // We don't use these. - public override MemoryHandle Pin(int elementIndex = 0) => throw new NotImplementedException(); + /// + public override MemoryHandle Pin(int elementIndex = 0) => default; - public override void Unpin() => throw new NotImplementedException(); + /// + public override void Unpin() + { + } + /// protected override void Dispose(bool disposing) { - // blank + } + } + + /// + /// A that wraps over empty memory. + /// + /// Type of the memory owner. + private sealed class EmptyMemoryOwner : IMemoryOwner + { + /// + /// Instance of this empty memory owner. + /// + public static readonly EmptyMemoryOwner Instance = new(); + + /// + public Memory Memory => Memory.Empty; + + /// + public void Dispose() + { } } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/FrameDecoder/JxlFrameDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/FrameDecoder/JxlFrameDecoder.cs index bf25a385d5..7a977fa906 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/FrameDecoder/JxlFrameDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/FrameDecoder/JxlFrameDecoder.cs @@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.FrameDecoder; internal sealed class JxlFrameDecoder { - private JxlPassesDecoderState decoderState; + private JxlPassesDecoderContext decoderState; private List toc = []; private ulong sectionSizesSum; private JxlFrameHeader frameHeader; @@ -154,7 +154,7 @@ private static int GetStride(int width, JxlPixelFormat format) return yStride; } - public static void DecodeGlobalDcInfo(Configuration configuration, JxlBitReader reader, bool isJpeg, JxlPassesDecoderState state) + public static void DecodeGlobalDcInfo(Configuration configuration, JxlBitReader reader, bool isJpeg, JxlPassesDecoderContext state) { state.SharedStorage.Quantizer.Decode(reader); @@ -176,7 +176,7 @@ public static void DecodeGlobalDcInfo(Configuration configuration, JxlBitReader state.SharedStorage.AcStrategy.FillInvalid(); } - public static void DecodeFrame(JxlPassesDecoderState decoderState, Stream stream, ref JxlFrameHeader header, JxlImageBundle decoded, JxlCodecMetadata metadata, bool useSlowRenderingPipeline) + public static void DecodeFrame(JxlPassesDecoderContext decoderState, Stream stream, ref JxlFrameHeader header, JxlImageBundle decoded, JxlCodecMetadata metadata, bool useSlowRenderingPipeline) { JxlFrameDecoder frameDecoder = new(decoderState, metadata, useSlowRenderingPipeline); JxlBitReader reader = new(stream); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/BlockLoader/JxlBitStreamBlockLoader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/BlockLoader/JxlBitStreamBlockLoader.cs index 6b52794d86..a4b1548df2 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/BlockLoader/JxlBitStreamBlockLoader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/BlockLoader/JxlBitStreamBlockLoader.cs @@ -171,7 +171,7 @@ public bool Init( int groupIndex, int histogramSelectorBits, Rectangle rect, - JxlPassesDecoderState decState, + JxlPassesDecoderContext decState, int firstPass) { for (int i = 0; i < 3; i++) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs index 2c96040bd5..b49d58ceac 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs @@ -128,7 +128,7 @@ private static bool DecodeGroupCore( JxlFrameHeader frameHeader, JxlGetBlock getBlock, JxlGroupDecoderCache groupDecCache, - JxlPassesDecoderState decState, + JxlPassesDecoderContext decState, int thread, int groupIdx, JxlRenderPipelineInput renderPipelineInput, @@ -618,7 +618,7 @@ public static bool DecodeGroup( JxlBitReader[] readers, int numPasses, int groupIndex, - JxlPassesDecoderState decState, + JxlPassesDecoderContext decState, JxlGroupDecoderCache groupDecCache, int thread, JxlRenderPipelineInput renderPipelineInput, @@ -813,7 +813,7 @@ public static bool DecodeGroupForRoundtrip( JxlFrameHeader frameHeader, IReadOnlyList ac, int groupIndex, - JxlPassesDecoderState decState, + JxlPassesDecoderContext decState, JxlGroupDecoderCache groupDecCache, int thread, JxlRenderPipelineInput renderPipelineInput, diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderInternal.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderInternal.cs index 779730875e..fce264d41e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderInternal.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderInternal.cs @@ -250,7 +250,7 @@ internal sealed class JxlDecoderInternal : ImageDecoderCore, IDisposable /// /// State for passes decoder. /// - private JxlPassesDecoderState? passesState; + private JxlPassesDecoderContext? passesState; /// /// State for frame decoder. diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs index 9621d6d608..8fcc2c5da3 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs @@ -81,7 +81,7 @@ public static void Random3Planes( } private static void PrepareNoiseInput( - JxlPassesDecoderState decState, + JxlPassesDecoderContext decState, JxlFrameDimensions frameDim, JxlFrameHeader frameHeader, int groupIndex, diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderContext.cs similarity index 90% rename from src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs rename to src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderContext.cs index 53dc70d924..d55d0ad3a5 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderContext.cs @@ -13,9 +13,12 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; -internal sealed class JxlPassesDecoderState +/// +/// Contains internal state for decoding passes of a progressive image. +/// +internal sealed class JxlPassesDecoderContext { - public JxlPassesDecoderState(JxlFrameHeader header, Configuration configuration) + public JxlPassesDecoderContext(JxlFrameHeader header, Configuration configuration) { this.XDmMultiplier = MathF.Pow(1 / 1.25f, header.XQmScale - 2f); this.BDmMultiplier = MathF.Pow(1 / 1.25f, header.BQmScale - 2f); @@ -38,9 +41,9 @@ public JxlPassesDecoderState(JxlFrameHeader header, Configuration configuration) this.Shared = this.SharedStorage; } - public JxlPassesSharedState SharedStorage { get; set; } + public JxlPassesCommonContext SharedStorage { get; set; } - public JxlPassesSharedState Shared { get; set; } + public JxlPassesCommonContext Shared { get; set; } public RenderPipelineStageBase Upsampler8x { get; set; } = new(); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs index 8302b826da..beb06b2261 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs @@ -689,7 +689,7 @@ private static JxlImageF TileDistMap(Configuration configuration, JxlImageF dist private static JxlImageBundle RoundtripImage(Configuration configuration, JxlFrameHeader frameHeader, JxlImage3F opsin, JxlPassesEncoderState encState, JxlCmsInterface cms) { - JxlPassesDecoderState decState = new(frameHeader, configuration); + JxlPassesDecoderContext decState = new(frameHeader, configuration); decState.OutputEncodingInfo.SetFromMetadata(encState.Shared.Metadata); decState.Shared = encState.Shared; @@ -725,7 +725,7 @@ private static JxlImageBundle RoundtripImage(Configuration configuration, JxlFra throw new InvalidOperationException("Could not set image bundle"); } - JxlPassesDecoderState.PipelineOptions options = new() + JxlPassesDecoderContext.PipelineOptions options = new() { UseSlowRenderPipeline = false, Coalescing = false, diff --git a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs index 487bce74cb..22b0dc0647 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs @@ -33,6 +33,12 @@ public JxlImageBundle() /// Initial image metadata. public JxlImageBundle(JxlImageMetadata? metadata) => this.Metadata = metadata; + /// + /// Gets a value indicating whether or + /// are 0. + /// + public bool XSizeOrYSizeIsZero => this.XSize == 0 || this.YSize == 0; + /// /// Gets or sets the optional name of the bundle. /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs index c7ea5b8365..5bc73ab1da 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs @@ -173,7 +173,7 @@ private static void RightMirror(Span span, int n) } } - public bool ComputeSigma(Rectangle blockRect, JxlPassesDecoderState state) + public bool ComputeSigma(Rectangle blockRect, JxlPassesDecoderContext state) { if (this.EpfIterations <= 0) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlPassesCommonContext.cs similarity index 91% rename from src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs rename to src/ImageSharp/Formats/Jxl/Processing/JxlPassesCommonContext.cs index f56689382a..a1ba01ce66 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlPassesCommonContext.cs @@ -7,13 +7,19 @@ using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Patch; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; -internal class JxlPassesSharedState +/// +/// Contains internal state for decoding and encoding of passes +/// in a progressive image. This class contains shared state used +/// both by the passes decoder and encoder. +/// +internal class JxlPassesCommonContext { // Make this a field so we can get a ref to it public JxlBlockContextMap BlockContextMap; @@ -52,7 +58,7 @@ internal class JxlPassesSharedState public int NumHistograms { get; set; } - public JxlPassesSharedState(Configuration configuration, JxlFrameHeader frameHeader, bool encoder) + public JxlPassesCommonContext(Configuration configuration, JxlFrameHeader frameHeader, bool encoder) { if (frameHeader.Metadata is null) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/BlendingStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/BlendingStage.cs new file mode 100644 index 0000000000..3d28c2364e --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/BlendingStage.cs @@ -0,0 +1,270 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; +using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Memory; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Blending; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Patch; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +/// +/// This render pipeline stage performs alpha blending. +/// +internal class BlendingStage : RenderPipelineStageBase +{ + private readonly Configuration configuration; + private readonly JxlFrameHeader frameHeader; + private readonly JxlPassesCommonContext context; + private readonly JxlBlendingInfo info; + private readonly JxlImageBundle background; + private readonly bool initialized = true; + private readonly int imageWidth; + private readonly int imageHeight; + private readonly List blendingInfo; + private readonly List extraChannelInfos; + private readonly IMemoryOwner zeroes; + + public BlendingStage(Configuration configuration, JxlFrameHeader frameHeader, JxlPassesDecoderContext decoderContext) + : base(configuration) + { + // Initialize private fields. + this.configuration = configuration; + this.frameHeader = frameHeader; + this.context = decoderContext.Shared; + this.imageWidth = frameHeader.Metadata!.XSize; + this.imageHeight = frameHeader.Metadata!.YSize; + this.extraChannelInfos = frameHeader.Metadata.ImageMetadata!.ExtraChannels; + this.info = frameHeader.BlendingInfo!; + + List extraChannelInfo = frameHeader.ExtraChannelBlendingInfo; + JxlImageBundle background = this.context.ReferenceFrames[this.info.Source].Frame; + this.background = background; + + if (background.XSize == 0 || background.YSize == 0) + { + this.zeroes = configuration.MemoryAllocator.Allocate(this.imageWidth); + } + else if (this.context.ReferenceFrames[this.info.Source].ImageBundleIsInXyb) + { + // The reference frame uses the XYB color transform, but the current frame + // doesn't use it. This isn't intended. + throw new InvalidOperationException("Cannot blend a reference frame that uses XYB with a frame that doesn't use XYB"); + } + else if (extraChannelInfo.Any(info => this.context.ReferenceFrames[this.info.Source].Frame.XSizeOrYSizeIsZero)) + { + // Reference frame to which the blending info belongs to is empty. + this.zeroes = configuration.MemoryAllocator.Allocate(this.imageWidth); + } + else + { + this.zeroes = JxlMemoryHelpers.GetEmptyMemoryOwner(); + } + + bool ok = VerifyBackgroundSize(this.background); + foreach (JxlExtraChannelInfo extraChannel in this.extraChannelInfos) + { + JxlImageBundle extraChannelBackground = this.context.ReferenceFrames[this.info.Source].Frame; + if (ok) + { + ok = VerifyBackgroundSize(extraChannelBackground); + } + } + + if (!ok) + { + this.initialized = false; + this.blendingInfo = []; + return; + } + + if (this.context.CodecMetadata.ImageMetadata!.XybEncoded) + { + // Check if we can blend this color space. + if (!decoderContext.OutputEncodingInfo.ColorEncodingIsOriginal) + { + throw new InvalidOperationException("Blending in an unsupported color space"); + } + } + + this.blendingInfo = new List(this.extraChannelInfos.Count + 1); + + bool VerifyBackgroundSize(JxlImageBundle background) + { + if (background.XSize != 0 && background.YSize != 0 && + (background.XSize < this.imageWidth || background.YSize > this.imageHeight || + background.Origin.X != 0 || background.Origin.Y != 0)) + { + return false; + } + + return true; + } + + static void MakeBlending(JxlBlendingInfo info, ref JxlPatchBlending pb) + { + pb.AlphaChannel = (int)info.AlphaChannel; + pb.Clamp = info.Clamp; + + // Translate the blending mode to a patch blending mode. + pb.Mode = info.BlendMode switch + { + JxlBlendMode.Replace => JxlPatchBlendMode.Replace, + JxlBlendMode.Add => JxlPatchBlendMode.Add, + JxlBlendMode.Multiply => JxlPatchBlendMode.Multiply, + JxlBlendMode.Blend => JxlPatchBlendMode.BlendAbove, + JxlBlendMode.AlphaWeightedBlend => JxlPatchBlendMode.AlphaWeightedAddAbove, + _ => throw new InvalidOperationException("The blend mode cannot be translated to a patch blend mode") + }; + } + + Span pb = CollectionsMarshal.AsSpan(this.blendingInfo); + + MakeBlending(this.info, ref pb[0]); + for (int i = 0; i < extraChannelInfo.Count; i++) + { + MakeBlending(extraChannelInfo[i], ref pb[i + 1]); + } + } + + /// + public override string Name => "Blending"; + + /// + public override bool IsInitialized => this.initialized; + + /// + public override bool SwitchToImageDimensions => true; + + /// + public override RenderPipelineChannelMode GetChannelMode(int channel) => RenderPipelineChannelMode.InPlace; + + /// + public override void GetImageDimensions(out int width, out int height, out Point frameOrigin) + { + width = this.imageWidth; + height = this.imageHeight; + frameOrigin = this.frameHeader.FrameOrigin; + } + + /// + public override void ProcessPaddingRow(Buffer2D> outputRows, int width, int xPos, int yPos) + { + if (this.background.XSize == 0 || this.background.YSize == 0) + { + // When we don't have a background, clear output rows. + this.GetInputRow(outputRows, 0, 0).Clear(); + this.GetInputRow(outputRows, 1, 0).Clear(); + this.GetInputRow(outputRows, 2, 0).Clear(); + } + else + { + // Copy a background. + for (int c = 0; c < 3; c++) + { + this.background.Color!.PlaneRow(c, yPos).Slice(xPos, width) + .CopyTo(this.GetInputRow(outputRows, c, 0)); + } + } + + // Copy the bundles + for (int ec = 0; ec < this.extraChannelInfos.Count; ec++) + { + // Get the background of an extra channel. + JxlImageBundle extraBackground = this.context.ReferenceFrames[this.frameHeader.ExtraChannelBlendingInfo[ec].Source].Frame; + if (extraBackground.XSize == 0 || extraBackground.YSize == 0) + { + // If the background of the extra channel is empty, + // clear the extra channel output. + this.GetInputRow(outputRows, 3 + ec, 0).Clear(); + } + else + { + // Otherwise, copy the extra channel's background. + this.GetInputRow(outputRows, 3 + ec, 0) + .CopyTo(extraBackground.GetExtraChannel(ec).GetRow(yPos)[xPos..]); + } + } + } + + /// + public override void ProcessRow(Buffer2D> inputRows, Buffer2D> outputRows, int xExtraLeft, int xExtraRight, int width, int xPos, int yPos) + { + if (!this.initialized) + { + // The blending stage must be correctly initialized. + throw new InvalidOperationException("The blending stage failed to initialize."); + } + + Point frameOrigin = this.frameHeader.FrameOrigin; + int backgroundXPos = frameOrigin.X + xPos; + int backgroundYPos = frameOrigin.Y + yPos; + int offset = 0; + + if (backgroundXPos + width <= 0 || + frameOrigin.X >= this.imageWidth || + backgroundYPos < 0 || backgroundYPos >= this.imageHeight) + { + throw new InvalidOperationException("The background is out of range."); + } + + if (backgroundXPos < 0) + { + // When the background X position is negative, + // set it to 0 and update the offset and width. + offset -= backgroundXPos; + width += backgroundXPos; + backgroundXPos = 0; + } + + if (backgroundXPos + width > this.imageWidth) + { + // The background offset should be within bounds. + width = Math.Max(0, this.imageWidth - backgroundXPos); + } + + // We can't stack-alloc a Memory, so we have to pool it. + Memory[] backgroundRowsPool = ArrayPool>.Shared.Rent(inputRows.Width); + Span> backgroundRows = backgroundRowsPool; + Memory[] foregroundRowsPool = ArrayPool>.Shared.Rent(inputRows.Width); + Span> foregroundRows = foregroundRowsPool; + + // Total number of channels (including extra channels). + int channels = Math.Min(inputRows.Width, this.extraChannelInfos.Count + 3); + + for (int c = 0; c < channels; c++) + { + foregroundRows[c] = this.GetInputRowMemory(inputRows, c, 0)[offset..]; + if (c < 3) + { + backgroundRows[c] = this.background.XSize != 0 && this.background.YSize != 0 + ? this.background.Color!.PlaneRowMemory(c, backgroundYPos)[backgroundXPos..] + : this.zeroes.Memory; + } + else + { + JxlImageBundle ecBackground = this.context.ReferenceFrames[this.frameHeader.ExtraChannelBlendingInfo[c - 3].Source].Frame; + backgroundRows[c] = ecBackground.XSize != 0 && ecBackground.YSize != 0 + ? ecBackground.GetExtraChannel(c - 3).GetRowMemory(backgroundYPos)[backgroundXPos..] + : this.zeroes.Memory; + } + } + + JxlBlending.PerformBlending( + this.configuration, + backgroundRows, + foregroundRows, + foregroundRows, + 0, + width, + this.blendingInfo[0], + CollectionsMarshal.AsSpan(this.blendingInfo)[1..], + this.extraChannelInfos); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf0Stage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf0Stage.cs index a7bcafcc3d..e469da25f1 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf0Stage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf0Stage.cs @@ -11,7 +11,8 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; /// -/// Edge Preserving Filter (type 0) stage +/// This render pipeline stage performs the Edge Preserving Filter, +/// type 0. /// internal sealed class Epf0Stage : RenderPipelineStageBase { diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf1Stage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf1Stage.cs index 5dc19b6626..1a0a6398c3 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf1Stage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf1Stage.cs @@ -11,7 +11,8 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; /// -/// Edge Preserving Filter (type 1) stage. +/// This render pipeline stage performs the Edge Preserving Filter, +/// type 1. /// internal sealed class Epf1Stage : RenderPipelineStageBase { diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf2Stage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf2Stage.cs index 7100df484c..95d0c29a52 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf2Stage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf2Stage.cs @@ -11,7 +11,8 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; /// -/// Edge Preserving Filter (type 2) stage +/// This render pipeline stage performs the Edge Preserving Filter, +/// type 2. /// internal sealed class Epf2Stage : RenderPipelineStageBase { diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/EpfUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/EpfUtils.cs index 52150ae27f..1fd4b1273e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/EpfUtils.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/EpfUtils.cs @@ -7,7 +7,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; /// -/// Utilities for EPF stages. +/// Utilities for Edge Preserving Filter stages. /// internal static class EpfUtils { diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/UpsamplingStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/UpsamplingStage.cs index d4b28a1226..705d695970 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/UpsamplingStage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/UpsamplingStage.cs @@ -1,24 +1,36 @@ // 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.Metadata; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; +/// +/// This render pipeline stage performs upsampling. +/// internal class UpsamplingStage : RenderPipelineStageBase { private const int ChunkSize = 1024; private readonly float[] kernel = new float[64 * 25]; // 1600 floats, 6400 bytes (6.25 KB) - private readonly int c; - private readonly InlineArray3>> temp = default; - public UpsamplingStage(JxlCustomTransformData upsFactors, int c, int shift) + /// + /// The offset of the channel. + /// + private readonly int channel; + + /// + /// Temporary scratch-space. + /// + private InlineArray3> temp; + + public UpsamplingStage(Configuration configuration, JxlCustomTransformData upsFactors, int c, int shift) + : base(configuration) { - this.c = c; + this.channel = c; this.Settings = RenderPipelineStageConfiguration.CreateSymmetric(shift, 2); Span kernel = this.kernel; @@ -59,10 +71,204 @@ public UpsamplingStage(JxlCustomTransformData upsFactors, int c, int shift) } } + /// public override string Name => "Upsample"; + /// public override RenderPipelineChannelMode GetChannelMode(int channel) => - channel == this.c + channel == this.channel ? RenderPipelineChannelMode.InOut : RenderPipelineChannelMode.Ignored; + + /// + public override void ProcessRow(Buffer2D> inputRows, Buffer2D> outputRows, int xExtraLeft, int xExtraRight, int width, int xPos, int yPos) + { + // The number of iterations. This can only be 2, 4 or 8. + int n = 1 << this.Settings.ShiftX; + + // Upsampling loop + for (int x = 0; x < width; x += ChunkSize) + { + int xEnd = Math.Min(x + ChunkSize, width); + int length = xEnd - x; + + // threadId would be equal to the index of multithreading. + // So for example in Parallel.For(0, 100, (x) => ...), 'x' + // is equivalent to threadId here. + this.PreprocessRow(inputRows, x, length, threadId); + this.ProcessRowInternal(n, inputRows, outputRows, x, length, threadId); + } + } + + public void PrepareForThreads(Configuration configuration, int threadCount) + { + int allocSize = ChunkSize + Math.Max(4, Vector.Count); + for (int i = 0; i < 3; i++) + { + this.temp[i] = configuration.MemoryAllocator.Allocate2D(threadCount, allocSize); + } + } + + private void PreprocessRow(Buffer2D> inputRows, int x0, int length, int threadId) + { + Span colMin = this.temp[0].DangerousGetRowSpan(threadId); + Span colMax = this.temp[1].DangerousGetRowSpan(threadId); + + InlineArray5> rows = default; + rows[0] = this.GetInputRowMemory(inputRows, this.channel, -2)[(x0 - 2)..]; + rows[1] = this.GetInputRowMemory(inputRows, this.channel, -1)[(x0 - 2)..]; + rows[2] = this.GetInputRowMemory(inputRows, this.channel, 0)[(x0 - 2)..]; + rows[3] = this.GetInputRowMemory(inputRows, this.channel, 1)[(x0 - 2)..]; + rows[4] = this.GetInputRowMemory(inputRows, this.channel, 2)[(x0 - 2)..]; + + for (int x = 0; x <= length - Vector.Count + 4; x += Vector.Count) + { + Vector v0 = Vector.Create(rows[0].Span[x..]); + Vector v1 = Vector.Create(rows[1].Span[x..]); + Vector min0 = Vector.Min(v0, v1); + Vector max0 = Vector.Max(v0, v1); + Vector v2 = Vector.Create(rows[2].Span[x..]); + Vector v3 = Vector.Create(rows[3].Span[x..]); + Vector min1 = Vector.Min(v2, v3); + Vector max1 = Vector.Max(v2, v3); + Vector v4 = Vector.Create(rows[4].Span[x..]); + Vector min2 = Vector.Min(min0, min1); + Vector max2 = Vector.Max(max0, max1); + Vector min = Vector.Min(v4, min2); + Vector max = Vector.Max(v4, max2); + min.CopyTo(colMin[x..]); + max.CopyTo(colMax[x..]); + } + + Span mins = this.temp[2].DangerousGetRowSpan(threadId); + for (int x = 0; x <= length - Vector.Count; x += Vector.Count) + { + Vector v0 = Vector.Create(colMin[x..]); + Vector v1 = Vector.Create(colMin[(x + 1)..]); + Vector min0 = Vector.Min(v0, v1); + Vector v2 = Vector.Create(colMin[(x + 2)..]); + Vector v3 = Vector.Create(colMin[(x + 3)..]); + Vector min1 = Vector.Min(v2, v3); + Vector v4 = Vector.Create(colMin[(x + 4)..]); + Vector min2 = Vector.Min(min0, min1); + Vector min = Vector.Min(v4, min2); + min.CopyTo(mins[x..]); + } + + Span maxs = this.temp[0].DangerousGetRowSpan(threadId); + for (int x = 0; x <= length - Vector.Count; x += Vector.Count) + { + Vector v0 = Vector.Create(colMax[x..]); + Vector v1 = Vector.Create(colMax[(x + 1)..]); + Vector max0 = Vector.Max(v0, v1); + Vector v2 = Vector.Create(colMax[(x + 2)..]); + Vector v3 = Vector.Create(colMax[(x + 3)..]); + Vector max1 = Vector.Max(v2, v3); + Vector v4 = Vector.Create(colMax[(x + 4)..]); + Vector max2 = Vector.Max(max0, max1); + Vector max = Vector.Max(v4, max2); + max.CopyTo(maxs[x..]); + } + } + + private float GetKernel(int k, int i) => this.kernel[(k * 25) + i]; + + private void ProcessRowInternal(int n, Buffer2D> inputRows, Buffer2D> outputRows, int x0, int length, int threadId) + { + InlineArray8> upsampling = default; + Span mins = this.temp[2].DangerousGetRowSpan(threadId); + Span maxs = this.temp[0].DangerousGetRowSpan(threadId); + InlineArray25> input = default; + + // Retrieve the input rows into the 'input' variable. + for (int iy = -2; iy <= 2; ++iy) + { + for (int ix = -2; ix <= 2; ++ix) + { + input[(5 * (iy + 2)) + (ix + 2)] = this.GetInputRowMemory(inputRows, this.channel, iy)[(x0 + ix)..]; + } + } + + // Upsampling loop + for (int x = 0; x <= length - Vector.Count; x += Vector.Count) + { + for (int oy = 0; oy < n; oy++) + { + Span destRow = GetOutputRow(outputRows, this.channel, oy)[(x0 * n)..]; + for (int ox = 0; ox < n; ox++) + { + // Main upsampling logic here. + int k = (n * oy) + ox; + + // Get input as SIMD vectors and multiply them by the kernel. + Vector acc0 = Vector.Create(input[0].Span) * Vector.Create(this.GetKernel(k, 0)); + Vector acc1 = Vector.Create(input[1].Span) * Vector.Create(this.GetKernel(k, 1)); + Vector acc2 = Vector.Create(input[2].Span) * Vector.Create(this.GetKernel(k, 2)); + + // Upsample using the kernel. + for (int i = 3; i < 24; i += 3) + { + acc0 = Vector.FusedMultiplyAdd(Vector.Create(input[i].Span), Vector.Create(this.GetKernel(k, i)), acc0); + acc1 = Vector.FusedMultiplyAdd(Vector.Create(input[i + 1].Span), Vector.Create(this.GetKernel(k, i + 1)), acc1); + acc2 = Vector.FusedMultiplyAdd(Vector.Create(input[i + 2].Span), Vector.Create(this.GetKernel(k, i + 2)), acc2); + } + + acc0 = Vector.FusedMultiplyAdd(Vector.Create(input[24].Span), Vector.Create(this.GetKernel(k, 24)), acc0); + + // Finalize + Vector result = acc0 + acc1 + acc2; + upsampling[ox] = Vector.Clamp(result, Vector.Create(mins[x..]), Vector.Create(maxs[x..])); + } + + // Write back the upsampled vectors to the destination row. + switch (n) + { + case 2: + JxlSimdUtils.StoreInterleaved( + upsampling[0], + upsampling[1], + ref destRow[x * n]); + break; + case 4: + JxlSimdUtils.StoreInterleaved( + upsampling[0], + upsampling[1], + upsampling[2], + upsampling[3], + ref destRow[x * n]); + break; + case 8: + JxlSimdUtils.StoreInterleaved( + upsampling[0], + upsampling[1], + upsampling[2], + upsampling[3], + upsampling[4], + upsampling[5], + upsampling[6], + upsampling[7], + ref destRow[x * n]); + break; + } + } + + // Shift them SIMD length forward. This is so the + // next iteration can start at the shifted offset. + for (int i = 0; i < 25; i++) + { + input[i] = input[i][Vector.Count..]; + } + } + } + + /// + public override void Dispose() + { + base.Dispose(); + + // We only have to dispose scratch-space. + this.temp[0].Dispose(); + this.temp[1].Dispose(); + this.temp[2].Dispose(); + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/XybStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/XybStage.cs new file mode 100644 index 0000000000..96db48e48d --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/XybStage.cs @@ -0,0 +1,110 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Jxl.Cms; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +/// +/// Accepts XYB as input channels. If the output is XYB, this stage +/// scales them. Otherwise, converts them to RGB. +/// +internal class XybStage : RenderPipelineStageBase +{ + /// + /// Parameters for XYB to RGB conversion. + /// + private readonly JxlOpsinParameters opsinParameters; + + /// + /// Is the output channel XYB? If so, this stage will + /// scale the input XYB channels for output. Otherwise, + /// the input XYB channels are converted to RGB. + /// + private readonly bool outputIsXYB; + + public XybStage(Configuration configuration, JxlOutputEncodingInfo outputEncodingInfo) + : base(configuration) + { + this.opsinParameters = outputEncodingInfo.OpsinParameters; + this.outputIsXYB = outputEncodingInfo.ColorEncoding.ColorSpace == JxlColorSpace.Xyb; + } + + /// + public override string Name => "XYB"; + + /// + public override RenderPipelineChannelMode GetChannelMode(int channel) + => channel < 3 ? RenderPipelineChannelMode.InPlace + : RenderPipelineChannelMode.Ignored; + + /// + public override void ProcessRow(Buffer2D> inputRows, Buffer2D> outputRows, int xExtraLeft, int xExtraRight, int width, int xPos, int yPos) + { + if (xExtraLeft == 0 || xExtraRight == 0) + { + throw new InvalidOperationException("xExtraLeft/xExtraRight must both be > 0"); + } + + Span row0 = this.GetInputRow(inputRows, 0, 0); + Span row1 = this.GetInputRow(inputRows, 1, 0); + Span row2 = this.GetInputRow(inputRows, 2, 0); + + if (this.outputIsXYB) + { + // If the output is preferred to be XYB, then + // we simply scale the input XYB channels. + Vector scaleX = Vector.Create(JxlOpsinConstants.ScaledXybScale[0]); + Vector scaleY = Vector.Create(JxlOpsinConstants.ScaledXybScale[1]); + Vector scaleBMY = Vector.Create(JxlOpsinConstants.ScaledXybScale[2]); + Vector offsetX = Vector.Create(JxlOpsinConstants.ScaledXybOffset[0]); + Vector offsetY = Vector.Create(JxlOpsinConstants.ScaledXybOffset[1]); + Vector offsetBMY = Vector.Create(JxlOpsinConstants.ScaledXybOffset[2]); + + // Main scaling loop + for (int x = 0; x <= width - Vector.Count; x += Vector.Count) + { + // Input XYB. + Vector inX = Vector.Create(row0[x..]); + Vector inY = Vector.Create(row1[x..]); + Vector inB = Vector.Create(row2[x..]); + + // Scaled input XYB. + Vector outX = (inX + offsetX) * scaleX; + Vector outY = (inY + offsetY) * scaleY; + Vector outB = ((inB - inY) + offsetBMY) * scaleBMY; + + // Write scaled XYB in-place. + outX.CopyTo(row0[x..]); + outY.CopyTo(row1[x..]); + outB.CopyTo(row2[x..]); + } + } + else + { + // In this case, we convert XYB to RGB in-place. + for (int x = 0; x <= width - Vector.Count; x += Vector.Count) + { + // Input XYB. + Vector inOpsinX = Vector.Create(row0[x..]); + Vector inOpsinY = Vector.Create(row1[x..]); + Vector inOpsinB = Vector.Create(row2[x..]); + + // Convert XYB->RGB + Unsafe.SkipInit(out Vector r); + Unsafe.SkipInit(out Vector g); + Unsafe.SkipInit(out Vector b); + JxlXybDecoder.ConvertXybToRgb(inOpsinX, inOpsinY, inOpsinB, this.opsinParameters, ref r, ref g, ref b); + + // Write RGB in-place. + r.CopyTo(row0[x..]); + g.CopyTo(row1[x..]); + b.CopyTo(row2[x..]); + } + } + } +} From 49d474dd26c214207a438f534177a1e387cd17a0 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Oct 2026 00:32:27 +1000 Subject: [PATCH 287/328] Reuse existing transpose kernels across codecs and group SIMD layouts --- ...olorPacking.cs => SimdUtils.Interleave.cs} | 158 ++--- .../Common/Helpers/SimdUtils.Transpose.cs | 635 +++++++++++++++--- ...traSuperblockEncoder.TransformTypeModel.cs | 2 +- .../Lookahead/Av1FirstPass.Wavelet.cs | 4 +- .../Av1/Transform/Av1ForwardTransformer.cs | 8 +- .../Av1/Transform/Av1Inverse2dTransformer.cs | 12 +- .../Av1InverseWalshHadamardTransformer.cs | 4 +- .../Av1/Transform/Av1Transform2dOperations.cs | 338 +--------- ...acking.cs => SimdUtilsTests.Interleave.cs} | 73 ++ .../Common/SimdUtilsTests.Transpose.cs | 225 ++++++- 10 files changed, 899 insertions(+), 560 deletions(-) rename src/ImageSharp/Common/Helpers/{SimdUtils.ColorPacking.cs => SimdUtils.Interleave.cs} (93%) rename tests/ImageSharp.Tests/Common/{SimdUtilsTests.ColorPacking.cs => SimdUtilsTests.Interleave.cs} (88%) diff --git a/src/ImageSharp/Common/Helpers/SimdUtils.ColorPacking.cs b/src/ImageSharp/Common/Helpers/SimdUtils.Interleave.cs similarity index 93% rename from src/ImageSharp/Common/Helpers/SimdUtils.ColorPacking.cs rename to src/ImageSharp/Common/Helpers/SimdUtils.Interleave.cs index 1aa1fd2504..e266011dc6 100644 --- a/src/ImageSharp/Common/Helpers/SimdUtils.ColorPacking.cs +++ b/src/ImageSharp/Common/Helpers/SimdUtils.Interleave.cs @@ -12,6 +12,55 @@ namespace SixLabors.ImageSharp; internal static partial class SimdUtils { + /// + /// Separates four consecutive integer pairs without changing their stored bits. + /// + /// The first two pairs. + /// The following two pairs. + /// The first value of every pair, in source order. + /// The second value of every pair, in source order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Deinterleave2(Vector128 lower, Vector128 upper, out Vector128 even, out Vector128 odd) + { + // Each shuffle places one component's two samples in the low 64 bits. Joining those halves restores the complete source order. + Vector128 lowEven = Vector128.ShuffleNative(lower, Vector128.Create(0, 2, 0, 2)); + Vector128 highEven = Vector128.ShuffleNative(upper, Vector128.Create(0, 2, 0, 2)); + Vector128 lowOdd = Vector128.ShuffleNative(lower, Vector128.Create(1, 3, 1, 3)); + Vector128 highOdd = Vector128.ShuffleNative(upper, Vector128.Create(1, 3, 1, 3)); + even = Vector128.Create(lowEven.GetLower(), highEven.GetLower()); + odd = Vector128.Create(lowOdd.GetLower(), highOdd.GetLower()); + } + + /// + /// Separates eight consecutive integer pairs without changing their stored bits. + /// + /// The first four pairs. + /// The following four pairs. + /// The first value of every pair, in source order. + /// The second value of every pair, in source order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Deinterleave2(Vector256 lower, Vector256 upper, out Vector256 even, out Vector256 odd) + { + // Indices 0..7 select lower and 8..15 select upper. Full-register selection crosses 128-bit lane boundaries in source order. + even = Vector256_.PermuteVar8x32x2(lower, Vector256.Create(0, 2, 4, 6, 8, 10, 12, 14), upper); + odd = Vector256_.PermuteVar8x32x2(lower, Vector256.Create(1, 3, 5, 7, 9, 11, 13, 15), upper); + } + + /// + /// Separates sixteen consecutive integer pairs without changing their stored bits. + /// + /// The first eight pairs. + /// The following eight pairs. + /// The first value of every pair, in source order. + /// The second value of every pair, in source order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Deinterleave2(Vector512 lower, Vector512 upper, out Vector512 even, out Vector512 odd) + { + // One native permutation selects each component from the combined 32-word input. The shared helper supplies its portable fallback. + even = Vector512_.PermuteVar16x32x2(lower, Vector512.Create(0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30), upper); + odd = Vector512_.PermuteVar16x32x2(lower, Vector512.Create(1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31), upper); + } + /// /// Interleaves four integer pairs without changing their stored bits. /// @@ -1338,113 +1387,4 @@ public static void PackedInvertNormalizeInterleave4( Unsafe.Add(ref packedRef, packedOffset + 3) = (maxValue - Unsafe.Add(ref wLaneRef, sourceOffset)) * scale; } } - - /// - /// Transposes four four-lane rows into four four-lane columns. - /// - /// The first matrix row. - /// The second matrix row. - /// The third matrix row. - /// The fourth matrix row. - /// The first matrix column. - /// The second matrix column. - /// The third matrix column. - /// The fourth matrix column. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Transpose4( - Vector128 row0, - Vector128 row1, - Vector128 row2, - Vector128 row3, - out Vector128 column0, - out Vector128 column1, - out Vector128 column2, - out Vector128 column3) - { - // The first unpack interleaves adjacent 32-bit lanes from rows 0/1 and 2/3: - // row01Low = [r0c0 r1c0 r0c1 r1c1] - // row23Low = [r2c0 r3c0 r2c1 r3c1] - // A second unpack treats each adjacent pair as one 64-bit lane and combines - // the row01 and row23 pairs into complete columns. The integer views only - // expose the cross-platform unpack helpers; every floating-point bit is preserved. - Vector128 row01Low = Vector128_.UnpackLow(row0.AsInt32(), row1.AsInt32()); - Vector128 row01High = Vector128_.UnpackHigh(row0.AsInt32(), row1.AsInt32()); - Vector128 row23Low = Vector128_.UnpackLow(row2.AsInt32(), row3.AsInt32()); - Vector128 row23High = Vector128_.UnpackHigh(row2.AsInt32(), row3.AsInt32()); - - column0 = Vector128_.UnpackLow(row01Low.AsInt64(), row23Low.AsInt64()).AsSingle(); - column1 = Vector128_.UnpackHigh(row01Low.AsInt64(), row23Low.AsInt64()).AsSingle(); - column2 = Vector128_.UnpackLow(row01High.AsInt64(), row23High.AsInt64()).AsSingle(); - column3 = Vector128_.UnpackHigh(row01High.AsInt64(), row23High.AsInt64()).AsSingle(); - } - - /// - /// Transposes four-by-four matrices independently within each 128-bit group. - /// - /// The first rows of the matrices. - /// The second rows of the matrices. - /// The third rows of the matrices. - /// The fourth rows of the matrices. - /// The first columns of the matrices. - /// The second columns of the matrices. - /// The third columns of the matrices. - /// The fourth columns of the matrices. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Transpose4( - Vector256 row0, - Vector256 row1, - Vector256 row2, - Vector256 row3, - out Vector256 column0, - out Vector256 column1, - out Vector256 column2, - out Vector256 column3) - { - // Match the 128-bit transpose: first pair adjacent 32-bit words, then pair the resulting 64-bit groups. - // Unpack keeps 128-bit groups separate. Callers permute those groups when they need consecutive color indices. - Vector256 row01Low = Vector256_.UnpackLow(row0.AsInt32(), row1.AsInt32()); - Vector256 row01High = Vector256_.UnpackHigh(row0.AsInt32(), row1.AsInt32()); - Vector256 row23Low = Vector256_.UnpackLow(row2.AsInt32(), row3.AsInt32()); - Vector256 row23High = Vector256_.UnpackHigh(row2.AsInt32(), row3.AsInt32()); - - column0 = Vector256_.UnpackLow(row01Low.AsInt64(), row23Low.AsInt64()).AsSingle(); - column1 = Vector256_.UnpackHigh(row01Low.AsInt64(), row23Low.AsInt64()).AsSingle(); - column2 = Vector256_.UnpackLow(row01High.AsInt64(), row23High.AsInt64()).AsSingle(); - column3 = Vector256_.UnpackHigh(row01High.AsInt64(), row23High.AsInt64()).AsSingle(); - } - - /// - /// Transposes four-by-four matrices independently within each 128-bit group. - /// - /// The first rows of the matrices. - /// The second rows of the matrices. - /// The third rows of the matrices. - /// The fourth rows of the matrices. - /// The first columns of the matrices. - /// The second columns of the matrices. - /// The third columns of the matrices. - /// The fourth columns of the matrices. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Transpose4( - Vector512 row0, - Vector512 row1, - Vector512 row2, - Vector512 row3, - out Vector512 column0, - out Vector512 column1, - out Vector512 column2, - out Vector512 column3) - { - // Match the 128-bit transpose: first pair adjacent 32-bit words, then pair the resulting 64-bit groups. - // Unpack keeps 128-bit groups separate. Callers permute those groups when they need consecutive color indices. - Vector512 row01Low = Vector512_.UnpackLow(row0.AsInt32(), row1.AsInt32()); - Vector512 row01High = Vector512_.UnpackHigh(row0.AsInt32(), row1.AsInt32()); - Vector512 row23Low = Vector512_.UnpackLow(row2.AsInt32(), row3.AsInt32()); - Vector512 row23High = Vector512_.UnpackHigh(row2.AsInt32(), row3.AsInt32()); - - column0 = Vector512_.UnpackLow(row01Low.AsInt64(), row23Low.AsInt64()).AsSingle(); - column1 = Vector512_.UnpackHigh(row01Low.AsInt64(), row23Low.AsInt64()).AsSingle(); - column2 = Vector512_.UnpackLow(row01High.AsInt64(), row23High.AsInt64()).AsSingle(); - column3 = Vector512_.UnpackHigh(row01High.AsInt64(), row23High.AsInt64()).AsSingle(); - } } diff --git a/src/ImageSharp/Common/Helpers/SimdUtils.Transpose.cs b/src/ImageSharp/Common/Helpers/SimdUtils.Transpose.cs index 4f71304488..68940cab3b 100644 --- a/src/ImageSharp/Common/Helpers/SimdUtils.Transpose.cs +++ b/src/ImageSharp/Common/Helpers/SimdUtils.Transpose.cs @@ -4,11 +4,12 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; namespace SixLabors.ImageSharp; /// -/// Provides SIMD operations for strided sample matrices. +/// Provides shared matrix transposition for SIMD registers and strided sample buffers. /// internal static partial class SimdUtils { @@ -39,9 +40,10 @@ public static void Transpose(ReadOnlySpan source, int sourceStride, Span.Count; + // Complete each sixteen-by-sixteen cache tile before advancing. A complete register kernel exchanges both row and column index bits. + // Smaller kernels consume incomplete tiles without reading source padding or writing destination padding. + const int tileSize = 16; + for (int rowBase = 0; rowBase < rows; rowBase += tileSize) { int rowEnd = Math.Min(rowBase + tileSize, rows); @@ -50,138 +52,573 @@ public static void Transpose(ReadOnlySpan source, int sourceStride, Span= 16 ? 16 + : Vector256.IsHardwareAccelerated && rowEnd - row >= 8 ? 8 + : Vector128.IsHardwareAccelerated && rowEnd - row >= 4 ? 4 : 1; + int column = columnBase; - if (Vector512.IsHardwareAccelerated) + + if (Vector512.IsHardwareAccelerated && rowCount >= 16) { - for (; column + Vector512.Count <= columnEnd; column += Vector512.Count) + for (; column <= columnEnd - 16; column += 16) { - Vector512 row0 = Vector512.LoadUnsafe(ref input, (nuint)((row * sourceStride) + column)).AsSingle(); - Vector512 row1 = Vector512.LoadUnsafe(ref input, (nuint)(((row + 1) * sourceStride) + column)).AsSingle(); - Vector512 row2 = Vector512.LoadUnsafe(ref input, (nuint)(((row + 2) * sourceStride) + column)).AsSingle(); - Vector512 row3 = Vector512.LoadUnsafe(ref input, (nuint)(((row + 3) * sourceStride) + column)).AsSingle(); - Transpose4( - row0, - row1, - row2, - row3, - out Vector512 column0, - out Vector512 column1, - out Vector512 column2, - out Vector512 column3); - - StoreTransposedColumns(column0.AsInt32(), ref output, destinationStride, row, column + 0); - StoreTransposedColumns(column1.AsInt32(), ref output, destinationStride, row, column + 1); - StoreTransposedColumns(column2.AsInt32(), ref output, destinationStride, row, column + 2); - StoreTransposedColumns(column3.AsInt32(), ref output, destinationStride, row, column + 3); + for (int subRow = row; subRow < row + rowCount; subRow += 16) + { + Vector512 row0 = Vector512.LoadUnsafe(ref input, (nuint)((subRow * sourceStride) + column)); + Vector512 row1 = Vector512.LoadUnsafe(ref input, (nuint)(((subRow + 1) * sourceStride) + column)); + Vector512 row2 = Vector512.LoadUnsafe(ref input, (nuint)(((subRow + 2) * sourceStride) + column)); + Vector512 row3 = Vector512.LoadUnsafe(ref input, (nuint)(((subRow + 3) * sourceStride) + column)); + Vector512 row4 = Vector512.LoadUnsafe(ref input, (nuint)(((subRow + 4) * sourceStride) + column)); + Vector512 row5 = Vector512.LoadUnsafe(ref input, (nuint)(((subRow + 5) * sourceStride) + column)); + Vector512 row6 = Vector512.LoadUnsafe(ref input, (nuint)(((subRow + 6) * sourceStride) + column)); + Vector512 row7 = Vector512.LoadUnsafe(ref input, (nuint)(((subRow + 7) * sourceStride) + column)); + Vector512 row8 = Vector512.LoadUnsafe(ref input, (nuint)(((subRow + 8) * sourceStride) + column)); + Vector512 row9 = Vector512.LoadUnsafe(ref input, (nuint)(((subRow + 9) * sourceStride) + column)); + Vector512 row10 = Vector512.LoadUnsafe(ref input, (nuint)(((subRow + 10) * sourceStride) + column)); + Vector512 row11 = Vector512.LoadUnsafe(ref input, (nuint)(((subRow + 11) * sourceStride) + column)); + Vector512 row12 = Vector512.LoadUnsafe(ref input, (nuint)(((subRow + 12) * sourceStride) + column)); + Vector512 row13 = Vector512.LoadUnsafe(ref input, (nuint)(((subRow + 13) * sourceStride) + column)); + Vector512 row14 = Vector512.LoadUnsafe(ref input, (nuint)(((subRow + 14) * sourceStride) + column)); + Vector512 row15 = Vector512.LoadUnsafe(ref input, (nuint)(((subRow + 15) * sourceStride) + column)); + + Transpose( + ref row0, + ref row1, + ref row2, + ref row3, + ref row4, + ref row5, + ref row6, + ref row7, + ref row8, + ref row9, + ref row10, + ref row11, + ref row12, + ref row13, + ref row14, + ref row15); + + row0.StoreUnsafe(ref output, (nuint)((column * destinationStride) + subRow)); + row1.StoreUnsafe(ref output, (nuint)(((column + 1) * destinationStride) + subRow)); + row2.StoreUnsafe(ref output, (nuint)(((column + 2) * destinationStride) + subRow)); + row3.StoreUnsafe(ref output, (nuint)(((column + 3) * destinationStride) + subRow)); + row4.StoreUnsafe(ref output, (nuint)(((column + 4) * destinationStride) + subRow)); + row5.StoreUnsafe(ref output, (nuint)(((column + 5) * destinationStride) + subRow)); + row6.StoreUnsafe(ref output, (nuint)(((column + 6) * destinationStride) + subRow)); + row7.StoreUnsafe(ref output, (nuint)(((column + 7) * destinationStride) + subRow)); + row8.StoreUnsafe(ref output, (nuint)(((column + 8) * destinationStride) + subRow)); + row9.StoreUnsafe(ref output, (nuint)(((column + 9) * destinationStride) + subRow)); + row10.StoreUnsafe(ref output, (nuint)(((column + 10) * destinationStride) + subRow)); + row11.StoreUnsafe(ref output, (nuint)(((column + 11) * destinationStride) + subRow)); + row12.StoreUnsafe(ref output, (nuint)(((column + 12) * destinationStride) + subRow)); + row13.StoreUnsafe(ref output, (nuint)(((column + 13) * destinationStride) + subRow)); + row14.StoreUnsafe(ref output, (nuint)(((column + 14) * destinationStride) + subRow)); + row15.StoreUnsafe(ref output, (nuint)(((column + 15) * destinationStride) + subRow)); + } } } - if (Vector256.IsHardwareAccelerated) + if (Vector256.IsHardwareAccelerated && rowCount >= 8) { - for (; column + Vector256.Count <= columnEnd; column += Vector256.Count) + for (; column <= columnEnd - 8; column += 8) { - Vector256 row0 = Vector256.LoadUnsafe(ref input, (nuint)((row * sourceStride) + column)).AsSingle(); - Vector256 row1 = Vector256.LoadUnsafe(ref input, (nuint)(((row + 1) * sourceStride) + column)).AsSingle(); - Vector256 row2 = Vector256.LoadUnsafe(ref input, (nuint)(((row + 2) * sourceStride) + column)).AsSingle(); - Vector256 row3 = Vector256.LoadUnsafe(ref input, (nuint)(((row + 3) * sourceStride) + column)).AsSingle(); - Transpose4( - row0, - row1, - row2, - row3, - out Vector256 column0, - out Vector256 column1, - out Vector256 column2, - out Vector256 column3); - - StoreTransposedColumns(column0.AsInt32(), ref output, destinationStride, row, column + 0); - StoreTransposedColumns(column1.AsInt32(), ref output, destinationStride, row, column + 1); - StoreTransposedColumns(column2.AsInt32(), ref output, destinationStride, row, column + 2); - StoreTransposedColumns(column3.AsInt32(), ref output, destinationStride, row, column + 3); + for (int subRow = row; subRow < row + rowCount; subRow += 8) + { + Vector256 row0 = Vector256.LoadUnsafe(ref input, (nuint)((subRow * sourceStride) + column)); + Vector256 row1 = Vector256.LoadUnsafe(ref input, (nuint)(((subRow + 1) * sourceStride) + column)); + Vector256 row2 = Vector256.LoadUnsafe(ref input, (nuint)(((subRow + 2) * sourceStride) + column)); + Vector256 row3 = Vector256.LoadUnsafe(ref input, (nuint)(((subRow + 3) * sourceStride) + column)); + Vector256 row4 = Vector256.LoadUnsafe(ref input, (nuint)(((subRow + 4) * sourceStride) + column)); + Vector256 row5 = Vector256.LoadUnsafe(ref input, (nuint)(((subRow + 5) * sourceStride) + column)); + Vector256 row6 = Vector256.LoadUnsafe(ref input, (nuint)(((subRow + 6) * sourceStride) + column)); + Vector256 row7 = Vector256.LoadUnsafe(ref input, (nuint)(((subRow + 7) * sourceStride) + column)); + + Transpose(ref row0, ref row1, ref row2, ref row3, ref row4, ref row5, ref row6, ref row7); + row0.StoreUnsafe(ref output, (nuint)((column * destinationStride) + subRow)); + row1.StoreUnsafe(ref output, (nuint)(((column + 1) * destinationStride) + subRow)); + row2.StoreUnsafe(ref output, (nuint)(((column + 2) * destinationStride) + subRow)); + row3.StoreUnsafe(ref output, (nuint)(((column + 3) * destinationStride) + subRow)); + row4.StoreUnsafe(ref output, (nuint)(((column + 4) * destinationStride) + subRow)); + row5.StoreUnsafe(ref output, (nuint)(((column + 5) * destinationStride) + subRow)); + row6.StoreUnsafe(ref output, (nuint)(((column + 6) * destinationStride) + subRow)); + row7.StoreUnsafe(ref output, (nuint)(((column + 7) * destinationStride) + subRow)); + } } } - if (Vector128.IsHardwareAccelerated) + if (Vector128.IsHardwareAccelerated && rowCount >= 4) { - for (; column + Vector128.Count <= columnEnd; column += Vector128.Count) + for (; column <= columnEnd - 4; column += 4) { - Vector128 row0 = Vector128.LoadUnsafe(ref input, (nuint)((row * sourceStride) + column)).AsSingle(); - Vector128 row1 = Vector128.LoadUnsafe(ref input, (nuint)(((row + 1) * sourceStride) + column)).AsSingle(); - Vector128 row2 = Vector128.LoadUnsafe(ref input, (nuint)(((row + 2) * sourceStride) + column)).AsSingle(); - Vector128 row3 = Vector128.LoadUnsafe(ref input, (nuint)(((row + 3) * sourceStride) + column)).AsSingle(); - Transpose4( - row0, - row1, - row2, - row3, - out Vector128 column0, - out Vector128 column1, - out Vector128 column2, - out Vector128 column3); - - column0.AsInt32().StoreUnsafe(ref output, (nuint)(((column + 0) * destinationStride) + row)); - column1.AsInt32().StoreUnsafe(ref output, (nuint)(((column + 1) * destinationStride) + row)); - column2.AsInt32().StoreUnsafe(ref output, (nuint)(((column + 2) * destinationStride) + row)); - column3.AsInt32().StoreUnsafe(ref output, (nuint)(((column + 3) * destinationStride) + row)); + for (int subRow = row; subRow < row + rowCount; subRow += 4) + { + Vector128 row0 = Vector128.LoadUnsafe(ref input, (nuint)((subRow * sourceStride) + column)); + Vector128 row1 = Vector128.LoadUnsafe(ref input, (nuint)(((subRow + 1) * sourceStride) + column)); + Vector128 row2 = Vector128.LoadUnsafe(ref input, (nuint)(((subRow + 2) * sourceStride) + column)); + Vector128 row3 = Vector128.LoadUnsafe(ref input, (nuint)(((subRow + 3) * sourceStride) + column)); + + Transpose(ref row0, ref row1, ref row2, ref row3); + row0.StoreUnsafe(ref output, (nuint)((column * destinationStride) + subRow)); + row1.StoreUnsafe(ref output, (nuint)(((column + 1) * destinationStride) + subRow)); + row2.StoreUnsafe(ref output, (nuint)(((column + 2) * destinationStride) + subRow)); + row3.StoreUnsafe(ref output, (nuint)(((column + 3) * destinationStride) + subRow)); + } } } - // The column remainder has fewer than four elements. Move four independent rows without reading padding. + // Fewer than four columns remain, or no vector kernel is available. Each assignment moves exactly one logical sample. for (; column < columnEnd; column++) { - for (int lane = 0; lane < 4; lane++) + for (int subRow = row; subRow < row + rowCount; subRow++) { - Unsafe.Add(ref output, (column * destinationStride) + row + lane) = - Unsafe.Add(ref input, ((row + lane) * sourceStride) + column); + Unsafe.Add(ref output, (column * destinationStride) + subRow) = Unsafe.Add(ref input, (subRow * sourceStride) + column); } } - } - // Up to three rows remain at the bottom of this tile. The same sample equation also covers scalar-only hardware. - for (; row < rowEnd; row++) - { - for (int column = columnBase; column < columnEnd; column++) - { - Unsafe.Add(ref output, (column * destinationStride) + row) = Unsafe.Add(ref input, (row * sourceStride) + column); - } + row += rowCount; } } } } /// - /// Places two independent four-word columns from a 256-bit transpose at their strided matrix positions. + /// Transposes a 4-by-4 matrix of 32-bit lanes. + /// + /// The first input row, replaced by the first output row. + /// The second input row, replaced by the second output row. + /// The third input row, replaced by the third output row. + /// The fourth input row, replaced by the fourth output row. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Transpose(ref Vector128 row0, ref Vector128 row1, ref Vector128 row2, ref Vector128 row3) + { + Vector128 pairs01Low = Vector128_.UnpackLow(row0, row1); + Vector128 pairs01High = Vector128_.UnpackHigh(row0, row1); + Vector128 pairs23Low = Vector128_.UnpackLow(row2, row3); + Vector128 pairs23High = Vector128_.UnpackHigh(row2, row3); + + row0 = Vector128_.UnpackLow(pairs01Low.AsInt64(), pairs23Low.AsInt64()).AsInt32(); + row1 = Vector128_.UnpackHigh(pairs01Low.AsInt64(), pairs23Low.AsInt64()).AsInt32(); + row2 = Vector128_.UnpackLow(pairs01High.AsInt64(), pairs23High.AsInt64()).AsInt32(); + row3 = Vector128_.UnpackHigh(pairs01High.AsInt64(), pairs23High.AsInt64()).AsInt32(); + } + + /// + /// Transposes an 8-by-8 matrix of 32-bit lanes. + /// + /// The first input row, replaced by the first output row. + /// The second input row, replaced by the second output row. + /// The third input row, replaced by the third output row. + /// The fourth input row, replaced by the fourth output row. + /// The fifth input row, replaced by the fifth output row. + /// The sixth input row, replaced by the sixth output row. + /// The seventh input row, replaced by the seventh output row. + /// The eighth input row, replaced by the eighth output row. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Transpose( + ref Vector256 row0, + ref Vector256 row1, + ref Vector256 row2, + ref Vector256 row3, + ref Vector256 row4, + ref Vector256 row5, + ref Vector256 row6, + ref Vector256 row7) + { + Vector256 pair0 = Vector256_.UnpackLow(row0, row1).AsInt64(); + Vector256 pair1 = Vector256_.UnpackHigh(row0, row1).AsInt64(); + Vector256 pair2 = Vector256_.UnpackLow(row2, row3).AsInt64(); + Vector256 pair3 = Vector256_.UnpackHigh(row2, row3).AsInt64(); + Vector256 pair4 = Vector256_.UnpackLow(row4, row5).AsInt64(); + Vector256 pair5 = Vector256_.UnpackHigh(row4, row5).AsInt64(); + Vector256 pair6 = Vector256_.UnpackLow(row6, row7).AsInt64(); + Vector256 pair7 = Vector256_.UnpackHigh(row6, row7).AsInt64(); + Vector256 quad0 = Vector256_.UnpackLow(pair0, pair2).AsInt32(); + Vector256 quad1 = Vector256_.UnpackHigh(pair0, pair2).AsInt32(); + Vector256 quad2 = Vector256_.UnpackLow(pair1, pair3).AsInt32(); + Vector256 quad3 = Vector256_.UnpackHigh(pair1, pair3).AsInt32(); + Vector256 quad4 = Vector256_.UnpackLow(pair4, pair6).AsInt32(); + Vector256 quad5 = Vector256_.UnpackHigh(pair4, pair6).AsInt32(); + Vector256 quad6 = Vector256_.UnpackLow(pair5, pair7).AsInt32(); + Vector256 quad7 = Vector256_.UnpackHigh(pair5, pair7).AsInt32(); + + // Each 128-bit lane now holds a 4x4 transpose. The exchange of the lanes completes the 8x8 transpose. + row0 = Vector256.Create(quad0.GetLower(), quad4.GetLower()); + row1 = Vector256.Create(quad1.GetLower(), quad5.GetLower()); + row2 = Vector256.Create(quad2.GetLower(), quad6.GetLower()); + row3 = Vector256.Create(quad3.GetLower(), quad7.GetLower()); + row4 = Vector256.Create(quad0.GetUpper(), quad4.GetUpper()); + row5 = Vector256.Create(quad1.GetUpper(), quad5.GetUpper()); + row6 = Vector256.Create(quad2.GetUpper(), quad6.GetUpper()); + row7 = Vector256.Create(quad3.GetUpper(), quad7.GetUpper()); + } + + /// + /// Transposes a 16-by-16 matrix of 32-bit lanes. + /// + /// The first input row, replaced by the first output row. + /// The second input row, replaced by the second output row. + /// The third input row, replaced by the third output row. + /// The fourth input row, replaced by the fourth output row. + /// The fifth input row, replaced by the fifth output row. + /// The sixth input row, replaced by the sixth output row. + /// The seventh input row, replaced by the seventh output row. + /// The eighth input row, replaced by the eighth output row. + /// The ninth input row, replaced by the ninth output row. + /// The tenth input row, replaced by the tenth output row. + /// The eleventh input row, replaced by the eleventh output row. + /// The twelfth input row, replaced by the twelfth output row. + /// The thirteenth input row, replaced by the thirteenth output row. + /// The fourteenth input row, replaced by the fourteenth output row. + /// The fifteenth input row, replaced by the fifteenth output row. + /// The sixteenth input row, replaced by the sixteenth output row. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Transpose( + ref Vector512 row0, + ref Vector512 row1, + ref Vector512 row2, + ref Vector512 row3, + ref Vector512 row4, + ref Vector512 row5, + ref Vector512 row6, + ref Vector512 row7, + ref Vector512 row8, + ref Vector512 row9, + ref Vector512 row10, + ref Vector512 row11, + ref Vector512 row12, + ref Vector512 row13, + ref Vector512 row14, + ref Vector512 row15) + { + if (Vector512.IsHardwareAccelerated) + { + // Each permutation stage exchanges one row-index bit with the matching column-index bit. + // After four stages, the vector index identifies the source column. The lane index identifies the source row. + // Keep the complete transpose in 512-bit registers. + Vector512 stage0Lower = Vector512.Create(0, 16, 2, 18, 4, 20, 6, 22, 8, 24, 10, 26, 12, 28, 14, 30); + Vector512 stage0Upper = Vector512.Create(1, 17, 3, 19, 5, 21, 7, 23, 9, 25, 11, 27, 13, 29, 15, 31); + Vector512 lowerSource = row0; + Vector512 upperSource = row1; + row0 = Vector512_.PermuteVar16x32x2(lowerSource, stage0Lower, upperSource); + row1 = Vector512_.PermuteVar16x32x2(lowerSource, stage0Upper, upperSource); + lowerSource = row2; + upperSource = row3; + row2 = Vector512_.PermuteVar16x32x2(lowerSource, stage0Lower, upperSource); + row3 = Vector512_.PermuteVar16x32x2(lowerSource, stage0Upper, upperSource); + lowerSource = row4; + upperSource = row5; + row4 = Vector512_.PermuteVar16x32x2(lowerSource, stage0Lower, upperSource); + row5 = Vector512_.PermuteVar16x32x2(lowerSource, stage0Upper, upperSource); + lowerSource = row6; + upperSource = row7; + row6 = Vector512_.PermuteVar16x32x2(lowerSource, stage0Lower, upperSource); + row7 = Vector512_.PermuteVar16x32x2(lowerSource, stage0Upper, upperSource); + lowerSource = row8; + upperSource = row9; + row8 = Vector512_.PermuteVar16x32x2(lowerSource, stage0Lower, upperSource); + row9 = Vector512_.PermuteVar16x32x2(lowerSource, stage0Upper, upperSource); + lowerSource = row10; + upperSource = row11; + row10 = Vector512_.PermuteVar16x32x2(lowerSource, stage0Lower, upperSource); + row11 = Vector512_.PermuteVar16x32x2(lowerSource, stage0Upper, upperSource); + lowerSource = row12; + upperSource = row13; + row12 = Vector512_.PermuteVar16x32x2(lowerSource, stage0Lower, upperSource); + row13 = Vector512_.PermuteVar16x32x2(lowerSource, stage0Upper, upperSource); + lowerSource = row14; + upperSource = row15; + row14 = Vector512_.PermuteVar16x32x2(lowerSource, stage0Lower, upperSource); + row15 = Vector512_.PermuteVar16x32x2(lowerSource, stage0Upper, upperSource); + + Vector512 stage1Lower = Vector512.Create(0, 1, 16, 17, 4, 5, 20, 21, 8, 9, 24, 25, 12, 13, 28, 29); + Vector512 stage1Upper = Vector512.Create(2, 3, 18, 19, 6, 7, 22, 23, 10, 11, 26, 27, 14, 15, 30, 31); + lowerSource = row0; + upperSource = row2; + row0 = Vector512_.PermuteVar16x32x2(lowerSource, stage1Lower, upperSource); + row2 = Vector512_.PermuteVar16x32x2(lowerSource, stage1Upper, upperSource); + lowerSource = row1; + upperSource = row3; + row1 = Vector512_.PermuteVar16x32x2(lowerSource, stage1Lower, upperSource); + row3 = Vector512_.PermuteVar16x32x2(lowerSource, stage1Upper, upperSource); + lowerSource = row4; + upperSource = row6; + row4 = Vector512_.PermuteVar16x32x2(lowerSource, stage1Lower, upperSource); + row6 = Vector512_.PermuteVar16x32x2(lowerSource, stage1Upper, upperSource); + lowerSource = row5; + upperSource = row7; + row5 = Vector512_.PermuteVar16x32x2(lowerSource, stage1Lower, upperSource); + row7 = Vector512_.PermuteVar16x32x2(lowerSource, stage1Upper, upperSource); + lowerSource = row8; + upperSource = row10; + row8 = Vector512_.PermuteVar16x32x2(lowerSource, stage1Lower, upperSource); + row10 = Vector512_.PermuteVar16x32x2(lowerSource, stage1Upper, upperSource); + lowerSource = row9; + upperSource = row11; + row9 = Vector512_.PermuteVar16x32x2(lowerSource, stage1Lower, upperSource); + row11 = Vector512_.PermuteVar16x32x2(lowerSource, stage1Upper, upperSource); + lowerSource = row12; + upperSource = row14; + row12 = Vector512_.PermuteVar16x32x2(lowerSource, stage1Lower, upperSource); + row14 = Vector512_.PermuteVar16x32x2(lowerSource, stage1Upper, upperSource); + lowerSource = row13; + upperSource = row15; + row13 = Vector512_.PermuteVar16x32x2(lowerSource, stage1Lower, upperSource); + row15 = Vector512_.PermuteVar16x32x2(lowerSource, stage1Upper, upperSource); + + Vector512 stage2Lower = Vector512.Create(0, 1, 2, 3, 16, 17, 18, 19, 8, 9, 10, 11, 24, 25, 26, 27); + Vector512 stage2Upper = Vector512.Create(4, 5, 6, 7, 20, 21, 22, 23, 12, 13, 14, 15, 28, 29, 30, 31); + lowerSource = row0; + upperSource = row4; + row0 = Vector512_.PermuteVar16x32x2(lowerSource, stage2Lower, upperSource); + row4 = Vector512_.PermuteVar16x32x2(lowerSource, stage2Upper, upperSource); + lowerSource = row1; + upperSource = row5; + row1 = Vector512_.PermuteVar16x32x2(lowerSource, stage2Lower, upperSource); + row5 = Vector512_.PermuteVar16x32x2(lowerSource, stage2Upper, upperSource); + lowerSource = row2; + upperSource = row6; + row2 = Vector512_.PermuteVar16x32x2(lowerSource, stage2Lower, upperSource); + row6 = Vector512_.PermuteVar16x32x2(lowerSource, stage2Upper, upperSource); + lowerSource = row3; + upperSource = row7; + row3 = Vector512_.PermuteVar16x32x2(lowerSource, stage2Lower, upperSource); + row7 = Vector512_.PermuteVar16x32x2(lowerSource, stage2Upper, upperSource); + lowerSource = row8; + upperSource = row12; + row8 = Vector512_.PermuteVar16x32x2(lowerSource, stage2Lower, upperSource); + row12 = Vector512_.PermuteVar16x32x2(lowerSource, stage2Upper, upperSource); + lowerSource = row9; + upperSource = row13; + row9 = Vector512_.PermuteVar16x32x2(lowerSource, stage2Lower, upperSource); + row13 = Vector512_.PermuteVar16x32x2(lowerSource, stage2Upper, upperSource); + lowerSource = row10; + upperSource = row14; + row10 = Vector512_.PermuteVar16x32x2(lowerSource, stage2Lower, upperSource); + row14 = Vector512_.PermuteVar16x32x2(lowerSource, stage2Upper, upperSource); + lowerSource = row11; + upperSource = row15; + row11 = Vector512_.PermuteVar16x32x2(lowerSource, stage2Lower, upperSource); + row15 = Vector512_.PermuteVar16x32x2(lowerSource, stage2Upper, upperSource); + + Vector512 stage3Lower = Vector512.Create(0, 1, 2, 3, 4, 5, 6, 7, 16, 17, 18, 19, 20, 21, 22, 23); + Vector512 stage3Upper = Vector512.Create(8, 9, 10, 11, 12, 13, 14, 15, 24, 25, 26, 27, 28, 29, 30, 31); + lowerSource = row0; + upperSource = row8; + row0 = Vector512_.PermuteVar16x32x2(lowerSource, stage3Lower, upperSource); + row8 = Vector512_.PermuteVar16x32x2(lowerSource, stage3Upper, upperSource); + lowerSource = row1; + upperSource = row9; + row1 = Vector512_.PermuteVar16x32x2(lowerSource, stage3Lower, upperSource); + row9 = Vector512_.PermuteVar16x32x2(lowerSource, stage3Upper, upperSource); + lowerSource = row2; + upperSource = row10; + row2 = Vector512_.PermuteVar16x32x2(lowerSource, stage3Lower, upperSource); + row10 = Vector512_.PermuteVar16x32x2(lowerSource, stage3Upper, upperSource); + lowerSource = row3; + upperSource = row11; + row3 = Vector512_.PermuteVar16x32x2(lowerSource, stage3Lower, upperSource); + row11 = Vector512_.PermuteVar16x32x2(lowerSource, stage3Upper, upperSource); + lowerSource = row4; + upperSource = row12; + row4 = Vector512_.PermuteVar16x32x2(lowerSource, stage3Lower, upperSource); + row12 = Vector512_.PermuteVar16x32x2(lowerSource, stage3Upper, upperSource); + lowerSource = row5; + upperSource = row13; + row5 = Vector512_.PermuteVar16x32x2(lowerSource, stage3Lower, upperSource); + row13 = Vector512_.PermuteVar16x32x2(lowerSource, stage3Upper, upperSource); + lowerSource = row6; + upperSource = row14; + row6 = Vector512_.PermuteVar16x32x2(lowerSource, stage3Lower, upperSource); + row14 = Vector512_.PermuteVar16x32x2(lowerSource, stage3Upper, upperSource); + lowerSource = row7; + upperSource = row15; + row7 = Vector512_.PermuteVar16x32x2(lowerSource, stage3Lower, upperSource); + row15 = Vector512_.PermuteVar16x32x2(lowerSource, stage3Upper, upperSource); + return; + } + + // The portable path transposes four independent 8-by-8 quadrants with the shared 256-bit kernel. + // Save the bottom-left quadrant before rows 8 through 15 receive the upper output columns. + // Publish the upper columns first to reduce the number of live quadrant registers. + Vector256 lowerBottom0 = row8.GetLower(); + Vector256 lowerBottom1 = row9.GetLower(); + Vector256 lowerBottom2 = row10.GetLower(); + Vector256 lowerBottom3 = row11.GetLower(); + Vector256 lowerBottom4 = row12.GetLower(); + Vector256 lowerBottom5 = row13.GetLower(); + Vector256 lowerBottom6 = row14.GetLower(); + Vector256 lowerBottom7 = row15.GetLower(); + Vector256 upperTop0 = row0.GetUpper(); + Vector256 upperTop1 = row1.GetUpper(); + Vector256 upperTop2 = row2.GetUpper(); + Vector256 upperTop3 = row3.GetUpper(); + Vector256 upperTop4 = row4.GetUpper(); + Vector256 upperTop5 = row5.GetUpper(); + Vector256 upperTop6 = row6.GetUpper(); + Vector256 upperTop7 = row7.GetUpper(); + Vector256 upperBottom0 = row8.GetUpper(); + Vector256 upperBottom1 = row9.GetUpper(); + Vector256 upperBottom2 = row10.GetUpper(); + Vector256 upperBottom3 = row11.GetUpper(); + Vector256 upperBottom4 = row12.GetUpper(); + Vector256 upperBottom5 = row13.GetUpper(); + Vector256 upperBottom6 = row14.GetUpper(); + Vector256 upperBottom7 = row15.GetUpper(); + + Transpose(ref upperTop0, ref upperTop1, ref upperTop2, ref upperTop3, ref upperTop4, ref upperTop5, ref upperTop6, ref upperTop7); + Transpose( + ref upperBottom0, + ref upperBottom1, + ref upperBottom2, + ref upperBottom3, + ref upperBottom4, + ref upperBottom5, + ref upperBottom6, + ref upperBottom7); + + row8 = Vector512.Create(upperTop0, upperBottom0); + row9 = Vector512.Create(upperTop1, upperBottom1); + row10 = Vector512.Create(upperTop2, upperBottom2); + row11 = Vector512.Create(upperTop3, upperBottom3); + row12 = Vector512.Create(upperTop4, upperBottom4); + row13 = Vector512.Create(upperTop5, upperBottom5); + row14 = Vector512.Create(upperTop6, upperBottom6); + row15 = Vector512.Create(upperTop7, upperBottom7); + + Vector256 lowerTop0 = row0.GetLower(); + Vector256 lowerTop1 = row1.GetLower(); + Vector256 lowerTop2 = row2.GetLower(); + Vector256 lowerTop3 = row3.GetLower(); + Vector256 lowerTop4 = row4.GetLower(); + Vector256 lowerTop5 = row5.GetLower(); + Vector256 lowerTop6 = row6.GetLower(); + Vector256 lowerTop7 = row7.GetLower(); + + Transpose(ref lowerTop0, ref lowerTop1, ref lowerTop2, ref lowerTop3, ref lowerTop4, ref lowerTop5, ref lowerTop6, ref lowerTop7); + Transpose( + ref lowerBottom0, + ref lowerBottom1, + ref lowerBottom2, + ref lowerBottom3, + ref lowerBottom4, + ref lowerBottom5, + ref lowerBottom6, + ref lowerBottom7); + + row0 = Vector512.Create(lowerTop0, lowerBottom0); + row1 = Vector512.Create(lowerTop1, lowerBottom1); + row2 = Vector512.Create(lowerTop2, lowerBottom2); + row3 = Vector512.Create(lowerTop3, lowerBottom3); + row4 = Vector512.Create(lowerTop4, lowerBottom4); + row5 = Vector512.Create(lowerTop5, lowerBottom5); + row6 = Vector512.Create(lowerTop6, lowerBottom6); + row7 = Vector512.Create(lowerTop7, lowerBottom7); + } + + /// + /// Transposes four four-lane rows into four four-lane columns. + /// + /// The first matrix row. + /// The second matrix row. + /// The third matrix row. + /// The fourth matrix row. + /// The first matrix column. + /// The second matrix column. + /// The third matrix column. + /// The fourth matrix column. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Transpose4( + Vector128 row0, + Vector128 row1, + Vector128 row2, + Vector128 row3, + out Vector128 column0, + out Vector128 column1, + out Vector128 column2, + out Vector128 column3) + { + // The integer core only moves words. Reinterpret the result so NaN payloads and signed zero stay unchanged. + Vector128 a = row0.AsInt32(); + Vector128 b = row1.AsInt32(); + Vector128 c = row2.AsInt32(); + Vector128 d = row3.AsInt32(); + Transpose(ref a, ref b, ref c, ref d); + column0 = a.AsSingle(); + column1 = b.AsSingle(); + column2 = c.AsSingle(); + column3 = d.AsSingle(); + } + + /// + /// Transposes four-by-four matrices independently within each 128-bit group. /// - /// Four source rows from column c followed by four source rows from column c + 4. - /// The first destination sample. - /// The destination row stride, in integers. - /// The first source row, which becomes a destination column. - /// The first source column, which becomes a destination row. + /// The first rows of the matrices. + /// The second rows of the matrices. + /// The third rows of the matrices. + /// The fourth rows of the matrices. + /// The first columns of the matrices. + /// The second columns of the matrices. + /// The third columns of the matrices. + /// The fourth columns of the matrices. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void StoreTransposedColumns(Vector256 columns, ref int destination, int stride, int row, int column) + public static void Transpose4( + Vector256 row0, + Vector256 row1, + Vector256 row2, + Vector256 row3, + out Vector256 column0, + out Vector256 column1, + out Vector256 column2, + out Vector256 column3) { - columns.GetLower().StoreUnsafe(ref destination, (nuint)((column * stride) + row)); - columns.GetUpper().StoreUnsafe(ref destination, (nuint)(((column + 4) * stride) + row)); + // Match the 128-bit transpose: first pair adjacent 32-bit words, then pair the resulting 64-bit groups. + // Unpack keeps 128-bit groups separate. Callers permute those groups when they need consecutive color indices. + Vector256 row01Low = Vector256_.UnpackLow(row0.AsInt32(), row1.AsInt32()); + Vector256 row01High = Vector256_.UnpackHigh(row0.AsInt32(), row1.AsInt32()); + Vector256 row23Low = Vector256_.UnpackLow(row2.AsInt32(), row3.AsInt32()); + Vector256 row23High = Vector256_.UnpackHigh(row2.AsInt32(), row3.AsInt32()); + + column0 = Vector256_.UnpackLow(row01Low.AsInt64(), row23Low.AsInt64()).AsSingle(); + column1 = Vector256_.UnpackHigh(row01Low.AsInt64(), row23Low.AsInt64()).AsSingle(); + column2 = Vector256_.UnpackLow(row01High.AsInt64(), row23High.AsInt64()).AsSingle(); + column3 = Vector256_.UnpackHigh(row01High.AsInt64(), row23High.AsInt64()).AsSingle(); } /// - /// Places four independent four-word columns from a 512-bit transpose at their strided matrix positions. + /// Transposes four-by-four matrices independently within each 128-bit group. /// - /// Four source rows from each of columns c, c + 4, c + 8 and c + 12, in that order. - /// The first destination sample. - /// The destination row stride, in integers. - /// The first source row, which becomes a destination column. - /// The first source column, which becomes a destination row. + /// The first rows of the matrices. + /// The second rows of the matrices. + /// The third rows of the matrices. + /// The fourth rows of the matrices. + /// The first columns of the matrices. + /// The second columns of the matrices. + /// The third columns of the matrices. + /// The fourth columns of the matrices. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void StoreTransposedColumns(Vector512 columns, ref int destination, int stride, int row, int column) + public static void Transpose4( + Vector512 row0, + Vector512 row1, + Vector512 row2, + Vector512 row3, + out Vector512 column0, + out Vector512 column1, + out Vector512 column2, + out Vector512 column3) { - columns.GetLower().GetLower().StoreUnsafe(ref destination, (nuint)((column * stride) + row)); - columns.GetLower().GetUpper().StoreUnsafe(ref destination, (nuint)(((column + 4) * stride) + row)); - columns.GetUpper().GetLower().StoreUnsafe(ref destination, (nuint)(((column + 8) * stride) + row)); - columns.GetUpper().GetUpper().StoreUnsafe(ref destination, (nuint)(((column + 12) * stride) + row)); + // Match the 128-bit transpose: first pair adjacent 32-bit words, then pair the resulting 64-bit groups. + // Unpack keeps 128-bit groups separate. Callers permute those groups when they need consecutive color indices. + Vector512 row01Low = Vector512_.UnpackLow(row0.AsInt32(), row1.AsInt32()); + Vector512 row01High = Vector512_.UnpackHigh(row0.AsInt32(), row1.AsInt32()); + Vector512 row23Low = Vector512_.UnpackLow(row2.AsInt32(), row3.AsInt32()); + Vector512 row23High = Vector512_.UnpackHigh(row2.AsInt32(), row3.AsInt32()); + + column0 = Vector512_.UnpackLow(row01Low.AsInt64(), row23Low.AsInt64()).AsSingle(); + column1 = Vector512_.UnpackHigh(row01Low.AsInt64(), row23Low.AsInt64()).AsSingle(); + column2 = Vector512_.UnpackLow(row01High.AsInt64(), row23High.AsInt64()).AsSingle(); + column3 = Vector512_.UnpackHigh(row01High.AsInt64(), row23High.AsInt64()).AsSingle(); } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.TransformTypeModel.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.TransformTypeModel.cs index b3cbe7ac92..01a6f8610f 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.TransformTypeModel.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.TransformTypeModel.cs @@ -930,7 +930,7 @@ internal static void GetEnergyDistribution( horizontal += cellValues[y]; } - Av1Transform2dOperations.Transpose( + SimdUtils.Transpose( ref Unsafe.As, Vector256>(ref cellValues[0]), ref Unsafe.As, Vector256>(ref cellValues[1]), ref Unsafe.As, Vector256>(ref cellValues[2]), diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Lookahead/Av1FirstPass.Wavelet.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Lookahead/Av1FirstPass.Wavelet.cs index a48c30006d..052bcc58e3 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Lookahead/Av1FirstPass.Wavelet.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Lookahead/Av1FirstPass.Wavelet.cs @@ -359,7 +359,7 @@ public static void LoadRow(ReadOnlySpan source, int index, Span block1 = TOperator.LoadWidened(source, start + 8, default(Vector128)); Vector128 block2 = TOperator.LoadWidened(source, start + 16, default(Vector128)); Vector128 block3 = TOperator.LoadWidened(source, start + 24, default(Vector128)); - Av1Transform2dOperations.Transpose(ref block0, ref block1, ref block2, ref block3); + SimdUtils.Transpose(ref block0, ref block1, ref block2, ref block3); row[half * 4] = block0 << 2; row[(half * 4) + 1] = block1 << 2; row[(half * 4) + 2] = block2 << 2; @@ -407,7 +407,7 @@ public static void LoadRow(ReadOnlySpan source, int index, Span block5 = TOperator.LoadWidened(source, index + 40, default(Vector256)); Vector256 block6 = TOperator.LoadWidened(source, index + 48, default(Vector256)); Vector256 block7 = TOperator.LoadWidened(source, index + 56, default(Vector256)); - Av1Transform2dOperations.Transpose(ref block0, ref block1, ref block2, ref block3, ref block4, ref block5, ref block6, ref block7); + SimdUtils.Transpose(ref block0, ref block1, ref block2, ref block3, ref block4, ref block5, ref block6, ref block7); row[0] = block0 << 2; row[1] = block1 << 2; row[2] = block2 << 2; diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1ForwardTransformer.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1ForwardTransformer.cs index 507136f2da..d60cbc17ef 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1ForwardTransformer.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1ForwardTransformer.cs @@ -182,9 +182,9 @@ private static void Hadamard4x4(ReadOnlySpan residual, int stride, Span row3 = LoadFourSamples(ref residualBase, 3 * stride); HadamardRows4(ref row0, ref row1, ref row2, ref row3); - Av1Transform2dOperations.Transpose(ref row0, ref row1, ref row2, ref row3); + SimdUtils.Transpose(ref row0, ref row1, ref row2, ref row3); HadamardRows4(ref row0, ref row1, ref row2, ref row3); - Av1Transform2dOperations.Transpose(ref row0, ref row1, ref row2, ref row3); + SimdUtils.Transpose(ref row0, ref row1, ref row2, ref row3); ref int coefficientBase = ref MemoryMarshal.GetReference(coefficients); row0.StoreUnsafe(ref coefficientBase); @@ -453,12 +453,12 @@ public static void TransformLossless4x4(ReadOnlySpan input, Span coe // Each lane initially holds one column. The first stage transforms those columns in parallel. The transpose then makes each transformed column a // row, so the same lane-wise network can process the other axis. TransformLosslessStage(ref row0, ref row1, ref row2, ref row3); - Av1Transform2dOperations.Transpose(ref row0, ref row1, ref row2, ref row3); + SimdUtils.Transpose(ref row0, ref row1, ref row2, ref row3); TransformLosslessStage(ref row0, ref row1, ref row2, ref row3); // The entropy pipeline stores transform positions in row-major order. The two reversible passes leave the two frequency axes exchanged. This // transpose restores row-major order before scan-order traversal. - Av1Transform2dOperations.Transpose(ref row0, ref row1, ref row2, ref row3); + SimdUtils.Transpose(ref row0, ref row1, ref row2, ref row3); ref int coefficientBase = ref MemoryMarshal.GetReference(coefficients); (row0 * 4).StoreUnsafe(ref coefficientBase); diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1Inverse2dTransformer.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1Inverse2dTransformer.cs index 0c35962e82..a446794922 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1Inverse2dTransformer.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1Inverse2dTransformer.cs @@ -855,7 +855,7 @@ private static unsafe void TransformRowsVector512( row15 = Vector512.Zero; } - Av1Transform2dOperations.Transpose( + SimdUtils.Transpose( ref row0, ref row1, ref row2, @@ -920,7 +920,7 @@ private static unsafe void TransformRowsVector512( Vector512 row14 = Av1Transform2dOperations.RoundShift(tempOut[column + 14], -shift0); Vector512 row15 = Av1Transform2dOperations.RoundShift(tempOut[column + 15], -shift0); - Av1Transform2dOperations.Transpose( + SimdUtils.Transpose( ref row0, ref row1, ref row2, @@ -1053,7 +1053,7 @@ private static unsafe void TransformRowsVector256( row7 = Vector256.Zero; } - Av1Transform2dOperations.Transpose(ref row0, ref row1, ref row2, ref row3, ref row4, ref row5, ref row6, ref row7); + SimdUtils.Transpose(ref row0, ref row1, ref row2, ref row3, ref row4, ref row5, ref row6, ref row7); tempIn[column + 0] = PrepareInverseRow(row0, normalizeRectangle, rowClampBits); tempIn[column + 1] = PrepareInverseRow(row1, normalizeRectangle, rowClampBits); @@ -1086,7 +1086,7 @@ private static unsafe void TransformRowsVector256( Vector256 row6 = Av1Transform2dOperations.RoundShift(tempOut[column + 6], -shift0); Vector256 row7 = Av1Transform2dOperations.RoundShift(tempOut[column + 7], -shift0); - Av1Transform2dOperations.Transpose(ref row0, ref row1, ref row2, ref row3, ref row4, ref row5, ref row6, ref row7); + SimdUtils.Transpose(ref row0, ref row1, ref row2, ref row3, ref row4, ref row5, ref row6, ref row7); row0.StoreUnsafe(ref bufferBase, (nuint)(((row + 0) * width) + column)); row1.StoreUnsafe(ref bufferBase, (nuint)(((row + 1) * width) + column)); @@ -1348,7 +1348,7 @@ private static unsafe void TransformRowsVector128( row3 = Vector128.Zero; } - Av1Transform2dOperations.Transpose(ref row0, ref row1, ref row2, ref row3); + SimdUtils.Transpose(ref row0, ref row1, ref row2, ref row3); tempIn[column + 0] = PrepareInverseRow(row0, normalizeRectangle, rowClampBits); tempIn[column + 1] = PrepareInverseRow(row1, normalizeRectangle, rowClampBits); @@ -1373,7 +1373,7 @@ private static unsafe void TransformRowsVector128( Vector128 row2 = Av1Transform2dOperations.RoundShift(tempOut[column + 2], -shift0); Vector128 row3 = Av1Transform2dOperations.RoundShift(tempOut[column + 3], -shift0); - Av1Transform2dOperations.Transpose(ref row0, ref row1, ref row2, ref row3); + SimdUtils.Transpose(ref row0, ref row1, ref row2, ref row3); row0.StoreUnsafe(ref bufferBase, (nuint)(((row + 0) * width) + column)); row1.StoreUnsafe(ref bufferBase, (nuint)(((row + 1) * width) + column)); diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseWalshHadamardTransformer.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseWalshHadamardTransformer.cs index 9f8069354f..f87688b806 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseWalshHadamardTransformer.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseWalshHadamardTransformer.cs @@ -166,12 +166,12 @@ private static void TransformVector( // Entropy decoding normalizes the column-major coefficient positions of AV1 to the row-major transform workspace. The transpose restores the // normative dimension order before either reversible butterfly does its signed half shift. A swap of the dimensions after those shifts breaks the // lossless rounding. - Av1Transform2dOperations.Transpose(ref row0, ref row1, ref row2, ref row3); + SimdUtils.Transpose(ref row0, ref row1, ref row2, ref row3); Transform(ref row0, ref row1, ref row2, ref row3); // After the first pass, vector i holds intermediate row i. The second transpose puts intermediate row j into lane j, so the same lane-wise // butterfly does the second dimension without a workspace. - Av1Transform2dOperations.Transpose(ref row0, ref row1, ref row2, ref row3); + SimdUtils.Transpose(ref row0, ref row1, ref row2, ref row3); Transform(ref row0, ref row1, ref row2, ref row3); } diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1Transform2dOperations.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1Transform2dOperations.cs index 1736a40a71..26f31c79ca 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1Transform2dOperations.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1Transform2dOperations.cs @@ -518,27 +518,6 @@ public static Vector512 Reverse(Vector512 value) value, Vector512.Create((short)31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)); - /// - /// Transposes a four-by-four matrix of signed thirty-two-bit lanes. - /// - /// The first input row, replaced by the first output row. - /// The second input row, replaced by the second output row. - /// The third input row, replaced by the third output row. - /// The fourth input row, replaced by the fourth output row. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Transpose(ref Vector128 row0, ref Vector128 row1, ref Vector128 row2, ref Vector128 row3) - { - Vector128 pairs01Low = Vector128_.UnpackLow(row0, row1); - Vector128 pairs01High = Vector128_.UnpackHigh(row0, row1); - Vector128 pairs23Low = Vector128_.UnpackLow(row2, row3); - Vector128 pairs23High = Vector128_.UnpackHigh(row2, row3); - - row0 = Vector128_.UnpackLow(pairs01Low.AsInt64(), pairs23Low.AsInt64()).AsInt32(); - row1 = Vector128_.UnpackHigh(pairs01Low.AsInt64(), pairs23Low.AsInt64()).AsInt32(); - row2 = Vector128_.UnpackLow(pairs01High.AsInt64(), pairs23High.AsInt64()).AsInt32(); - row3 = Vector128_.UnpackHigh(pairs01High.AsInt64(), pairs23High.AsInt64()).AsInt32(); - } - /// /// Transposes one 4-by-4 tile of signed thirty-two-bit values and applies the configured pipeline operations. /// @@ -561,63 +540,13 @@ public static void Transpose4x4Int32( Vector128 row2 = Finish(Vector128.LoadUnsafe(ref source, (nuint)(2 * sourceStride)), roundShift, normalizeRectangle); Vector128 row3 = Finish(Vector128.LoadUnsafe(ref source, (nuint)(3 * sourceStride)), roundShift, normalizeRectangle); - Transpose(ref row0, ref row1, ref row2, ref row3); + SimdUtils.Transpose(ref row0, ref row1, ref row2, ref row3); row0.StoreUnsafe(ref destination); row1.StoreUnsafe(ref destination, (nuint)destinationStride); row2.StoreUnsafe(ref destination, (nuint)(2 * destinationStride)); row3.StoreUnsafe(ref destination, (nuint)(3 * destinationStride)); } - /// - /// Transposes an eight-by-eight matrix of signed thirty-two-bit lanes. - /// - /// The first input row, replaced by the first output row. - /// The second input row, replaced by the second output row. - /// The third input row, replaced by the third output row. - /// The fourth input row, replaced by the fourth output row. - /// The fifth input row, replaced by the fifth output row. - /// The sixth input row, replaced by the sixth output row. - /// The seventh input row, replaced by the seventh output row. - /// The eighth input row, replaced by the eighth output row. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Transpose( - ref Vector256 row0, - ref Vector256 row1, - ref Vector256 row2, - ref Vector256 row3, - ref Vector256 row4, - ref Vector256 row5, - ref Vector256 row6, - ref Vector256 row7) - { - Vector256 pair0 = Vector256_.UnpackLow(row0, row1).AsInt64(); - Vector256 pair1 = Vector256_.UnpackHigh(row0, row1).AsInt64(); - Vector256 pair2 = Vector256_.UnpackLow(row2, row3).AsInt64(); - Vector256 pair3 = Vector256_.UnpackHigh(row2, row3).AsInt64(); - Vector256 pair4 = Vector256_.UnpackLow(row4, row5).AsInt64(); - Vector256 pair5 = Vector256_.UnpackHigh(row4, row5).AsInt64(); - Vector256 pair6 = Vector256_.UnpackLow(row6, row7).AsInt64(); - Vector256 pair7 = Vector256_.UnpackHigh(row6, row7).AsInt64(); - Vector256 quad0 = Vector256_.UnpackLow(pair0, pair2).AsInt32(); - Vector256 quad1 = Vector256_.UnpackHigh(pair0, pair2).AsInt32(); - Vector256 quad2 = Vector256_.UnpackLow(pair1, pair3).AsInt32(); - Vector256 quad3 = Vector256_.UnpackHigh(pair1, pair3).AsInt32(); - Vector256 quad4 = Vector256_.UnpackLow(pair4, pair6).AsInt32(); - Vector256 quad5 = Vector256_.UnpackHigh(pair4, pair6).AsInt32(); - Vector256 quad6 = Vector256_.UnpackLow(pair5, pair7).AsInt32(); - Vector256 quad7 = Vector256_.UnpackHigh(pair5, pair7).AsInt32(); - - // Each 128-bit lane now holds a 4x4 transpose. The exchange of the lanes completes the 8x8 transpose. - row0 = Vector256.Create(quad0.GetLower(), quad4.GetLower()); - row1 = Vector256.Create(quad1.GetLower(), quad5.GetLower()); - row2 = Vector256.Create(quad2.GetLower(), quad6.GetLower()); - row3 = Vector256.Create(quad3.GetLower(), quad7.GetLower()); - row4 = Vector256.Create(quad0.GetUpper(), quad4.GetUpper()); - row5 = Vector256.Create(quad1.GetUpper(), quad5.GetUpper()); - row6 = Vector256.Create(quad2.GetUpper(), quad6.GetUpper()); - row7 = Vector256.Create(quad3.GetUpper(), quad7.GetUpper()); - } - /// /// Transposes one 8-by-8 tile of signed thirty-two-bit values and applies the configured pipeline operations. /// @@ -644,7 +573,7 @@ public static void Transpose8x8Int32( Vector256 row6 = Finish(Vector256.LoadUnsafe(ref source, (nuint)(6 * sourceStride)), roundShift, normalizeRectangle); Vector256 row7 = Finish(Vector256.LoadUnsafe(ref source, (nuint)(7 * sourceStride)), roundShift, normalizeRectangle); - Transpose(ref row0, ref row1, ref row2, ref row3, ref row4, ref row5, ref row6, ref row7); + SimdUtils.Transpose(ref row0, ref row1, ref row2, ref row3, ref row4, ref row5, ref row6, ref row7); row0.StoreUnsafe(ref destination); row1.StoreUnsafe(ref destination, (nuint)destinationStride); row2.StoreUnsafe(ref destination, (nuint)(2 * destinationStride)); @@ -739,269 +668,6 @@ public static void Transpose16x16Avx512( } } - /// - /// Transposes a sixteen-by-sixteen matrix of signed thirty-two-bit lanes. - /// - /// The first input row, replaced by the first output row. - /// The second input row, replaced by the second output row. - /// The third input row, replaced by the third output row. - /// The fourth input row, replaced by the fourth output row. - /// The fifth input row, replaced by the fifth output row. - /// The sixth input row, replaced by the sixth output row. - /// The seventh input row, replaced by the seventh output row. - /// The eighth input row, replaced by the eighth output row. - /// The ninth input row, replaced by the ninth output row. - /// The tenth input row, replaced by the tenth output row. - /// The eleventh input row, replaced by the eleventh output row. - /// The twelfth input row, replaced by the twelfth output row. - /// The thirteenth input row, replaced by the thirteenth output row. - /// The fourteenth input row, replaced by the fourteenth output row. - /// The fifteenth input row, replaced by the fifteenth output row. - /// The sixteenth input row, replaced by the sixteenth output row. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Transpose( - ref Vector512 row0, - ref Vector512 row1, - ref Vector512 row2, - ref Vector512 row3, - ref Vector512 row4, - ref Vector512 row5, - ref Vector512 row6, - ref Vector512 row7, - ref Vector512 row8, - ref Vector512 row9, - ref Vector512 row10, - ref Vector512 row11, - ref Vector512 row12, - ref Vector512 row13, - ref Vector512 row14, - ref Vector512 row15) - { - if (Vector512.IsHardwareAccelerated) - { - // Each permutation stage exchanges one row-index bit with the matching column-index bit. After four stages the vector index identifies the source - // column and the lane index identifies the source row. This keeps the complete transpose in 512-bit registers instead of decomposing it into - // 128-bit tiles. - Vector512 stage0Lower = Vector512.Create(0, 16, 2, 18, 4, 20, 6, 22, 8, 24, 10, 26, 12, 28, 14, 30); - Vector512 stage0Upper = Vector512.Create(1, 17, 3, 19, 5, 21, 7, 23, 9, 25, 11, 27, 13, 29, 15, 31); - Vector512 lowerSource = row0; - Vector512 upperSource = row1; - row0 = Vector512_.PermuteVar16x32x2(lowerSource, stage0Lower, upperSource); - row1 = Vector512_.PermuteVar16x32x2(lowerSource, stage0Upper, upperSource); - lowerSource = row2; - upperSource = row3; - row2 = Vector512_.PermuteVar16x32x2(lowerSource, stage0Lower, upperSource); - row3 = Vector512_.PermuteVar16x32x2(lowerSource, stage0Upper, upperSource); - lowerSource = row4; - upperSource = row5; - row4 = Vector512_.PermuteVar16x32x2(lowerSource, stage0Lower, upperSource); - row5 = Vector512_.PermuteVar16x32x2(lowerSource, stage0Upper, upperSource); - lowerSource = row6; - upperSource = row7; - row6 = Vector512_.PermuteVar16x32x2(lowerSource, stage0Lower, upperSource); - row7 = Vector512_.PermuteVar16x32x2(lowerSource, stage0Upper, upperSource); - lowerSource = row8; - upperSource = row9; - row8 = Vector512_.PermuteVar16x32x2(lowerSource, stage0Lower, upperSource); - row9 = Vector512_.PermuteVar16x32x2(lowerSource, stage0Upper, upperSource); - lowerSource = row10; - upperSource = row11; - row10 = Vector512_.PermuteVar16x32x2(lowerSource, stage0Lower, upperSource); - row11 = Vector512_.PermuteVar16x32x2(lowerSource, stage0Upper, upperSource); - lowerSource = row12; - upperSource = row13; - row12 = Vector512_.PermuteVar16x32x2(lowerSource, stage0Lower, upperSource); - row13 = Vector512_.PermuteVar16x32x2(lowerSource, stage0Upper, upperSource); - lowerSource = row14; - upperSource = row15; - row14 = Vector512_.PermuteVar16x32x2(lowerSource, stage0Lower, upperSource); - row15 = Vector512_.PermuteVar16x32x2(lowerSource, stage0Upper, upperSource); - - Vector512 stage1Lower = Vector512.Create(0, 1, 16, 17, 4, 5, 20, 21, 8, 9, 24, 25, 12, 13, 28, 29); - Vector512 stage1Upper = Vector512.Create(2, 3, 18, 19, 6, 7, 22, 23, 10, 11, 26, 27, 14, 15, 30, 31); - lowerSource = row0; - upperSource = row2; - row0 = Vector512_.PermuteVar16x32x2(lowerSource, stage1Lower, upperSource); - row2 = Vector512_.PermuteVar16x32x2(lowerSource, stage1Upper, upperSource); - lowerSource = row1; - upperSource = row3; - row1 = Vector512_.PermuteVar16x32x2(lowerSource, stage1Lower, upperSource); - row3 = Vector512_.PermuteVar16x32x2(lowerSource, stage1Upper, upperSource); - lowerSource = row4; - upperSource = row6; - row4 = Vector512_.PermuteVar16x32x2(lowerSource, stage1Lower, upperSource); - row6 = Vector512_.PermuteVar16x32x2(lowerSource, stage1Upper, upperSource); - lowerSource = row5; - upperSource = row7; - row5 = Vector512_.PermuteVar16x32x2(lowerSource, stage1Lower, upperSource); - row7 = Vector512_.PermuteVar16x32x2(lowerSource, stage1Upper, upperSource); - lowerSource = row8; - upperSource = row10; - row8 = Vector512_.PermuteVar16x32x2(lowerSource, stage1Lower, upperSource); - row10 = Vector512_.PermuteVar16x32x2(lowerSource, stage1Upper, upperSource); - lowerSource = row9; - upperSource = row11; - row9 = Vector512_.PermuteVar16x32x2(lowerSource, stage1Lower, upperSource); - row11 = Vector512_.PermuteVar16x32x2(lowerSource, stage1Upper, upperSource); - lowerSource = row12; - upperSource = row14; - row12 = Vector512_.PermuteVar16x32x2(lowerSource, stage1Lower, upperSource); - row14 = Vector512_.PermuteVar16x32x2(lowerSource, stage1Upper, upperSource); - lowerSource = row13; - upperSource = row15; - row13 = Vector512_.PermuteVar16x32x2(lowerSource, stage1Lower, upperSource); - row15 = Vector512_.PermuteVar16x32x2(lowerSource, stage1Upper, upperSource); - - Vector512 stage2Lower = Vector512.Create(0, 1, 2, 3, 16, 17, 18, 19, 8, 9, 10, 11, 24, 25, 26, 27); - Vector512 stage2Upper = Vector512.Create(4, 5, 6, 7, 20, 21, 22, 23, 12, 13, 14, 15, 28, 29, 30, 31); - lowerSource = row0; - upperSource = row4; - row0 = Vector512_.PermuteVar16x32x2(lowerSource, stage2Lower, upperSource); - row4 = Vector512_.PermuteVar16x32x2(lowerSource, stage2Upper, upperSource); - lowerSource = row1; - upperSource = row5; - row1 = Vector512_.PermuteVar16x32x2(lowerSource, stage2Lower, upperSource); - row5 = Vector512_.PermuteVar16x32x2(lowerSource, stage2Upper, upperSource); - lowerSource = row2; - upperSource = row6; - row2 = Vector512_.PermuteVar16x32x2(lowerSource, stage2Lower, upperSource); - row6 = Vector512_.PermuteVar16x32x2(lowerSource, stage2Upper, upperSource); - lowerSource = row3; - upperSource = row7; - row3 = Vector512_.PermuteVar16x32x2(lowerSource, stage2Lower, upperSource); - row7 = Vector512_.PermuteVar16x32x2(lowerSource, stage2Upper, upperSource); - lowerSource = row8; - upperSource = row12; - row8 = Vector512_.PermuteVar16x32x2(lowerSource, stage2Lower, upperSource); - row12 = Vector512_.PermuteVar16x32x2(lowerSource, stage2Upper, upperSource); - lowerSource = row9; - upperSource = row13; - row9 = Vector512_.PermuteVar16x32x2(lowerSource, stage2Lower, upperSource); - row13 = Vector512_.PermuteVar16x32x2(lowerSource, stage2Upper, upperSource); - lowerSource = row10; - upperSource = row14; - row10 = Vector512_.PermuteVar16x32x2(lowerSource, stage2Lower, upperSource); - row14 = Vector512_.PermuteVar16x32x2(lowerSource, stage2Upper, upperSource); - lowerSource = row11; - upperSource = row15; - row11 = Vector512_.PermuteVar16x32x2(lowerSource, stage2Lower, upperSource); - row15 = Vector512_.PermuteVar16x32x2(lowerSource, stage2Upper, upperSource); - - Vector512 stage3Lower = Vector512.Create(0, 1, 2, 3, 4, 5, 6, 7, 16, 17, 18, 19, 20, 21, 22, 23); - Vector512 stage3Upper = Vector512.Create(8, 9, 10, 11, 12, 13, 14, 15, 24, 25, 26, 27, 28, 29, 30, 31); - lowerSource = row0; - upperSource = row8; - row0 = Vector512_.PermuteVar16x32x2(lowerSource, stage3Lower, upperSource); - row8 = Vector512_.PermuteVar16x32x2(lowerSource, stage3Upper, upperSource); - lowerSource = row1; - upperSource = row9; - row1 = Vector512_.PermuteVar16x32x2(lowerSource, stage3Lower, upperSource); - row9 = Vector512_.PermuteVar16x32x2(lowerSource, stage3Upper, upperSource); - lowerSource = row2; - upperSource = row10; - row2 = Vector512_.PermuteVar16x32x2(lowerSource, stage3Lower, upperSource); - row10 = Vector512_.PermuteVar16x32x2(lowerSource, stage3Upper, upperSource); - lowerSource = row3; - upperSource = row11; - row3 = Vector512_.PermuteVar16x32x2(lowerSource, stage3Lower, upperSource); - row11 = Vector512_.PermuteVar16x32x2(lowerSource, stage3Upper, upperSource); - lowerSource = row4; - upperSource = row12; - row4 = Vector512_.PermuteVar16x32x2(lowerSource, stage3Lower, upperSource); - row12 = Vector512_.PermuteVar16x32x2(lowerSource, stage3Upper, upperSource); - lowerSource = row5; - upperSource = row13; - row5 = Vector512_.PermuteVar16x32x2(lowerSource, stage3Lower, upperSource); - row13 = Vector512_.PermuteVar16x32x2(lowerSource, stage3Upper, upperSource); - lowerSource = row6; - upperSource = row14; - row6 = Vector512_.PermuteVar16x32x2(lowerSource, stage3Lower, upperSource); - row14 = Vector512_.PermuteVar16x32x2(lowerSource, stage3Upper, upperSource); - lowerSource = row7; - upperSource = row15; - row7 = Vector512_.PermuteVar16x32x2(lowerSource, stage3Lower, upperSource); - row15 = Vector512_.PermuteVar16x32x2(lowerSource, stage3Upper, upperSource); - return; - } - - // A 16x16 transpose consists of four independent 8x8 quadrants. The reuse of the 256-bit transpose keeps the portable layout path branch-free, while - // the transform arithmetic stays in 512-bit lanes. The code saves the bottom-left quadrant before row8 to row15 receive the upper output columns. The - // upper columns come first, so the four quadrants are not all live at the same time. - Vector256 lowerBottom0 = row8.GetLower(); - Vector256 lowerBottom1 = row9.GetLower(); - Vector256 lowerBottom2 = row10.GetLower(); - Vector256 lowerBottom3 = row11.GetLower(); - Vector256 lowerBottom4 = row12.GetLower(); - Vector256 lowerBottom5 = row13.GetLower(); - Vector256 lowerBottom6 = row14.GetLower(); - Vector256 lowerBottom7 = row15.GetLower(); - Vector256 upperTop0 = row0.GetUpper(); - Vector256 upperTop1 = row1.GetUpper(); - Vector256 upperTop2 = row2.GetUpper(); - Vector256 upperTop3 = row3.GetUpper(); - Vector256 upperTop4 = row4.GetUpper(); - Vector256 upperTop5 = row5.GetUpper(); - Vector256 upperTop6 = row6.GetUpper(); - Vector256 upperTop7 = row7.GetUpper(); - Vector256 upperBottom0 = row8.GetUpper(); - Vector256 upperBottom1 = row9.GetUpper(); - Vector256 upperBottom2 = row10.GetUpper(); - Vector256 upperBottom3 = row11.GetUpper(); - Vector256 upperBottom4 = row12.GetUpper(); - Vector256 upperBottom5 = row13.GetUpper(); - Vector256 upperBottom6 = row14.GetUpper(); - Vector256 upperBottom7 = row15.GetUpper(); - - Transpose(ref upperTop0, ref upperTop1, ref upperTop2, ref upperTop3, ref upperTop4, ref upperTop5, ref upperTop6, ref upperTop7); - Transpose( - ref upperBottom0, - ref upperBottom1, - ref upperBottom2, - ref upperBottom3, - ref upperBottom4, - ref upperBottom5, - ref upperBottom6, - ref upperBottom7); - - row8 = Vector512.Create(upperTop0, upperBottom0); - row9 = Vector512.Create(upperTop1, upperBottom1); - row10 = Vector512.Create(upperTop2, upperBottom2); - row11 = Vector512.Create(upperTop3, upperBottom3); - row12 = Vector512.Create(upperTop4, upperBottom4); - row13 = Vector512.Create(upperTop5, upperBottom5); - row14 = Vector512.Create(upperTop6, upperBottom6); - row15 = Vector512.Create(upperTop7, upperBottom7); - - Vector256 lowerTop0 = row0.GetLower(); - Vector256 lowerTop1 = row1.GetLower(); - Vector256 lowerTop2 = row2.GetLower(); - Vector256 lowerTop3 = row3.GetLower(); - Vector256 lowerTop4 = row4.GetLower(); - Vector256 lowerTop5 = row5.GetLower(); - Vector256 lowerTop6 = row6.GetLower(); - Vector256 lowerTop7 = row7.GetLower(); - - Transpose(ref lowerTop0, ref lowerTop1, ref lowerTop2, ref lowerTop3, ref lowerTop4, ref lowerTop5, ref lowerTop6, ref lowerTop7); - Transpose( - ref lowerBottom0, - ref lowerBottom1, - ref lowerBottom2, - ref lowerBottom3, - ref lowerBottom4, - ref lowerBottom5, - ref lowerBottom6, - ref lowerBottom7); - - row0 = Vector512.Create(lowerTop0, lowerBottom0); - row1 = Vector512.Create(lowerTop1, lowerBottom1); - row2 = Vector512.Create(lowerTop2, lowerBottom2); - row3 = Vector512.Create(lowerTop3, lowerBottom3); - row4 = Vector512.Create(lowerTop4, lowerBottom4); - row5 = Vector512.Create(lowerTop5, lowerBottom5); - row6 = Vector512.Create(lowerTop6, lowerBottom6); - row7 = Vector512.Create(lowerTop7, lowerBottom7); - } - /// /// Applies the final stage shift and the rectangular normalization before the transpose of a signed sixteen-bit tile. /// diff --git a/tests/ImageSharp.Tests/Common/SimdUtilsTests.ColorPacking.cs b/tests/ImageSharp.Tests/Common/SimdUtilsTests.Interleave.cs similarity index 88% rename from tests/ImageSharp.Tests/Common/SimdUtilsTests.ColorPacking.cs rename to tests/ImageSharp.Tests/Common/SimdUtilsTests.Interleave.cs index 718dbea6fa..c65c717272 100644 --- a/tests/ImageSharp.Tests/Common/SimdUtilsTests.ColorPacking.cs +++ b/tests/ImageSharp.Tests/Common/SimdUtilsTests.Interleave.cs @@ -14,6 +14,79 @@ namespace SixLabors.ImageSharp.Tests.Common; /// public partial class SimdUtilsTests { + /// + /// Checks every separated lane against independently packed integer pairs, including complete-register lane boundaries. + /// + [Fact] + public void Deinterleave2_PreservesPairOrder() + { + static void RunTest() + { + int[] packed = new int[2 * Vector512.Count]; + for (int i = 0; i < packed.Length / 2; i++) + { + packed[2 * i] = int.MinValue + (i * 12345); + packed[(2 * i) + 1] = int.MaxValue - (i * 54321); + } + + ref int source = ref MemoryMarshal.GetArrayDataReference(packed); + foreach (int count in new[] { Vector128.Count, Vector256.Count, Vector512.Count }) + { + int[] separated = new int[(2 * count) + 2]; + separated.AsSpan().Fill(123); + ref int evenDestination = ref separated[1]; + ref int oddDestination = ref separated[count + 1]; + if (count == Vector512.Count) + { + SimdUtils.Deinterleave2( + Vector512.LoadUnsafe(ref source), + Vector512.LoadUnsafe(ref source, (uint)count), + out Vector512 even, + out Vector512 odd); + + even.StoreUnsafe(ref evenDestination); + odd.StoreUnsafe(ref oddDestination); + } + else if (count == Vector256.Count) + { + SimdUtils.Deinterleave2( + Vector256.LoadUnsafe(ref source), + Vector256.LoadUnsafe(ref source, (uint)count), + out Vector256 even, + out Vector256 odd); + + even.StoreUnsafe(ref evenDestination); + odd.StoreUnsafe(ref oddDestination); + } + else + { + SimdUtils.Deinterleave2( + Vector128.LoadUnsafe(ref source), + Vector128.LoadUnsafe(ref source, (uint)count), + out Vector128 even, + out Vector128 odd); + + even.StoreUnsafe(ref evenDestination); + odd.StoreUnsafe(ref oddDestination); + } + + for (int i = 0; i < count; i++) + { + Assert.Equal(packed[2 * i], separated[1 + i]); + Assert.Equal(packed[(2 * i) + 1], separated[1 + count + i]); + } + + Assert.Equal(123, separated[0]); + Assert.Equal(123, separated[^1]); + } + } + + // Explicit calls also check the portable implementation of each fixed register width when hardware acceleration is disabled. + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } + /// /// Checks two-stream alternation, complete-register boundaries and exact floating-point bits with each hardware configuration. /// diff --git a/tests/ImageSharp.Tests/Common/SimdUtilsTests.Transpose.cs b/tests/ImageSharp.Tests/Common/SimdUtilsTests.Transpose.cs index 88e32f3717..5192ce6833 100644 --- a/tests/ImageSharp.Tests/Common/SimdUtilsTests.Transpose.cs +++ b/tests/ImageSharp.Tests/Common/SimdUtilsTests.Transpose.cs @@ -2,12 +2,231 @@ // Licensed under the Six Labors Split License. using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; using SixLabors.ImageSharp.Tests.TestUtilities; namespace SixLabors.ImageSharp.Tests.Common; public partial class SimdUtilsTests { + /// + /// Checks complete-register kernels directly, including their portable paths when the current process disables hardware intrinsics. + /// + [Fact] + public void Transpose_RegistersPreserveCoordinates() + { + static void RunTest() + { + ReadOnlySpan counts = [4, 8, 16]; + foreach (int count in counts) + { + int[] source = new int[count * count]; + int[] destination = new int[source.Length + 2]; + destination.AsSpan().Fill(123); + for (int i = 0; i < source.Length; i++) + { + source[i] = unchecked((int)0x80000000) | (i * 131071); + } + + ref int input = ref MemoryMarshal.GetArrayDataReference(source); + ref int output = ref destination[1]; + if (count == 16) + { + Vector512 row0 = Vector512.LoadUnsafe(ref input, 0U); + Vector512 row1 = Vector512.LoadUnsafe(ref input, 16U); + Vector512 row2 = Vector512.LoadUnsafe(ref input, 32U); + Vector512 row3 = Vector512.LoadUnsafe(ref input, 48U); + Vector512 row4 = Vector512.LoadUnsafe(ref input, 64U); + Vector512 row5 = Vector512.LoadUnsafe(ref input, 80U); + Vector512 row6 = Vector512.LoadUnsafe(ref input, 96U); + Vector512 row7 = Vector512.LoadUnsafe(ref input, 112U); + Vector512 row8 = Vector512.LoadUnsafe(ref input, 128U); + Vector512 row9 = Vector512.LoadUnsafe(ref input, 144U); + Vector512 row10 = Vector512.LoadUnsafe(ref input, 160U); + Vector512 row11 = Vector512.LoadUnsafe(ref input, 176U); + Vector512 row12 = Vector512.LoadUnsafe(ref input, 192U); + Vector512 row13 = Vector512.LoadUnsafe(ref input, 208U); + Vector512 row14 = Vector512.LoadUnsafe(ref input, 224U); + Vector512 row15 = Vector512.LoadUnsafe(ref input, 240U); + + SimdUtils.Transpose( + ref row0, + ref row1, + ref row2, + ref row3, + ref row4, + ref row5, + ref row6, + ref row7, + ref row8, + ref row9, + ref row10, + ref row11, + ref row12, + ref row13, + ref row14, + ref row15); + + row0.StoreUnsafe(ref output, 0U); + row1.StoreUnsafe(ref output, 16U); + row2.StoreUnsafe(ref output, 32U); + row3.StoreUnsafe(ref output, 48U); + row4.StoreUnsafe(ref output, 64U); + row5.StoreUnsafe(ref output, 80U); + row6.StoreUnsafe(ref output, 96U); + row7.StoreUnsafe(ref output, 112U); + row8.StoreUnsafe(ref output, 128U); + row9.StoreUnsafe(ref output, 144U); + row10.StoreUnsafe(ref output, 160U); + row11.StoreUnsafe(ref output, 176U); + row12.StoreUnsafe(ref output, 192U); + row13.StoreUnsafe(ref output, 208U); + row14.StoreUnsafe(ref output, 224U); + row15.StoreUnsafe(ref output, 240U); + } + else if (count == 8) + { + Vector256 row0 = Vector256.LoadUnsafe(ref input, 0U); + Vector256 row1 = Vector256.LoadUnsafe(ref input, 8U); + Vector256 row2 = Vector256.LoadUnsafe(ref input, 16U); + Vector256 row3 = Vector256.LoadUnsafe(ref input, 24U); + Vector256 row4 = Vector256.LoadUnsafe(ref input, 32U); + Vector256 row5 = Vector256.LoadUnsafe(ref input, 40U); + Vector256 row6 = Vector256.LoadUnsafe(ref input, 48U); + Vector256 row7 = Vector256.LoadUnsafe(ref input, 56U); + + SimdUtils.Transpose(ref row0, ref row1, ref row2, ref row3, ref row4, ref row5, ref row6, ref row7); + row0.StoreUnsafe(ref output, 0U); + row1.StoreUnsafe(ref output, 8U); + row2.StoreUnsafe(ref output, 16U); + row3.StoreUnsafe(ref output, 24U); + row4.StoreUnsafe(ref output, 32U); + row5.StoreUnsafe(ref output, 40U); + row6.StoreUnsafe(ref output, 48U); + row7.StoreUnsafe(ref output, 56U); + } + else + { + Vector128 row0 = Vector128.LoadUnsafe(ref input, 0U); + Vector128 row1 = Vector128.LoadUnsafe(ref input, 4U); + Vector128 row2 = Vector128.LoadUnsafe(ref input, 8U); + Vector128 row3 = Vector128.LoadUnsafe(ref input, 12U); + + SimdUtils.Transpose(ref row0, ref row1, ref row2, ref row3); + row0.StoreUnsafe(ref output, 0U); + row1.StoreUnsafe(ref output, 4U); + row2.StoreUnsafe(ref output, 8U); + row3.StoreUnsafe(ref output, 12U); + } + + for (int row = 0; row < count; row++) + { + for (int column = 0; column < count; column++) + { + Assert.Equal(source[(column * count) + row], destination[1 + (row * count) + column]); + } + } + + Assert.Equal(123, destination[0]); + Assert.Equal(123, destination[^1]); + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } + + /// + /// Checks the separate four-by-four matrices in each 128-bit group of the wider floating-point registers. + /// + [Fact] + public void Transpose4_PreservesLaneLocalMatrices() + { + static void RunTest() + { + ReadOnlySpan counts = [4, 8, 16]; + foreach (int count in counts) + { + int[] source = new int[4 * count]; + int[] destination = new int[source.Length]; + for (int i = 0; i < source.Length; i++) + { + // These bit patterns include NaNs and signed values. No numeric conversion or arithmetic belongs in a transpose. + source[i] = unchecked((int)0xFF800000) | (i * 12345); + } + + ref int input = ref MemoryMarshal.GetArrayDataReference(source); + ref int output = ref MemoryMarshal.GetArrayDataReference(destination); + if (count == 16) + { + SimdUtils.Transpose4( + Vector512.LoadUnsafe(ref input, 0U).AsSingle(), + Vector512.LoadUnsafe(ref input, 16U).AsSingle(), + Vector512.LoadUnsafe(ref input, 32U).AsSingle(), + Vector512.LoadUnsafe(ref input, 48U).AsSingle(), + out Vector512 column0, + out Vector512 column1, + out Vector512 column2, + out Vector512 column3); + + column0.AsInt32().StoreUnsafe(ref output, 0U); + column1.AsInt32().StoreUnsafe(ref output, 16U); + column2.AsInt32().StoreUnsafe(ref output, 32U); + column3.AsInt32().StoreUnsafe(ref output, 48U); + } + else if (count == 8) + { + SimdUtils.Transpose4( + Vector256.LoadUnsafe(ref input, 0U).AsSingle(), + Vector256.LoadUnsafe(ref input, 8U).AsSingle(), + Vector256.LoadUnsafe(ref input, 16U).AsSingle(), + Vector256.LoadUnsafe(ref input, 24U).AsSingle(), + out Vector256 column0, + out Vector256 column1, + out Vector256 column2, + out Vector256 column3); + + column0.AsInt32().StoreUnsafe(ref output, 0U); + column1.AsInt32().StoreUnsafe(ref output, 8U); + column2.AsInt32().StoreUnsafe(ref output, 16U); + column3.AsInt32().StoreUnsafe(ref output, 24U); + } + else + { + SimdUtils.Transpose4( + Vector128.LoadUnsafe(ref input, 0U).AsSingle(), + Vector128.LoadUnsafe(ref input, 4U).AsSingle(), + Vector128.LoadUnsafe(ref input, 8U).AsSingle(), + Vector128.LoadUnsafe(ref input, 12U).AsSingle(), + out Vector128 column0, + out Vector128 column1, + out Vector128 column2, + out Vector128 column3); + + column0.AsInt32().StoreUnsafe(ref output, 0U); + column1.AsInt32().StoreUnsafe(ref output, 4U); + column2.AsInt32().StoreUnsafe(ref output, 8U); + column3.AsInt32().StoreUnsafe(ref output, 12U); + } + + for (int row = 0; row < 4; row++) + { + for (int lane = 0; lane < count; lane++) + { + int groupStart = lane & ~3; + int sourceRow = lane & 3; + Assert.Equal(source[(sourceRow * count) + groupStart + row], destination[(row * count) + lane]); + } + } + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } + /// /// Checks rectangular coordinates, raw floating-point bits, strided padding and scalar tails with each hardware configuration. /// @@ -16,7 +235,11 @@ public void Transpose_PreservesCoordinatesAndPadding() { static void RunTest() { - ReadOnlySpan<(int Rows, int Columns)> sizes = [(0, 5), (5, 0), (1, 1), (3, 7), (4, 4), (8, 8), (16, 16), (17, 19), (33, 65)]; + ReadOnlySpan<(int Rows, int Columns)> sizes = + [ + (0, 5), (5, 0), (1, 1), (3, 7), (4, 4), (8, 8), (16, 16), (16, 8), (8, 16), (16, 12), (12, 16), (17, 19), (33, 65) + ]; + ReadOnlySpan floatingPointViews = [false, true]; ReadOnlySpan payloads = [0x7FC12345, int.MinValue, 0x7F800000, unchecked((int)0xFF800000), 0x3F800000, 0]; const int prefix = 3; From d04ddebff981a302045bb4aa73368d449d7bc297 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Oct 2026 00:39:18 +1000 Subject: [PATCH 288/328] Reconstruct JPEG XL squeeze channels with shared SIMD operators --- .../Common/Helpers/Vector256Utilities.cs | 48 - .../Formats/Jxl/Processing/JxlSimdUtils.cs | 91 -- .../Transforms/JxlSqueeze.Operators.cs | 488 +++++++ .../Modular/Transforms/JxlSqueeze.cs | 1131 ++++++++++------- .../Formats/Jxl/Processing/SqueezeTests.cs | 222 ++++ 5 files changed, 1404 insertions(+), 576 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.Operators.cs create mode 100644 tests/ImageSharp.Tests/Formats/Jxl/Processing/SqueezeTests.cs diff --git a/src/ImageSharp/Common/Helpers/Vector256Utilities.cs b/src/ImageSharp/Common/Helpers/Vector256Utilities.cs index 60cdf54824..3d3870a847 100644 --- a/src/ImageSharp/Common/Helpers/Vector256Utilities.cs +++ b/src/ImageSharp/Common/Helpers/Vector256Utilities.cs @@ -994,54 +994,6 @@ public static Vector256 UnpackLow(Vector256 left, Vector256 ri return Vector256.Create(lo, hi); } - /// - /// Multiplies only the even indices of the two 256-bit vectors, - /// producing half as many elements of twice the element width. - /// - /// Left vector to multiply. - /// Right vector to multiply - /// - /// - /// { - /// A[0] * B[0], - /// A[2] * B[2], - /// A[4] * B[4], - /// A[6] * B[6] - /// } - /// - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector256 MultiplyEven(Vector256 left, Vector256 right) - => Vector256.Create( - left[0] * right[0], - left[2] * right[2], - left[4] * right[4], - left[6] * right[6]); - - /// - /// Multiplies only the odd indices of the two 256-bit vectors, - /// producing half as many elements of twice the element width. - /// - /// Left vector to multiply. - /// Right vector to multiply - /// - /// - /// { - /// A[1] * B[1], - /// A[3] * B[3], - /// A[5] * B[5], - /// A[7] * B[7] - /// } - /// - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector256 MultiplyOdd(Vector256 left, Vector256 right) - => Vector256.Create( - left[1] * right[1], - left[3] * right[3], - left[5] * right[5], - left[7] * right[7]); - /// /// Produces a vector by interleaving the even-indexed elements /// of the left and right vectors. diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs index d242431c13..a0508f91f8 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs @@ -16,97 +16,6 @@ internal static partial class JxlSimdUtils { public static int MaxVectorSize => Vector.Count * sizeof(float); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector256 ConcatLowerLower(Vector256 a, Vector256 b) - where T : unmanaged => Vector256.Create(a.GetLower(), b.GetLower()); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector256 ConcatUpperUpper(Vector256 a, Vector256 b) - where T : unmanaged => Vector256.Create(a.GetUpper(), b.GetUpper()); - - public static void Transpose8x8Block(Span fromSpan, Span toSpan, int stride) - { - ref int from = ref MemoryMarshal.GetReference(fromSpan); - ref int to = ref MemoryMarshal.GetReference(toSpan); - - if (Vector256.IsHardwareAccelerated) - { - Vector256 i0 = Vector256.LoadUnsafe(ref from); - Vector256 i1 = Vector256.LoadUnsafe(ref Unsafe.Add(ref from, stride)); - Vector256 i2 = Vector256.LoadUnsafe(ref Unsafe.Add(ref from, 2 * stride)); - Vector256 i3 = Vector256.LoadUnsafe(ref Unsafe.Add(ref from, 3 * stride)); - Vector256 i4 = Vector256.LoadUnsafe(ref Unsafe.Add(ref from, 4 * stride)); - Vector256 i5 = Vector256.LoadUnsafe(ref Unsafe.Add(ref from, 5 * stride)); - Vector256 i6 = Vector256.LoadUnsafe(ref Unsafe.Add(ref from, 6 * stride)); - Vector256 i7 = Vector256.LoadUnsafe(ref Unsafe.Add(ref from, 7 * stride)); - - Vector256 q0 = Vector256_.InterleaveLower(i0, i2); - Vector256 q1 = Vector256_.InterleaveLower(i1, i3); - Vector256 q2 = Vector256_.InterleaveUpper(i0, i2); - Vector256 q3 = Vector256_.InterleaveUpper(i1, i3); - Vector256 q4 = Vector256_.InterleaveLower(i4, i6); - Vector256 q5 = Vector256_.InterleaveLower(i5, i7); - Vector256 q6 = Vector256_.InterleaveUpper(i4, i6); - Vector256 q7 = Vector256_.InterleaveUpper(i5, i7); - - Vector256 r0 = Vector256_.InterleaveLower(q0, q1); - Vector256 r1 = Vector256_.InterleaveUpper(q0, q1); - Vector256 r2 = Vector256_.InterleaveLower(q2, q3); - Vector256 r3 = Vector256_.InterleaveUpper(q2, q3); - Vector256 r4 = Vector256_.InterleaveLower(q4, q5); - Vector256 r5 = Vector256_.InterleaveUpper(q4, q5); - Vector256 r6 = Vector256_.InterleaveLower(q6, q7); - Vector256 r7 = Vector256_.InterleaveUpper(q6, q7); - - i0 = ConcatLowerLower(r4, r0); - i1 = ConcatLowerLower(r5, r1); - i2 = ConcatLowerLower(r6, r2); - i3 = ConcatLowerLower(r7, r3); - i4 = ConcatUpperUpper(r4, r0); - i5 = ConcatUpperUpper(r5, r1); - i6 = ConcatUpperUpper(r6, r2); - i7 = ConcatUpperUpper(r7, r3); - - i0.StoreUnsafe(ref to); - i1.StoreUnsafe(ref Unsafe.Add(ref to, 8)); - i2.StoreUnsafe(ref Unsafe.Add(ref to, 16)); - i3.StoreUnsafe(ref Unsafe.Add(ref to, 24)); - i4.StoreUnsafe(ref Unsafe.Add(ref to, 32)); - i5.StoreUnsafe(ref Unsafe.Add(ref to, 40)); - i6.StoreUnsafe(ref Unsafe.Add(ref to, 48)); - i7.StoreUnsafe(ref Unsafe.Add(ref to, 56)); - } - else - { - // Vector128 fallback - for (int n = 0; n < 8; n += 4) - { - for (int m = 0; m < 8; m += 4) - { - Vector128 p0 = Vector128.LoadUnsafe(ref Unsafe.Add(ref from, (n * stride) + m)); - Vector128 p1 = Vector128.LoadUnsafe(ref Unsafe.Add(ref from, ((n + 1) * stride) + m)); - Vector128 p2 = Vector128.LoadUnsafe(ref Unsafe.Add(ref from, ((n + 2) * stride) + m)); - Vector128 p3 = Vector128.LoadUnsafe(ref Unsafe.Add(ref from, ((n + 3) * stride) + m)); - - Vector128 q0 = Vector128_.InterleaveLower(p0, p2); - Vector128 q1 = Vector128_.InterleaveLower(p1, p3); - Vector128 q2 = Vector128_.InterleaveUpper(p0, p2); - Vector128 q3 = Vector128_.InterleaveUpper(p1, p3); - - Vector128 r0 = Vector128_.InterleaveLower(q0, q1); - Vector128 r1 = Vector128_.InterleaveUpper(q0, q1); - Vector128 r2 = Vector128_.InterleaveLower(q2, q3); - Vector128 r3 = Vector128_.InterleaveUpper(q2, q3); - - r0.StoreUnsafe(ref Unsafe.Add(ref to, (m * 8) + n)); - r1.StoreUnsafe(ref Unsafe.Add(ref to, ((m + 1) * 8) + n)); - r2.StoreUnsafe(ref Unsafe.Add(ref to, ((m + 2) * 8) + n)); - r3.StoreUnsafe(ref Unsafe.Add(ref to, ((m + 3) * 8) + n)); - } - } - } - } - /// /// Fills consecutive integers, starting with the supplied value. /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.Operators.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.Operators.cs new file mode 100644 index 0000000000..3208d6a8d7 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.Operators.cs @@ -0,0 +1,488 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; + +internal static partial class JxlSqueeze +{ + /// + /// Bounds arguments only for the 32-bit SIMD calculation. Other arguments use the exact 64-bit scalar calculation. + /// + /// + /// With M = 2^29 - 1, differences have magnitude at most 2M and doubled slope limits are at most 4M + 1. + /// The division shortcut adds at most 8M/3 + 2 before shifting. Residual addition and reconstructed pairs also fit Int32. + /// This is not a sample limit: lanes outside this range are corrected before their results are stored or used for prediction. + /// + private const int MaximumFastArgument = int.MaxValue / 4; + + /// + /// The fixed-point multiplier ceil(2^32 / 3). The high word of the widened product gives floor(value / 3) in the fast range. + /// + private const int DivisionByThreeMultiplier = 0x55555556; + + /// + /// Restores one pair from its average, residual, next average and preceding restored sample. + /// + private readonly struct InverseSqueezeOperator + { + /// + /// Restores a pair with native 64-bit intermediate arithmetic and Int32 sample storage. + /// + /// The current pair average. + /// The encoded difference after tendency prediction. + /// The following pair average, or the current average at an even edge. + /// The preceding restored odd sample, or the average at the first pair. + /// The first restored sample. + /// The second restored sample, used as the next pair's predecessor. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Invoke(int average, int residual, int nextAverage, int previous, out int even, out int odd) + { + long difference = residual + SmoothTendency(previous, average, nextAverage); + + long first = average + (difference / 2); + even = unchecked((int)first); + odd = unchecked((int)(first - difference)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Invoke( + Vector128 average, + Vector128 residual, + Vector128 nextAverage, + Vector128 previous, + out Vector128 even, + out Vector128 odd) + { + Vector128 difference = residual + SmoothTendency(previous, average, nextAverage); + + // Logical sign extraction adds one only to a negative difference. The following arithmetic shift therefore truncates division toward zero. + even = average + ((difference + (difference.AsUInt32() >> 31).AsInt32()) >> 1); + odd = even - difference; + uint wideLanes = GetWideArithmeticMask(average, residual, nextAverage, previous); + + while (wideLanes != 0) + { + // Only these lanes need wide arithmetic. Narrow each stored result before it becomes the next predecessor, as the scalar decoder does. + int lane = BitOperations.TrailingZeroCount(wideLanes); + + this.Invoke(average.GetElement(lane), residual.GetElement(lane), nextAverage.GetElement(lane), previous.GetElement(lane), out int a, out int b); + even = even.WithElement(lane, a); + odd = odd.WithElement(lane, b); + wideLanes &= wideLanes - 1; + } + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Invoke( + Vector256 average, + Vector256 residual, + Vector256 nextAverage, + Vector256 previous, + out Vector256 even, + out Vector256 odd) + { + Vector256 difference = residual + SmoothTendency(previous, average, nextAverage); + + // Logical sign extraction adds one only to a negative difference. The following arithmetic shift therefore truncates division toward zero. + even = average + ((difference + (difference.AsUInt32() >> 31).AsInt32()) >> 1); + odd = even - difference; + uint wideLanes = GetWideArithmeticMask(average, residual, nextAverage, previous); + + while (wideLanes != 0) + { + // Only these lanes need wide arithmetic. Narrow each stored result before it becomes the next predecessor, as the scalar decoder does. + int lane = BitOperations.TrailingZeroCount(wideLanes); + + this.Invoke(average.GetElement(lane), residual.GetElement(lane), nextAverage.GetElement(lane), previous.GetElement(lane), out int a, out int b); + even = even.WithElement(lane, a); + odd = odd.WithElement(lane, b); + wideLanes &= wideLanes - 1; + } + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Invoke( + Vector512 average, + Vector512 residual, + Vector512 nextAverage, + Vector512 previous, + out Vector512 even, + out Vector512 odd) + { + Vector512 difference = residual + SmoothTendency(previous, average, nextAverage); + + // Logical sign extraction adds one only to a negative difference. The following arithmetic shift therefore truncates division toward zero. + even = average + ((difference + (difference.AsUInt32() >> 31).AsInt32()) >> 1); + odd = even - difference; + uint wideLanes = GetWideArithmeticMask(average, residual, nextAverage, previous); + + while (wideLanes != 0) + { + // Only these lanes need wide arithmetic. Narrow each stored result before it becomes the next predecessor, as the scalar decoder does. + int lane = BitOperations.TrailingZeroCount(wideLanes); + + this.Invoke(average.GetElement(lane), residual.GetElement(lane), nextAverage.GetElement(lane), previous.GetElement(lane), out int a, out int b); + even = even.WithElement(lane, a); + odd = odd.WithElement(lane, b); + wideLanes &= wideLanes - 1; + } + } + } + + /// + /// Calculates the encoded residual from original pair samples and neighboring averages. + /// + private readonly struct ForwardSqueezeOperator + { + /// + /// Calculates one residual with 64-bit intermediates, then stores its Int32 representation. + /// + /// The first original sample. + /// The second original sample. + /// The already calculated pair average. + /// The following pair average, or the current average at an even edge. + /// The preceding original odd sample, or the average at the first pair. + /// The encoded difference minus the predicted tendency. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int Invoke(int even, int odd, int average, int nextAverage, int previous) + => unchecked((int)(((long)even - odd) - SmoothTendency(previous, average, nextAverage))); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector128 Invoke(Vector128 even, Vector128 odd, Vector128 average, Vector128 nextAverage, Vector128 previous) + { + Vector128 result = even - odd - SmoothTendency(previous, average, nextAverage); + + uint wideLanes = GetWideArithmeticMask(even, odd, nextAverage, previous); + + while (wideLanes != 0) + { + // Average lies between even and odd, so the four checked arguments also bound it. Other lanes use the same wide equation as scalar traversal. + int lane = BitOperations.TrailingZeroCount(wideLanes); + + int residual = this.Invoke( + even.GetElement(lane), + odd.GetElement(lane), + average.GetElement(lane), + nextAverage.GetElement(lane), + previous.GetElement(lane)); + result = result.WithElement(lane, residual); + + wideLanes &= wideLanes - 1; + } + + return result; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector256 Invoke(Vector256 even, Vector256 odd, Vector256 average, Vector256 nextAverage, Vector256 previous) + { + Vector256 result = even - odd - SmoothTendency(previous, average, nextAverage); + + uint wideLanes = GetWideArithmeticMask(even, odd, nextAverage, previous); + + while (wideLanes != 0) + { + // Average lies between even and odd, so the four checked arguments also bound it. Other lanes use the same wide equation as scalar traversal. + int lane = BitOperations.TrailingZeroCount(wideLanes); + + int residual = this.Invoke( + even.GetElement(lane), + odd.GetElement(lane), + average.GetElement(lane), + nextAverage.GetElement(lane), + previous.GetElement(lane)); + result = result.WithElement(lane, residual); + + wideLanes &= wideLanes - 1; + } + + return result; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector512 Invoke(Vector512 even, Vector512 odd, Vector512 average, Vector512 nextAverage, Vector512 previous) + { + Vector512 result = even - odd - SmoothTendency(previous, average, nextAverage); + + uint wideLanes = GetWideArithmeticMask(even, odd, nextAverage, previous); + + while (wideLanes != 0) + { + // Average lies between even and odd, so the four checked arguments also bound it. Other lanes use the same wide equation as scalar traversal. + int lane = BitOperations.TrailingZeroCount(wideLanes); + + int residual = this.Invoke( + even.GetElement(lane), + odd.GetElement(lane), + average.GetElement(lane), + nextAverage.GetElement(lane), + previous.GetElement(lane)); + result = result.WithElement(lane, residual); + + wideLanes &= wideLanes - 1; + } + + return result; + } + } + + /// + /// Finds lanes whose four arguments require 64-bit arithmetic. + /// + /// The first argument. + /// The second argument. + /// The third argument. + /// The fourth argument. + /// One set bit per lane outside the proved fast range. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint GetWideArithmeticMask(Vector128 first, Vector128 second, Vector128 third, Vector128 fourth) + { + // M consists of 29 low set bits. OR of unsigned magnitudes is at most M exactly when every argument is in range. + // Abs(Int32.MinValue) retains its high bit; interpreting it as unsigned correctly selects the wide calculation. + Vector128 magnitudes = Vector128.Abs(first).AsUInt32() | Vector128.Abs(second).AsUInt32() + | Vector128.Abs(third).AsUInt32() | Vector128.Abs(fourth).AsUInt32(); + + return Vector128.ExtractMostSignificantBits(Vector128.GreaterThan(magnitudes, Vector128.Create((uint)MaximumFastArgument))); + } + + /// + /// Calculates pair averages without overflowing the sum of two Int32 samples. + /// + /// The first samples. + /// The second samples. + /// The same biased integer averages as the scalar 64-bit equation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 Average(Vector128 first, Vector128 second) + { + // (a & b) + ((a ^ b) >> 1) gives floor((a+b)/2) without forming the possibly overflowing sum. + // An odd sum rounds up only when the first sample is greater, matching (a+b+(a>b))/2 with an arithmetic shift. + Vector128 different = first ^ second; + return (first & second) + (different >> 1) + (Vector128.GreaterThan(first, second) & different & Vector128.One); + } + + /// + /// Calculates the native integer tendency for independent sample lanes. + /// + /// The preceding original or restored samples. + /// The current pair averages. + /// The following pair averages. + /// The predicted difference. Callers correct lanes outside MaximumFastArgument with the 64-bit scalar equation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 SmoothTendency(Vector128 previous, Vector128 average, Vector128 nextAverage) + { + Vector128 before = previous - average; + Vector128 after = average - nextAverage; + Vector128 absoluteBefore = Vector128.Abs(before); + + Vector128 absoluteAfter = Vector128.Abs(after); + Vector128 multiplier = Vector128.Create(DivisionByThreeMultiplier); + + Vector128 evenProducts = Vector128_.MultiplyWideningEven(absoluteBefore, multiplier); + + // Move each odd word into its preceding even word before widening. Endianness changes which half of a 64-bit lane holds that word. + Vector128 oddWords = BitConverter.IsLittleEndian + ? (absoluteBefore.AsUInt64() >> 32).AsInt32() + : (absoluteBefore.AsUInt64() << 32).AsInt32(); + + Vector128 oddProducts = Vector128_.MultiplyWideningEven(oddWords, multiplier); + + // The high product words give floor(abs(before)/3) within the fast range. Narrow yields all even quotients, then all odd quotients; + // a complete-register shuffle restores their original sample order. This avoids multiplying Int32 values before widening. + Vector128 quotient = Vector128.Narrow(evenProducts >> 32, oddProducts >> 32); + quotient = Vector128.ShuffleNative(quotient, Vector128.Create(0, 2, 1, 3)); + Vector128 magnitude = (quotient + Vector128.Abs(previous - nextAverage) + Vector128.Create(2)) >> 2; + Vector128 twiceBefore = absoluteBefore << 1; + magnitude = Vector128.ConditionalSelect( + Vector128.GreaterThan(magnitude, twiceBefore + (magnitude & Vector128.One)), + twiceBefore + Vector128.One, + magnitude); + + Vector128 twiceAfter = absoluteAfter << 1; + magnitude = Vector128.ConditionalSelect(Vector128.GreaterThan(magnitude + (magnitude & Vector128.One), twiceAfter), twiceAfter, magnitude); + + // Only a strict change in slope direction suppresses the tendency. Equal neighbors still use the native parity-sensitive limits above. + Vector128 nonmonotonic = ~Vector128.Equals(before, Vector128.Zero) & ~Vector128.Equals(after, Vector128.Zero) + & Vector128.LessThan(before ^ after, Vector128.Zero); + + Vector128 signed = Vector128.ConditionalSelect(Vector128.LessThan(previous, nextAverage), -magnitude, magnitude); + + return signed & ~nonmonotonic; + } + + /// + /// Finds lanes whose four arguments require 64-bit arithmetic. + /// + /// The first argument. + /// The second argument. + /// The third argument. + /// The fourth argument. + /// One set bit per lane outside the proved fast range. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint GetWideArithmeticMask(Vector256 first, Vector256 second, Vector256 third, Vector256 fourth) + { + // M consists of 29 low set bits. OR of unsigned magnitudes is at most M exactly when every argument is in range. + // Abs(Int32.MinValue) retains its high bit; interpreting it as unsigned correctly selects the wide calculation. + Vector256 magnitudes = Vector256.Abs(first).AsUInt32() | Vector256.Abs(second).AsUInt32() + | Vector256.Abs(third).AsUInt32() | Vector256.Abs(fourth).AsUInt32(); + + return Vector256.ExtractMostSignificantBits(Vector256.GreaterThan(magnitudes, Vector256.Create((uint)MaximumFastArgument))); + } + + /// + /// Calculates pair averages without overflowing the sum of two Int32 samples. + /// + /// The first samples. + /// The second samples. + /// The same biased integer averages as the scalar 64-bit equation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 Average(Vector256 first, Vector256 second) + { + // (a & b) + ((a ^ b) >> 1) gives floor((a+b)/2) without forming the possibly overflowing sum. + // An odd sum rounds up only when the first sample is greater, matching (a+b+(a>b))/2 with an arithmetic shift. + Vector256 different = first ^ second; + return (first & second) + (different >> 1) + (Vector256.GreaterThan(first, second) & different & Vector256.One); + } + + /// + /// Calculates the native integer tendency for independent sample lanes. + /// + /// The preceding original or restored samples. + /// The current pair averages. + /// The following pair averages. + /// The predicted difference. Callers correct lanes outside MaximumFastArgument with the 64-bit scalar equation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 SmoothTendency(Vector256 previous, Vector256 average, Vector256 nextAverage) + { + Vector256 before = previous - average; + Vector256 after = average - nextAverage; + Vector256 absoluteBefore = Vector256.Abs(before); + + Vector256 absoluteAfter = Vector256.Abs(after); + Vector256 multiplier = Vector256.Create(DivisionByThreeMultiplier); + + Vector256 evenProducts = Vector256_.MultiplyWideningEven(absoluteBefore, multiplier); + + // Move each odd word into its preceding even word before widening. Endianness changes which half of a 64-bit lane holds that word. + Vector256 oddWords = BitConverter.IsLittleEndian + ? (absoluteBefore.AsUInt64() >> 32).AsInt32() + : (absoluteBefore.AsUInt64() << 32).AsInt32(); + + Vector256 oddProducts = Vector256_.MultiplyWideningEven(oddWords, multiplier); + + // The high product words give floor(abs(before)/3) within the fast range. Narrow yields all even quotients, then all odd quotients; + // a complete-register shuffle restores their original sample order. This avoids multiplying Int32 values before widening. + Vector256 quotient = Vector256.Narrow(evenProducts >> 32, oddProducts >> 32); + quotient = Vector256.ShuffleNative(quotient, Vector256.Create(0, 4, 1, 5, 2, 6, 3, 7)); + Vector256 magnitude = (quotient + Vector256.Abs(previous - nextAverage) + Vector256.Create(2)) >> 2; + Vector256 twiceBefore = absoluteBefore << 1; + magnitude = Vector256.ConditionalSelect( + Vector256.GreaterThan(magnitude, twiceBefore + (magnitude & Vector256.One)), + twiceBefore + Vector256.One, + magnitude); + + Vector256 twiceAfter = absoluteAfter << 1; + magnitude = Vector256.ConditionalSelect(Vector256.GreaterThan(magnitude + (magnitude & Vector256.One), twiceAfter), twiceAfter, magnitude); + + // Only a strict change in slope direction suppresses the tendency. Equal neighbors still use the native parity-sensitive limits above. + Vector256 nonmonotonic = ~Vector256.Equals(before, Vector256.Zero) & ~Vector256.Equals(after, Vector256.Zero) + & Vector256.LessThan(before ^ after, Vector256.Zero); + + Vector256 signed = Vector256.ConditionalSelect(Vector256.LessThan(previous, nextAverage), -magnitude, magnitude); + + return signed & ~nonmonotonic; + } + + /// + /// Finds lanes whose four arguments require 64-bit arithmetic. + /// + /// The first argument. + /// The second argument. + /// The third argument. + /// The fourth argument. + /// One set bit per lane outside the proved fast range. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint GetWideArithmeticMask(Vector512 first, Vector512 second, Vector512 third, Vector512 fourth) + { + // M consists of 29 low set bits. OR of unsigned magnitudes is at most M exactly when every argument is in range. + // Abs(Int32.MinValue) retains its high bit; interpreting it as unsigned correctly selects the wide calculation. + Vector512 magnitudes = Vector512.Abs(first).AsUInt32() | Vector512.Abs(second).AsUInt32() + | Vector512.Abs(third).AsUInt32() | Vector512.Abs(fourth).AsUInt32(); + + return (uint)Vector512.ExtractMostSignificantBits(Vector512.GreaterThan(magnitudes, Vector512.Create((uint)MaximumFastArgument))); + } + + /// + /// Calculates pair averages without overflowing the sum of two Int32 samples. + /// + /// The first samples. + /// The second samples. + /// The same biased integer averages as the scalar 64-bit equation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 Average(Vector512 first, Vector512 second) + { + // (a & b) + ((a ^ b) >> 1) gives floor((a+b)/2) without forming the possibly overflowing sum. + // An odd sum rounds up only when the first sample is greater, matching (a+b+(a>b))/2 with an arithmetic shift. + Vector512 different = first ^ second; + return (first & second) + (different >> 1) + (Vector512.GreaterThan(first, second) & different & Vector512.One); + } + + /// + /// Calculates the native integer tendency for independent sample lanes. + /// + /// The preceding original or restored samples. + /// The current pair averages. + /// The following pair averages. + /// The predicted difference. Callers correct lanes outside MaximumFastArgument with the 64-bit scalar equation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 SmoothTendency(Vector512 previous, Vector512 average, Vector512 nextAverage) + { + Vector512 before = previous - average; + Vector512 after = average - nextAverage; + Vector512 absoluteBefore = Vector512.Abs(before); + + Vector512 absoluteAfter = Vector512.Abs(after); + Vector512 multiplier = Vector512.Create(DivisionByThreeMultiplier); + + Vector512 evenProducts = Vector512_.MultiplyWideningEven(absoluteBefore, multiplier); + + // Move each odd word into its preceding even word before widening. Endianness changes which half of a 64-bit lane holds that word. + Vector512 oddWords = BitConverter.IsLittleEndian + ? (absoluteBefore.AsUInt64() >> 32).AsInt32() + : (absoluteBefore.AsUInt64() << 32).AsInt32(); + + Vector512 oddProducts = Vector512_.MultiplyWideningEven(oddWords, multiplier); + + // The high product words give floor(abs(before)/3) within the fast range. Narrow yields all even quotients, then all odd quotients; + // a complete-register shuffle restores their original sample order. This avoids multiplying Int32 values before widening. + Vector512 quotient = Vector512.Narrow(evenProducts >> 32, oddProducts >> 32); + quotient = Vector512.ShuffleNative(quotient, Vector512.Create(0, 8, 1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15)); + Vector512 magnitude = (quotient + Vector512.Abs(previous - nextAverage) + Vector512.Create(2)) >> 2; + Vector512 twiceBefore = absoluteBefore << 1; + magnitude = Vector512.ConditionalSelect( + Vector512.GreaterThan(magnitude, twiceBefore + (magnitude & Vector512.One)), + twiceBefore + Vector512.One, + magnitude); + + Vector512 twiceAfter = absoluteAfter << 1; + magnitude = Vector512.ConditionalSelect(Vector512.GreaterThan(magnitude + (magnitude & Vector512.One), twiceAfter), twiceAfter, magnitude); + + // Only a strict change in slope direction suppresses the tendency. Equal neighbors still use the native parity-sensitive limits above. + Vector512 nonmonotonic = ~Vector512.Equals(before, Vector512.Zero) & ~Vector512.Equals(after, Vector512.Zero) + & Vector512.LessThan(before ^ after, Vector512.Zero); + + Vector512 signed = Vector512.ConditionalSelect(Vector512.LessThan(previous, nextAverage), -magnitude, magnitude); + + return signed & ~nonmonotonic; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs index 8bf1ee97b9..c2eee055a3 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs @@ -11,472 +11,586 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; /// -/// Implements the squeeze transform. +/// Splits modular channels into pair averages and predicted differences, or restores their original samples. /// /// -/// The squeeze transform in JXL is a reversible -/// wavelet-like decomposition used in the modular mode -/// to reduce redundancy and improve compression, -/// especially for structured or synthetic images. -/// It works by hierarchically splitting channesl -/// into lower-resolution representations plus -/// residuals, giving us multi-resolution coding while -/// remaining lossless. +/// Horizontal reconstruction depends on the preceding restored sample. Transposed row tiles put independent rows in SIMD lanes. +/// Vertical reconstruction puts independent columns in SIMD lanes. Both paths preserve the scalar signed division and stored Int32 results. /// -internal static class JxlSqueeze +internal static partial class JxlSqueeze { private const int MaxFirstPreviewSize = 8; /// - /// Computes the average of two integers. + /// Computes the squeeze average with its first-sample-dependent rounding. /// - /// - /// This method is specific to the Squeeze transform. - /// It is not a generic average method. - /// - /// First integer - /// Second integer - /// The average of x, y. + /// The first original sample. + /// The second original sample. + /// The biased average. The sum uses Int64 so every Int32 pair is representable before division. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int Average(int x, int y) => (x + y + ((x > y) ? 1 : 0)) >> 1; + public static int Average(int first, int second) => (int)(((long)first + second + (first > second ? 1 : 0)) >> 1); /// - /// Computes a smoothed prediction tendency from three neighboring values. + /// Estimates the difference inside a pair from its predecessor and adjacent averages. /// - /// - /// The value preceding . - /// - /// - /// The central value used to determine the local gradient. - /// - /// - /// The value following . - /// - /// - /// The smoothed prediction tendency, constrained according to the local - /// gradients when the three values are monotonically ordered; otherwise, - /// zero. - /// - /// - /// - /// When the input values are monotonically non-increasing or non-decreasing, - /// this method estimates a tendency using a weighted combination of the - /// neighboring values. - /// - /// - /// The result is adjusted to respect the gradients between - /// and , and between and - /// . These adjustments use parity-sensitive bounds - /// to constrain the tendency. - /// - /// - /// If the values are not monotonically ordered, the method returns zero, - /// indicating no prediction tendency. - /// - /// + /// The preceding original or restored odd sample. + /// The current pair average. + /// The following pair average. + /// The parity-limited tendency, using native 64-bit arithmetic throughout. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int SmoothTendency(int b, int a, int n) + private static long SmoothTendency(long previous, long average, long nextAverage) { - int diff = 0; - - if (b >= a && a >= n) + long difference = 0; + if (previous >= average && average >= nextAverage) { - diff = ((4 * b) - (3 * n) - a + 6) / 12; - - if (diff - (diff & 1) > 2 * (b - a)) + // Estimate the difference only along a monotonic run. Division truncates toward zero, as in the native scalar decoder. + difference = ((4 * previous) - (3 * nextAverage) - average + 6) / 12; + if (difference - (difference & 1) > 2 * (previous - average)) { - diff = (2 * (b - a)) + 1; + difference = (2 * (previous - average)) + 1; } - if (diff + (diff & 1) > 2 * (a - n)) + if (difference + (difference & 1) > 2 * (average - nextAverage)) { - diff = 2 * (a - n); + difference = 2 * (average - nextAverage); } } - else if (b <= a && a <= n) + else if (previous <= average && average <= nextAverage) { - diff = ((4 * b) - (3 * n) - a - 6) / 12; - - if (diff + (diff & 1) < 2 * (b - a)) + difference = ((4 * previous) - (3 * nextAverage) - average - 6) / 12; + if (difference + (difference & 1) < 2 * (previous - average)) { - diff = (2 * (b - a)) - 1; + difference = (2 * (previous - average)) - 1; } - if (diff - (diff & 1) < 2 * (a - n)) + if (difference - (difference & 1) < 2 * (average - nextAverage)) { - diff = 2 * (a - n); + difference = 2 * (average - nextAverage); } } - return diff; - } - - // The function operates on 256-bit fixed size vectors, - // 8 elements at a time. It should still work even on CPUs - // without 256-bit vector support (the JIT will translate - // these into 128-bit halves, or scalar without SIMD support). - // - // The FastUnsqueeze method CAN operate on vectors below - // 256-bit, but not above. It's better to simply use Vector256 - // rather than duplicate everything. Vector may be a problem - // as its number of elements can be greater than 8 which is too - // much for this method. - [MethodImpl(InliningOptions.HotPath)] // Called on an entire image - private static void FastUnsqueeze(Span pResidual, Span pAvg, Span pNAvg, Span pPout, Span pOut, Span pNOut) - { - Vector256 oneThird = Vector256.Create(0x55555556); - - ref int pAvgRef = ref MemoryMarshal.GetReference(pAvg); - ref int pNAvgRef = ref MemoryMarshal.GetReference(pNAvg); - ref int pPoutReference = ref MemoryMarshal.GetReference(pPout); - ref int pResidualRef = ref MemoryMarshal.GetReference(pResidual); - ref int pOutRef = ref MemoryMarshal.GetReference(pOut); - ref int pNOutRef = ref MemoryMarshal.GetReference(pNOut); - - Vector256 avg = Vector256.LoadUnsafe(ref pAvgRef); - Vector256 nextAvg = Vector256.LoadUnsafe(ref pNAvgRef); - Vector256 top = Vector256.LoadUnsafe(ref pPoutReference); - - Vector256 ba = top - avg; - Vector256 an = avg - nextAvg; - Vector256 nonmono = ba ^ an; - Vector256 absba = Vector256.Abs(ba); - Vector256 absan = Vector256.Abs(an); - Vector256 absbn = Vector256.Abs(top - nextAvg); - - Vector256 a3eh = Vector256_.MultiplyEven(absba, oneThird); - Vector256 a3oh = Vector256_.MultiplyOdd(absba, oneThird); - - Vector256 a3 = BitConverter.IsLittleEndian - ? Vector256_.InterleaveOdd(a3eh.AsInt32(), a3oh.AsInt32()) - : Vector256_.InterleaveEven(a3eh.AsInt32(), a3oh.AsInt32()); - - a3 += absbn + Vector256.Create(2); - - Vector256 absdiff = a3 >> 2; - - Vector256 skipdiff = Vector256_.NotEqual(ba, Vector256.Zero); - skipdiff &= Vector256_.NotEqual(an, Vector256.Zero); - skipdiff &= Vector256.LessThan(nonmono, Vector256.Zero); - - Vector256 absBa2 = (absba << 1) + (absdiff & Vector256.One); - - absdiff = Vector256.ConditionalSelect( - Vector256.GreaterThan(absdiff, absBa2), - (absba << 1) + Vector256.One, - absdiff); - - Vector256 absan2 = absan << 1; - absdiff = Vector256.ConditionalSelect( - Vector256.GreaterThan(absdiff + (absdiff & Vector256.One), absan2), - absan2, - absdiff); - - Vector256 diff1 = Vector256.ConditionalSelect( - Vector256.LessThan(top, nextAvg), - -absdiff, - absdiff); - - Vector256 tendency = diff1 & ~skipdiff; - Vector256 diffMinusTendency = Vector256.LoadUnsafe(ref pResidualRef); - Vector256 diff = diffMinusTendency + tendency; - Vector256 output = avg + (diff + (diff << 31)); - - output.StoreUnsafe(ref pOutRef); - (output - diff).StoreUnsafe(ref pNOutRef); + // These bounds prevent a predicted pair from extending beyond a monotonic neighborhood. + // Parity matters because inverse reconstruction divides a signed difference toward zero. + return difference; } /// /// Restores a horizontal channel from its average and residual planes, then releases the displaced average storage. /// - /// The configuration used for the restored samples. + /// The configuration used for restored samples. /// The owner of the average and residual channels. /// The average channel index, which receives the restored samples. - /// The borrowed residual channel index. InverseSqueeze removes it after reconstruction. + /// The residual channel index. InverseSqueeze removes it after reconstruction. public static void InverseHorizontalSqueeze(Configuration configuration, JxlModularImage input, int c, int rc) { - // Channel offsets should not overflow. DebugGuard.MustBeLessThan(c, input.Channels.Count, nameof(c)); - DebugGuard.MustBeLessThan(rc, input.Channels.Count, nameof(c)); - - JxlModularChannel inputChannel = input.Channels[c]; - JxlModularChannel inputResidualChannel = input.Channels[rc]; - if (inputChannel.Width != JxlMath.DivCeil(inputChannel.Width + inputResidualChannel.Width, 2)) + DebugGuard.MustBeLessThan(rc, input.Channels.Count, nameof(rc)); + JxlModularChannel average = input.Channels[c]; + JxlModularChannel residual = input.Channels[rc]; + if (average.Width != JxlMath.DivCeil(average.Width + residual.Width, 2)) { throw new InvalidOperationException("Invalid width"); } - if (inputChannel.Height != inputResidualChannel.Height) + if (average.Height != residual.Height) { throw new InvalidOperationException("Height of the input channel must be equal to the height of the residual channel"); } - if (inputResidualChannel.Width == 0) + if (residual.Width == 0) { - input.Channels[c].HorizontalShift--; + average.HorizontalShift--; return; } - // Keep the image's channel wrapper stable. Swapping at completion transfers the result and releases the old average allocation. - using JxlModularChannel outputChannel = new( - configuration, - inputChannel.Width + inputResidualChannel.Width, - inputChannel.Height, - inputChannel.HorizontalShift - 1, - inputChannel.VerticalShift); + using JxlModularChannel output = new(configuration, average.Width + residual.Width, average.Height, average.HorizontalShift - 1, average.VerticalShift); - if (inputResidualChannel.Height == 0) + // Reconstruction changes channel resolution but retains the represented component. + output.Component = average.Component; + + if (residual.Height != 0) { - inputChannel.Swap(outputChannel); - return; - } + ReadOnlySpan averages = average.GetRow(0); + ReadOnlySpan residuals = residual.GetRow(0); + Span restored = output.GetRow(0); + + int averageStride = average.Plane.PixelsPerRow; + int residualStride = residual.Plane.PixelsPerRow; + int outputStride = output.Plane.PixelsPerRow; + + // Three 16-by-16 buffers use at most 3 KiB. Every active tile is written before reading; unused capacity is never addressed. + // Reuse consumed average/residual columns for even/odd output, then reuse dead average storage during the final transpose. + Unsafe.SkipInit(out InlineArray256 averageStorage); + Unsafe.SkipInit(out InlineArray256 residualStorage); + Unsafe.SkipInit(out InlineArray256 rowStorage); + + Span averageTile = averageStorage; + Span residualTile = residualStorage; + Span rowTile = rowStorage; + int y = 0; + + if (Vector512.IsHardwareAccelerated && residual.Width >= Vector512.Count) + { + for (; y <= average.Height - Vector512.Count; y += Vector512.Count) + { + InverseHorizontalRows( + averages, + residuals, + restored, + averageStride, + residualStride, + outputStride, + average.Width, + residual.Width, + output.Width, + y, + Vector512.Count, + averageTile, + residualTile, + rowTile); + } + } + + if (Vector256.IsHardwareAccelerated && residual.Width >= Vector256.Count) + { + for (; y <= average.Height - Vector256.Count; y += Vector256.Count) + { + InverseHorizontalRows( + averages, + residuals, + restored, + averageStride, + residualStride, + outputStride, + average.Width, + residual.Width, + output.Width, + y, + Vector256.Count, + averageTile, + residualTile, + rowTile); + } + } + + if (Vector128.IsHardwareAccelerated && residual.Width >= Vector128.Count) + { + for (; y <= average.Height - Vector128.Count; y += Vector128.Count) + { + InverseHorizontalRows( + averages, + residuals, + restored, + averageStride, + residualStride, + outputStride, + average.Width, + residual.Width, + output.Width, + y, + Vector128.Count, + averageTile, + residualTile, + rowTile); + } + } - // Transposition works on eight rows at a time. Reuse its stack storage for all blocks. - const int rowsPerBlock = 8; + for (; y < average.Height; y++) + { + InverseHorizontalRow( + averages.Slice(y * averageStride, average.Width), + residuals.Slice(y * residualStride, residual.Width), + restored.Slice(y * outputStride, output.Width), + 0); + } + } - // The average block has one extra column for the next-sample predictor. Each of the - // other planes contains an eight-by-eight block. Allocate them once, outside traversal. - Span bpAvg = stackalloc int[rowsPerBlock * 9]; - Span bpResidual = stackalloc int[rowsPerBlock * 8]; - Span bpOutEven = stackalloc int[rowsPerBlock * 8]; - Span bpOutOdd = stackalloc int[rowsPerBlock * 8]; - Span bpOutEvenT = stackalloc int[rowsPerBlock * 8]; - Span bpOutOddT = stackalloc int[rowsPerBlock * 8]; - int rowBlockCount = JxlMath.DivCeil(inputChannel.Height, rowsPerBlock); + // The channel keeps its identity. The temporary receives and releases the old average allocation. + average.Swap(output); + } - // Borrow all three allocations before traversal. Their strides remain fixed until the completed output is swapped into the channel. - Span residualSamples = inputResidualChannel.GetRow(0); - Span averageSamples = inputChannel.GetRow(0); - Span outputSamples = outputChannel.GetRow(0); - int onerow_in = inputChannel.Plane.PixelsPerRow; - int onerow_inr = inputResidualChannel.Plane.PixelsPerRow; - int onerow_out = outputChannel.Plane.PixelsPerRow; + /// + /// Restores complete horizontal tiles across independent rows and finishes each row's scalar remainder. + /// + /// The borrowed average allocation from its logical origin. + /// The borrowed residual allocation from its logical origin. + /// The restored allocation from its logical origin. + /// The average row stride in integers. + /// The residual row stride in integers. + /// The restored row stride in integers. + /// The logical average width. + /// The logical residual width. + /// The logical restored width. + /// The first row in this group. + /// The selected register's lane count: four, eight or sixteen. + /// Reusable storage for at least rows squared integers. + /// Reusable storage for at least rows squared integers. + /// Reusable storage for at least rows squared integers. + private static void InverseHorizontalRows( + ReadOnlySpan averages, + ReadOnlySpan residuals, + Span output, + int averageStride, + int residualStride, + int outputStride, + int averageWidth, + int residualWidth, + int outputWidth, + int firstRow, + int rows, + Span averageTile, + Span residualTile, + Span rowTile) + { + int area = rows * rows; + averageTile = averageTile[..area]; + residualTile = residualTile[..area]; + rowTile = rowTile[..area]; + int x = 0; - for (int idx = 0; idx < rowBlockCount; idx++) + for (; x <= residualWidth - rows; x += rows) { - int y0 = idx * rowsPerBlock; - int rows = Math.Min(rowsPerBlock, inputChannel.Height - y0); - int x = 0; + // Transposed columns put different image rows in SIMD lanes. Horizontal dependency remains between consecutive columns, not lanes. + SimdUtils.Transpose(averages[((firstRow * averageStride) + x)..], averageStride, averageTile, rows, rows, rows); + + SimdUtils.Transpose(residuals[((firstRow * residualStride) + x)..], residualStride, residualTile, rows, rows, rows); + + // Until arithmetic finishes, the third tile holds only the predecessor and the one-column average lookahead. + if (x == 0) + { + averageTile[..rows].CopyTo(rowTile); + } + else + { + CopyColumn(output, outputStride, firstRow, (2 * x) - 1, rowTile[..rows]); + } - Span pResidual = residualSamples[(y0 * onerow_inr)..]; - Span pAverage = averageSamples[(y0 * onerow_in)..]; - Span pOut = outputSamples[(y0 * onerow_out)..]; - ref int pOutRef = ref MemoryMarshal.GetReference(pOut); + if (x + rows < averageWidth) + { + CopyColumn(averages, averageStride, firstRow, x + rows, rowTile.Slice(rows, rows)); + } + else + { + averageTile.Slice((rows - 1) * rows, rows).CopyTo(rowTile.Slice(rows, rows)); + } - ref int bpOutEvenTRef = ref MemoryMarshal.GetReference(bpOutEvenT); - ref int bpOutOddTRef = ref MemoryMarshal.GetReference(bpOutOddT); + InverseSqueezeOperator operation = default; + ref int averageStart = ref MemoryMarshal.GetReference(averageTile); - int n = Vector256.Count; + ref int residualStart = ref MemoryMarshal.GetReference(residualTile); + ref int neighbors = ref MemoryMarshal.GetReference(rowTile); - if (inputResidualChannel.Width > 16 && rows == rowsPerBlock) + // Select the width outside the dependent column loop. Keep the preceding restored odd samples in a register. + if (rows == Vector512.Count) { - for (; x < inputResidualChannel.Width - 9; x += 8) + Vector512 previous = Vector512.LoadUnsafe(ref neighbors); + + Vector512 current = Vector512.LoadUnsafe(ref averageStart); + + for (int column = 0; column < rows; column++) { - JxlSimdUtils.Transpose8x8Block(pResidual[x..], bpResidual, onerow_inr); - JxlSimdUtils.Transpose8x8Block(pAverage[x..], bpAvg, onerow_in); + uint offset = (uint)(column * rows); - for (int y = 0; y < rowsPerBlock; y++) - { - bpAvg[64 + y] = pAverage[x + 8 + (onerow_in * y)]; - } + Vector512 next = column + 1 < rows + ? Vector512.LoadUnsafe(ref averageStart, offset + (uint)rows) + : Vector512.LoadUnsafe(ref neighbors, (uint)rows); - for (int i = 0; i < 8; i++) - { - // i * 8 - int i8 = i << 3; - - FastUnsqueeze( - bpResidual[i8..], - bpAvg[i8..], - bpAvg[(8 * (i + 1))..], - (x + i > 0) ? bpOutOdd[(8 * ((x + i - 1) & 7))..] : bpAvg[i8..], - bpOutEven[i8..], - bpOutOdd[i8..]); - } + operation.Invoke(current, Vector512.LoadUnsafe(ref residualStart, offset), next, previous, out Vector512 even, out previous); + even.StoreUnsafe(ref averageStart, offset); + previous.StoreUnsafe(ref residualStart, offset); + current = next; + } + } + else if (rows == Vector256.Count) + { + Vector256 previous = Vector256.LoadUnsafe(ref neighbors); - JxlSimdUtils.Transpose8x8Block(bpOutEven, bpOutEvenT, 8); - JxlSimdUtils.Transpose8x8Block(bpOutOdd, bpOutOddT, 8); + Vector256 current = Vector256.LoadUnsafe(ref averageStart); - for (int y = 0; y < rowsPerBlock; y++) - { - // y * 8 - int y8 = y << 3; + for (int column = 0; column < rows; column++) + { + uint offset = (uint)(column * rows); + + Vector256 next = column + 1 < rows + ? Vector256.LoadUnsafe(ref averageStart, offset + (uint)rows) + : Vector256.LoadUnsafe(ref neighbors, (uint)rows); - for (int i = 0; i < rowsPerBlock; i += n) - { - int offset = y8 + i; + operation.Invoke(current, Vector256.LoadUnsafe(ref residualStart, offset), next, previous, out Vector256 even, out previous); + even.StoreUnsafe(ref averageStart, offset); + previous.StoreUnsafe(ref residualStart, offset); + current = next; + } + } + else + { + Vector128 previous = Vector128.LoadUnsafe(ref neighbors); - Vector256 even = Vector256.LoadUnsafe(ref Unsafe.Add(ref bpOutEvenTRef, offset)); - Vector256 odd = Vector256.LoadUnsafe(ref Unsafe.Add(ref bpOutOddTRef, offset)); + Vector128 current = Vector128.LoadUnsafe(ref averageStart); - SimdUtils.Interleave2(even, odd, ref Unsafe.Add(ref pOutRef, ((x + i) << 1) + (onerow_out * y))); - } - } + for (int column = 0; column < rows; column++) + { + uint offset = (uint)(column * rows); + + Vector128 next = column + 1 < rows + ? Vector128.LoadUnsafe(ref averageStart, offset + (uint)rows) + : Vector128.LoadUnsafe(ref neighbors, (uint)rows); + + operation.Invoke(current, Vector128.LoadUnsafe(ref residualStart, offset), next, previous, out Vector128 even, out previous); + even.StoreUnsafe(ref averageStart, offset); + previous.StoreUnsafe(ref residualStart, offset); + current = next; } } - for (int y = 0; y < rows; y++) + SimdUtils.Transpose(averageTile, rows, rowTile, rows, rows, rows); + + SimdUtils.Transpose(residualTile, rows, averageTile, rows, rows, rows); + + // Both transposed buffers now contain image rows. Select the width before storing them; each store writes exactly two samples per pair. + if (rows == Vector512.Count) + { + for (int row = 0; row < rows; row++) + { + ref int even = ref rowTile[row * rows]; + ref int odd = ref averageTile[row * rows]; + ref int destination = ref output[((firstRow + row) * outputStride) + (2 * x)]; + SimdUtils.Interleave2(Vector512.LoadUnsafe(ref even), Vector512.LoadUnsafe(ref odd), ref destination); + } + } + else if (rows == Vector256.Count) { - UnsqueezeRow(y0 + y, x, residualSamples, averageSamples, outputSamples); + for (int row = 0; row < rows; row++) + { + ref int even = ref rowTile[row * rows]; + ref int odd = ref averageTile[row * rows]; + ref int destination = ref output[((firstRow + row) * outputStride) + (2 * x)]; + SimdUtils.Interleave2(Vector256.LoadUnsafe(ref even), Vector256.LoadUnsafe(ref odd), ref destination); + } } + else + { + for (int row = 0; row < rows; row++) + { + ref int even = ref rowTile[row * rows]; + ref int odd = ref averageTile[row * rows]; + ref int destination = ref output[((firstRow + row) * outputStride) + (2 * x)]; + SimdUtils.Interleave2(Vector128.LoadUnsafe(ref even), Vector128.LoadUnsafe(ref odd), ref destination); + } + } + } + + for (int row = 0; row < rows; row++) + { + InverseHorizontalRow( + averages.Slice((firstRow + row) * averageStride, averageWidth), + residuals.Slice((firstRow + row) * residualStride, residualWidth), + output.Slice((firstRow + row) * outputStride, outputWidth), + x); } + } + + /// + /// Borrows a strided matrix column into a complete-register scratch span. + /// + /// The complete source allocation from its logical origin. + /// The physical row stride in integers. + /// The first source row. + /// The source column. + /// Exactly four, eight or sixteen scratch samples, matching a supported register width. + private static void CopyColumn(ReadOnlySpan source, int stride, int firstRow, int column, Span destination) + { + ref int input = ref MemoryMarshal.GetReference(source); - inputChannel.Swap(outputChannel); + ref int output = ref MemoryMarshal.GetReference(destination); + int offset = (firstRow * stride) + column; - // Restore the scalar remainder of row y, starting at residual x0. The three spans borrow complete allocations - // at their logical origins. Pass them explicitly because a local function cannot capture spans from its caller. - void UnsqueezeRow(int y, int x0, Span residualStorage, Span averageStorage, Span outputStorage) + // Each index addresses one real sample. Plane allocation already bounds row offsets to Int32; the shared gather owns its pin scope. + if (destination.Length == Vector512.Count) { - Span residual = residualStorage[(y * onerow_inr)..]; - Span average = averageStorage[(y * onerow_in)..]; - Span output = outputStorage[(y * onerow_out)..]; - int inputChannelWidth = inputChannel.Width; - int outputChannelWidth = outputChannel.Width; + Vector512 offsets = (Vector512.Indices * Vector512.Create(stride)) + Vector512.Create(offset); - for (int x = x0; x < inputResidualChannel.Width; x++) - { - int xLsh1 = x << 1; // Prevents left shifting three times. Saves on CPU cycles. + Vector512_.Gather(ref input, offsets).StoreUnsafe(ref output); + } + else if (destination.Length == Vector256.Count) + { + Vector256 offsets = (Vector256.Indices * Vector256.Create(stride)) + Vector256.Create(offset); - int diffMinusTendency = residual[x]; - int avg = average[x]; - int nextAverage = x + 1 < inputChannelWidth ? average[x + 1] : avg; + Vector256_.Gather(ref input, offsets).StoreUnsafe(ref output); + } + else + { + Vector128 offsets = (Vector128.Indices * Vector128.Create(stride)) + Vector128.Create(offset); - int left = x > 0 ? output[xLsh1 - 1] : avg; - int tendency = SmoothTendency(left, avg, nextAverage); - int diff = diffMinusTendency + tendency; + Vector128_.Gather(ref input, offsets).StoreUnsafe(ref output); + } + } - int a = avg + (diff / 2); - output[xLsh1] = a; + /// + /// Restores the remaining pairs of one row with exact 64-bit intermediate arithmetic. + /// + /// The logical average row. + /// The logical residual row. + /// The logical restored row. + /// The first pair not already restored by tiled SIMD traversal. + private static void InverseHorizontalRow(ReadOnlySpan averages, ReadOnlySpan residuals, Span output, int firstPair) + { + InverseSqueezeOperator operation = default; + for (int x = firstPair; x < residuals.Length; x++) + { + int average = averages[x]; + int nextAverage = x + 1 < averages.Length ? averages[x + 1] : average; + int previous = x > 0 ? output[(2 * x) - 1] : average; + operation.Invoke(average, residuals[x], nextAverage, previous, out int even, out int odd); - int b = a - diff; - output[xLsh1 + 1] = b; - } + output[2 * x] = even; + output[(2 * x) + 1] = odd; + } - if ((outputChannelWidth & 1) > 0) - { - output[outputChannelWidth - 1] = average[inputChannelWidth - 1]; - } + if ((output.Length & 1) != 0) + { + output[^1] = averages[^1]; } } /// /// Restores a vertical channel from its average and residual planes, then releases the displaced average storage. /// - /// The configuration used for the restored samples. + /// The configuration used for restored samples. /// The owner of the average and residual channels. - /// The average channel index, which receives the restored samples. - /// The borrowed residual channel index. InverseSqueeze removes it after reconstruction. + /// The average channel index, which receives restored samples. + /// The residual channel index. InverseSqueeze removes it after reconstruction. public static void InverseVerticalSqueeze(Configuration configuration, JxlModularImage input, int c, int rc) { - // Channel offsets should not overflow. DebugGuard.MustBeLessThan(c, input.Channels.Count, nameof(c)); - DebugGuard.MustBeLessThan(rc, input.Channels.Count, nameof(c)); - JxlModularChannel inputChannel = input.Channels[c]; - JxlModularChannel inputResidualChannel = input.Channels[rc]; - - if (inputChannel.Height != JxlMath.DivCeil(inputChannel.Height + inputResidualChannel.Height, 2)) + DebugGuard.MustBeLessThan(rc, input.Channels.Count, nameof(rc)); + JxlModularChannel average = input.Channels[c]; + JxlModularChannel residual = input.Channels[rc]; + if (average.Height != JxlMath.DivCeil(average.Height + residual.Height, 2)) { throw new InvalidOperationException("Invalid height"); } - if (inputChannel.Width != inputResidualChannel.Width) + if (average.Width != residual.Width) { throw new InvalidOperationException("Width of the input channel must be equal to the width of the residual channel"); } - if (inputResidualChannel.Height == 0) + if (residual.Height == 0) { - input.Channels[c].VerticalShift--; + average.VerticalShift--; return; } - using JxlModularChannel outputChannel = new( + using JxlModularChannel output = new( configuration, - inputChannel.Width, - inputChannel.Height + inputResidualChannel.Height, - inputChannel.HorizontalShift, - inputChannel.VerticalShift - 1); - - if (inputResidualChannel.Width == 0) - { - inputChannel.Swap(outputChannel); - return; - } - - // Process one vector-width column block at a time. The previous output row - // remains available for prediction while traversal stays on the calling thread. - const int columnsPerBlock = 8; - int columnBlockCount = JxlMath.DivCeil(inputChannel.Width, columnsPerBlock); - - Span residualSamples = inputResidualChannel.GetRow(0); - Span averageSamples = inputChannel.GetRow(0); - Span outputSamples = outputChannel.GetRow(0); - int residualStride = inputResidualChannel.Plane.PixelsPerRow; - int averageStride = inputChannel.Plane.PixelsPerRow; - int outputStride = outputChannel.Plane.PixelsPerRow; - - for (int idx = 0; idx < columnBlockCount; idx++) + average.Width, + average.Height + residual.Height, + average.HorizontalShift, + average.VerticalShift - 1); + + // Reconstruction changes channel resolution but retains the represented component. + output.Component = average.Component; + + ReadOnlySpan averages = average.GetRow(0); + ReadOnlySpan residuals = residual.GetRow(0); + Span restored = output.GetRow(0); + + int averageStride = average.Plane.PixelsPerRow; + int residualStride = residual.Plane.PixelsPerRow; + int outputStride = output.Plane.PixelsPerRow; + int width = average.Width; + InverseSqueezeOperator operation = default; + + // Columns are independent. Each vector contains adjacent columns of the same row; rows remain sequential for predecessor reconstruction. + for (int y = 0; y < residual.Height; y++) { - int x0 = idx * columnsPerBlock; - int x1 = Math.Min((idx + 1) * columnsPerBlock, inputChannel.Width); - int w = x1 - x0; + ReadOnlySpan currentAverage = averages.Slice(y * averageStride, width); + ReadOnlySpan currentResidual = residuals.Slice(y * residualStride, width); + int nextRow = y + 1 < average.Height ? y + 1 : y; + ReadOnlySpan nextAverage = averages.Slice(nextRow * averageStride, width); + ReadOnlySpan previous = y > 0 ? restored.Slice(((2 * y) - 1) * outputStride, width) : currentAverage; + Span evenRow = restored.Slice(2 * y * outputStride, width); + Span oddRow = restored.Slice(((2 * y) + 1) * outputStride, width); + ref int a = ref MemoryMarshal.GetReference(currentAverage); + + ref int r = ref MemoryMarshal.GetReference(currentResidual); + ref int n = ref MemoryMarshal.GetReference(nextAverage); + + ref int p = ref MemoryMarshal.GetReference(previous); + ref int evenOutput = ref MemoryMarshal.GetReference(evenRow); + + ref int oddOutput = ref MemoryMarshal.GetReference(oddRow); + int x = 0; - for (int y = 0; y < inputResidualChannel.Height; y++) + if (Vector512.IsHardwareAccelerated) { - int yLsh1 = y << 1; - - Span pResidual = residualSamples[((y * residualStride) + x0)..]; - Span pAverage = averageSamples[((y * averageStride) + x0)..]; - int nextAverageRow = y + 1 < inputChannel.Height ? y + 1 : y; - Span pNAvg = averageSamples[((nextAverageRow * averageStride) + x0)..]; - Span pOut = outputSamples[((yLsh1 * outputStride) + x0)..]; - Span pNOut = outputSamples[(((yLsh1 + 1) * outputStride) + x0)..]; - Span pPOut = y > 0 ? outputSamples[(((yLsh1 - 1) * outputStride) + x0)..] : pNAvg; - int x = 0; - - for (; x + 7 < w; x += 8) + for (; x <= width - Vector512.Count; x += Vector512.Count) { - FastUnsqueeze( - pResidual[x..], - pAverage[x..], - pNAvg[x..], - pPOut[x..], - pOut[x..], - pNOut[x..]); + operation.Invoke( + Vector512.LoadUnsafe(ref a, (uint)x), + Vector512.LoadUnsafe(ref r, (uint)x), + Vector512.LoadUnsafe(ref n, (uint)x), + Vector512.LoadUnsafe(ref p, (uint)x), + out Vector512 even, + out Vector512 odd); + + even.StoreUnsafe(ref evenOutput, (uint)x); + odd.StoreUnsafe(ref oddOutput, (uint)x); } + } - // Remainder - for (; x < w; x++) + if (Vector256.IsHardwareAccelerated) + { + for (; x <= width - Vector256.Count; x += Vector256.Count) { - int avg = pNAvg[x]; - int nextAvg = pNAvg[x]; - int top = pPOut[x]; - int tendency = SmoothTendency(top, avg, nextAvg); - int diffMinusTendency = pResidual[x]; - int diff = diffMinusTendency + tendency; - int output = avg + (diff >> 1); - pOut[x] = output; - pNOut[x] = output - diff; + operation.Invoke( + Vector256.LoadUnsafe(ref a, (uint)x), + Vector256.LoadUnsafe(ref r, (uint)x), + Vector256.LoadUnsafe(ref n, (uint)x), + Vector256.LoadUnsafe(ref p, (uint)x), + out Vector256 even, + out Vector256 odd); + + even.StoreUnsafe(ref evenOutput, (uint)x); + odd.StoreUnsafe(ref oddOutput, (uint)x); } } - } - - if ((outputChannel.Height & 1) > 0) - { - int y = inputChannel.Height - 1; - Span pAverage = averageSamples[(y * averageStride)..]; - Span pOutput = outputSamples[((y << 1) * outputStride)..]; + if (Vector128.IsHardwareAccelerated) + { + for (; x <= width - Vector128.Count; x += Vector128.Count) + { + operation.Invoke( + Vector128.LoadUnsafe(ref a, (uint)x), + Vector128.LoadUnsafe(ref r, (uint)x), + Vector128.LoadUnsafe(ref n, (uint)x), + Vector128.LoadUnsafe(ref p, (uint)x), + out Vector128 even, + out Vector128 odd); + + even.StoreUnsafe(ref evenOutput, (uint)x); + odd.StoreUnsafe(ref oddOutput, (uint)x); + } + } - for (int x = 0; x < inputChannel.Width; x++) + for (; x < width; x++) { - pOutput[x] = pAverage[x]; + operation.Invoke(currentAverage[x], currentResidual[x], nextAverage[x], previous[x], out int even, out int odd); + + evenRow[x] = even; + oddRow[x] = odd; } } - inputChannel.Swap(outputChannel); + if ((output.Height & 1) != 0) + { + // An unmatched final average is already a complete source row, not another pair with a residual. + averages.Slice((average.Height - 1) * averageStride, width).CopyTo(restored.Slice((output.Height - 1) * outputStride, width)); + } + + average.Swap(output); } /// @@ -577,6 +691,7 @@ public static void DefaultSqueezeParameters(List squeezePa }; squeezeParameters.Add(parameters); + parameters.Horizontal = false; squeezeParameters.Add(parameters); } @@ -594,6 +709,7 @@ public static void DefaultSqueezeParameters(List squeezePa { newParameters.Horizontal = false; squeezeParameters.Add(newParameters); + h = (h + 1) >> 1; } } @@ -604,6 +720,7 @@ public static void DefaultSqueezeParameters(List squeezePa { newParameters.Horizontal = true; squeezeParameters.Add(newParameters); + w = (w + 1) >> 1; } @@ -611,6 +728,7 @@ public static void DefaultSqueezeParameters(List squeezePa { newParameters.Horizontal = false; squeezeParameters.Add(newParameters); + h = (h + 1) >> 1; } } @@ -733,157 +851,296 @@ public static void MetaSqueeze(Configuration configuration, JxlModularImage imag /// /// Splits a horizontal channel into owned average and residual planes and releases its original samples. /// - /// The configuration used for the output samples. - /// The owner of all input and output channels. + /// The configuration used for output samples. + /// The owner of input and output channels. /// The channel to split. - /// The insertion index for the residual channel. + /// The insertion index for its residual channel. public static void ForwardHorizontalSqueeze(Configuration configuration, JxlModularImage input, int c, int rc) { - JxlModularChannel inputChannel = input.Channels[c]; - - // Attach the residual before processing. The image then owns it even if reconstruction of another output fails. + JxlModularChannel source = input.Channels[c]; input.Channels.EnsureCapacity(input.Channels.Count + 1); - using JxlModularChannel outputChannel = new( - configuration, (inputChannel.Width + 1) >> 1, inputChannel.Height, inputChannel.HorizontalShift + 1, inputChannel.VerticalShift); - JxlModularChannel outputChannelResidual = new( - configuration, inputChannel.Width - outputChannel.Width, outputChannel.Height, inputChannel.HorizontalShift + 1, inputChannel.VerticalShift); + using JxlModularChannel average = new(configuration, JxlMath.DivCeil(source.Width, 2), source.Height, source.HorizontalShift + 1, source.VerticalShift); + + JxlModularChannel residual = new(configuration, source.Width - average.Width, source.Height, source.HorizontalShift + 1, source.VerticalShift); - input.Channels.Insert(rc, outputChannelResidual); + // Attach the second owner before processing. The image releases it if any later operation fails. + input.Channels.Insert(rc, residual); - outputChannel.Component = inputChannel.Component; - outputChannelResidual.Component = inputChannel.Component; + average.Component = source.Component; + residual.Component = source.Component; + ReadOnlySpan samples = source.GetRow(0); + Span averages = average.GetRow(0); + Span residuals = residual.GetRow(0); - Span inputSamples = inputChannel.GetRow(0); - Span outputSamples = outputChannel.GetRow(0); - Span residualSamples = outputChannelResidual.GetRow(0); - int inputStride = inputChannel.Plane.PixelsPerRow; - int outputStride = outputChannel.Plane.PixelsPerRow; - int residualStride = outputChannelResidual.Plane.PixelsPerRow; + int sourceStride = source.Plane.PixelsPerRow; + int averageStride = average.Plane.PixelsPerRow; + int residualStride = residual.Plane.PixelsPerRow; - for (int y = 0; y < outputChannel.Height; y++) + for (int y = 0; y < source.Height; y++) { - Span pIn = inputSamples[(y * inputStride)..]; - Span pOut = outputSamples[(y * outputStride)..]; - Span pRes = residualSamples[(y * residualStride)..]; + ForwardHorizontalRow( + samples.Slice(y * sourceStride, source.Width), + averages.Slice(y * averageStride, average.Width), + residuals.Slice(y * residualStride, residual.Width)); + } - for (int x = 0; x < outputChannelResidual.Width; x++) - { - int x2 = x << 1; // x * 2 + source.Swap(average); + } - int a = pIn[x2]; - int b = pIn[x2 + 1]; - int avg = Average(a, b); - pOut[x] = avg; - int diff = a - b; - int nextAvg = avg; + /// + /// Splits a complete source row using independent original sample pairs. + /// + /// The logical source row. + /// The logical average row. + /// The logical residual row, one sample shorter for an odd source width. + private static void ForwardHorizontalRow(ReadOnlySpan source, Span averages, Span residuals) + { + ref int input = ref MemoryMarshal.GetReference(source); - if (x + 1 < outputChannelResidual.Width) - { - int c2 = pIn[x2 + 2]; // actually C, but 1. variable 'c' already defined 2. names should be camelCase - int d = pIn[x2 + 3]; + ref int averageOutput = ref MemoryMarshal.GetReference(averages); + ref int residualOutput = ref MemoryMarshal.GetReference(residuals); - nextAvg = Average(c2, d); - } - else if ((inputChannel.Width & 1) != 0) - { - nextAvg = pIn[x2 + 2]; - } + ForwardSqueezeOperator operation = default; + int x = 0; - int left = x > 0 ? pIn[x2 - 1] : avg; - int tendency = SmoothTendency(left, avg, nextAvg); + // Forward prediction reads original neighbors, so adjacent pairs are independent and require no transposed tile. + if (Vector512.IsHardwareAccelerated) + { + for (; x <= residuals.Length - Vector512.Count; x += Vector512.Count) + { + SimdUtils.Deinterleave2( + Vector512.LoadUnsafe(ref input, (uint)(2 * x)), + Vector512.LoadUnsafe(ref input, (uint)((2 * x) + Vector512.Count)), + out Vector512 even, + out Vector512 odd); + + Vector512 average = Average(even, odd); + + int nextPair = x + Vector512.Count; + int lastAverage = nextPair < residuals.Length ? Average(source[2 * nextPair], source[(2 * nextPair) + 1]) + : (source.Length & 1) != 0 ? source[^1] : average.GetElement(Vector512.Count - 1); + + // Shift the already calculated averages rather than reloading each following pair. Only the final lane needs a new average. + Vector512 nextAverage = Vector512.ShuffleNative(average, Vector512.Create(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 15)); + nextAverage = nextAverage.WithElement(Vector512.Count - 1, lastAverage); + Vector512 previous = Vector512.ShuffleNative(odd, Vector512.Create(0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14)); + previous = previous.WithElement(0, x > 0 ? source[(2 * x) - 1] : average.ToScalar()); + average.StoreUnsafe(ref averageOutput, (uint)x); + + operation.Invoke(even, odd, average, nextAverage, previous).StoreUnsafe(ref residualOutput, (uint)x); + } + } - pRes[x] = diff - tendency; + if (Vector256.IsHardwareAccelerated) + { + for (; x <= residuals.Length - Vector256.Count; x += Vector256.Count) + { + SimdUtils.Deinterleave2( + Vector256.LoadUnsafe(ref input, (uint)(2 * x)), + Vector256.LoadUnsafe(ref input, (uint)((2 * x) + Vector256.Count)), + out Vector256 even, + out Vector256 odd); + + Vector256 average = Average(even, odd); + + int nextPair = x + Vector256.Count; + int lastAverage = nextPair < residuals.Length ? Average(source[2 * nextPair], source[(2 * nextPair) + 1]) + : (source.Length & 1) != 0 ? source[^1] : average.GetElement(Vector256.Count - 1); + + // Shift the already calculated averages rather than reloading each following pair. Only the final lane needs a new average. + Vector256 nextAverage = Vector256.ShuffleNative(average, Vector256.Create(1, 2, 3, 4, 5, 6, 7, 7)); + nextAverage = nextAverage.WithElement(Vector256.Count - 1, lastAverage); + Vector256 previous = Vector256.ShuffleNative(odd, Vector256.Create(0, 0, 1, 2, 3, 4, 5, 6)); + previous = previous.WithElement(0, x > 0 ? source[(2 * x) - 1] : average.ToScalar()); + average.StoreUnsafe(ref averageOutput, (uint)x); + + operation.Invoke(even, odd, average, nextAverage, previous).StoreUnsafe(ref residualOutput, (uint)x); } + } - if ((inputChannel.Width & 1) != 0) + if (Vector128.IsHardwareAccelerated) + { + for (; x <= residuals.Length - Vector128.Count; x += Vector128.Count) { - int x = outputChannel.Width - 1; - pOut[x] = pIn[x * 2]; + SimdUtils.Deinterleave2( + Vector128.LoadUnsafe(ref input, (uint)(2 * x)), + Vector128.LoadUnsafe(ref input, (uint)((2 * x) + Vector128.Count)), + out Vector128 even, + out Vector128 odd); + + Vector128 average = Average(even, odd); + + int nextPair = x + Vector128.Count; + int lastAverage = nextPair < residuals.Length ? Average(source[2 * nextPair], source[(2 * nextPair) + 1]) + : (source.Length & 1) != 0 ? source[^1] : average.GetElement(Vector128.Count - 1); + + // Shift the already calculated averages rather than reloading each following pair. Only the final lane needs a new average. + Vector128 nextAverage = Vector128.ShuffleNative(average, Vector128.Create(1, 2, 3, 3)); + nextAverage = nextAverage.WithElement(Vector128.Count - 1, lastAverage); + Vector128 previous = Vector128.ShuffleNative(odd, Vector128.Create(0, 0, 1, 2)); + previous = previous.WithElement(0, x > 0 ? source[(2 * x) - 1] : average.ToScalar()); + average.StoreUnsafe(ref averageOutput, (uint)x); + + operation.Invoke(even, odd, average, nextAverage, previous).StoreUnsafe(ref residualOutput, (uint)x); } } - inputChannel.Swap(outputChannel); + for (; x < residuals.Length; x++) + { + int even = source[2 * x]; + int odd = source[(2 * x) + 1]; + int average = Average(even, odd); + + int nextAverage = x + 1 < residuals.Length ? Average(source[(2 * x) + 2], source[(2 * x) + 3]) + : (source.Length & 1) != 0 ? source[^1] : average; + + int previous = x > 0 ? source[(2 * x) - 1] : average; + averages[x] = average; + residuals[x] = operation.Invoke(even, odd, average, nextAverage, previous); + } + + if ((source.Length & 1) != 0) + { + averages[^1] = source[^1]; + } } /// /// Splits a vertical channel into owned average and residual planes and releases its original samples. /// - /// The configuration used for the output samples. - /// The owner of all input and output channels. + /// The configuration used for output samples. + /// The owner of input and output channels. /// The channel to split. - /// The insertion index for the residual channel. + /// The insertion index for its residual channel. public static void ForwardVerticalSqueeze(Configuration configuration, JxlModularImage input, int c, int rc) { - JxlModularChannel inputChannel = input.Channels[c]; - + JxlModularChannel source = input.Channels[c]; input.Channels.EnsureCapacity(input.Channels.Count + 1); - using JxlModularChannel outputChannel = new( - configuration, inputChannel.Width, (inputChannel.Height + 1) >> 1, inputChannel.HorizontalShift, inputChannel.VerticalShift + 1); - JxlModularChannel outputResidualChannel = new( - configuration, inputChannel.Width, inputChannel.Height - outputChannel.Height, inputChannel.HorizontalShift, inputChannel.VerticalShift + 1); + using JxlModularChannel average = new(configuration, source.Width, JxlMath.DivCeil(source.Height, 2), source.HorizontalShift, source.VerticalShift + 1); - input.Channels.Insert(rc, outputResidualChannel); + JxlModularChannel residual = new(configuration, source.Width, source.Height - average.Height, source.HorizontalShift, source.VerticalShift + 1); - outputChannel.Component = inputChannel.Component; - outputResidualChannel.Component = inputChannel.Component; + input.Channels.Insert(rc, residual); - int oneRowInput = inputChannel.Plane.PixelsPerRow; + average.Component = source.Component; + residual.Component = source.Component; + ReadOnlySpan samples = source.GetRow(0); + Span averages = average.GetRow(0); + Span residuals = residual.GetRow(0); - Span inputSamples = inputChannel.GetRow(0); - Span outputSamples = outputChannel.GetRow(0); - Span residualSamples = outputResidualChannel.GetRow(0); - int outputStride = outputChannel.Plane.PixelsPerRow; - int residualStride = outputResidualChannel.Plane.PixelsPerRow; + int sourceStride = source.Plane.PixelsPerRow; + int averageStride = average.Plane.PixelsPerRow; + int residualStride = residual.Plane.PixelsPerRow; + int width = source.Width; + ForwardSqueezeOperator operation = default; - for (int y = 0; y < outputChannel.Height; y++) + // Only paired rows have residuals. The final unpaired row is copied below and must not enter this loop. + for (int y = 0; y < residual.Height; y++) { - Span pIn = inputSamples[(y * 2 * oneRowInput)..]; - Span pOut = outputSamples[(y * outputStride)..]; - Span pResidual = residualSamples[(y * residualStride)..]; + ReadOnlySpan evenRow = samples.Slice(2 * y * sourceStride, width); + ReadOnlySpan oddRow = samples.Slice(((2 * y) + 1) * sourceStride, width); + ReadOnlySpan previousRow = y > 0 ? samples.Slice(((2 * y) - 1) * sourceStride, width) : evenRow; + ReadOnlySpan nextEvenRow = evenRow; + ReadOnlySpan nextOddRow = oddRow; + if (y + 1 < residual.Height) + { + nextEvenRow = samples.Slice(((2 * y) + 2) * sourceStride, width); - for (int x = 0; x < outputChannel.Width; x++) + nextOddRow = samples.Slice(((2 * y) + 3) * sourceStride, width); + } + else if ((source.Height & 1) != 0) { - int a = pIn[x]; - int b = pIn[x + oneRowInput]; - int avg = Average(a, b); - pOut[x] = avg; - int diff = a - b; - int nextAvg = avg; + // Treating the same unpaired row as both samples leaves its average unchanged. + nextEvenRow = samples.Slice((source.Height - 1) * sourceStride, width); + + nextOddRow = nextEvenRow; + } + + Span averageRow = averages.Slice(y * averageStride, width); + Span residualRow = residuals.Slice(y * residualStride, width); + ref int a = ref MemoryMarshal.GetReference(evenRow); + + ref int b = ref MemoryMarshal.GetReference(oddRow); + ref int nextA = ref MemoryMarshal.GetReference(nextEvenRow); - if (y + 1 < outputResidualChannel.Height) + ref int nextB = ref MemoryMarshal.GetReference(nextOddRow); + ref int p = ref MemoryMarshal.GetReference(previousRow); + + ref int averageOutput = ref MemoryMarshal.GetReference(averageRow); + ref int residualOutput = ref MemoryMarshal.GetReference(residualRow); + + int x = 0; + + if (Vector512.IsHardwareAccelerated) + { + for (; x <= width - Vector512.Count; x += Vector512.Count) { - int c2 = pIn[x + (2 * oneRowInput)]; // actually C, but 1. variable 'c' already defined 2. names should be camelCase - int d = pIn[x + (3 * oneRowInput)]; - nextAvg = Average(c2, d); + Vector512 even = Vector512.LoadUnsafe(ref a, (uint)x); + + Vector512 odd = Vector512.LoadUnsafe(ref b, (uint)x); + Vector512 averageValue = Average(even, odd); + + Vector512 nextAverage = Average(Vector512.LoadUnsafe(ref nextA, (uint)x), Vector512.LoadUnsafe(ref nextB, (uint)x)); + Vector512 previous = y > 0 ? Vector512.LoadUnsafe(ref p, (uint)x) : averageValue; + averageValue.StoreUnsafe(ref averageOutput, (uint)x); + + operation.Invoke(even, odd, averageValue, nextAverage, previous).StoreUnsafe(ref residualOutput, (uint)x); } - else if ((inputChannel.Height & 1) != 0) + } + + if (Vector256.IsHardwareAccelerated) + { + for (; x <= width - Vector256.Count; x += Vector256.Count) { - nextAvg = pIn[x + (2 * oneRowInput)]; - } + Vector256 even = Vector256.LoadUnsafe(ref a, (uint)x); - int top = y > 0 ? pIn[x - oneRowInput] : avg; - int tendency = SmoothTendency(top, avg, nextAvg); + Vector256 odd = Vector256.LoadUnsafe(ref b, (uint)x); + Vector256 averageValue = Average(even, odd); - pResidual[x] = diff - tendency; + Vector256 nextAverage = Average(Vector256.LoadUnsafe(ref nextA, (uint)x), Vector256.LoadUnsafe(ref nextB, (uint)x)); + Vector256 previous = y > 0 ? Vector256.LoadUnsafe(ref p, (uint)x) : averageValue; + averageValue.StoreUnsafe(ref averageOutput, (uint)x); + + operation.Invoke(even, odd, averageValue, nextAverage, previous).StoreUnsafe(ref residualOutput, (uint)x); + } } - } - if ((inputChannel.Height & 1) != 0) - { - int y = outputChannel.Height - 1; + if (Vector128.IsHardwareAccelerated) + { + for (; x <= width - Vector128.Count; x += Vector128.Count) + { + Vector128 even = Vector128.LoadUnsafe(ref a, (uint)x); - Span pIn = inputSamples[(y * 2 * oneRowInput)..]; - Span pOut = outputSamples[(y * outputStride)..]; + Vector128 odd = Vector128.LoadUnsafe(ref b, (uint)x); + Vector128 averageValue = Average(even, odd); - for (int x = 0; x < outputChannel.Width; x++) + Vector128 nextAverage = Average(Vector128.LoadUnsafe(ref nextA, (uint)x), Vector128.LoadUnsafe(ref nextB, (uint)x)); + Vector128 previous = y > 0 ? Vector128.LoadUnsafe(ref p, (uint)x) : averageValue; + averageValue.StoreUnsafe(ref averageOutput, (uint)x); + + operation.Invoke(even, odd, averageValue, nextAverage, previous).StoreUnsafe(ref residualOutput, (uint)x); + } + } + + for (; x < width; x++) { - pOut[x] = pIn[x]; + int averageValue = Average(evenRow[x], oddRow[x]); + + int nextAverage = Average(nextEvenRow[x], nextOddRow[x]); + int previous = y > 0 ? previousRow[x] : averageValue; + averageRow[x] = averageValue; + residualRow[x] = operation.Invoke(evenRow[x], oddRow[x], averageValue, nextAverage, previous); } } - inputChannel.Swap(outputChannel); + if ((source.Height & 1) != 0) + { + samples.Slice((source.Height - 1) * sourceStride, width).CopyTo(averages.Slice((average.Height - 1) * averageStride, width)); + } + + source.Swap(average); } public static void ForwardSqueeze(Configuration configuration, JxlModularImage input, List parameters) diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/SqueezeTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/SqueezeTests.cs new file mode 100644 index 0000000000..a6c8821a0b --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/SqueezeTests.cs @@ -0,0 +1,222 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; +using SixLabors.ImageSharp.Tests.Memory; +using SixLabors.ImageSharp.Tests.TestUtilities; + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; + +/// +/// Tests squeeze pair arithmetic, sequential prediction and channel storage at each SIMD width. +/// +public class SqueezeTests +{ + /// + /// Checks independently known linear-ramp averages and residuals before restoring odd and even channel shapes. + /// + [Fact] + public void Squeeze_LinearRampsPreservePairsAndStrides() + { + static void RunTest() + { + ReadOnlySpan lengths = [1, 2, 7, 8, 9, 31, 32, 33, 34, 65, 66, 67]; + ReadOnlySpan orientations = [false, true]; + ReadOnlySpan directions = [-1, 1]; + foreach (int length in lengths) + { + foreach (bool horizontal in orientations) + { + foreach (int direction in directions) + { + TestMemoryAllocator allocator = new(173); + allocator.EnableNonThreadSafeLogging(); + Configuration configuration = TestEnvironment.Configuration.Clone(); + configuration.MemoryAllocator = allocator; + int width = horizontal ? length : 33; + int height = horizontal ? 33 : length; + using (JxlModularImage image = JxlModularImage.Create(configuration, width + 19, height + 1, 16, 1)) + { + // Shrink retains a wider physical stride. New average, residual and restored planes have their own strides. + JxlModularChannel source = image.Channels[0]; + source.Shrink(width, height); + source.Component = 0; + Span samples = source.GetRow(0); + int stride = source.Plane.PixelsPerRow; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + samples[(y * stride) + x] = direction * (horizontal ? (100 * y) + x : (100 * x) + y); + } + } + + if (horizontal) + { + JxlSqueeze.ForwardHorizontalSqueeze(configuration, image, 0, 1); + } + else + { + JxlSqueeze.ForwardVerticalSqueeze(configuration, image, 0, 1); + } + + JxlModularChannel average = image.Channels[0]; + JxlModularChannel residual = image.Channels[1]; + ReadOnlySpan averages = average.GetRow(0); + ReadOnlySpan residuals = residual.Width != 0 && residual.Height != 0 ? residual.GetRow(0) : []; + for (int y = 0; y < average.Height; y++) + { + for (int x = 0; x < average.Width; x++) + { + int coordinate = horizontal ? x : y; + int other = horizontal ? y : x; + // A decreasing odd sum rounds toward its first sample. An unmatched edge is already a complete sample. + int first = direction * ((100 * other) + (2 * coordinate)); + Assert.Equal(first, averages[(y * average.Plane.PixelsPerRow) + x]); + } + } + + for (int y = 0; y < residual.Height; y++) + { + for (int x = 0; x < residual.Width; x++) + { + int coordinate = horizontal ? x : y; + // Interior slope prediction exactly removes a unit ramp. At an even final edge the repeated average predicts zero. + int expected = (length & 1) == 0 && coordinate == (length / 2) - 1 ? -direction : 0; + Assert.Equal(expected, residuals[(y * residual.Plane.PixelsPerRow) + x]); + } + } + + if (horizontal) + { + JxlSqueeze.InverseHorizontalSqueeze(configuration, image, 0, 1); + } + else + { + JxlSqueeze.InverseVerticalSqueeze(configuration, image, 0, 1); + } + + Assert.Same(source, image.Channels[0]); + Assert.Equal(width, source.Width); + Assert.Equal(height, source.Height); + Assert.Equal(0, source.Component); + ReadOnlySpan restored = source.GetRow(0); + stride = source.Plane.PixelsPerRow; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + int expected = direction * (horizontal ? (100 * y) + x : (100 * x) + y); + Assert.Equal(expected, restored[(y * stride) + x]); + } + } + } + + Assert.Equal(0, allocator.LiveBufferBytes); + } + } + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } + + /// + /// Checks native 64-bit reconstruction and narrowing before the next pair consumes its stored predecessor. + /// + [Fact] + public void InverseSqueeze_WideArithmeticPreservesStoredPredecessor() + { + static void RunTest() + { + ReadOnlySpan orientations = [false, true]; + foreach (bool horizontal in orientations) + { + Configuration configuration = TestEnvironment.Configuration; + using JxlModularImage image = new(horizontal ? 66 : 33, horizontal ? 33 : 4, 32); + JxlModularChannel average = new(configuration, 33, horizontal ? 33 : 2, 1, 1); + image.Channels.Add(average); + JxlModularChannel residual = new(configuration, average.Width, average.Height, 1, 1); + image.Channels.Add(residual); + for (int y = 0; y < average.Height; y++) + { + Span a = average.GetRow(y); + Span r = residual.GetRow(y); + for (int x = 0; x < average.Width; x++) + { + a[x] = (horizontal ? x : y) == 0 ? int.MaxValue : int.MinValue; + r[x] = (horizontal ? x : y) < 2 ? a[x] : 0; + } + } + + if (horizontal) + { + JxlSqueeze.InverseHorizontalSqueeze(configuration, image, 0, 1); + } + else + { + JxlSqueeze.InverseVerticalSqueeze(configuration, image, 0, 1); + } + + // The first wide pair is (3221225471, 1073741823). Store its first sample modulo 2^32. + // The next pair uses the stored odd sample 1073741823, yielding (-3221225472, -1073741824) before narrowing. + ReadOnlySpan expected = [-1073741825, 1073741823, 1073741824, -1073741824]; + for (int y = 0; y < average.Height; y++) + { + ReadOnlySpan row = average.GetRow(y); + for (int x = 0; x < average.Width; x++) + { + Assert.Equal(horizontal && x >= 4 ? int.MinValue : expected[horizontal ? x : y], row[x]); + } + } + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } + + /// + /// Checks signed division and mixed wide/fast lanes at the exact arithmetic boundary without restricting legal stored samples. + /// + [Fact] + public void InverseSqueeze_MixedMagnitudeLanesTruncateSignedDivision() + { + static void RunTest() + { + const int fastMaximum = int.MaxValue / 4; + ReadOnlySpan values = [0, 1, -1, fastMaximum, fastMaximum + 1, int.MinValue, int.MaxValue]; + Configuration configuration = TestEnvironment.Configuration; + using JxlModularImage image = new(33, 2, 32); + JxlModularChannel average = new(configuration, 33, 1, 0, 1); + image.Channels.Add(average); + JxlModularChannel residual = new(configuration, 33, 1, 0, 1); + image.Channels.Add(residual); + Span a = average.GetRow(0); + residual.GetRow(0)[..33].Fill(-1); + for (int x = 0; x < 33; x++) + { + a[x] = values[x % values.Length]; + } + + JxlSqueeze.InverseVerticalSqueeze(configuration, image, 0, 1); + ReadOnlySpan even = average.GetRow(0); + ReadOnlySpan odd = average.GetRow(1); + for (int x = 0; x < 33; x++) + { + // Repeated averages predict zero. Signed -1/2 truncates to zero, so the pair is (average, average+1), with Int32 storage. + int value = values[x % values.Length]; + Assert.Equal(value, even[x]); + Assert.Equal(unchecked(value + 1), odd[x]); + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } +} From 592c0fba6e8aac7ab1ce6c2c270308bfa7618b64 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 29 Oct 2026 18:51:03 +0400 Subject: [PATCH 289/328] Document render pipeline stages --- .../RenderPipeline/WriteToImage3FStage.cs | 13 ++++++++++++- .../RenderPipeline/WriteToImageBundleStage.cs | 8 +++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToImage3FStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToImage3FStage.cs index 6d50f8531f..19f6affee8 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToImage3FStage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToImage3FStage.cs @@ -9,14 +9,17 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; internal class WriteToImage3FStage(Configuration configuration, JxlImage3 image) : RenderPipelineStageBase(configuration) { + /// public override string Name => "WriteI3F"; + /// public override void SetInputSizes(Span inputSizes) { DebugGuard.MustBeGreaterThanOrEqualTo(inputSizes.Length, 3, nameof(inputSizes)); for (int c = 1; c < 3; c++) { + // Each channel must have equal size. if (inputSizes[c].Width != inputSizes[0].Width || inputSizes[c].Height != inputSizes[0].Height) { @@ -24,17 +27,21 @@ public override void SetInputSizes(Span inputSizes) } } + // Create a new image with appropriate size. image.Dispose(); image = new(this.GetConfiguration(), inputSizes[0].Width, inputSizes[0].Height); } + /// public override void ProcessRow(Buffer2D> inputRows, Buffer2D> outputRows, int xExtraLeft, int xExtraRight, int width, int xPos, int yPos) { + // We don't need any extra width. if (xExtraLeft != 0 || xExtraRight != 0) { throw new InvalidOperationException("xExtraLeft and xExtraRight must be 0"); } + // Copy the rows from input to the image specified in the constructor. for (int c = 0; c < 3; c++) { this.GetInputRow(inputRows, c, 0)[..width] @@ -42,6 +49,10 @@ public override void ProcessRow(Buffer2D> inputRows, Buffer2D public override RenderPipelineChannelMode GetChannelMode(int channel) - => channel < 3 ? RenderPipelineChannelMode.Input : RenderPipelineChannelMode.Ignored; + // This stage is read-only and needs exactly 3 channels. + // When there are not enough channels, the stage should not run. + => channel < 3 ? RenderPipelineChannelMode.Input + : RenderPipelineChannelMode.Ignored; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToImageBundleStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToImageBundleStage.cs index 62df6f0a97..a2fcd744f2 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToImageBundleStage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToImageBundleStage.cs @@ -11,8 +11,10 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; internal class WriteToImageBundleStage(Configuration configuration, JxlImageBundle bundle, JxlColorEncoding colorEncoding) : RenderPipelineStageBase(configuration) { + /// public override string Name => "WriteIB"; + /// public override void SetInputSizes(Span inputSizes) { if (inputSizes.Length < 3) @@ -39,6 +41,7 @@ public override void SetInputSizes(Span inputSizes) throw new InvalidOperationException("Could not set the image of an image bundle"); } + // Add all extra channels to the image bundle. for (int c = 3; c < inputSizes.Length; c++) { JxlPlane ch = new(this.GetConfiguration(), inputSizes[c].Width, inputSizes[c].Height); @@ -46,6 +49,7 @@ public override void SetInputSizes(Span inputSizes) } } + /// public override void ProcessRow( Buffer2D> inputRows, Buffer2D> outputRows, @@ -79,5 +83,7 @@ public override void ProcessRow( } } - public override RenderPipelineChannelMode GetChannelMode(int channel) => RenderPipelineChannelMode.Input; + /// + public override RenderPipelineChannelMode GetChannelMode(int channel) + => RenderPipelineChannelMode.Input; // We only decode input. We don't write anything back to the output. } From f971d6854b30c59ad3617f3277a8566abaa8083c Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 29 Oct 2026 18:57:59 +0400 Subject: [PATCH 290/328] Cleanup Mostly reduces syntax warnings --- .../Formats/Jxl/Processing/Decoder/JxlHuffmanDecoder.cs | 2 +- .../Formats/Jxl/Processing/Decoder/JxlIccReader.cs | 4 ++-- .../Jxl/Processing/Decoder/JxlTransformsDecoder.cs | 1 - .../Encoder/AuxiliaryOutput/JxlAuxiliaryOutput.cs | 6 ++++-- .../AuxiliaryOutput/JxlAuxiliaryOutputConstants.cs | 9 --------- .../Encoder/Heuristics/JxlAcStrategyHeuristics.cs | 5 +---- .../Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs | 1 - .../Formats/Jxl/Processing/Encoder/JxlGaborish.cs | 1 - src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs | 1 - .../Jxl/Processing/RenderPipeline/UpsamplingStage.cs | 5 ++++- 10 files changed, 12 insertions(+), 23 deletions(-) delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlAuxiliaryOutputConstants.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlHuffmanDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlHuffmanDecoder.cs index 6515233ce4..34e5cd4283 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlHuffmanDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlHuffmanDecoder.cs @@ -272,7 +272,7 @@ public bool ReadFromBitStream(int alphabetSize, JxlBitReader br) DebugGuard.MustBeLessThanOrEqualTo(alphabetSize, 256, nameof(alphabetSize)); - Span codeLengths = stackalloc byte[256].Slice(0, alphabetSize); + Span codeLengths = (stackalloc byte[256])[..alphabetSize]; codeLengths.Clear(); // Zero-initialized in reference software Span codeLengthCodeLengths = stackalloc byte[CodeLengthCodes]; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlIccReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlIccReader.cs index 25b2045f7f..1870e0b8c2 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlIccReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlIccReader.cs @@ -165,12 +165,12 @@ public static byte[] Read(Configuration configuration, JxlBitReader bitReader) /// The context component assigned by the compressed ICC syntax. private static int GetByteKind(byte value, bool second) { - if (value is >= (byte)'a' and <= (byte)'z' or >= (byte)'A' and <= (byte)'Z') + if (value is (>= (byte)'a' and <= (byte)'z') or (>= (byte)'A' and <= (byte)'Z')) { return 0; } - if (value is >= (byte)'0' and <= (byte)'9' or (byte)'.' or (byte)',') + if (value is (>= (byte)'0' and <= (byte)'9') or (byte)'.' or (byte)',') { return 1; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlTransformsDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlTransformsDecoder.cs index 61cc3a6e53..f537b15bf3 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlTransformsDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlTransformsDecoder.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Numerics; -using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlAuxiliaryOutput.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlAuxiliaryOutput.cs index d39b707bc2..61769cbc41 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlAuxiliaryOutput.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlAuxiliaryOutput.cs @@ -8,7 +8,9 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; /// internal sealed class JxlAuxiliaryOutput { - public JxlLayerTotals[] Layers { get; set; } = new JxlLayerTotals[JxlAuxiliaryOutputConstants.NumberOfImageLayers]; + public const int NumberOfImageLayers = (int)JxlLayerType.ModularAcGroup + 1; + + public JxlLayerTotals[] Layers { get; set; } = new JxlLayerTotals[NumberOfImageLayers]; public long NumberOfBlocks { get; set; } @@ -60,7 +62,7 @@ internal sealed class JxlAuxiliaryOutput public void Assimilate(JxlAuxiliaryOutput victim) { - for (int i = 0; i < JxlAuxiliaryOutputConstants.NumberOfImageLayers; i++) + for (int i = 0; i < NumberOfImageLayers; i++) { this.Layers[i].Assimilate(victim.Layers[i]); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlAuxiliaryOutputConstants.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlAuxiliaryOutputConstants.cs deleted file mode 100644 index 1c1b4edd5c..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlAuxiliaryOutputConstants.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; - -internal class JxlAuxiliaryOutputConstants -{ - public const int NumberOfImageLayers = (int)JxlLayerType.ModularAcGroup + 1; -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlAcStrategyHeuristics.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlAcStrategyHeuristics.cs index d2bf6a4042..efc8f470f3 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlAcStrategyHeuristics.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlAcStrategyHeuristics.cs @@ -79,10 +79,7 @@ public JxlAcStrategyHeuristics(Configuration configuration, JxlCompressionParame /// /// Releases owned scratch without disposing the borrowed source, masks or matrices. /// - public void Dispose() - { - this.memoryOwner?.Dispose(); - } + public void Dispose() => this.memoryOwner?.Dispose(); /// /// Binds source and mask rows and prepares the matrices needed by the configured strategy search. diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs index 89da5432f2..9842fc4d71 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs @@ -11,7 +11,6 @@ using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; -using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Group; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Comparator; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGaborish.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGaborish.cs index 45f9fb8ee5..d07512ab5e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGaborish.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGaborish.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs index a0508f91f8..c8c4252058 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs @@ -5,7 +5,6 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; -using SixLabors.ImageSharp.Common.Helpers; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/UpsamplingStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/UpsamplingStage.cs index 705d695970..5490c0bd4c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/UpsamplingStage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/UpsamplingStage.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Numerics; -using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Memory; @@ -15,6 +14,9 @@ internal class UpsamplingStage : RenderPipelineStageBase { private const int ChunkSize = 1024; + /// + /// The upsampling kernels. + /// private readonly float[] kernel = new float[64 * 25]; // 1600 floats, 6400 bytes (6.25 KB) /// @@ -42,6 +44,7 @@ public UpsamplingStage(Configuration configuration, JxlCustomTransformData upsFa int n = 1 << shift; int h = n / 2; + // Initialize the upsampling kernels. for (int ky = 0; ky < h; ++ky) { for (int kx = 0; kx < h; ++kx) From d46afb204f796091396eb1e1fd4b7623f6c32e47 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 29 Oct 2026 19:04:08 +0400 Subject: [PATCH 291/328] Remove unused parameter --- .../Jxl/Processing/Encoder/JxlQuantizerWeightsEncoder.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlQuantizerWeightsEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlQuantizerWeightsEncoder.cs index af84af99fb..1228ca0735 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlQuantizerWeightsEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlQuantizerWeightsEncoder.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; @@ -235,7 +234,7 @@ public static bool DequantMatricesEncodeDc(JxlDequantMatrices matrices, JxlBitWr }); } - public static bool DequantMatricesSetCustomDc(Configuration configuration, JxlDequantMatrices matrices, InlineArray3 dc) + public static bool DequantMatricesSetCustomDc(JxlDequantMatrices matrices, InlineArray3 dc) { matrices.SetDcQuant(dc); @@ -263,14 +262,14 @@ public static bool DequantMatricesSetCustomDc(Configuration configuration, JxlDe return true; } - public static bool DequantMatricesScaleDc(Configuration configuration, JxlDequantMatrices matrices, float scale) + public static bool DequantMatricesScaleDc(JxlDequantMatrices matrices, float scale) { InlineArray3 dc = default; dc[0] = matrices.GetInverseDcQuant(0) * (1.0f / scale); dc[1] = matrices.GetInverseDcQuant(1) * (1.0f / scale); dc[2] = matrices.GetInverseDcQuant(2) * (1.0f / scale); - return DequantMatricesSetCustomDc(configuration, matrices, dc); + return DequantMatricesSetCustomDc(matrices, dc); } public static bool DequantMatricesRoundtrip(Configuration configuration, JxlDequantMatrices matrices) From f31e77aa683142610ceef1a7fea10ae125a8331e Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Oct 2026 01:09:24 +1000 Subject: [PATCH 292/328] Correct JPEG XL coefficient bias and share SIMD dequantization --- .../Decoder/Group/JxlGroupDecoder.cs | 123 +++---- .../Jxl/Processing/Encoder/JxlGroupEncoder.cs | 47 +-- .../Formats/Jxl/Processing/JxlSimdUtils.cs | 3 - .../JxlQuantizer.Dequantization.cs | 324 ++++++++++++++++++ .../JxlQuantizer.QuantizationBiasOperator.cs | 89 +++++ .../Processing/Quantization/JxlQuantizer.cs | 2 +- .../Quantization/JxlQuantizerSimd.cs | 56 --- .../Jxl/Processing/QuantizationTests.cs | 100 ++++++ 8 files changed, 580 insertions(+), 164 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.Dequantization.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.QuantizationBiasOperator.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerSimd.cs create mode 100644 tests/ImageSharp.Tests/Formats/Jxl/Processing/QuantizationTests.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs index 49cd05beda..71739defab 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs @@ -33,63 +33,37 @@ public static void Transpose8x8InPlace(Span block) } } - public static void DequantLane( - JxlDctAcType acType, - Vector scaledDequantX, - Vector scaledDequantY, - Vector scaledDequantB, - ReadOnlySpan dequantMatrices, - int size, - int k, - Vector xCCMul, - Vector bCCMul, - ReadOnlySpan biases, - JxlDctAcPointer qblock0, - JxlDctAcPointer qblock1, - JxlDctAcPointer qblock2, - Span block) - { - Vector xMul = Vector.Create(dequantMatrices[k..]) * scaledDequantX; - Vector yMul = Vector.Create(dequantMatrices[(k + size)..]) * scaledDequantY; - Vector bMul = Vector.Create(dequantMatrices[(k + (size * 2))..]) * scaledDequantB; - - Vector quantizedXInt; - Vector quantizedYInt; - Vector quantizedBInt; - - if (acType == JxlDctAcType.Ac16) - { - quantizedXInt = Vector.WidenLower(Vector.Create(qblock0.Pointer16[k..])); - quantizedYInt = Vector.WidenLower(Vector.Create(qblock1.Pointer16[k..])); - quantizedBInt = Vector.WidenLower(Vector.Create(qblock2.Pointer16[k..])); - } - else - { - quantizedXInt = Vector.Create(qblock0.Pointer32[k..]); - quantizedYInt = Vector.Create(qblock1.Pointer32[k..]); - quantizedBInt = Vector.Create(qblock2.Pointer32[k..]); - } - - Vector dequantXCC = JxlQuantizerSimd.AdjustQuantBias(0, quantizedXInt, biases) * xMul; - Vector dequantY = JxlQuantizerSimd.AdjustQuantBias(1, quantizedYInt, biases) * yMul; - Vector dequantBCC = JxlQuantizerSimd.AdjustQuantBias(2, quantizedBInt, biases) * bMul; - - Vector dequantX = (xCCMul * dequantY) + dequantXCC; - Vector dequantB = (bCCMul * dequantY) + dequantBCC; - - dequantX.CopyTo(block[k..]); - dequantY.CopyTo(block[(k + size)..]); - dequantB.CopyTo(block[(k + (size * 2))..]); - } - + /// + /// Restores one transform's AC planes, color correlation and separately coded lowest frequencies. + /// + /// The stored integer coefficient width. + /// The inverse frame quantization scale. + /// The positive block quantizer. + /// The X matrix multiplier. + /// The B matrix multiplier. + /// The Y contribution to X. + /// The Y contribution to B. + /// The transform strategy. + /// The coefficient count per channel. + /// The prepared frame quantizer and matrices. + /// The number of 8-by-8 blocks covered by the transform. + /// The DC image's horizontal offset for each channel. + /// The current DC samples. + /// The DC row stride, in samples. + /// The channel biases and shared reciprocal numerator. + /// The X integer coefficients. + /// The Y integer coefficients. + /// The B integer coefficients. + /// The caller-owned consecutive X, Y and B float planes. + /// The caller-owned transform scratch. private static void DequantBlock( JxlDctAcType acType, float invGlobalScale, int quant, float xDmMultiplier, float bDmMultiplier, - Vector xCcMul, - Vector bCcMul, + float xCcMul, + float bCcMul, JxlAcStrategyType kind, int size, JxlQuantizer quantizer, @@ -104,19 +78,46 @@ private static void DequantBlock( Span block, Span scratch) { - float scaledDequantS = invGlobalScale / quant; - - Vector scaledDequantX = new(scaledDequantS * xDmMultiplier); - Vector scaledDequantY = new(scaledDequantS); - Vector scaledDequantB = new(scaledDequantS * bDmMultiplier); - - ReadOnlySpan dequantMatrices = quantizer.DequantMatrix(kind, 0); - - for (int k = 0; k < coveredBlocks * JxlFrameDimensions.DctBlockSize; k += Vector.Count) + float inverseQuantization = invGlobalScale / quant; + float xScale = inverseQuantization * xDmMultiplier; + float bScale = inverseQuantization * bDmMultiplier; + ReadOnlySpan matrices = quantizer.DequantMatrix(kind, 0); + int coefficientCount = coveredBlocks * JxlFrameDimensions.DctBlockSize; + + // The matrix table stores X, Y and B consecutively for this transform. Only logical coefficients participate. + // Select the stored integer width once. Both paths share numeric conversion, bias correction and correlation. + if (acType == JxlDctAcType.Ac16) + { + JxlQuantizer.Dequantize( + qblock0.Pointer16[..coefficientCount], + qblock1.Pointer16[..coefficientCount], + qblock2.Pointer16[..coefficientCount], + matrices, + xScale, + inverseQuantization, + bScale, + xCcMul, + bCcMul, + biases, + block); + } + else { - DequantLane(acType, scaledDequantX, scaledDequantY, scaledDequantB, dequantMatrices, size, k, xCcMul, bCcMul, biases, qblock0, qblock1, qblock2, block); + JxlQuantizer.Dequantize( + qblock0.Pointer32[..coefficientCount], + qblock1.Pointer32[..coefficientCount], + qblock2.Pointer32[..coefficientCount], + matrices, + xScale, + inverseQuantization, + bScale, + xCcMul, + bCcMul, + biases, + block); } + // AC dequantization does not supply the transform's lowest frequencies. Restore them from the separate DC image. for (int c = 0; c < 3; c++) { JxlTransformsDecoder.LowestFrequenciesFromDc(kind, dcRow[sbx[c]..], dcStride, block[(c * size)..], scratch); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs index 66535185c0..2060290354 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs @@ -3,8 +3,6 @@ using System.Buffers; using System.Numerics.Tensors; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using System.Runtime.Intrinsics; using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; @@ -455,47 +453,10 @@ public static void QuantizeRoundtripYBlockAc( ReadOnlySpan dequantMatrix = quantizer.DequantMatrix(quantKind, 1); - /* - Implementing this is slightly tricky as we have to - process at 64 lanes at a time. - - This is the reference code snippet from libjxl: - - HWY_CAPPED(float, kDCTBlockSize) df; - HWY_CAPPED(int32_t, kDCTBlockSize) di; - const auto inv_qac = Set(df, quantizer.inv_quant_ac(*quant)); - for (size_t k = 0; k < kDCTBlockSize * xsize * ysize; k += Lanes(df)) { - const auto oquant = Load(di, quantized + size + k); - const auto adj_quant = AdjustQuantBias(di, 1, oquant, biases); - const auto dequantm = Load(df, dequant_matrix + k); - Store(Mul(Mul(adj_quant, dequantm), inv_qac), df, inout + size + k); - } - */ - - // Don't call .Clear(). All items will be populated. - Span shared = stackalloc float[64 * 6]; - - Span inverseQAc = shared.Slice(64 * 0, 64); - Span oquant = MemoryMarshal.Cast(shared.Slice(64 * 1, 64)); - Span adjQuant = shared.Slice(64 * 2, 64); - Span dequantm = shared.Slice(64 * 3, 64); - - Span mulAdjQuantDequantm = shared.Slice(64 * 4, 64); - Span mulAdjQuantDequantmInverseQAc = shared.Slice(64 * 5, 64); - - inverseQAc.Fill(quantizer.InverseQuantAc(quant)); - - for (int k = 0; k < JxlFrameDimensions.DctBlockSize * width * height; k += 64) - { - quantized.Slice(size + k, 64).CopyTo(oquant); - JxlQuantizerSimd.AdjustQuantBias64(1, oquant, biases, adjQuant); - dequantMatrix.Slice(k, 64).CopyTo(dequantm); - - TensorPrimitives.Multiply(adjQuant, dequantm, mulAdjQuantDequantm); - TensorPrimitives.Multiply(mulAdjQuantDequantm, inverseQAc, mulAdjQuantDequantmInverseQAc); - - inverseQAc.CopyTo(inOut.Slice(size + k, 64)); - } + // Reconstruct Y before quantizing its correlated X/B residuals. Bias, matrix and scale stay in registers; + // the quantizer writes directly into the existing float coefficient plane and leaves integer output unchanged. + int coefficientCount = JxlFrameDimensions.DctBlockSize * width * height; + JxlQuantizer.Dequantize(quantized.Slice(size, coefficientCount), dequantMatrix, quantizer.InverseQuantAc(quant), 1, biases, inOut[size..]); } public static void ComputeCoefficients( diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs index a0508f91f8..a7c56cfaaf 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs @@ -5,7 +5,6 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; -using SixLabors.ImageSharp.Common.Helpers; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; @@ -14,8 +13,6 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing; /// internal static partial class JxlSimdUtils { - public static int MaxVectorSize => Vector.Count * sizeof(float); - /// /// Fills consecutive integers, starting with the supplied value. /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.Dequantization.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.Dequantization.cs new file mode 100644 index 0000000000..716b7156fe --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.Dequantization.cs @@ -0,0 +1,324 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; + +internal sealed partial class JxlQuantizer +{ + /// + /// Reconstructs one quantized channel in the encoder's coefficient workspace. + /// + /// The logical integer coefficients. + /// The corresponding dequantization weights. + /// The block's inverse AC quantization scale. + /// The XYB channel index. + /// The three channel biases and shared reciprocal numerator. + /// The caller-owned float coefficient workspace. + public static void Dequantize( + ReadOnlySpan coefficients, + ReadOnlySpan matrix, + float inverseQuantization, + int channelIndex, + ReadOnlySpan biases, + Span destination) + { + QuantizationBiasOperator bias = new(channelIndex, biases); + ref int input = ref MemoryMarshal.GetReference(coefficients); + ref float weights = ref MemoryMarshal.GetReference(matrix); + ref float output = ref MemoryMarshal.GetReference(destination); + int i = 0; + + // The encoder first applies the matrix, then the inverse quantization scale. + // Keep both products separate and in this order, as required by enc_group.cc. + if (Vector512.IsHardwareAccelerated) + { + nuint count = (coefficients.Length - i).Vector512Count(); + Vector512 scale = Vector512.Create(inverseQuantization); + + for (nuint block = 0; block < count; block++, i += Vector512.Count) + { + Vector512 quantized = Vector512.ConvertToSingle(Vector512.LoadUnsafe(ref input, (uint)i)); + Vector512 weight = Vector512.LoadUnsafe(ref weights, (uint)i); + ((bias.Invoke(quantized) * weight) * scale).StoreUnsafe(ref output, (uint)i); + } + } + + if (Vector256.IsHardwareAccelerated) + { + nuint count = (coefficients.Length - i).Vector256Count(); + Vector256 scale = Vector256.Create(inverseQuantization); + + for (nuint block = 0; block < count; block++, i += Vector256.Count) + { + Vector256 quantized = Vector256.ConvertToSingle(Vector256.LoadUnsafe(ref input, (uint)i)); + Vector256 weight = Vector256.LoadUnsafe(ref weights, (uint)i); + ((bias.Invoke(quantized) * weight) * scale).StoreUnsafe(ref output, (uint)i); + } + } + + if (Vector128.IsHardwareAccelerated) + { + nuint count = (coefficients.Length - i).Vector128Count(); + Vector128 scale = Vector128.Create(inverseQuantization); + + for (nuint block = 0; block < count; block++, i += Vector128.Count) + { + Vector128 quantized = Vector128.ConvertToSingle(Vector128.LoadUnsafe(ref input, (uint)i)); + Vector128 weight = Vector128.LoadUnsafe(ref weights, (uint)i); + ((bias.Invoke(quantized) * weight) * scale).StoreUnsafe(ref output, (uint)i); + } + } + + // Complete vectors never read padding. The tail uses the same numeric conversion and product order. + for (; i < coefficients.Length; i++) + { + Unsafe.Add(ref output, i) = (bias.Invoke(Unsafe.Add(ref input, i)) * Unsafe.Add(ref weights, i)) * inverseQuantization; + } + } + + /// + /// Restores three 16-bit AC coefficient planes and their X/B correlation with Y. + /// + /// The logical X coefficients. + /// The logical Y coefficients, with the same length as X. + /// The logical B coefficients, with the same length as X. + /// Three consecutive dequantization matrices in X, Y, B order. + /// The inverse quantization scale including the X matrix multiplier. + /// The inverse quantization scale. + /// The inverse quantization scale including the B matrix multiplier. + /// The Y contribution to X. + /// The Y contribution to B. + /// The three channel biases and shared reciprocal numerator. + /// Three consecutive caller-owned output planes in X, Y, B order. + public static void Dequantize( + ReadOnlySpan x, + ReadOnlySpan y, + ReadOnlySpan b, + ReadOnlySpan matrices, + float xScale, + float yScale, + float bScale, + float xCorrelation, + float bCorrelation, + ReadOnlySpan biases, + Span destination) + => DequantizeCore(x, y, b, matrices, xScale, yScale, bScale, xCorrelation, bCorrelation, biases, destination); + + /// + /// Restores three 32-bit AC coefficient planes and their X/B correlation with Y. + /// + /// The logical X coefficients. + /// The logical Y coefficients, with the same length as X. + /// The logical B coefficients, with the same length as X. + /// Three consecutive dequantization matrices in X, Y, B order. + /// The inverse quantization scale including the X matrix multiplier. + /// The inverse quantization scale. + /// The inverse quantization scale including the B matrix multiplier. + /// The Y contribution to X. + /// The Y contribution to B. + /// The three channel biases and shared reciprocal numerator. + /// Three consecutive caller-owned output planes in X, Y, B order. + public static void Dequantize( + ReadOnlySpan x, + ReadOnlySpan y, + ReadOnlySpan b, + ReadOnlySpan matrices, + float xScale, + float yScale, + float bScale, + float xCorrelation, + float bCorrelation, + ReadOnlySpan biases, + Span destination) + => DequantizeCore(x, y, b, matrices, xScale, yScale, bScale, xCorrelation, bCorrelation, biases, destination); + + /// + /// Traverses three coefficient planes without staging converted samples or making a separate correlation pass. + /// + /// Either short or int, selected by the two storage-specific entry points. + /// The logical X coefficients. + /// The logical Y coefficients. + /// The logical B coefficients. + /// The consecutive X, Y and B matrices. + /// The X matrix scale. + /// The Y matrix scale. + /// The B matrix scale. + /// The Y contribution to X. + /// The Y contribution to B. + /// The channel biases and shared reciprocal numerator. + /// The consecutive X, Y and B output planes. + private static void DequantizeCore( + ReadOnlySpan x, + ReadOnlySpan y, + ReadOnlySpan b, + ReadOnlySpan matrices, + float xScale, + float yScale, + float bScale, + float xCorrelation, + float bCorrelation, + ReadOnlySpan biases, + Span destination) + where TCoefficient : unmanaged, IBinaryInteger + { + int length = x.Length; + QuantizationBiasOperator xBias = new(0, biases); + QuantizationBiasOperator yBias = new(1, biases); + QuantizationBiasOperator bBias = new(2, biases); + ref TCoefficient inputX = ref MemoryMarshal.GetReference(x); + ref TCoefficient inputY = ref MemoryMarshal.GetReference(y); + ref TCoefficient inputB = ref MemoryMarshal.GetReference(b); + ref float matrixX = ref MemoryMarshal.GetReference(matrices); + ref float matrixY = ref Unsafe.Add(ref matrixX, length); + ref float matrixB = ref Unsafe.Add(ref matrixY, length); + ref float outputX = ref MemoryMarshal.GetReference(destination); + ref float outputY = ref Unsafe.Add(ref outputX, length); + ref float outputB = ref Unsafe.Add(ref outputY, length); + int i = 0; + + // Each lane holds the same frequency in all three channels. Scale each matrix before applying its bias-adjusted coefficient. + // Correlation uses the restored Y value at that frequency. It does not mix neighboring coefficients or pixel samples. + if (Vector512.IsHardwareAccelerated) + { + nuint count = (length - i).Vector512Count(); + Vector512 scaleX = Vector512.Create(xScale); + Vector512 scaleY = Vector512.Create(yScale); + Vector512 scaleB = Vector512.Create(bScale); + Vector512 correlateX = Vector512.Create(xCorrelation); + Vector512 correlateB = Vector512.Create(bCorrelation); + + for (nuint block = 0; block < count; block++, i += Vector512.Count) + { + Vector512 restoredX = xBias.Invoke(Load512(ref inputX, (uint)i)) * (Vector512.LoadUnsafe(ref matrixX, (uint)i) * scaleX); + Vector512 restoredY = yBias.Invoke(Load512(ref inputY, (uint)i)) * (Vector512.LoadUnsafe(ref matrixY, (uint)i) * scaleY); + Vector512 restoredB = bBias.Invoke(Load512(ref inputB, (uint)i)) * (Vector512.LoadUnsafe(ref matrixB, (uint)i) * scaleB); + Vector512.MultiplyAddEstimate(correlateX, restoredY, restoredX).StoreUnsafe(ref outputX, (uint)i); + restoredY.StoreUnsafe(ref outputY, (uint)i); + Vector512.MultiplyAddEstimate(correlateB, restoredY, restoredB).StoreUnsafe(ref outputB, (uint)i); + } + } + + if (Vector256.IsHardwareAccelerated) + { + nuint count = (length - i).Vector256Count(); + Vector256 scaleX = Vector256.Create(xScale); + Vector256 scaleY = Vector256.Create(yScale); + Vector256 scaleB = Vector256.Create(bScale); + Vector256 correlateX = Vector256.Create(xCorrelation); + Vector256 correlateB = Vector256.Create(bCorrelation); + + for (nuint block = 0; block < count; block++, i += Vector256.Count) + { + Vector256 restoredX = xBias.Invoke(Load256(ref inputX, (uint)i)) * (Vector256.LoadUnsafe(ref matrixX, (uint)i) * scaleX); + Vector256 restoredY = yBias.Invoke(Load256(ref inputY, (uint)i)) * (Vector256.LoadUnsafe(ref matrixY, (uint)i) * scaleY); + Vector256 restoredB = bBias.Invoke(Load256(ref inputB, (uint)i)) * (Vector256.LoadUnsafe(ref matrixB, (uint)i) * scaleB); + Vector256.MultiplyAddEstimate(correlateX, restoredY, restoredX).StoreUnsafe(ref outputX, (uint)i); + restoredY.StoreUnsafe(ref outputY, (uint)i); + Vector256.MultiplyAddEstimate(correlateB, restoredY, restoredB).StoreUnsafe(ref outputB, (uint)i); + } + } + + if (Vector128.IsHardwareAccelerated) + { + nuint count = (length - i).Vector128Count(); + Vector128 scaleX = Vector128.Create(xScale); + Vector128 scaleY = Vector128.Create(yScale); + Vector128 scaleB = Vector128.Create(bScale); + Vector128 correlateX = Vector128.Create(xCorrelation); + Vector128 correlateB = Vector128.Create(bCorrelation); + + for (nuint block = 0; block < count; block++, i += Vector128.Count) + { + Vector128 restoredX = xBias.Invoke(Load128(ref inputX, (uint)i)) * (Vector128.LoadUnsafe(ref matrixX, (uint)i) * scaleX); + Vector128 restoredY = yBias.Invoke(Load128(ref inputY, (uint)i)) * (Vector128.LoadUnsafe(ref matrixY, (uint)i) * scaleY); + Vector128 restoredB = bBias.Invoke(Load128(ref inputB, (uint)i)) * (Vector128.LoadUnsafe(ref matrixB, (uint)i) * scaleB); + Vector128.MultiplyAddEstimate(correlateX, restoredY, restoredX).StoreUnsafe(ref outputX, (uint)i); + restoredY.StoreUnsafe(ref outputY, (uint)i); + Vector128.MultiplyAddEstimate(correlateB, restoredY, restoredB).StoreUnsafe(ref outputB, (uint)i); + } + } + + // Smaller widths continue from the first unprocessed frequency. Scalar conversion also sign-extends short coefficients. + for (; i < length; i++) + { + float restoredX = xBias.Invoke(float.CreateTruncating(Unsafe.Add(ref inputX, i))) * (Unsafe.Add(ref matrixX, i) * xScale); + float restoredY = yBias.Invoke(float.CreateTruncating(Unsafe.Add(ref inputY, i))) * (Unsafe.Add(ref matrixY, i) * yScale); + float restoredB = bBias.Invoke(float.CreateTruncating(Unsafe.Add(ref inputB, i))) * (Unsafe.Add(ref matrixB, i) * bScale); + Unsafe.Add(ref outputX, i) = float.MultiplyAddEstimate(xCorrelation, restoredY, restoredX); + Unsafe.Add(ref outputY, i) = restoredY; + Unsafe.Add(ref outputB, i) = float.MultiplyAddEstimate(bCorrelation, restoredY, restoredB); + } + } + + /// + /// Numerically converts 4 signed coefficients without reading beyond those coefficients. + /// + /// The short or int storage selected at the owning entry point. + /// The start of a coefficient plane. + /// The coefficient offset, in storage elements. + /// Float values in their original frequency order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 Load128(ref TCoefficient source, uint offset) + where TCoefficient : unmanaged, IBinaryInteger + { + // The closed short/int worker makes this storage-width choice constant. It adds no per-frequency type dispatch. + // Short storage needs half as many bytes as the float result. Load only that half, then sign-extend its low lanes. + if (Unsafe.SizeOf() == sizeof(short)) + { + Vector64 packed = Vector64.LoadUnsafe(ref Unsafe.As(ref source), offset); + return Vector128.ConvertToSingle(Vector128.WidenLower(packed.ToVector128())); + } + + return Vector128.ConvertToSingle(Vector128.LoadUnsafe(ref Unsafe.As(ref source), offset)); + } + + /// + /// Numerically converts 8 signed coefficients without reading beyond those coefficients. + /// + /// The short or int storage selected at the owning entry point. + /// The start of a coefficient plane. + /// The coefficient offset, in storage elements. + /// Float values in their original frequency order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 Load256(ref TCoefficient source, uint offset) + where TCoefficient : unmanaged, IBinaryInteger + { + // The closed short/int worker makes this storage-width choice constant. It adds no per-frequency type dispatch. + // Short storage needs half as many bytes as the float result. Load only that half, then sign-extend its low lanes. + if (Unsafe.SizeOf() == sizeof(short)) + { + Vector128 packed = Vector128.LoadUnsafe(ref Unsafe.As(ref source), offset); + return Vector256.ConvertToSingle(Vector256_.Widen(packed)); + } + + return Vector256.ConvertToSingle(Vector256.LoadUnsafe(ref Unsafe.As(ref source), offset)); + } + + /// + /// Numerically converts 16 signed coefficients without reading beyond those coefficients. + /// + /// The short or int storage selected at the owning entry point. + /// The start of a coefficient plane. + /// The coefficient offset, in storage elements. + /// Float values in their original frequency order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 Load512(ref TCoefficient source, uint offset) + where TCoefficient : unmanaged, IBinaryInteger + { + // The closed short/int worker makes this storage-width choice constant. It adds no per-frequency type dispatch. + // Short storage needs half as many bytes as the float result. Load only that half, then sign-extend its low lanes. + if (Unsafe.SizeOf() == sizeof(short)) + { + Vector256 packed = Vector256.LoadUnsafe(ref Unsafe.As(ref source), offset); + return Vector512.ConvertToSingle(Vector512.WidenLower(packed.ToVector512())); + } + + return Vector512.ConvertToSingle(Vector512.LoadUnsafe(ref Unsafe.As(ref source), offset)); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.QuantizationBiasOperator.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.QuantizationBiasOperator.cs new file mode 100644 index 0000000000..208379722c --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.QuantizationBiasOperator.cs @@ -0,0 +1,89 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; + +internal sealed partial class JxlQuantizer +{ + /// + /// Restores quantized coefficient magnitudes before matrix scaling. + /// + /// + /// Inputs are numeric integer-to-float conversions, not reinterpreted integer bits. + /// Zero remains zero. Values of magnitude one use the channel bias; larger values use q - numerator / q. + /// This uses the equation documented in libjxl's quantizer-inl.h, with float division instead of its approximate reciprocal. + /// + public readonly struct QuantizationBiasOperator : TensorPrimitives_.IUnaryOperator + { + private readonly float unitBias; + private readonly float numerator; + + /// + /// Initializes a new instance of the struct. + /// + /// The XYB channel index. + /// The three channel biases followed by the shared reciprocal numerator. + public QuantizationBiasOperator(int channelIndex, ReadOnlySpan biases) + { + this.unitBias = biases[channelIndex]; + this.numerator = biases[3]; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float Invoke(float value) + => value == 0F ? 0F : MathF.Abs(value) <= 1F ? value * this.unitBias : value - (this.numerator / value); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector128 Invoke(Vector128 value) + { + // Every lane is one signed coefficient. The low-magnitude mask selects exactly 0 and +/-1. + // Division can produce infinity in zero lanes; selection replaces those lanes with exact zero. + // A negative encoded bias must not turn a zero coefficient into negative zero. + Vector128 unit = Vector128.ConditionalSelect( + Vector128.Equals(value, Vector128.Zero), + Vector128.Zero, + value * Vector128.Create(this.unitBias)); + + Vector128 adjusted = value - (Vector128.Create(this.numerator) / value); + return Vector128.ConditionalSelect(Vector128.LessThanOrEqual(Vector128.Abs(value), Vector128.One), unit, adjusted); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector256 Invoke(Vector256 value) + { + // Every lane is one signed coefficient. The low-magnitude mask selects exactly 0 and +/-1. + // Division can produce infinity in zero lanes; selection replaces those lanes with exact zero. + // A negative encoded bias must not turn a zero coefficient into negative zero. + Vector256 unit = Vector256.ConditionalSelect( + Vector256.Equals(value, Vector256.Zero), + Vector256.Zero, + value * Vector256.Create(this.unitBias)); + + Vector256 adjusted = value - (Vector256.Create(this.numerator) / value); + return Vector256.ConditionalSelect(Vector256.LessThanOrEqual(Vector256.Abs(value), Vector256.One), unit, adjusted); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector512 Invoke(Vector512 value) + { + // Every lane is one signed coefficient. The low-magnitude mask selects exactly 0 and +/-1. + // Division can produce infinity in zero lanes; selection replaces those lanes with exact zero. + // A negative encoded bias must not turn a zero coefficient into negative zero. + Vector512 unit = Vector512.ConditionalSelect( + Vector512.Equals(value, Vector512.Zero), + Vector512.Zero, + value * Vector512.Create(this.unitBias)); + + Vector512 adjusted = value - (Vector512.Create(this.numerator) / value); + return Vector512.ConditionalSelect(Vector512.LessThanOrEqual(Vector512.Abs(value), Vector512.One), unit, adjusted); + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs index 184120f022..a7f9d1c2fe 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs @@ -22,7 +22,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; /// useful for variable-length coding where the amount /// of bits depend on how large the number is. /// -internal sealed class JxlQuantizer +internal sealed partial class JxlQuantizer { /// /// Denominator for the global_scale value. diff --git a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerSimd.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerSimd.cs deleted file mode 100644 index 6c80d29151..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerSimd.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; - -/// -/// SIMD utilities used by the quantizer. -/// -internal static class JxlQuantizerSimd -{ - public static Vector AdjustQuantBias(int c, Vector quantI, ReadOnlySpan biases) - { - Vector quant = quantI.As(); - Vector constSign = Vector.Create(int.MinValue).As(); - Vector sign = quant & constSign; - Vector absoluteQuant = Vector.AndNot(constSign, quant); - - Vector is01 = Vector.LessThan(absoluteQuant, Vector.Create(1.125f)); - Vector not0 = Vector.GreaterThan(absoluteQuant, Vector.One); - - Vector oneBias = Vector.ConditionalSelect(not0, Vector.Create(biases[c]) ^ sign, Vector.Zero); - Vector bias = -(Vector.Create(biases[3]) * quant.ReciprocalEstimate()) + quant; - - return Vector.ConditionalSelect(is01, oneBias, bias); - } - - // a variant with 64 element spans - public static void AdjustQuantBias64(int c, Span quantI, ReadOnlySpan biases, Span output) - { - if (Vector.Count > 64 || Vector.Count > 64) - { - // TODO: in the future we may want to support > 64 items if users start encountering the problem - // Problem is, this function works on strictly 64 items, and if the Vector has > 64 items, - // then that's too much to process. - throw new NotImplementedException("This CPU has more than 64 lanes for float or int types"); - } - - ref int quantIRef = ref MemoryMarshal.GetReference(quantI); - ref float outputRef = ref MemoryMarshal.GetReference(output); - - // This loop should be SIMD-aligned - for (int i = 0; i < 64; i += Vector.Count) - { - Vector result = AdjustQuantBias( - c: c, - quantI: Vector.LoadUnsafe(ref Unsafe.Add(ref quantIRef, i)), - biases); - - result.StoreUnsafe(ref Unsafe.Add(ref outputRef, i)); - } - } -} diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/QuantizationTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/QuantizationTests.cs new file mode 100644 index 0000000000..e36c8df0f1 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/QuantizationTests.cs @@ -0,0 +1,100 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; +using SixLabors.ImageSharp.Tests.TestUtilities; + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; + +public class QuantizationTests +{ + /// + /// Checks numeric conversion, zero/unit bias, matrix scaling and correlation for both coefficient storage widths. + /// + [Fact] + public void Dequantize_RestoresSignedCoefficientsAcrossStorageWidths() + { + static void RunTest() + { + ReadOnlySpan pattern = [0, 1, -1, 2, -2, 4, -4]; + ReadOnlySpan biases = [0.5F, 0.75F, 1.25F, 0.5F]; + + // These are the independently evaluated piecewise equation and X/B correlation for the fixed input above. + // All values and scales are binary fractions, so every path must match exactly without a tolerance. + ReadOnlySpan expectedY = [0F, 0.75F, -0.75F, 1.75F, -1.75F, 3.875F, -3.875F]; + ReadOnlySpan expectedX = [0F, 0.875F, -0.875F, 2.625F, -2.625F, 5.8125F, -5.8125F]; + ReadOnlySpan expectedB = [0F, 1.0625F, -1.0625F, 1.3125F, -1.3125F, 2.90625F, -2.90625F]; + ReadOnlySpan lengths = [0, 1, 3, 4, 7, 8, 15, 16, 17, 31, 32, 33, 65]; + + foreach (int length in lengths) + { + int[] integers = new int[length + 1]; + short[] shorts = new short[length + 1]; + float[] matrix = new float[(3 * length) + 1]; + float[] reconstructed = new float[length + 2]; + float[] decoded32 = new float[(3 * length) + 2]; + float[] decoded16 = new float[(3 * length) + 2]; + reconstructed.AsSpan().Fill(float.NaN); + decoded32.AsSpan().Fill(float.NaN); + decoded16.AsSpan().Fill(float.NaN); + + for (int i = 0; i < length; i++) + { + integers[i] = pattern[i % pattern.Length]; + shorts[i] = (short)integers[i]; + matrix[i] = 1F; + matrix[length + i] = 2F; + matrix[(2 * length) + i] = 4F; + } + + // Source padding must not participate. Destination guards detect stores beyond each logical output. + integers[length] = int.MaxValue; + shorts[length] = short.MinValue; + matrix[3 * length] = float.NaN; + ReadOnlySpan input32 = integers.AsSpan(0, length); + ReadOnlySpan input16 = shorts.AsSpan(0, length); + JxlQuantizer.Dequantize(input32, matrix, 1F, 1, biases, reconstructed.AsSpan(1, length)); + JxlQuantizer.Dequantize(input32, input32, input32, matrix, 1F, 0.5F, 0.25F, 0.5F, -0.25F, biases, decoded32.AsSpan(1)); + JxlQuantizer.Dequantize(input16, input16, input16, matrix, 1F, 0.5F, 0.25F, 0.5F, -0.25F, biases, decoded16.AsSpan(1)); + + for (int i = 0; i < length; i++) + { + int p = i % pattern.Length; + Assert.Equal(expectedY[p], reconstructed[1 + i]); + Assert.Equal(expectedX[p], decoded32[1 + i]); + Assert.Equal(expectedY[p], decoded32[1 + length + i]); + Assert.Equal(expectedB[p], decoded32[1 + (2 * length) + i]); + } + + Assert.Equal(decoded32, decoded16); + Assert.True(float.IsNaN(reconstructed[0])); + Assert.True(float.IsNaN(reconstructed[^1])); + Assert.True(float.IsNaN(decoded32[0])); + Assert.True(float.IsNaN(decoded32[^1])); + Assert.Equal(int.MaxValue, integers[length]); + Assert.Equal(short.MinValue, shorts[length]); + } + + // Encoded channel biases can be negative or negative zero. A zero integer coefficient must still reconstruct as positive zero. + ReadOnlySpan signedBiases = [-0.5F, -0F, -1F, 0.5F]; + int[] zeros = new int[33]; + float[] unitMatrix = new float[33]; + float[] zeroOutput = new float[33]; + unitMatrix.AsSpan().Fill(1F); + + for (int channel = 0; channel < 3; channel++) + { + JxlQuantizer.Dequantize(zeros, unitMatrix, 1F, channel, signedBiases, zeroOutput); + + foreach (float value in zeroOutput) + { + Assert.Equal(0U, BitConverter.SingleToUInt32Bits(value)); + } + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } +} From 2ef1886c35a6d7a3fa50c5e0ce087e82b348579f Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Oct 2026 01:10:45 +1000 Subject: [PATCH 293/328] Reuse shared SIMD clearing for JPEG XL invisible samples --- src/ImageSharp/Formats/EncodingUtilities.cs | 35 +++++ .../Jxl/Processing/Encoder/JxlFrameEncoder.cs | 148 ++++++++++-------- .../Jxl/Processing/ImageOperationsTests.cs | 133 ++++++++++++++++ 3 files changed, 253 insertions(+), 63 deletions(-) diff --git a/src/ImageSharp/Formats/EncodingUtilities.cs b/src/ImageSharp/Formats/EncodingUtilities.cs index 28295199af..0eb74ca7d4 100644 --- a/src/ImageSharp/Formats/EncodingUtilities.cs +++ b/src/ImageSharp/Formats/EncodingUtilities.cs @@ -6,6 +6,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -166,4 +167,38 @@ public static void ReplaceTransparentPixels(Span source) } } } + + /// + /// Replaces component values with positive zero where the corresponding alpha is zero. + /// + /// The borrowed alpha values, with one value per component. + /// The component values to update, with the same length as alpha. + public static void ReplaceTransparentPixels(ReadOnlySpan alpha, Span components) + => TensorPrimitives_.Invoke(alpha, components, components, default(ClearTransparentComponentOperator)); + + /// + /// Clears only components selected by exact zero alpha, without multiplying or normalizing their values. + /// + private readonly struct ClearTransparentComponentOperator : TensorPrimitives_.IBinaryOperator + { + // Each lane pairs one alpha value with one component. Selection clears even NaN components and preserves all unselected bits. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float Invoke(float first, float second) => first == 0F ? 0F : second; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector128 Invoke(Vector128 first, Vector128 second) + => Vector128.ConditionalSelect(Vector128.Equals(first, Vector128.Zero), Vector128.Zero, second); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector256 Invoke(Vector256 first, Vector256 second) + => Vector256.ConditionalSelect(Vector256.Equals(first, Vector256.Zero), Vector256.Zero, second); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector512 Invoke(Vector512 first, Vector512 second) + => Vector512.ConditionalSelect(Vector512.Equals(first, Vector512.Zero), Vector512.Zero, second); + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFrameEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFrameEncoder.cs index 8a1c6440ee..5ecae95f75 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFrameEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFrameEncoder.cs @@ -40,93 +40,115 @@ public static uint GetGroupSizeShift(int width, int height, JxlCompressParameter #pragma warning restore format } - public static unsafe void SimplifyInvisible(JxlImage3 image, JxlPlane alpha, bool lossless) + /// + /// Replaces invisible samples in encoder-owned working planes with zeros or the native causal neighbor estimate. + /// + /// The owned working color planes to update. + /// The borrowed unassociated alpha plane with matching logical dimensions. + /// Whether to clear invisible samples instead of estimating them from neighbors. + /// + /// The encoder caller decides whether invisible samples may change. This method does not select a transparency policy. + /// + public static void SimplifyInvisible(JxlImage3 image, JxlPlane alpha, bool lossless) { + int width = image.XSize; + int height = image.YSize; + ReadOnlySpan alphaSamples = alpha.GetRow(0); + int alphaStride = alpha.PixelsPerRow; + for (int c = 0; c < 3; c++) { - for (int y = 0; y < image.YSize; y++) + JxlPlane plane = image.Plane(c); + Span samples = plane.GetRow(0); + int stride = plane.PixelsPerRow; + + if (lossless) + { + // Independent components share the existing SIMD traversal. Exclude physical padding from both input and output. + for (int y = 0; y < height; y++) + { + EncodingUtilities.ReplaceTransparentPixels(alphaSamples.Slice(y * alphaStride, width), samples.Slice(y * stride, width)); + } + + continue; + } + + // Native traversal uses the updated left and previous-row samples, but the original right and next-row samples. + // Preserve channel, row, and column order. Adjacent transparent pixels cannot be evaluated independently with SIMD. + for (int y = 0; y < height; y++) { - Span row = image.PlaneRow(c, y); - Span prow = y > 0 ? image.PlaneRow(c, y - 1) : []; - Span nrow = y + 1 < image.YSize ? image.PlaneRow(c, y + 1) : []; - Span a = alpha.GetRow(y); - Span pa = y > 0 ? alpha.GetRow(y - 1) : []; - Span na = y + 1 < image.YSize ? alpha.GetRow(y + 1) : []; - - // We create a pointer to 'a', to remove bounds checks in a hot loop. - // We also use a debug guard, so the range is valid. - DebugGuard.MustBeGreaterThanOrEqualTo(a.Length, image.XSize, nameof(a)); - fixed (float* unsafeA = a) + Span row = samples.Slice(y * stride, width); + ReadOnlySpan previousRow = y > 0 ? samples.Slice((y - 1) * stride, width) : default; + ReadOnlySpan nextRow = y + 1 < height ? samples.Slice((y + 1) * stride, width) : default; + ReadOnlySpan alphaRow = alphaSamples.Slice(y * alphaStride, width); + ReadOnlySpan previousAlpha = y > 0 ? alphaSamples.Slice((y - 1) * alphaStride, width) : default; + ReadOnlySpan nextAlpha = y + 1 < height ? alphaSamples.Slice((y + 1) * alphaStride, width) : default; + + for (int x = 0; x < width; ++x) { - for (int x = 0; x < image.XSize; ++x) + if (alphaRow[x] == 0F) { - if (unsafeA[x] == 0) - { - if (lossless) - { - row[x] = 0; - continue; - } + float weightSum = 0F; + row[x] = 0; - float d = 0.0f; - row[x] = 0; + // Left and upper-right samples extend the existing estimate with weight one, even when invisible. + // Visible neighbors receive weight two. Keep the native addition order to preserve its float rounding. + if (x > 0) + { + row[x] += row[x - 1]; + weightSum++; - if (x > 0) + if (alphaRow[x - 1] > 0F) { row[x] += row[x - 1]; - d++; - - if (unsafeA[x - 1] > 0.0f) - { - row[x] += row[x - 1]; - d++; - } + weightSum++; } + } - if (x < image.XSize - 1) + if (x < width - 1) + { + if (y > 0) { - if (y > 0) - { - row[x] += prow[x + 1]; - d++; - } - - if (unsafeA[x + 1] > 0.0f) - { - row[x] += 2.0f * row[x + 1]; - d += 2.0f; - } - - if (y > 0 && pa[x + 1] > 0.0f) - { - row[x] += 2.0f * prow[x + 1]; - d += 2.0f; - } - - if (y + 1 < image.YSize && na[x + 1] > 0.0f) - { - row[x] += 2.0f * nrow[x + 1]; - d += 2.0f; - } + row[x] += previousRow[x + 1]; + weightSum++; } - if (y > 0 && pa[x] > 0.0f) + if (alphaRow[x + 1] > 0F) { - row[x] += 2.0f * prow[x]; - d += 2.0f; + row[x] += 2F * row[x + 1]; + weightSum += 2F; } - if (y + 1 < image.YSize && na[x] > 0.0f) + if (y > 0 && previousAlpha[x + 1] > 0F) { - row[x] += 2.0f * nrow[x]; - d += 2.0f; + row[x] += 2F * previousRow[x + 1]; + weightSum += 2F; } - if (d > 1.0f) + if (y + 1 < height && nextAlpha[x + 1] > 0F) { - row[x] /= d; + row[x] += 2F * nextRow[x + 1]; + weightSum += 2F; } } + + if (y > 0 && previousAlpha[x] > 0F) + { + row[x] += 2F * previousRow[x]; + weightSum += 2F; + } + + if (y + 1 < height && nextAlpha[x] > 0F) + { + row[x] += 2F * nextRow[x]; + weightSum += 2F; + } + + // No contributing neighbor leaves zero. A single weight-one contribution needs no division. + if (weightSum > 1F) + { + row[x] /= weightSum; + } } } } diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/ImageOperationsTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/ImageOperationsTests.cs index 8cd2afcf53..20d0ff43aa 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/ImageOperationsTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/ImageOperationsTests.cs @@ -4,6 +4,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Group; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using SixLabors.ImageSharp.Memory; @@ -14,6 +15,138 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; public class ImageOperationsTests { + /// + /// Checks exact zero-alpha clearing, independent physical strides, padding, and tails through the shared planar operation. + /// + [Fact] + public void SimplifyInvisible_LosslessClearsOnlyZeroAlpha() + { + static void RunTest() + { + ReadOnlySpan widths = [0, 1, 3, 4, 7, 8, 15, 16, 17, 31, 32, 33]; + ReadOnlySpan alphaValues = [0F, -0F, 1F, -1F, float.NaN, 0.5F]; + + foreach (int width in widths) + { + using JxlImage3 image = new(TestEnvironment.Configuration, width, 2); + using JxlPlane alpha = new(TestEnvironment.Configuration, width + 17, 2, 2); + Assert.True(alpha.ShrinkTo(width, 2)); + Span alphaAllocation = alpha.GetRowMinus(0, 2); + alphaAllocation.Fill(-77F); + Span alphaSamples = alpha.GetRow(0); + + // Shrinking retains the larger alpha stride. Neither its padding nor the color padding belongs to the operation. + for (int y = 0; y < 2; y++) + { + for (int x = 0; x < width; x++) + { + alphaSamples[(y * alpha.PixelsPerRow) + x] = alphaValues[(x + y) % alphaValues.Length]; + } + } + + for (int c = 0; c < 3; c++) + { + Span samples = image.PlaneRow(c, 0); + samples.Fill(-99F); + + for (int y = 0; y < 2; y++) + { + for (int x = 0; x < width; x++) + { + samples[(y * image.PixelsPerRow) + x] = x % 3 == 0 ? -0F : (c + 1) * (x - 7F); + } + } + } + + JxlFrameEncoder.SimplifyInvisible(image, alpha, true); + + for (int c = 0; c < 3; c++) + { + ReadOnlySpan samples = image.PlaneRow(c, 0); + for (int y = 0; y < 2; y++) + { + for (int x = 0; x < image.PixelsPerRow; x++) + { + float expected = -99F; + if (x < width) + { + float opacity = alphaValues[(x + y) % alphaValues.Length]; + float original = x % 3 == 0 ? -0F : (c + 1) * (x - 7F); + expected = opacity == 0F ? 0F : original; + } + + // Bit equality distinguishes the required positive replacement zero from retained negative zero. + Assert.Equal(BitConverter.SingleToUInt32Bits(expected), BitConverter.SingleToUInt32Bits(samples[(y * image.PixelsPerRow) + x])); + } + } + } + + for (int y = 0; y < 2; y++) + { + for (int x = 0; x < alpha.PixelsPerRow; x++) + { + float expected = x < width ? alphaValues[(x + y) % alphaValues.Length] : -77F; + Assert.Equal(BitConverter.SingleToUInt32Bits(expected), BitConverter.SingleToUInt32Bits(alphaSamples[(y * alpha.PixelsPerRow) + x])); + } + } + + Assert.Equal(-77F, alphaAllocation[0]); + Assert.Equal(-77F, alphaAllocation[1]); + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } + + /// + /// Checks the native sequential neighbor estimate at all four borders and across adjacent invisible samples. + /// + [Fact] + public void SimplifyInvisible_LossyRetainsCausalNeighborOrder() + { + ReadOnlySpan original = [8F, 100F, 100F, 100F, 100F, 24F]; + ReadOnlySpan opacity = [1F, 0F, 0F, 0F, 0F, 1F]; + ReadOnlySpan expected = [8F, 16F, 64F / 3F, 32F / 3F, 20F, 24F]; + using JxlImage3 image = new(TestEnvironment.Configuration, 3, 2); + using JxlPlane alpha = new(TestEnvironment.Configuration, 3, 2); + Span alphaSamples = alpha.GetRow(0); + + for (int y = 0; y < 2; y++) + { + opacity.Slice(y * 3, 3).CopyTo(alphaSamples.Slice(y * alpha.PixelsPerRow, 3)); + } + + for (int c = 0; c < 3; c++) + { + Span samples = image.PlaneRow(c, 0); + for (int y = 0; y < 2; y++) + { + for (int x = 0; x < 3; x++) + { + samples[(y * image.PixelsPerRow) + x] = original[(y * 3) + x] * (1 << c); + } + } + } + + JxlFrameEncoder.SimplifyInvisible(image, alpha, false); + + // Native weights produce 16=(8+8+48)/4, then 64/3 and 32/3. Their rounded sum is 32; (32+48)/4 gives 20. + // Scaling by powers of two checks independent color planes without changing the float addition and division results. + for (int c = 0; c < 3; c++) + { + ReadOnlySpan samples = image.PlaneRow(c, 0); + for (int y = 0; y < 2; y++) + { + for (int x = 0; x < 3; x++) + { + Assert.Equal(expected[(y * 3) + x] * (1 << c), samples[(y * image.PixelsPerRow) + x]); + } + } + } + } + /// /// Checks that range scans exclude a prefix, padding and retained rows at every SIMD width and in the scalar path. /// From 62dad4b658cae877dafa26981642267392f751a0 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 29 Oct 2026 19:27:39 +0400 Subject: [PATCH 294/328] Add coefficient order tests --- .../Jxl/Processing/ButteraugliTests.cs | 1 - .../Jxl/Processing/CoefficientOrderTests.cs | 112 +++++++++++++++++- 2 files changed, 110 insertions(+), 3 deletions(-) diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs index 60c27d4f55..1b7643918a 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using Microsoft.Diagnostics.Runtime.Interop; using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing; using SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/CoefficientOrderTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/CoefficientOrderTests.cs index 3e2923f40f..46ba83994e 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/CoefficientOrderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/CoefficientOrderTests.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; +using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Jxl.Processing; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; @@ -51,7 +53,7 @@ private enum Permutation : byte Random } - private static void RoundtripPermutation(Span permutation, Span output, int len, out int size) + private static void RoundtripPermutation(Span permutation, Span output, int len, out int size) { using MemoryStream ms = new(); JxlBitWriter writer = new(ms); @@ -60,8 +62,114 @@ private static void RoundtripPermutation(Span permutation, Span outpu writer.ZeroPadToByte(); ms.Position = 0; + JxlBitReader reader = new(ms); + Assert.True(JxlCoefficientOrder.DecodePermutation(TestEnvironment.Configuration, 0, len, output, reader)); + + size = (int)ms.Length; + } + + /// + /// Ensures that permutations can be decoded exactly as they + /// were encoded, losslessly. + /// + /// The kind of permutation. + /// Number of permutations to round-trip. + private static void TestPermutation(Permutation permutationKind, int length) + { + // The permutations can be very large, so use + // configuration.MemoryAllocator.Allocate. A stackalloc + // can lead to a stack overflow exception. + using IMemoryOwner permOwner = TestEnvironment.Configuration.MemoryAllocator.Allocate(length); + Span perm = permOwner.Memory.Span; + JxlSimdUtils.Iota(perm, 0u); + + Rng rng = new(0); + + switch (permutationKind) + { + case Permutation.FewSwaps: + for (int i = 0; i < Swaps; i++) + { + int a = (int)rng.UniformU(0, (ulong)(length - 1)); + int b = (int)rng.UniformU(0, (ulong)(length - 1)); + RuntimeUtility.Swap(ref perm[a], ref perm[b]); + } + + break; + + case Permutation.FewSlides: + for (int i = 0; i < Swaps; i++) + { + int a = (int)rng.UniformU(0, (ulong)(length - 1)); + int b = (int)rng.UniformU(0, (ulong)(length - 1)); + int from = Math.Min(a, b); + int to = Math.Max(a, b); + uint start = perm[from]; + + for (int j = from; j < to; j++) + { + perm[j] = perm[j + 1]; + } + + perm[to] = start; + } + + break; + + case Permutation.Random: + rng.Shuffle(perm); + break; + } + + using IMemoryOwner outOwner = TestEnvironment.Configuration.MemoryAllocator.Allocate(length); + Span output = outOwner.Memory.Span; - //TODO + RoundtripPermutation(perm, output, length, out int size); + + // We want to make sure the permutations are lossless. + for (int idx = 0; idx < length; idx++) + { + Assert.Equal(perm[idx], output[idx]); + } + } + + /// + /// Tests a small number of permutations. + /// + [Fact] + public void TestSmall() + { + const int permutationCount = 256; + TestPermutation(Permutation.Identity, permutationCount); + TestPermutation(Permutation.FewSlides, permutationCount); + TestPermutation(Permutation.FewSwaps, permutationCount); + TestPermutation(Permutation.Random, permutationCount); + } + + /// + /// Tests a medium number of permutations. + /// + [Fact] + public void TestMedium() + { + const int permutationCount = 1 << 12; + TestPermutation(Permutation.Identity, permutationCount); + TestPermutation(Permutation.FewSlides, permutationCount); + TestPermutation(Permutation.FewSwaps, permutationCount); + TestPermutation(Permutation.Random, permutationCount); + } + + /// + /// Tests a large number of permutations. + /// + [Fact] + public void TestLarge() + { + const int permutationCount = 1 << 16; + TestPermutation(Permutation.Identity, permutationCount); + TestPermutation(Permutation.FewSlides, permutationCount); + TestPermutation(Permutation.FewSwaps, permutationCount); + TestPermutation(Permutation.Random, permutationCount); } } From 57fde9386a698817823ec85650263ab929976fa5 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Oct 2026 01:36:50 +1000 Subject: [PATCH 295/328] Correct JPEG XL transform and quantization kernels --- .../Dct/JxlDct.Kernels.Generated.cs | 5874 +++++++++++++++++ .../Dct/JxlDct.Kernels.Generated.tt | 424 ++ .../Formats/Jxl/Processing/Dct/JxlDct.cs | 459 +- .../Jxl/Processing/Dct/JxlDctOutput.cs | 71 - .../Jxl/Processing/Dct/JxlDctSource.cs | 76 - .../Decoder/JxlTransformsDecoder.cs | 160 +- .../JxlChromaFromLumaEncoder.cs | 3 +- .../Heuristics/JxlAcStrategyHeuristics.cs | 5 +- .../Encoder/Heuristics/JxlCflHeuristics.cs | 3 +- .../Jxl/Processing/Encoder/JxlGroupEncoder.cs | 110 +- .../Encoder/JxlTransformsEncoder.cs | 258 +- .../Quantization/JxlDequantMatrices.cs | 60 +- .../Quantization/JxlQuantWeights.cs | 297 +- .../Quantization/JxlQuantizer.Quantization.cs | 173 + src/ImageSharp/ImageSharp.csproj | 9 + .../Formats/Jxl/Processing/DctTests.cs | 348 + .../Jxl/Processing/QuantizationTests.cs | 114 + 17 files changed, 7625 insertions(+), 819 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDct.Kernels.Generated.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDct.Kernels.Generated.tt delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctOutput.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctSource.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.Quantization.cs create mode 100644 tests/ImageSharp.Tests/Formats/Jxl/Processing/DctTests.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDct.Kernels.Generated.cs b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDct.Kernels.Generated.cs new file mode 100644 index 0000000000..e54bd0aeb8 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDct.Kernels.Generated.cs @@ -0,0 +1,5874 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; + +internal static partial class JxlDct +{ + /// + /// Computes independent forward transforms using two nonoverlapping borrowed vector banks. + /// + /// One vector per transform position. Its length is a power of two from one through 256. + /// At least values.Length vectors. Previous contents are not read. + private static void Forward(Span values, Span scratch) + { + // Small transforms stay in registers. The one-point transform is an identity and must not copy any trailing samples. + switch (values.Length) + { + case 1: + return; + case 2: + Forward2(values); + return; + case 4: + Forward4(values); + return; + case 8: + Forward8(values); + return; + case 16: + Forward16(values); + return; + case 32: + Forward32(values); + return; + } + + int half = values.Length / 2; + ReadOnlySpan weights = JxlDctScales.GetMultipliers(values.Length); + + // Consume both original halves before reusing their bank as recursive scratch. Neither recursive call can then destroy unread input. + for (int i = 0; i < half; i++) + { + float first = values[i]; + float last = values[values.Length - i - 1]; + scratch[i] = first + last; + scratch[half + i] = (first - last) * weights[i]; + } + + Forward(scratch[..half], values); + Forward(scratch.Slice(half, half), values); + scratch[half] = float.MultiplyAddEstimate(scratch[half], JxlDctScales.Sqrt2, scratch[half + 1]); + + for (int i = 1; i + 1 < half; i++) + { + scratch[half + i] += scratch[half + i + 1]; + } + + for (int i = 0; i < half; i++) + { + values[2 * i] = scratch[i]; + values[(2 * i) + 1] = scratch[half + i]; + } + } + + /// + /// Computes independent 2-point forward transforms without intermediate memory passes. + /// + /// Exactly 2 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Forward2(Span values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + float input0 = values[0]; + float input1 = values[1]; + + float v0 = input0 + input1; + float v1 = input0 - input1; + + values[0] = v0; + values[1] = v1; + } + + /// + /// Computes independent 4-point forward transforms without intermediate memory passes. + /// + /// Exactly 4 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Forward4(Span values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + float input0 = values[0]; + float input1 = values[1]; + float input2 = values[2]; + float input3 = values[3]; + + float v0 = input0 + input3; + float v1 = (input0 - input3) * 0.541196100146197f; + float v2 = input1 + input2; + float v3 = (input1 - input2) * 1.3065629648763764f; + float v4 = v0 + v2; + float v5 = v0 - v2; + float v6 = v1 + v3; + float v7 = v1 - v3; + float v8 = float.MultiplyAddEstimate(v6, JxlDctScales.Sqrt2, v7); + + values[0] = v4; + values[1] = v8; + values[2] = v5; + values[3] = v7; + } + + /// + /// Computes independent 8-point forward transforms without intermediate memory passes. + /// + /// Exactly 8 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Forward8(Span values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + float input0 = values[0]; + float input1 = values[1]; + float input2 = values[2]; + float input3 = values[3]; + float input4 = values[4]; + float input5 = values[5]; + float input6 = values[6]; + float input7 = values[7]; + + float v0 = input0 + input7; + float v1 = (input0 - input7) * 0.5097955791041592f; + float v2 = input1 + input6; + float v3 = (input1 - input6) * 0.6013448869350453f; + float v4 = input2 + input5; + float v5 = (input2 - input5) * 0.8999762231364156f; + float v6 = input3 + input4; + float v7 = (input3 - input4) * 2.5629154477415055f; + float v8 = v0 + v6; + float v9 = (v0 - v6) * 0.541196100146197f; + float v10 = v2 + v4; + float v11 = (v2 - v4) * 1.3065629648763764f; + float v12 = v8 + v10; + float v13 = v8 - v10; + float v14 = v9 + v11; + float v15 = v9 - v11; + float v16 = float.MultiplyAddEstimate(v14, JxlDctScales.Sqrt2, v15); + float v17 = v1 + v7; + float v18 = (v1 - v7) * 0.541196100146197f; + float v19 = v3 + v5; + float v20 = (v3 - v5) * 1.3065629648763764f; + float v21 = v17 + v19; + float v22 = v17 - v19; + float v23 = v18 + v20; + float v24 = v18 - v20; + float v25 = float.MultiplyAddEstimate(v23, JxlDctScales.Sqrt2, v24); + float v26 = float.MultiplyAddEstimate(v21, JxlDctScales.Sqrt2, v25); + float v27 = v25 + v22; + float v28 = v22 + v24; + + values[0] = v12; + values[1] = v26; + values[2] = v16; + values[3] = v27; + values[4] = v13; + values[5] = v28; + values[6] = v15; + values[7] = v24; + } + + /// + /// Computes independent 16-point forward transforms without intermediate memory passes. + /// + /// Exactly 16 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Forward16(Span values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + float input0 = values[0]; + float input1 = values[1]; + float input2 = values[2]; + float input3 = values[3]; + float input4 = values[4]; + float input5 = values[5]; + float input6 = values[6]; + float input7 = values[7]; + float input8 = values[8]; + float input9 = values[9]; + float input10 = values[10]; + float input11 = values[11]; + float input12 = values[12]; + float input13 = values[13]; + float input14 = values[14]; + float input15 = values[15]; + + float v0 = input0 + input15; + float v1 = (input0 - input15) * 0.5024192861881557f; + float v2 = input1 + input14; + float v3 = (input1 - input14) * 0.5224986149396889f; + float v4 = input2 + input13; + float v5 = (input2 - input13) * 0.5669440348163577f; + float v6 = input3 + input12; + float v7 = (input3 - input12) * 0.6468217833599901f; + float v8 = input4 + input11; + float v9 = (input4 - input11) * 0.7881546234512502f; + float v10 = input5 + input10; + float v11 = (input5 - input10) * 1.060677685990347f; + float v12 = input6 + input9; + float v13 = (input6 - input9) * 1.7224470982383342f; + float v14 = input7 + input8; + float v15 = (input7 - input8) * 5.101148618689155f; + float v16 = v0 + v14; + float v17 = (v0 - v14) * 0.5097955791041592f; + float v18 = v2 + v12; + float v19 = (v2 - v12) * 0.6013448869350453f; + float v20 = v4 + v10; + float v21 = (v4 - v10) * 0.8999762231364156f; + float v22 = v6 + v8; + float v23 = (v6 - v8) * 2.5629154477415055f; + float v24 = v16 + v22; + float v25 = (v16 - v22) * 0.541196100146197f; + float v26 = v18 + v20; + float v27 = (v18 - v20) * 1.3065629648763764f; + float v28 = v24 + v26; + float v29 = v24 - v26; + float v30 = v25 + v27; + float v31 = v25 - v27; + float v32 = float.MultiplyAddEstimate(v30, JxlDctScales.Sqrt2, v31); + float v33 = v17 + v23; + float v34 = (v17 - v23) * 0.541196100146197f; + float v35 = v19 + v21; + float v36 = (v19 - v21) * 1.3065629648763764f; + float v37 = v33 + v35; + float v38 = v33 - v35; + float v39 = v34 + v36; + float v40 = v34 - v36; + float v41 = float.MultiplyAddEstimate(v39, JxlDctScales.Sqrt2, v40); + float v42 = float.MultiplyAddEstimate(v37, JxlDctScales.Sqrt2, v41); + float v43 = v41 + v38; + float v44 = v38 + v40; + float v45 = v1 + v15; + float v46 = (v1 - v15) * 0.5097955791041592f; + float v47 = v3 + v13; + float v48 = (v3 - v13) * 0.6013448869350453f; + float v49 = v5 + v11; + float v50 = (v5 - v11) * 0.8999762231364156f; + float v51 = v7 + v9; + float v52 = (v7 - v9) * 2.5629154477415055f; + float v53 = v45 + v51; + float v54 = (v45 - v51) * 0.541196100146197f; + float v55 = v47 + v49; + float v56 = (v47 - v49) * 1.3065629648763764f; + float v57 = v53 + v55; + float v58 = v53 - v55; + float v59 = v54 + v56; + float v60 = v54 - v56; + float v61 = float.MultiplyAddEstimate(v59, JxlDctScales.Sqrt2, v60); + float v62 = v46 + v52; + float v63 = (v46 - v52) * 0.541196100146197f; + float v64 = v48 + v50; + float v65 = (v48 - v50) * 1.3065629648763764f; + float v66 = v62 + v64; + float v67 = v62 - v64; + float v68 = v63 + v65; + float v69 = v63 - v65; + float v70 = float.MultiplyAddEstimate(v68, JxlDctScales.Sqrt2, v69); + float v71 = float.MultiplyAddEstimate(v66, JxlDctScales.Sqrt2, v70); + float v72 = v70 + v67; + float v73 = v67 + v69; + float v74 = float.MultiplyAddEstimate(v57, JxlDctScales.Sqrt2, v71); + float v75 = v71 + v61; + float v76 = v61 + v72; + float v77 = v72 + v58; + float v78 = v58 + v73; + float v79 = v73 + v60; + float v80 = v60 + v69; + + values[0] = v28; + values[1] = v74; + values[2] = v42; + values[3] = v75; + values[4] = v32; + values[5] = v76; + values[6] = v43; + values[7] = v77; + values[8] = v29; + values[9] = v78; + values[10] = v44; + values[11] = v79; + values[12] = v31; + values[13] = v80; + values[14] = v40; + values[15] = v69; + } + + /// + /// Computes independent 32-point forward transforms without intermediate memory passes. + /// + /// Exactly 32 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Forward32(Span values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + float input0 = values[0]; + float input1 = values[1]; + float input2 = values[2]; + float input3 = values[3]; + float input4 = values[4]; + float input5 = values[5]; + float input6 = values[6]; + float input7 = values[7]; + float input8 = values[8]; + float input9 = values[9]; + float input10 = values[10]; + float input11 = values[11]; + float input12 = values[12]; + float input13 = values[13]; + float input14 = values[14]; + float input15 = values[15]; + float input16 = values[16]; + float input17 = values[17]; + float input18 = values[18]; + float input19 = values[19]; + float input20 = values[20]; + float input21 = values[21]; + float input22 = values[22]; + float input23 = values[23]; + float input24 = values[24]; + float input25 = values[25]; + float input26 = values[26]; + float input27 = values[27]; + float input28 = values[28]; + float input29 = values[29]; + float input30 = values[30]; + float input31 = values[31]; + + float v0 = input0 + input31; + float v1 = (input0 - input31) * 0.5006029982351963f; + float v2 = input1 + input30; + float v3 = (input1 - input30) * 0.5054709598975436f; + float v4 = input2 + input29; + float v5 = (input2 - input29) * 0.5154473099226246f; + float v6 = input3 + input28; + float v7 = (input3 - input28) * 0.5310425910897841f; + float v8 = input4 + input27; + float v9 = (input4 - input27) * 0.5531038960344445f; + float v10 = input5 + input26; + float v11 = (input5 - input26) * 0.5829349682061339f; + float v12 = input6 + input25; + float v13 = (input6 - input25) * 0.6225041230356648f; + float v14 = input7 + input24; + float v15 = (input7 - input24) * 0.6748083414550057f; + float v16 = input8 + input23; + float v17 = (input8 - input23) * 0.7445362710022986f; + float v18 = input9 + input22; + float v19 = (input9 - input22) * 0.8393496454155268f; + float v20 = input10 + input21; + float v21 = (input10 - input21) * 0.9725682378619608f; + float v22 = input11 + input20; + float v23 = (input11 - input20) * 1.1694399334328847f; + float v24 = input12 + input19; + float v25 = (input12 - input19) * 1.4841646163141662f; + float v26 = input13 + input18; + float v27 = (input13 - input18) * 2.057781009953411f; + float v28 = input14 + input17; + float v29 = (input14 - input17) * 3.407608418468719f; + float v30 = input15 + input16; + float v31 = (input15 - input16) * 10.190008123548033f; + float v32 = v0 + v30; + float v33 = (v0 - v30) * 0.5024192861881557f; + float v34 = v2 + v28; + float v35 = (v2 - v28) * 0.5224986149396889f; + float v36 = v4 + v26; + float v37 = (v4 - v26) * 0.5669440348163577f; + float v38 = v6 + v24; + float v39 = (v6 - v24) * 0.6468217833599901f; + float v40 = v8 + v22; + float v41 = (v8 - v22) * 0.7881546234512502f; + float v42 = v10 + v20; + float v43 = (v10 - v20) * 1.060677685990347f; + float v44 = v12 + v18; + float v45 = (v12 - v18) * 1.7224470982383342f; + float v46 = v14 + v16; + float v47 = (v14 - v16) * 5.101148618689155f; + float v48 = v32 + v46; + float v49 = (v32 - v46) * 0.5097955791041592f; + float v50 = v34 + v44; + float v51 = (v34 - v44) * 0.6013448869350453f; + float v52 = v36 + v42; + float v53 = (v36 - v42) * 0.8999762231364156f; + float v54 = v38 + v40; + float v55 = (v38 - v40) * 2.5629154477415055f; + float v56 = v48 + v54; + float v57 = (v48 - v54) * 0.541196100146197f; + float v58 = v50 + v52; + float v59 = (v50 - v52) * 1.3065629648763764f; + float v60 = v56 + v58; + float v61 = v56 - v58; + float v62 = v57 + v59; + float v63 = v57 - v59; + float v64 = float.MultiplyAddEstimate(v62, JxlDctScales.Sqrt2, v63); + float v65 = v49 + v55; + float v66 = (v49 - v55) * 0.541196100146197f; + float v67 = v51 + v53; + float v68 = (v51 - v53) * 1.3065629648763764f; + float v69 = v65 + v67; + float v70 = v65 - v67; + float v71 = v66 + v68; + float v72 = v66 - v68; + float v73 = float.MultiplyAddEstimate(v71, JxlDctScales.Sqrt2, v72); + float v74 = float.MultiplyAddEstimate(v69, JxlDctScales.Sqrt2, v73); + float v75 = v73 + v70; + float v76 = v70 + v72; + float v77 = v33 + v47; + float v78 = (v33 - v47) * 0.5097955791041592f; + float v79 = v35 + v45; + float v80 = (v35 - v45) * 0.6013448869350453f; + float v81 = v37 + v43; + float v82 = (v37 - v43) * 0.8999762231364156f; + float v83 = v39 + v41; + float v84 = (v39 - v41) * 2.5629154477415055f; + float v85 = v77 + v83; + float v86 = (v77 - v83) * 0.541196100146197f; + float v87 = v79 + v81; + float v88 = (v79 - v81) * 1.3065629648763764f; + float v89 = v85 + v87; + float v90 = v85 - v87; + float v91 = v86 + v88; + float v92 = v86 - v88; + float v93 = float.MultiplyAddEstimate(v91, JxlDctScales.Sqrt2, v92); + float v94 = v78 + v84; + float v95 = (v78 - v84) * 0.541196100146197f; + float v96 = v80 + v82; + float v97 = (v80 - v82) * 1.3065629648763764f; + float v98 = v94 + v96; + float v99 = v94 - v96; + float v100 = v95 + v97; + float v101 = v95 - v97; + float v102 = float.MultiplyAddEstimate(v100, JxlDctScales.Sqrt2, v101); + float v103 = float.MultiplyAddEstimate(v98, JxlDctScales.Sqrt2, v102); + float v104 = v102 + v99; + float v105 = v99 + v101; + float v106 = float.MultiplyAddEstimate(v89, JxlDctScales.Sqrt2, v103); + float v107 = v103 + v93; + float v108 = v93 + v104; + float v109 = v104 + v90; + float v110 = v90 + v105; + float v111 = v105 + v92; + float v112 = v92 + v101; + float v113 = v1 + v31; + float v114 = (v1 - v31) * 0.5024192861881557f; + float v115 = v3 + v29; + float v116 = (v3 - v29) * 0.5224986149396889f; + float v117 = v5 + v27; + float v118 = (v5 - v27) * 0.5669440348163577f; + float v119 = v7 + v25; + float v120 = (v7 - v25) * 0.6468217833599901f; + float v121 = v9 + v23; + float v122 = (v9 - v23) * 0.7881546234512502f; + float v123 = v11 + v21; + float v124 = (v11 - v21) * 1.060677685990347f; + float v125 = v13 + v19; + float v126 = (v13 - v19) * 1.7224470982383342f; + float v127 = v15 + v17; + float v128 = (v15 - v17) * 5.101148618689155f; + float v129 = v113 + v127; + float v130 = (v113 - v127) * 0.5097955791041592f; + float v131 = v115 + v125; + float v132 = (v115 - v125) * 0.6013448869350453f; + float v133 = v117 + v123; + float v134 = (v117 - v123) * 0.8999762231364156f; + float v135 = v119 + v121; + float v136 = (v119 - v121) * 2.5629154477415055f; + float v137 = v129 + v135; + float v138 = (v129 - v135) * 0.541196100146197f; + float v139 = v131 + v133; + float v140 = (v131 - v133) * 1.3065629648763764f; + float v141 = v137 + v139; + float v142 = v137 - v139; + float v143 = v138 + v140; + float v144 = v138 - v140; + float v145 = float.MultiplyAddEstimate(v143, JxlDctScales.Sqrt2, v144); + float v146 = v130 + v136; + float v147 = (v130 - v136) * 0.541196100146197f; + float v148 = v132 + v134; + float v149 = (v132 - v134) * 1.3065629648763764f; + float v150 = v146 + v148; + float v151 = v146 - v148; + float v152 = v147 + v149; + float v153 = v147 - v149; + float v154 = float.MultiplyAddEstimate(v152, JxlDctScales.Sqrt2, v153); + float v155 = float.MultiplyAddEstimate(v150, JxlDctScales.Sqrt2, v154); + float v156 = v154 + v151; + float v157 = v151 + v153; + float v158 = v114 + v128; + float v159 = (v114 - v128) * 0.5097955791041592f; + float v160 = v116 + v126; + float v161 = (v116 - v126) * 0.6013448869350453f; + float v162 = v118 + v124; + float v163 = (v118 - v124) * 0.8999762231364156f; + float v164 = v120 + v122; + float v165 = (v120 - v122) * 2.5629154477415055f; + float v166 = v158 + v164; + float v167 = (v158 - v164) * 0.541196100146197f; + float v168 = v160 + v162; + float v169 = (v160 - v162) * 1.3065629648763764f; + float v170 = v166 + v168; + float v171 = v166 - v168; + float v172 = v167 + v169; + float v173 = v167 - v169; + float v174 = float.MultiplyAddEstimate(v172, JxlDctScales.Sqrt2, v173); + float v175 = v159 + v165; + float v176 = (v159 - v165) * 0.541196100146197f; + float v177 = v161 + v163; + float v178 = (v161 - v163) * 1.3065629648763764f; + float v179 = v175 + v177; + float v180 = v175 - v177; + float v181 = v176 + v178; + float v182 = v176 - v178; + float v183 = float.MultiplyAddEstimate(v181, JxlDctScales.Sqrt2, v182); + float v184 = float.MultiplyAddEstimate(v179, JxlDctScales.Sqrt2, v183); + float v185 = v183 + v180; + float v186 = v180 + v182; + float v187 = float.MultiplyAddEstimate(v170, JxlDctScales.Sqrt2, v184); + float v188 = v184 + v174; + float v189 = v174 + v185; + float v190 = v185 + v171; + float v191 = v171 + v186; + float v192 = v186 + v173; + float v193 = v173 + v182; + float v194 = float.MultiplyAddEstimate(v141, JxlDctScales.Sqrt2, v187); + float v195 = v187 + v155; + float v196 = v155 + v188; + float v197 = v188 + v145; + float v198 = v145 + v189; + float v199 = v189 + v156; + float v200 = v156 + v190; + float v201 = v190 + v142; + float v202 = v142 + v191; + float v203 = v191 + v157; + float v204 = v157 + v192; + float v205 = v192 + v144; + float v206 = v144 + v193; + float v207 = v193 + v153; + float v208 = v153 + v182; + + values[0] = v60; + values[1] = v194; + values[2] = v106; + values[3] = v195; + values[4] = v74; + values[5] = v196; + values[6] = v107; + values[7] = v197; + values[8] = v64; + values[9] = v198; + values[10] = v108; + values[11] = v199; + values[12] = v75; + values[13] = v200; + values[14] = v109; + values[15] = v201; + values[16] = v61; + values[17] = v202; + values[18] = v110; + values[19] = v203; + values[20] = v76; + values[21] = v204; + values[22] = v111; + values[23] = v205; + values[24] = v63; + values[25] = v206; + values[26] = v112; + values[27] = v207; + values[28] = v72; + values[29] = v208; + values[30] = v101; + values[31] = v182; + } + + /// + /// Processes complete 1-lane forward batches along input columns. + /// + /// The number of positions in each transform. + /// The number of independent transforms. + /// The borrowed source matrix. + /// The source row stride, in floats. + /// The borrowed output matrix. + /// The destination row stride, in floats. + /// The first independent transform not already processed by a wider batch. + /// Borrowed storage for two length-by-1 banks. Only the first bank needs initialized inputs. + /// The first independent transform left after the complete batches. + private static int ForwardColumnsScalar( + int length, + int count, + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int first, + Span workspace) + { + Span values = workspace[..length]; + Span scratch = workspace.Slice(length, length); + ref float input = ref MemoryMarshal.GetReference(source); + ref float output = ref MemoryMarshal.GetReference(destination); + float scale = 1F / length; + + for (; first <= count - 1; first += 1) + { + // One vector covers adjacent columns at the same transform position. Physical strides never enter the coefficient-bank layout. + for (int i = 0; i < length; i++) + { + values[i] = Unsafe.Add(ref input, (i * sourceStride) + first); + } + + Forward(values, scratch); + for (int i = 0; i < length; i++) + { + Unsafe.Add(ref output, (i * destinationStride) + first) = values[i] * scale; + } + } + + return first; + } + + /// + /// Processes complete 1-lane forward batches along input rows. + /// + /// The number of positions in each transform. + /// The number of independent transforms. + /// The borrowed source matrix. + /// The source row stride, in floats. + /// The borrowed output matrix. + /// The destination row stride, in floats. + /// Whether transform positions become output rows and input rows become output columns. + /// The first independent transform not already processed by a wider batch. + /// Borrowed storage for two length-by-1 banks. Only the first bank needs initialized inputs. + /// The first independent transform left after the complete batches. + private static int ForwardRowsScalar( + int length, + int count, + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + bool transposed, + int first, + Span workspace) + { + Span values = workspace[..length]; + Span scratch = workspace.Slice(length, length); + ref float input = ref MemoryMarshal.GetReference(source); + ref float output = ref MemoryMarshal.GetReference(destination); + float scale = 1F / length; + + for (; first <= count - 1; first += 1) + { + source.Slice(first * sourceStride, length).CopyTo(values); + + Forward(values, scratch); + if (transposed) + { + // A complete vector is one contiguous output row. Only logical samples are stored; destination padding remains untouched. + for (int i = 0; i < length; i++) + { + Unsafe.Add(ref output, (i * destinationStride) + first) = values[i] * scale; + } + } + else + { + for (int i = 0; i < length; i++) + { + Unsafe.Add(ref output, (first * destinationStride) + i) = values[i] * scale; + } + } + } + + return first; + } + + /// + /// Computes independent inverse transforms using two nonoverlapping borrowed vector banks. + /// + /// One vector per transform position. Its length is a power of two from one through 256. + /// At least values.Length vectors. Previous contents are not read. + private static void Inverse(Span values, Span scratch) + { + // Small transforms stay in registers. The one-point transform is an identity and must not copy any trailing samples. + switch (values.Length) + { + case 1: + return; + case 2: + Inverse2(values); + return; + case 4: + Inverse4(values); + return; + case 8: + Inverse8(values); + return; + case 16: + Inverse16(values); + return; + case 32: + Inverse32(values); + return; + } + + int half = values.Length / 2; + // Separate frequency parity before either recursive call. The original bank can then serve as recursive scratch. + for (int i = 0; i < half; i++) + { + scratch[i] = values[2 * i]; + scratch[half + i] = values[(2 * i) + 1]; + } + + Inverse(scratch[..half], values); + + // Descend so every odd-frequency predecessor is read before its own entry changes. + for (int i = half - 1; i > 0; i--) + { + scratch[half + i] += scratch[half + i - 1]; + } + + scratch[half] *= JxlDctScales.Sqrt2; + Inverse(scratch.Slice(half, half), values); + ReadOnlySpan weights = JxlDctScales.GetMultipliers(values.Length); + + for (int i = 0; i < half; i++) + { + float weight = weights[i]; + float even = scratch[i]; + float odd = scratch[half + i]; + values[i] = float.MultiplyAddEstimate(weight, odd, even); + values[values.Length - i - 1] = float.MultiplyAddEstimate(-weight, odd, even); + } + } + + /// + /// Computes independent 2-point inverse transforms without intermediate memory passes. + /// + /// Exactly 2 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Inverse2(Span values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + float input0 = values[0]; + float input1 = values[1]; + + float v0 = input0 + input1; + float v1 = input0 - input1; + + values[0] = v0; + values[1] = v1; + } + + /// + /// Computes independent 4-point inverse transforms without intermediate memory passes. + /// + /// Exactly 4 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Inverse4(Span values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + float input0 = values[0]; + float input1 = values[1]; + float input2 = values[2]; + float input3 = values[3]; + + float v0 = input0 + input2; + float v1 = input0 - input2; + float v2 = input3 + input1; + float v3 = input1 * JxlDctScales.Sqrt2; + float v4 = v3 + v2; + float v5 = v3 - v2; + float v6 = float.MultiplyAddEstimate(0.541196100146197f, v4, v0); + float v7 = float.MultiplyAddEstimate(-0.541196100146197f, v4, v0); + float v8 = float.MultiplyAddEstimate(1.3065629648763764f, v5, v1); + float v9 = float.MultiplyAddEstimate(-1.3065629648763764f, v5, v1); + + values[0] = v6; + values[1] = v8; + values[2] = v9; + values[3] = v7; + } + + /// + /// Computes independent 8-point inverse transforms without intermediate memory passes. + /// + /// Exactly 8 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Inverse8(Span values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + float input0 = values[0]; + float input1 = values[1]; + float input2 = values[2]; + float input3 = values[3]; + float input4 = values[4]; + float input5 = values[5]; + float input6 = values[6]; + float input7 = values[7]; + + float v0 = input0 + input4; + float v1 = input0 - input4; + float v2 = input6 + input2; + float v3 = input2 * JxlDctScales.Sqrt2; + float v4 = v3 + v2; + float v5 = v3 - v2; + float v6 = float.MultiplyAddEstimate(0.541196100146197f, v4, v0); + float v7 = float.MultiplyAddEstimate(-0.541196100146197f, v4, v0); + float v8 = float.MultiplyAddEstimate(1.3065629648763764f, v5, v1); + float v9 = float.MultiplyAddEstimate(-1.3065629648763764f, v5, v1); + float v10 = input7 + input5; + float v11 = input5 + input3; + float v12 = input3 + input1; + float v13 = input1 * JxlDctScales.Sqrt2; + float v14 = v13 + v11; + float v15 = v13 - v11; + float v16 = v10 + v12; + float v17 = v12 * JxlDctScales.Sqrt2; + float v18 = v17 + v16; + float v19 = v17 - v16; + float v20 = float.MultiplyAddEstimate(0.541196100146197f, v18, v14); + float v21 = float.MultiplyAddEstimate(-0.541196100146197f, v18, v14); + float v22 = float.MultiplyAddEstimate(1.3065629648763764f, v19, v15); + float v23 = float.MultiplyAddEstimate(-1.3065629648763764f, v19, v15); + float v24 = float.MultiplyAddEstimate(0.5097955791041592f, v20, v6); + float v25 = float.MultiplyAddEstimate(-0.5097955791041592f, v20, v6); + float v26 = float.MultiplyAddEstimate(0.6013448869350453f, v22, v8); + float v27 = float.MultiplyAddEstimate(-0.6013448869350453f, v22, v8); + float v28 = float.MultiplyAddEstimate(0.8999762231364156f, v23, v9); + float v29 = float.MultiplyAddEstimate(-0.8999762231364156f, v23, v9); + float v30 = float.MultiplyAddEstimate(2.5629154477415055f, v21, v7); + float v31 = float.MultiplyAddEstimate(-2.5629154477415055f, v21, v7); + + values[0] = v24; + values[1] = v26; + values[2] = v28; + values[3] = v30; + values[4] = v31; + values[5] = v29; + values[6] = v27; + values[7] = v25; + } + + /// + /// Computes independent 16-point inverse transforms without intermediate memory passes. + /// + /// Exactly 16 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Inverse16(Span values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + float input0 = values[0]; + float input1 = values[1]; + float input2 = values[2]; + float input3 = values[3]; + float input4 = values[4]; + float input5 = values[5]; + float input6 = values[6]; + float input7 = values[7]; + float input8 = values[8]; + float input9 = values[9]; + float input10 = values[10]; + float input11 = values[11]; + float input12 = values[12]; + float input13 = values[13]; + float input14 = values[14]; + float input15 = values[15]; + + float v0 = input0 + input8; + float v1 = input0 - input8; + float v2 = input12 + input4; + float v3 = input4 * JxlDctScales.Sqrt2; + float v4 = v3 + v2; + float v5 = v3 - v2; + float v6 = float.MultiplyAddEstimate(0.541196100146197f, v4, v0); + float v7 = float.MultiplyAddEstimate(-0.541196100146197f, v4, v0); + float v8 = float.MultiplyAddEstimate(1.3065629648763764f, v5, v1); + float v9 = float.MultiplyAddEstimate(-1.3065629648763764f, v5, v1); + float v10 = input14 + input10; + float v11 = input10 + input6; + float v12 = input6 + input2; + float v13 = input2 * JxlDctScales.Sqrt2; + float v14 = v13 + v11; + float v15 = v13 - v11; + float v16 = v10 + v12; + float v17 = v12 * JxlDctScales.Sqrt2; + float v18 = v17 + v16; + float v19 = v17 - v16; + float v20 = float.MultiplyAddEstimate(0.541196100146197f, v18, v14); + float v21 = float.MultiplyAddEstimate(-0.541196100146197f, v18, v14); + float v22 = float.MultiplyAddEstimate(1.3065629648763764f, v19, v15); + float v23 = float.MultiplyAddEstimate(-1.3065629648763764f, v19, v15); + float v24 = float.MultiplyAddEstimate(0.5097955791041592f, v20, v6); + float v25 = float.MultiplyAddEstimate(-0.5097955791041592f, v20, v6); + float v26 = float.MultiplyAddEstimate(0.6013448869350453f, v22, v8); + float v27 = float.MultiplyAddEstimate(-0.6013448869350453f, v22, v8); + float v28 = float.MultiplyAddEstimate(0.8999762231364156f, v23, v9); + float v29 = float.MultiplyAddEstimate(-0.8999762231364156f, v23, v9); + float v30 = float.MultiplyAddEstimate(2.5629154477415055f, v21, v7); + float v31 = float.MultiplyAddEstimate(-2.5629154477415055f, v21, v7); + float v32 = input15 + input13; + float v33 = input13 + input11; + float v34 = input11 + input9; + float v35 = input9 + input7; + float v36 = input7 + input5; + float v37 = input5 + input3; + float v38 = input3 + input1; + float v39 = input1 * JxlDctScales.Sqrt2; + float v40 = v39 + v35; + float v41 = v39 - v35; + float v42 = v33 + v37; + float v43 = v37 * JxlDctScales.Sqrt2; + float v44 = v43 + v42; + float v45 = v43 - v42; + float v46 = float.MultiplyAddEstimate(0.541196100146197f, v44, v40); + float v47 = float.MultiplyAddEstimate(-0.541196100146197f, v44, v40); + float v48 = float.MultiplyAddEstimate(1.3065629648763764f, v45, v41); + float v49 = float.MultiplyAddEstimate(-1.3065629648763764f, v45, v41); + float v50 = v32 + v34; + float v51 = v34 + v36; + float v52 = v36 + v38; + float v53 = v38 * JxlDctScales.Sqrt2; + float v54 = v53 + v51; + float v55 = v53 - v51; + float v56 = v50 + v52; + float v57 = v52 * JxlDctScales.Sqrt2; + float v58 = v57 + v56; + float v59 = v57 - v56; + float v60 = float.MultiplyAddEstimate(0.541196100146197f, v58, v54); + float v61 = float.MultiplyAddEstimate(-0.541196100146197f, v58, v54); + float v62 = float.MultiplyAddEstimate(1.3065629648763764f, v59, v55); + float v63 = float.MultiplyAddEstimate(-1.3065629648763764f, v59, v55); + float v64 = float.MultiplyAddEstimate(0.5097955791041592f, v60, v46); + float v65 = float.MultiplyAddEstimate(-0.5097955791041592f, v60, v46); + float v66 = float.MultiplyAddEstimate(0.6013448869350453f, v62, v48); + float v67 = float.MultiplyAddEstimate(-0.6013448869350453f, v62, v48); + float v68 = float.MultiplyAddEstimate(0.8999762231364156f, v63, v49); + float v69 = float.MultiplyAddEstimate(-0.8999762231364156f, v63, v49); + float v70 = float.MultiplyAddEstimate(2.5629154477415055f, v61, v47); + float v71 = float.MultiplyAddEstimate(-2.5629154477415055f, v61, v47); + float v72 = float.MultiplyAddEstimate(0.5024192861881557f, v64, v24); + float v73 = float.MultiplyAddEstimate(-0.5024192861881557f, v64, v24); + float v74 = float.MultiplyAddEstimate(0.5224986149396889f, v66, v26); + float v75 = float.MultiplyAddEstimate(-0.5224986149396889f, v66, v26); + float v76 = float.MultiplyAddEstimate(0.5669440348163577f, v68, v28); + float v77 = float.MultiplyAddEstimate(-0.5669440348163577f, v68, v28); + float v78 = float.MultiplyAddEstimate(0.6468217833599901f, v70, v30); + float v79 = float.MultiplyAddEstimate(-0.6468217833599901f, v70, v30); + float v80 = float.MultiplyAddEstimate(0.7881546234512502f, v71, v31); + float v81 = float.MultiplyAddEstimate(-0.7881546234512502f, v71, v31); + float v82 = float.MultiplyAddEstimate(1.060677685990347f, v69, v29); + float v83 = float.MultiplyAddEstimate(-1.060677685990347f, v69, v29); + float v84 = float.MultiplyAddEstimate(1.7224470982383342f, v67, v27); + float v85 = float.MultiplyAddEstimate(-1.7224470982383342f, v67, v27); + float v86 = float.MultiplyAddEstimate(5.101148618689155f, v65, v25); + float v87 = float.MultiplyAddEstimate(-5.101148618689155f, v65, v25); + + values[0] = v72; + values[1] = v74; + values[2] = v76; + values[3] = v78; + values[4] = v80; + values[5] = v82; + values[6] = v84; + values[7] = v86; + values[8] = v87; + values[9] = v85; + values[10] = v83; + values[11] = v81; + values[12] = v79; + values[13] = v77; + values[14] = v75; + values[15] = v73; + } + + /// + /// Computes independent 32-point inverse transforms without intermediate memory passes. + /// + /// Exactly 32 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Inverse32(Span values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + float input0 = values[0]; + float input1 = values[1]; + float input2 = values[2]; + float input3 = values[3]; + float input4 = values[4]; + float input5 = values[5]; + float input6 = values[6]; + float input7 = values[7]; + float input8 = values[8]; + float input9 = values[9]; + float input10 = values[10]; + float input11 = values[11]; + float input12 = values[12]; + float input13 = values[13]; + float input14 = values[14]; + float input15 = values[15]; + float input16 = values[16]; + float input17 = values[17]; + float input18 = values[18]; + float input19 = values[19]; + float input20 = values[20]; + float input21 = values[21]; + float input22 = values[22]; + float input23 = values[23]; + float input24 = values[24]; + float input25 = values[25]; + float input26 = values[26]; + float input27 = values[27]; + float input28 = values[28]; + float input29 = values[29]; + float input30 = values[30]; + float input31 = values[31]; + + float v0 = input0 + input16; + float v1 = input0 - input16; + float v2 = input24 + input8; + float v3 = input8 * JxlDctScales.Sqrt2; + float v4 = v3 + v2; + float v5 = v3 - v2; + float v6 = float.MultiplyAddEstimate(0.541196100146197f, v4, v0); + float v7 = float.MultiplyAddEstimate(-0.541196100146197f, v4, v0); + float v8 = float.MultiplyAddEstimate(1.3065629648763764f, v5, v1); + float v9 = float.MultiplyAddEstimate(-1.3065629648763764f, v5, v1); + float v10 = input28 + input20; + float v11 = input20 + input12; + float v12 = input12 + input4; + float v13 = input4 * JxlDctScales.Sqrt2; + float v14 = v13 + v11; + float v15 = v13 - v11; + float v16 = v10 + v12; + float v17 = v12 * JxlDctScales.Sqrt2; + float v18 = v17 + v16; + float v19 = v17 - v16; + float v20 = float.MultiplyAddEstimate(0.541196100146197f, v18, v14); + float v21 = float.MultiplyAddEstimate(-0.541196100146197f, v18, v14); + float v22 = float.MultiplyAddEstimate(1.3065629648763764f, v19, v15); + float v23 = float.MultiplyAddEstimate(-1.3065629648763764f, v19, v15); + float v24 = float.MultiplyAddEstimate(0.5097955791041592f, v20, v6); + float v25 = float.MultiplyAddEstimate(-0.5097955791041592f, v20, v6); + float v26 = float.MultiplyAddEstimate(0.6013448869350453f, v22, v8); + float v27 = float.MultiplyAddEstimate(-0.6013448869350453f, v22, v8); + float v28 = float.MultiplyAddEstimate(0.8999762231364156f, v23, v9); + float v29 = float.MultiplyAddEstimate(-0.8999762231364156f, v23, v9); + float v30 = float.MultiplyAddEstimate(2.5629154477415055f, v21, v7); + float v31 = float.MultiplyAddEstimate(-2.5629154477415055f, v21, v7); + float v32 = input30 + input26; + float v33 = input26 + input22; + float v34 = input22 + input18; + float v35 = input18 + input14; + float v36 = input14 + input10; + float v37 = input10 + input6; + float v38 = input6 + input2; + float v39 = input2 * JxlDctScales.Sqrt2; + float v40 = v39 + v35; + float v41 = v39 - v35; + float v42 = v33 + v37; + float v43 = v37 * JxlDctScales.Sqrt2; + float v44 = v43 + v42; + float v45 = v43 - v42; + float v46 = float.MultiplyAddEstimate(0.541196100146197f, v44, v40); + float v47 = float.MultiplyAddEstimate(-0.541196100146197f, v44, v40); + float v48 = float.MultiplyAddEstimate(1.3065629648763764f, v45, v41); + float v49 = float.MultiplyAddEstimate(-1.3065629648763764f, v45, v41); + float v50 = v32 + v34; + float v51 = v34 + v36; + float v52 = v36 + v38; + float v53 = v38 * JxlDctScales.Sqrt2; + float v54 = v53 + v51; + float v55 = v53 - v51; + float v56 = v50 + v52; + float v57 = v52 * JxlDctScales.Sqrt2; + float v58 = v57 + v56; + float v59 = v57 - v56; + float v60 = float.MultiplyAddEstimate(0.541196100146197f, v58, v54); + float v61 = float.MultiplyAddEstimate(-0.541196100146197f, v58, v54); + float v62 = float.MultiplyAddEstimate(1.3065629648763764f, v59, v55); + float v63 = float.MultiplyAddEstimate(-1.3065629648763764f, v59, v55); + float v64 = float.MultiplyAddEstimate(0.5097955791041592f, v60, v46); + float v65 = float.MultiplyAddEstimate(-0.5097955791041592f, v60, v46); + float v66 = float.MultiplyAddEstimate(0.6013448869350453f, v62, v48); + float v67 = float.MultiplyAddEstimate(-0.6013448869350453f, v62, v48); + float v68 = float.MultiplyAddEstimate(0.8999762231364156f, v63, v49); + float v69 = float.MultiplyAddEstimate(-0.8999762231364156f, v63, v49); + float v70 = float.MultiplyAddEstimate(2.5629154477415055f, v61, v47); + float v71 = float.MultiplyAddEstimate(-2.5629154477415055f, v61, v47); + float v72 = float.MultiplyAddEstimate(0.5024192861881557f, v64, v24); + float v73 = float.MultiplyAddEstimate(-0.5024192861881557f, v64, v24); + float v74 = float.MultiplyAddEstimate(0.5224986149396889f, v66, v26); + float v75 = float.MultiplyAddEstimate(-0.5224986149396889f, v66, v26); + float v76 = float.MultiplyAddEstimate(0.5669440348163577f, v68, v28); + float v77 = float.MultiplyAddEstimate(-0.5669440348163577f, v68, v28); + float v78 = float.MultiplyAddEstimate(0.6468217833599901f, v70, v30); + float v79 = float.MultiplyAddEstimate(-0.6468217833599901f, v70, v30); + float v80 = float.MultiplyAddEstimate(0.7881546234512502f, v71, v31); + float v81 = float.MultiplyAddEstimate(-0.7881546234512502f, v71, v31); + float v82 = float.MultiplyAddEstimate(1.060677685990347f, v69, v29); + float v83 = float.MultiplyAddEstimate(-1.060677685990347f, v69, v29); + float v84 = float.MultiplyAddEstimate(1.7224470982383342f, v67, v27); + float v85 = float.MultiplyAddEstimate(-1.7224470982383342f, v67, v27); + float v86 = float.MultiplyAddEstimate(5.101148618689155f, v65, v25); + float v87 = float.MultiplyAddEstimate(-5.101148618689155f, v65, v25); + float v88 = input31 + input29; + float v89 = input29 + input27; + float v90 = input27 + input25; + float v91 = input25 + input23; + float v92 = input23 + input21; + float v93 = input21 + input19; + float v94 = input19 + input17; + float v95 = input17 + input15; + float v96 = input15 + input13; + float v97 = input13 + input11; + float v98 = input11 + input9; + float v99 = input9 + input7; + float v100 = input7 + input5; + float v101 = input5 + input3; + float v102 = input3 + input1; + float v103 = input1 * JxlDctScales.Sqrt2; + float v104 = v103 + v95; + float v105 = v103 - v95; + float v106 = v91 + v99; + float v107 = v99 * JxlDctScales.Sqrt2; + float v108 = v107 + v106; + float v109 = v107 - v106; + float v110 = float.MultiplyAddEstimate(0.541196100146197f, v108, v104); + float v111 = float.MultiplyAddEstimate(-0.541196100146197f, v108, v104); + float v112 = float.MultiplyAddEstimate(1.3065629648763764f, v109, v105); + float v113 = float.MultiplyAddEstimate(-1.3065629648763764f, v109, v105); + float v114 = v89 + v93; + float v115 = v93 + v97; + float v116 = v97 + v101; + float v117 = v101 * JxlDctScales.Sqrt2; + float v118 = v117 + v115; + float v119 = v117 - v115; + float v120 = v114 + v116; + float v121 = v116 * JxlDctScales.Sqrt2; + float v122 = v121 + v120; + float v123 = v121 - v120; + float v124 = float.MultiplyAddEstimate(0.541196100146197f, v122, v118); + float v125 = float.MultiplyAddEstimate(-0.541196100146197f, v122, v118); + float v126 = float.MultiplyAddEstimate(1.3065629648763764f, v123, v119); + float v127 = float.MultiplyAddEstimate(-1.3065629648763764f, v123, v119); + float v128 = float.MultiplyAddEstimate(0.5097955791041592f, v124, v110); + float v129 = float.MultiplyAddEstimate(-0.5097955791041592f, v124, v110); + float v130 = float.MultiplyAddEstimate(0.6013448869350453f, v126, v112); + float v131 = float.MultiplyAddEstimate(-0.6013448869350453f, v126, v112); + float v132 = float.MultiplyAddEstimate(0.8999762231364156f, v127, v113); + float v133 = float.MultiplyAddEstimate(-0.8999762231364156f, v127, v113); + float v134 = float.MultiplyAddEstimate(2.5629154477415055f, v125, v111); + float v135 = float.MultiplyAddEstimate(-2.5629154477415055f, v125, v111); + float v136 = v88 + v90; + float v137 = v90 + v92; + float v138 = v92 + v94; + float v139 = v94 + v96; + float v140 = v96 + v98; + float v141 = v98 + v100; + float v142 = v100 + v102; + float v143 = v102 * JxlDctScales.Sqrt2; + float v144 = v143 + v139; + float v145 = v143 - v139; + float v146 = v137 + v141; + float v147 = v141 * JxlDctScales.Sqrt2; + float v148 = v147 + v146; + float v149 = v147 - v146; + float v150 = float.MultiplyAddEstimate(0.541196100146197f, v148, v144); + float v151 = float.MultiplyAddEstimate(-0.541196100146197f, v148, v144); + float v152 = float.MultiplyAddEstimate(1.3065629648763764f, v149, v145); + float v153 = float.MultiplyAddEstimate(-1.3065629648763764f, v149, v145); + float v154 = v136 + v138; + float v155 = v138 + v140; + float v156 = v140 + v142; + float v157 = v142 * JxlDctScales.Sqrt2; + float v158 = v157 + v155; + float v159 = v157 - v155; + float v160 = v154 + v156; + float v161 = v156 * JxlDctScales.Sqrt2; + float v162 = v161 + v160; + float v163 = v161 - v160; + float v164 = float.MultiplyAddEstimate(0.541196100146197f, v162, v158); + float v165 = float.MultiplyAddEstimate(-0.541196100146197f, v162, v158); + float v166 = float.MultiplyAddEstimate(1.3065629648763764f, v163, v159); + float v167 = float.MultiplyAddEstimate(-1.3065629648763764f, v163, v159); + float v168 = float.MultiplyAddEstimate(0.5097955791041592f, v164, v150); + float v169 = float.MultiplyAddEstimate(-0.5097955791041592f, v164, v150); + float v170 = float.MultiplyAddEstimate(0.6013448869350453f, v166, v152); + float v171 = float.MultiplyAddEstimate(-0.6013448869350453f, v166, v152); + float v172 = float.MultiplyAddEstimate(0.8999762231364156f, v167, v153); + float v173 = float.MultiplyAddEstimate(-0.8999762231364156f, v167, v153); + float v174 = float.MultiplyAddEstimate(2.5629154477415055f, v165, v151); + float v175 = float.MultiplyAddEstimate(-2.5629154477415055f, v165, v151); + float v176 = float.MultiplyAddEstimate(0.5024192861881557f, v168, v128); + float v177 = float.MultiplyAddEstimate(-0.5024192861881557f, v168, v128); + float v178 = float.MultiplyAddEstimate(0.5224986149396889f, v170, v130); + float v179 = float.MultiplyAddEstimate(-0.5224986149396889f, v170, v130); + float v180 = float.MultiplyAddEstimate(0.5669440348163577f, v172, v132); + float v181 = float.MultiplyAddEstimate(-0.5669440348163577f, v172, v132); + float v182 = float.MultiplyAddEstimate(0.6468217833599901f, v174, v134); + float v183 = float.MultiplyAddEstimate(-0.6468217833599901f, v174, v134); + float v184 = float.MultiplyAddEstimate(0.7881546234512502f, v175, v135); + float v185 = float.MultiplyAddEstimate(-0.7881546234512502f, v175, v135); + float v186 = float.MultiplyAddEstimate(1.060677685990347f, v173, v133); + float v187 = float.MultiplyAddEstimate(-1.060677685990347f, v173, v133); + float v188 = float.MultiplyAddEstimate(1.7224470982383342f, v171, v131); + float v189 = float.MultiplyAddEstimate(-1.7224470982383342f, v171, v131); + float v190 = float.MultiplyAddEstimate(5.101148618689155f, v169, v129); + float v191 = float.MultiplyAddEstimate(-5.101148618689155f, v169, v129); + float v192 = float.MultiplyAddEstimate(0.5006029982351963f, v176, v72); + float v193 = float.MultiplyAddEstimate(-0.5006029982351963f, v176, v72); + float v194 = float.MultiplyAddEstimate(0.5054709598975436f, v178, v74); + float v195 = float.MultiplyAddEstimate(-0.5054709598975436f, v178, v74); + float v196 = float.MultiplyAddEstimate(0.5154473099226246f, v180, v76); + float v197 = float.MultiplyAddEstimate(-0.5154473099226246f, v180, v76); + float v198 = float.MultiplyAddEstimate(0.5310425910897841f, v182, v78); + float v199 = float.MultiplyAddEstimate(-0.5310425910897841f, v182, v78); + float v200 = float.MultiplyAddEstimate(0.5531038960344445f, v184, v80); + float v201 = float.MultiplyAddEstimate(-0.5531038960344445f, v184, v80); + float v202 = float.MultiplyAddEstimate(0.5829349682061339f, v186, v82); + float v203 = float.MultiplyAddEstimate(-0.5829349682061339f, v186, v82); + float v204 = float.MultiplyAddEstimate(0.6225041230356648f, v188, v84); + float v205 = float.MultiplyAddEstimate(-0.6225041230356648f, v188, v84); + float v206 = float.MultiplyAddEstimate(0.6748083414550057f, v190, v86); + float v207 = float.MultiplyAddEstimate(-0.6748083414550057f, v190, v86); + float v208 = float.MultiplyAddEstimate(0.7445362710022986f, v191, v87); + float v209 = float.MultiplyAddEstimate(-0.7445362710022986f, v191, v87); + float v210 = float.MultiplyAddEstimate(0.8393496454155268f, v189, v85); + float v211 = float.MultiplyAddEstimate(-0.8393496454155268f, v189, v85); + float v212 = float.MultiplyAddEstimate(0.9725682378619608f, v187, v83); + float v213 = float.MultiplyAddEstimate(-0.9725682378619608f, v187, v83); + float v214 = float.MultiplyAddEstimate(1.1694399334328847f, v185, v81); + float v215 = float.MultiplyAddEstimate(-1.1694399334328847f, v185, v81); + float v216 = float.MultiplyAddEstimate(1.4841646163141662f, v183, v79); + float v217 = float.MultiplyAddEstimate(-1.4841646163141662f, v183, v79); + float v218 = float.MultiplyAddEstimate(2.057781009953411f, v181, v77); + float v219 = float.MultiplyAddEstimate(-2.057781009953411f, v181, v77); + float v220 = float.MultiplyAddEstimate(3.407608418468719f, v179, v75); + float v221 = float.MultiplyAddEstimate(-3.407608418468719f, v179, v75); + float v222 = float.MultiplyAddEstimate(10.190008123548033f, v177, v73); + float v223 = float.MultiplyAddEstimate(-10.190008123548033f, v177, v73); + + values[0] = v192; + values[1] = v194; + values[2] = v196; + values[3] = v198; + values[4] = v200; + values[5] = v202; + values[6] = v204; + values[7] = v206; + values[8] = v208; + values[9] = v210; + values[10] = v212; + values[11] = v214; + values[12] = v216; + values[13] = v218; + values[14] = v220; + values[15] = v222; + values[16] = v223; + values[17] = v221; + values[18] = v219; + values[19] = v217; + values[20] = v215; + values[21] = v213; + values[22] = v211; + values[23] = v209; + values[24] = v207; + values[25] = v205; + values[26] = v203; + values[27] = v201; + values[28] = v199; + values[29] = v197; + values[30] = v195; + values[31] = v193; + } + + /// + /// Processes complete 1-lane inverse batches along input columns. + /// + /// The number of positions in each transform. + /// The number of independent transforms. + /// The borrowed source matrix. + /// The source row stride, in floats. + /// The borrowed output matrix. + /// The destination row stride, in floats. + /// The first independent transform not already processed by a wider batch. + /// Borrowed storage for two length-by-1 banks. Only the first bank needs initialized inputs. + /// The first independent transform left after the complete batches. + private static int InverseColumnsScalar( + int length, + int count, + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int first, + Span workspace) + { + Span values = workspace[..length]; + Span scratch = workspace.Slice(length, length); + ref float input = ref MemoryMarshal.GetReference(source); + ref float output = ref MemoryMarshal.GetReference(destination); + + for (; first <= count - 1; first += 1) + { + // One vector covers adjacent columns at the same transform position. Physical strides never enter the coefficient-bank layout. + for (int i = 0; i < length; i++) + { + values[i] = Unsafe.Add(ref input, (i * sourceStride) + first); + } + + Inverse(values, scratch); + for (int i = 0; i < length; i++) + { + Unsafe.Add(ref output, (i * destinationStride) + first) = values[i]; + } + } + + return first; + } + + /// + /// Processes complete 1-lane inverse batches along input rows. + /// + /// The number of positions in each transform. + /// The number of independent transforms. + /// The borrowed source matrix. + /// The source row stride, in floats. + /// The borrowed output matrix. + /// The destination row stride, in floats. + /// Whether transform positions become output rows and input rows become output columns. + /// The first independent transform not already processed by a wider batch. + /// Borrowed storage for two length-by-1 banks. Only the first bank needs initialized inputs. + /// The first independent transform left after the complete batches. + private static int InverseRowsScalar( + int length, + int count, + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + bool transposed, + int first, + Span workspace) + { + Span values = workspace[..length]; + Span scratch = workspace.Slice(length, length); + ref float input = ref MemoryMarshal.GetReference(source); + ref float output = ref MemoryMarshal.GetReference(destination); + + for (; first <= count - 1; first += 1) + { + source.Slice(first * sourceStride, length).CopyTo(values); + + Inverse(values, scratch); + if (transposed) + { + // A complete vector is one contiguous output row. Only logical samples are stored; destination padding remains untouched. + for (int i = 0; i < length; i++) + { + Unsafe.Add(ref output, (i * destinationStride) + first) = values[i]; + } + } + else + { + for (int i = 0; i < length; i++) + { + Unsafe.Add(ref output, (first * destinationStride) + i) = values[i]; + } + } + } + + return first; + } + + /// + /// Computes independent forward transforms using two nonoverlapping borrowed vector banks. + /// + /// One vector per transform position. Its length is a power of two from one through 256. + /// At least values.Length vectors. Previous contents are not read. + private static void Forward(Span> values, Span> scratch) + { + // Small transforms stay in registers. The one-point transform is an identity and must not copy any trailing samples. + switch (values.Length) + { + case 1: + return; + case 2: + Forward2(values); + return; + case 4: + Forward4(values); + return; + case 8: + Forward8(values); + return; + case 16: + Forward16(values); + return; + case 32: + Forward32(values); + return; + } + + int half = values.Length / 2; + ReadOnlySpan weights = JxlDctScales.GetMultipliers(values.Length); + + // Consume both original halves before reusing their bank as recursive scratch. Neither recursive call can then destroy unread input. + for (int i = 0; i < half; i++) + { + Vector128 first = values[i]; + Vector128 last = values[values.Length - i - 1]; + scratch[i] = first + last; + scratch[half + i] = (first - last) * Vector128.Create(weights[i]); + } + + Forward(scratch[..half], values); + Forward(scratch.Slice(half, half), values); + scratch[half] = Vector128.MultiplyAddEstimate(scratch[half], Vector128.Create(JxlDctScales.Sqrt2), scratch[half + 1]); + + for (int i = 1; i + 1 < half; i++) + { + scratch[half + i] += scratch[half + i + 1]; + } + + for (int i = 0; i < half; i++) + { + values[2 * i] = scratch[i]; + values[(2 * i) + 1] = scratch[half + i]; + } + } + + /// + /// Computes independent 2-point forward transforms without intermediate memory passes. + /// + /// Exactly 2 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Forward2(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector128 input0 = values[0]; + Vector128 input1 = values[1]; + + Vector128 v0 = input0 + input1; + Vector128 v1 = input0 - input1; + + values[0] = v0; + values[1] = v1; + } + + /// + /// Computes independent 4-point forward transforms without intermediate memory passes. + /// + /// Exactly 4 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Forward4(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector128 input0 = values[0]; + Vector128 input1 = values[1]; + Vector128 input2 = values[2]; + Vector128 input3 = values[3]; + + Vector128 v0 = input0 + input3; + Vector128 v1 = (input0 - input3) * Vector128.Create(0.541196100146197f); + Vector128 v2 = input1 + input2; + Vector128 v3 = (input1 - input2) * Vector128.Create(1.3065629648763764f); + Vector128 v4 = v0 + v2; + Vector128 v5 = v0 - v2; + Vector128 v6 = v1 + v3; + Vector128 v7 = v1 - v3; + Vector128 v8 = Vector128.MultiplyAddEstimate(v6, Vector128.Create(JxlDctScales.Sqrt2), v7); + + values[0] = v4; + values[1] = v8; + values[2] = v5; + values[3] = v7; + } + + /// + /// Computes independent 8-point forward transforms without intermediate memory passes. + /// + /// Exactly 8 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Forward8(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector128 input0 = values[0]; + Vector128 input1 = values[1]; + Vector128 input2 = values[2]; + Vector128 input3 = values[3]; + Vector128 input4 = values[4]; + Vector128 input5 = values[5]; + Vector128 input6 = values[6]; + Vector128 input7 = values[7]; + + Vector128 v0 = input0 + input7; + Vector128 v1 = (input0 - input7) * Vector128.Create(0.5097955791041592f); + Vector128 v2 = input1 + input6; + Vector128 v3 = (input1 - input6) * Vector128.Create(0.6013448869350453f); + Vector128 v4 = input2 + input5; + Vector128 v5 = (input2 - input5) * Vector128.Create(0.8999762231364156f); + Vector128 v6 = input3 + input4; + Vector128 v7 = (input3 - input4) * Vector128.Create(2.5629154477415055f); + Vector128 v8 = v0 + v6; + Vector128 v9 = (v0 - v6) * Vector128.Create(0.541196100146197f); + Vector128 v10 = v2 + v4; + Vector128 v11 = (v2 - v4) * Vector128.Create(1.3065629648763764f); + Vector128 v12 = v8 + v10; + Vector128 v13 = v8 - v10; + Vector128 v14 = v9 + v11; + Vector128 v15 = v9 - v11; + Vector128 v16 = Vector128.MultiplyAddEstimate(v14, Vector128.Create(JxlDctScales.Sqrt2), v15); + Vector128 v17 = v1 + v7; + Vector128 v18 = (v1 - v7) * Vector128.Create(0.541196100146197f); + Vector128 v19 = v3 + v5; + Vector128 v20 = (v3 - v5) * Vector128.Create(1.3065629648763764f); + Vector128 v21 = v17 + v19; + Vector128 v22 = v17 - v19; + Vector128 v23 = v18 + v20; + Vector128 v24 = v18 - v20; + Vector128 v25 = Vector128.MultiplyAddEstimate(v23, Vector128.Create(JxlDctScales.Sqrt2), v24); + Vector128 v26 = Vector128.MultiplyAddEstimate(v21, Vector128.Create(JxlDctScales.Sqrt2), v25); + Vector128 v27 = v25 + v22; + Vector128 v28 = v22 + v24; + + values[0] = v12; + values[1] = v26; + values[2] = v16; + values[3] = v27; + values[4] = v13; + values[5] = v28; + values[6] = v15; + values[7] = v24; + } + + /// + /// Computes independent 16-point forward transforms without intermediate memory passes. + /// + /// Exactly 16 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Forward16(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector128 input0 = values[0]; + Vector128 input1 = values[1]; + Vector128 input2 = values[2]; + Vector128 input3 = values[3]; + Vector128 input4 = values[4]; + Vector128 input5 = values[5]; + Vector128 input6 = values[6]; + Vector128 input7 = values[7]; + Vector128 input8 = values[8]; + Vector128 input9 = values[9]; + Vector128 input10 = values[10]; + Vector128 input11 = values[11]; + Vector128 input12 = values[12]; + Vector128 input13 = values[13]; + Vector128 input14 = values[14]; + Vector128 input15 = values[15]; + + Vector128 v0 = input0 + input15; + Vector128 v1 = (input0 - input15) * Vector128.Create(0.5024192861881557f); + Vector128 v2 = input1 + input14; + Vector128 v3 = (input1 - input14) * Vector128.Create(0.5224986149396889f); + Vector128 v4 = input2 + input13; + Vector128 v5 = (input2 - input13) * Vector128.Create(0.5669440348163577f); + Vector128 v6 = input3 + input12; + Vector128 v7 = (input3 - input12) * Vector128.Create(0.6468217833599901f); + Vector128 v8 = input4 + input11; + Vector128 v9 = (input4 - input11) * Vector128.Create(0.7881546234512502f); + Vector128 v10 = input5 + input10; + Vector128 v11 = (input5 - input10) * Vector128.Create(1.060677685990347f); + Vector128 v12 = input6 + input9; + Vector128 v13 = (input6 - input9) * Vector128.Create(1.7224470982383342f); + Vector128 v14 = input7 + input8; + Vector128 v15 = (input7 - input8) * Vector128.Create(5.101148618689155f); + Vector128 v16 = v0 + v14; + Vector128 v17 = (v0 - v14) * Vector128.Create(0.5097955791041592f); + Vector128 v18 = v2 + v12; + Vector128 v19 = (v2 - v12) * Vector128.Create(0.6013448869350453f); + Vector128 v20 = v4 + v10; + Vector128 v21 = (v4 - v10) * Vector128.Create(0.8999762231364156f); + Vector128 v22 = v6 + v8; + Vector128 v23 = (v6 - v8) * Vector128.Create(2.5629154477415055f); + Vector128 v24 = v16 + v22; + Vector128 v25 = (v16 - v22) * Vector128.Create(0.541196100146197f); + Vector128 v26 = v18 + v20; + Vector128 v27 = (v18 - v20) * Vector128.Create(1.3065629648763764f); + Vector128 v28 = v24 + v26; + Vector128 v29 = v24 - v26; + Vector128 v30 = v25 + v27; + Vector128 v31 = v25 - v27; + Vector128 v32 = Vector128.MultiplyAddEstimate(v30, Vector128.Create(JxlDctScales.Sqrt2), v31); + Vector128 v33 = v17 + v23; + Vector128 v34 = (v17 - v23) * Vector128.Create(0.541196100146197f); + Vector128 v35 = v19 + v21; + Vector128 v36 = (v19 - v21) * Vector128.Create(1.3065629648763764f); + Vector128 v37 = v33 + v35; + Vector128 v38 = v33 - v35; + Vector128 v39 = v34 + v36; + Vector128 v40 = v34 - v36; + Vector128 v41 = Vector128.MultiplyAddEstimate(v39, Vector128.Create(JxlDctScales.Sqrt2), v40); + Vector128 v42 = Vector128.MultiplyAddEstimate(v37, Vector128.Create(JxlDctScales.Sqrt2), v41); + Vector128 v43 = v41 + v38; + Vector128 v44 = v38 + v40; + Vector128 v45 = v1 + v15; + Vector128 v46 = (v1 - v15) * Vector128.Create(0.5097955791041592f); + Vector128 v47 = v3 + v13; + Vector128 v48 = (v3 - v13) * Vector128.Create(0.6013448869350453f); + Vector128 v49 = v5 + v11; + Vector128 v50 = (v5 - v11) * Vector128.Create(0.8999762231364156f); + Vector128 v51 = v7 + v9; + Vector128 v52 = (v7 - v9) * Vector128.Create(2.5629154477415055f); + Vector128 v53 = v45 + v51; + Vector128 v54 = (v45 - v51) * Vector128.Create(0.541196100146197f); + Vector128 v55 = v47 + v49; + Vector128 v56 = (v47 - v49) * Vector128.Create(1.3065629648763764f); + Vector128 v57 = v53 + v55; + Vector128 v58 = v53 - v55; + Vector128 v59 = v54 + v56; + Vector128 v60 = v54 - v56; + Vector128 v61 = Vector128.MultiplyAddEstimate(v59, Vector128.Create(JxlDctScales.Sqrt2), v60); + Vector128 v62 = v46 + v52; + Vector128 v63 = (v46 - v52) * Vector128.Create(0.541196100146197f); + Vector128 v64 = v48 + v50; + Vector128 v65 = (v48 - v50) * Vector128.Create(1.3065629648763764f); + Vector128 v66 = v62 + v64; + Vector128 v67 = v62 - v64; + Vector128 v68 = v63 + v65; + Vector128 v69 = v63 - v65; + Vector128 v70 = Vector128.MultiplyAddEstimate(v68, Vector128.Create(JxlDctScales.Sqrt2), v69); + Vector128 v71 = Vector128.MultiplyAddEstimate(v66, Vector128.Create(JxlDctScales.Sqrt2), v70); + Vector128 v72 = v70 + v67; + Vector128 v73 = v67 + v69; + Vector128 v74 = Vector128.MultiplyAddEstimate(v57, Vector128.Create(JxlDctScales.Sqrt2), v71); + Vector128 v75 = v71 + v61; + Vector128 v76 = v61 + v72; + Vector128 v77 = v72 + v58; + Vector128 v78 = v58 + v73; + Vector128 v79 = v73 + v60; + Vector128 v80 = v60 + v69; + + values[0] = v28; + values[1] = v74; + values[2] = v42; + values[3] = v75; + values[4] = v32; + values[5] = v76; + values[6] = v43; + values[7] = v77; + values[8] = v29; + values[9] = v78; + values[10] = v44; + values[11] = v79; + values[12] = v31; + values[13] = v80; + values[14] = v40; + values[15] = v69; + } + + /// + /// Computes independent 32-point forward transforms without intermediate memory passes. + /// + /// Exactly 32 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Forward32(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector128 input0 = values[0]; + Vector128 input1 = values[1]; + Vector128 input2 = values[2]; + Vector128 input3 = values[3]; + Vector128 input4 = values[4]; + Vector128 input5 = values[5]; + Vector128 input6 = values[6]; + Vector128 input7 = values[7]; + Vector128 input8 = values[8]; + Vector128 input9 = values[9]; + Vector128 input10 = values[10]; + Vector128 input11 = values[11]; + Vector128 input12 = values[12]; + Vector128 input13 = values[13]; + Vector128 input14 = values[14]; + Vector128 input15 = values[15]; + Vector128 input16 = values[16]; + Vector128 input17 = values[17]; + Vector128 input18 = values[18]; + Vector128 input19 = values[19]; + Vector128 input20 = values[20]; + Vector128 input21 = values[21]; + Vector128 input22 = values[22]; + Vector128 input23 = values[23]; + Vector128 input24 = values[24]; + Vector128 input25 = values[25]; + Vector128 input26 = values[26]; + Vector128 input27 = values[27]; + Vector128 input28 = values[28]; + Vector128 input29 = values[29]; + Vector128 input30 = values[30]; + Vector128 input31 = values[31]; + + Vector128 v0 = input0 + input31; + Vector128 v1 = (input0 - input31) * Vector128.Create(0.5006029982351963f); + Vector128 v2 = input1 + input30; + Vector128 v3 = (input1 - input30) * Vector128.Create(0.5054709598975436f); + Vector128 v4 = input2 + input29; + Vector128 v5 = (input2 - input29) * Vector128.Create(0.5154473099226246f); + Vector128 v6 = input3 + input28; + Vector128 v7 = (input3 - input28) * Vector128.Create(0.5310425910897841f); + Vector128 v8 = input4 + input27; + Vector128 v9 = (input4 - input27) * Vector128.Create(0.5531038960344445f); + Vector128 v10 = input5 + input26; + Vector128 v11 = (input5 - input26) * Vector128.Create(0.5829349682061339f); + Vector128 v12 = input6 + input25; + Vector128 v13 = (input6 - input25) * Vector128.Create(0.6225041230356648f); + Vector128 v14 = input7 + input24; + Vector128 v15 = (input7 - input24) * Vector128.Create(0.6748083414550057f); + Vector128 v16 = input8 + input23; + Vector128 v17 = (input8 - input23) * Vector128.Create(0.7445362710022986f); + Vector128 v18 = input9 + input22; + Vector128 v19 = (input9 - input22) * Vector128.Create(0.8393496454155268f); + Vector128 v20 = input10 + input21; + Vector128 v21 = (input10 - input21) * Vector128.Create(0.9725682378619608f); + Vector128 v22 = input11 + input20; + Vector128 v23 = (input11 - input20) * Vector128.Create(1.1694399334328847f); + Vector128 v24 = input12 + input19; + Vector128 v25 = (input12 - input19) * Vector128.Create(1.4841646163141662f); + Vector128 v26 = input13 + input18; + Vector128 v27 = (input13 - input18) * Vector128.Create(2.057781009953411f); + Vector128 v28 = input14 + input17; + Vector128 v29 = (input14 - input17) * Vector128.Create(3.407608418468719f); + Vector128 v30 = input15 + input16; + Vector128 v31 = (input15 - input16) * Vector128.Create(10.190008123548033f); + Vector128 v32 = v0 + v30; + Vector128 v33 = (v0 - v30) * Vector128.Create(0.5024192861881557f); + Vector128 v34 = v2 + v28; + Vector128 v35 = (v2 - v28) * Vector128.Create(0.5224986149396889f); + Vector128 v36 = v4 + v26; + Vector128 v37 = (v4 - v26) * Vector128.Create(0.5669440348163577f); + Vector128 v38 = v6 + v24; + Vector128 v39 = (v6 - v24) * Vector128.Create(0.6468217833599901f); + Vector128 v40 = v8 + v22; + Vector128 v41 = (v8 - v22) * Vector128.Create(0.7881546234512502f); + Vector128 v42 = v10 + v20; + Vector128 v43 = (v10 - v20) * Vector128.Create(1.060677685990347f); + Vector128 v44 = v12 + v18; + Vector128 v45 = (v12 - v18) * Vector128.Create(1.7224470982383342f); + Vector128 v46 = v14 + v16; + Vector128 v47 = (v14 - v16) * Vector128.Create(5.101148618689155f); + Vector128 v48 = v32 + v46; + Vector128 v49 = (v32 - v46) * Vector128.Create(0.5097955791041592f); + Vector128 v50 = v34 + v44; + Vector128 v51 = (v34 - v44) * Vector128.Create(0.6013448869350453f); + Vector128 v52 = v36 + v42; + Vector128 v53 = (v36 - v42) * Vector128.Create(0.8999762231364156f); + Vector128 v54 = v38 + v40; + Vector128 v55 = (v38 - v40) * Vector128.Create(2.5629154477415055f); + Vector128 v56 = v48 + v54; + Vector128 v57 = (v48 - v54) * Vector128.Create(0.541196100146197f); + Vector128 v58 = v50 + v52; + Vector128 v59 = (v50 - v52) * Vector128.Create(1.3065629648763764f); + Vector128 v60 = v56 + v58; + Vector128 v61 = v56 - v58; + Vector128 v62 = v57 + v59; + Vector128 v63 = v57 - v59; + Vector128 v64 = Vector128.MultiplyAddEstimate(v62, Vector128.Create(JxlDctScales.Sqrt2), v63); + Vector128 v65 = v49 + v55; + Vector128 v66 = (v49 - v55) * Vector128.Create(0.541196100146197f); + Vector128 v67 = v51 + v53; + Vector128 v68 = (v51 - v53) * Vector128.Create(1.3065629648763764f); + Vector128 v69 = v65 + v67; + Vector128 v70 = v65 - v67; + Vector128 v71 = v66 + v68; + Vector128 v72 = v66 - v68; + Vector128 v73 = Vector128.MultiplyAddEstimate(v71, Vector128.Create(JxlDctScales.Sqrt2), v72); + Vector128 v74 = Vector128.MultiplyAddEstimate(v69, Vector128.Create(JxlDctScales.Sqrt2), v73); + Vector128 v75 = v73 + v70; + Vector128 v76 = v70 + v72; + Vector128 v77 = v33 + v47; + Vector128 v78 = (v33 - v47) * Vector128.Create(0.5097955791041592f); + Vector128 v79 = v35 + v45; + Vector128 v80 = (v35 - v45) * Vector128.Create(0.6013448869350453f); + Vector128 v81 = v37 + v43; + Vector128 v82 = (v37 - v43) * Vector128.Create(0.8999762231364156f); + Vector128 v83 = v39 + v41; + Vector128 v84 = (v39 - v41) * Vector128.Create(2.5629154477415055f); + Vector128 v85 = v77 + v83; + Vector128 v86 = (v77 - v83) * Vector128.Create(0.541196100146197f); + Vector128 v87 = v79 + v81; + Vector128 v88 = (v79 - v81) * Vector128.Create(1.3065629648763764f); + Vector128 v89 = v85 + v87; + Vector128 v90 = v85 - v87; + Vector128 v91 = v86 + v88; + Vector128 v92 = v86 - v88; + Vector128 v93 = Vector128.MultiplyAddEstimate(v91, Vector128.Create(JxlDctScales.Sqrt2), v92); + Vector128 v94 = v78 + v84; + Vector128 v95 = (v78 - v84) * Vector128.Create(0.541196100146197f); + Vector128 v96 = v80 + v82; + Vector128 v97 = (v80 - v82) * Vector128.Create(1.3065629648763764f); + Vector128 v98 = v94 + v96; + Vector128 v99 = v94 - v96; + Vector128 v100 = v95 + v97; + Vector128 v101 = v95 - v97; + Vector128 v102 = Vector128.MultiplyAddEstimate(v100, Vector128.Create(JxlDctScales.Sqrt2), v101); + Vector128 v103 = Vector128.MultiplyAddEstimate(v98, Vector128.Create(JxlDctScales.Sqrt2), v102); + Vector128 v104 = v102 + v99; + Vector128 v105 = v99 + v101; + Vector128 v106 = Vector128.MultiplyAddEstimate(v89, Vector128.Create(JxlDctScales.Sqrt2), v103); + Vector128 v107 = v103 + v93; + Vector128 v108 = v93 + v104; + Vector128 v109 = v104 + v90; + Vector128 v110 = v90 + v105; + Vector128 v111 = v105 + v92; + Vector128 v112 = v92 + v101; + Vector128 v113 = v1 + v31; + Vector128 v114 = (v1 - v31) * Vector128.Create(0.5024192861881557f); + Vector128 v115 = v3 + v29; + Vector128 v116 = (v3 - v29) * Vector128.Create(0.5224986149396889f); + Vector128 v117 = v5 + v27; + Vector128 v118 = (v5 - v27) * Vector128.Create(0.5669440348163577f); + Vector128 v119 = v7 + v25; + Vector128 v120 = (v7 - v25) * Vector128.Create(0.6468217833599901f); + Vector128 v121 = v9 + v23; + Vector128 v122 = (v9 - v23) * Vector128.Create(0.7881546234512502f); + Vector128 v123 = v11 + v21; + Vector128 v124 = (v11 - v21) * Vector128.Create(1.060677685990347f); + Vector128 v125 = v13 + v19; + Vector128 v126 = (v13 - v19) * Vector128.Create(1.7224470982383342f); + Vector128 v127 = v15 + v17; + Vector128 v128 = (v15 - v17) * Vector128.Create(5.101148618689155f); + Vector128 v129 = v113 + v127; + Vector128 v130 = (v113 - v127) * Vector128.Create(0.5097955791041592f); + Vector128 v131 = v115 + v125; + Vector128 v132 = (v115 - v125) * Vector128.Create(0.6013448869350453f); + Vector128 v133 = v117 + v123; + Vector128 v134 = (v117 - v123) * Vector128.Create(0.8999762231364156f); + Vector128 v135 = v119 + v121; + Vector128 v136 = (v119 - v121) * Vector128.Create(2.5629154477415055f); + Vector128 v137 = v129 + v135; + Vector128 v138 = (v129 - v135) * Vector128.Create(0.541196100146197f); + Vector128 v139 = v131 + v133; + Vector128 v140 = (v131 - v133) * Vector128.Create(1.3065629648763764f); + Vector128 v141 = v137 + v139; + Vector128 v142 = v137 - v139; + Vector128 v143 = v138 + v140; + Vector128 v144 = v138 - v140; + Vector128 v145 = Vector128.MultiplyAddEstimate(v143, Vector128.Create(JxlDctScales.Sqrt2), v144); + Vector128 v146 = v130 + v136; + Vector128 v147 = (v130 - v136) * Vector128.Create(0.541196100146197f); + Vector128 v148 = v132 + v134; + Vector128 v149 = (v132 - v134) * Vector128.Create(1.3065629648763764f); + Vector128 v150 = v146 + v148; + Vector128 v151 = v146 - v148; + Vector128 v152 = v147 + v149; + Vector128 v153 = v147 - v149; + Vector128 v154 = Vector128.MultiplyAddEstimate(v152, Vector128.Create(JxlDctScales.Sqrt2), v153); + Vector128 v155 = Vector128.MultiplyAddEstimate(v150, Vector128.Create(JxlDctScales.Sqrt2), v154); + Vector128 v156 = v154 + v151; + Vector128 v157 = v151 + v153; + Vector128 v158 = v114 + v128; + Vector128 v159 = (v114 - v128) * Vector128.Create(0.5097955791041592f); + Vector128 v160 = v116 + v126; + Vector128 v161 = (v116 - v126) * Vector128.Create(0.6013448869350453f); + Vector128 v162 = v118 + v124; + Vector128 v163 = (v118 - v124) * Vector128.Create(0.8999762231364156f); + Vector128 v164 = v120 + v122; + Vector128 v165 = (v120 - v122) * Vector128.Create(2.5629154477415055f); + Vector128 v166 = v158 + v164; + Vector128 v167 = (v158 - v164) * Vector128.Create(0.541196100146197f); + Vector128 v168 = v160 + v162; + Vector128 v169 = (v160 - v162) * Vector128.Create(1.3065629648763764f); + Vector128 v170 = v166 + v168; + Vector128 v171 = v166 - v168; + Vector128 v172 = v167 + v169; + Vector128 v173 = v167 - v169; + Vector128 v174 = Vector128.MultiplyAddEstimate(v172, Vector128.Create(JxlDctScales.Sqrt2), v173); + Vector128 v175 = v159 + v165; + Vector128 v176 = (v159 - v165) * Vector128.Create(0.541196100146197f); + Vector128 v177 = v161 + v163; + Vector128 v178 = (v161 - v163) * Vector128.Create(1.3065629648763764f); + Vector128 v179 = v175 + v177; + Vector128 v180 = v175 - v177; + Vector128 v181 = v176 + v178; + Vector128 v182 = v176 - v178; + Vector128 v183 = Vector128.MultiplyAddEstimate(v181, Vector128.Create(JxlDctScales.Sqrt2), v182); + Vector128 v184 = Vector128.MultiplyAddEstimate(v179, Vector128.Create(JxlDctScales.Sqrt2), v183); + Vector128 v185 = v183 + v180; + Vector128 v186 = v180 + v182; + Vector128 v187 = Vector128.MultiplyAddEstimate(v170, Vector128.Create(JxlDctScales.Sqrt2), v184); + Vector128 v188 = v184 + v174; + Vector128 v189 = v174 + v185; + Vector128 v190 = v185 + v171; + Vector128 v191 = v171 + v186; + Vector128 v192 = v186 + v173; + Vector128 v193 = v173 + v182; + Vector128 v194 = Vector128.MultiplyAddEstimate(v141, Vector128.Create(JxlDctScales.Sqrt2), v187); + Vector128 v195 = v187 + v155; + Vector128 v196 = v155 + v188; + Vector128 v197 = v188 + v145; + Vector128 v198 = v145 + v189; + Vector128 v199 = v189 + v156; + Vector128 v200 = v156 + v190; + Vector128 v201 = v190 + v142; + Vector128 v202 = v142 + v191; + Vector128 v203 = v191 + v157; + Vector128 v204 = v157 + v192; + Vector128 v205 = v192 + v144; + Vector128 v206 = v144 + v193; + Vector128 v207 = v193 + v153; + Vector128 v208 = v153 + v182; + + values[0] = v60; + values[1] = v194; + values[2] = v106; + values[3] = v195; + values[4] = v74; + values[5] = v196; + values[6] = v107; + values[7] = v197; + values[8] = v64; + values[9] = v198; + values[10] = v108; + values[11] = v199; + values[12] = v75; + values[13] = v200; + values[14] = v109; + values[15] = v201; + values[16] = v61; + values[17] = v202; + values[18] = v110; + values[19] = v203; + values[20] = v76; + values[21] = v204; + values[22] = v111; + values[23] = v205; + values[24] = v63; + values[25] = v206; + values[26] = v112; + values[27] = v207; + values[28] = v72; + values[29] = v208; + values[30] = v101; + values[31] = v182; + } + + /// + /// Processes complete 4-lane forward batches along input columns. + /// + /// The number of positions in each transform. + /// The number of independent transforms. + /// The borrowed source matrix. + /// The source row stride, in floats. + /// The borrowed output matrix. + /// The destination row stride, in floats. + /// The first independent transform not already processed by a wider batch. + /// Borrowed storage for two length-by-4 banks. Only the first bank needs initialized inputs. + /// The first independent transform left after the complete batches. + private static int ForwardColumns128( + int length, + int count, + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int first, + Span workspace) + { + Span> values = MemoryMarshal.Cast>(workspace[..(length * 4)]); + Span> scratch = MemoryMarshal.Cast>(workspace.Slice(length * 4, length * 4)); + ref float input = ref MemoryMarshal.GetReference(source); + ref float output = ref MemoryMarshal.GetReference(destination); + Vector128 scale = Vector128.Create(1F / length); + + for (; first <= count - 4; first += 4) + { + // One vector covers adjacent columns at the same transform position. Physical strides never enter the coefficient-bank layout. + for (int i = 0; i < length; i++) + { + values[i] = Vector128.LoadUnsafe(ref input, (nuint)((i * sourceStride) + first)); + } + + Forward(values, scratch); + for (int i = 0; i < length; i++) + { + (values[i] * scale).StoreUnsafe(ref output, (nuint)((i * destinationStride) + first)); + } + } + + return first; + } + + /// + /// Processes complete 4-lane forward batches along input rows. + /// + /// The number of positions in each transform. + /// The number of independent transforms. + /// The borrowed source matrix. + /// The source row stride, in floats. + /// The borrowed output matrix. + /// The destination row stride, in floats. + /// Whether transform positions become output rows and input rows become output columns. + /// The first independent transform not already processed by a wider batch. + /// Borrowed storage for two length-by-4 banks. Only the first bank needs initialized inputs. + /// The first independent transform left after the complete batches. + private static int ForwardRows128( + int length, + int count, + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + bool transposed, + int first, + Span workspace) + { + Span> values = MemoryMarshal.Cast>(workspace[..(length * 4)]); + Span> scratch = MemoryMarshal.Cast>(workspace.Slice(length * 4, length * 4)); + ref float input = ref MemoryMarshal.GetReference(source); + ref float output = ref MemoryMarshal.GetReference(destination); + Vector128 scale = Vector128.Create(1F / length); + + for (; first <= count - 4; first += 4) + { + // Each square load contains adjacent samples from independent rows. Transposition puts those rows into SIMD lanes. + // The complete row band is loaded before any same-orientation output is written, so source and destination can be the same matrix. + for (int column = 0; column < length; column += 4) + { + Vector128 row0 = Vector128.LoadUnsafe(ref input, (nuint)(((first + 0) * sourceStride) + column)).AsInt32(); + Vector128 row1 = Vector128.LoadUnsafe(ref input, (nuint)(((first + 1) * sourceStride) + column)).AsInt32(); + Vector128 row2 = Vector128.LoadUnsafe(ref input, (nuint)(((first + 2) * sourceStride) + column)).AsInt32(); + Vector128 row3 = Vector128.LoadUnsafe(ref input, (nuint)(((first + 3) * sourceStride) + column)).AsInt32(); + + SimdUtils.Transpose(ref row0, ref row1, ref row2, ref row3); + values[column + 0] = row0.AsSingle(); + values[column + 1] = row1.AsSingle(); + values[column + 2] = row2.AsSingle(); + values[column + 3] = row3.AsSingle(); + } + + Forward(values, scratch); + if (transposed) + { + // A complete vector is one contiguous output row. Only logical samples are stored; destination padding remains untouched. + for (int i = 0; i < length; i++) + { + (values[i] * scale).StoreUnsafe(ref output, (nuint)((i * destinationStride) + first)); + } + } + else + { + // Restore row-major order one register tile at a time. This replaces a separate full-block transpose and copy. + for (int column = 0; column < length; column += 4) + { + Vector128 row0 = (values[column + 0] * scale).AsInt32(); + Vector128 row1 = (values[column + 1] * scale).AsInt32(); + Vector128 row2 = (values[column + 2] * scale).AsInt32(); + Vector128 row3 = (values[column + 3] * scale).AsInt32(); + + SimdUtils.Transpose(ref row0, ref row1, ref row2, ref row3); + row0.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 0) * destinationStride) + column)); + row1.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 1) * destinationStride) + column)); + row2.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 2) * destinationStride) + column)); + row3.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 3) * destinationStride) + column)); + } + } + } + + return first; + } + + /// + /// Computes independent inverse transforms using two nonoverlapping borrowed vector banks. + /// + /// One vector per transform position. Its length is a power of two from one through 256. + /// At least values.Length vectors. Previous contents are not read. + private static void Inverse(Span> values, Span> scratch) + { + // Small transforms stay in registers. The one-point transform is an identity and must not copy any trailing samples. + switch (values.Length) + { + case 1: + return; + case 2: + Inverse2(values); + return; + case 4: + Inverse4(values); + return; + case 8: + Inverse8(values); + return; + case 16: + Inverse16(values); + return; + case 32: + Inverse32(values); + return; + } + + int half = values.Length / 2; + // Separate frequency parity before either recursive call. The original bank can then serve as recursive scratch. + for (int i = 0; i < half; i++) + { + scratch[i] = values[2 * i]; + scratch[half + i] = values[(2 * i) + 1]; + } + + Inverse(scratch[..half], values); + + // Descend so every odd-frequency predecessor is read before its own entry changes. + for (int i = half - 1; i > 0; i--) + { + scratch[half + i] += scratch[half + i - 1]; + } + + scratch[half] *= Vector128.Create(JxlDctScales.Sqrt2); + Inverse(scratch.Slice(half, half), values); + ReadOnlySpan weights = JxlDctScales.GetMultipliers(values.Length); + + for (int i = 0; i < half; i++) + { + Vector128 weight = Vector128.Create(weights[i]); + Vector128 even = scratch[i]; + Vector128 odd = scratch[half + i]; + values[i] = Vector128.MultiplyAddEstimate(weight, odd, even); + values[values.Length - i - 1] = Vector128.MultiplyAddEstimate(-weight, odd, even); + } + } + + /// + /// Computes independent 2-point inverse transforms without intermediate memory passes. + /// + /// Exactly 2 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Inverse2(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector128 input0 = values[0]; + Vector128 input1 = values[1]; + + Vector128 v0 = input0 + input1; + Vector128 v1 = input0 - input1; + + values[0] = v0; + values[1] = v1; + } + + /// + /// Computes independent 4-point inverse transforms without intermediate memory passes. + /// + /// Exactly 4 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Inverse4(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector128 input0 = values[0]; + Vector128 input1 = values[1]; + Vector128 input2 = values[2]; + Vector128 input3 = values[3]; + + Vector128 v0 = input0 + input2; + Vector128 v1 = input0 - input2; + Vector128 v2 = input3 + input1; + Vector128 v3 = input1 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v4 = v3 + v2; + Vector128 v5 = v3 - v2; + Vector128 v6 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v4, v0); + Vector128 v7 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v4, v0); + Vector128 v8 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v5, v1); + Vector128 v9 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v5, v1); + + values[0] = v6; + values[1] = v8; + values[2] = v9; + values[3] = v7; + } + + /// + /// Computes independent 8-point inverse transforms without intermediate memory passes. + /// + /// Exactly 8 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Inverse8(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector128 input0 = values[0]; + Vector128 input1 = values[1]; + Vector128 input2 = values[2]; + Vector128 input3 = values[3]; + Vector128 input4 = values[4]; + Vector128 input5 = values[5]; + Vector128 input6 = values[6]; + Vector128 input7 = values[7]; + + Vector128 v0 = input0 + input4; + Vector128 v1 = input0 - input4; + Vector128 v2 = input6 + input2; + Vector128 v3 = input2 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v4 = v3 + v2; + Vector128 v5 = v3 - v2; + Vector128 v6 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v4, v0); + Vector128 v7 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v4, v0); + Vector128 v8 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v5, v1); + Vector128 v9 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v5, v1); + Vector128 v10 = input7 + input5; + Vector128 v11 = input5 + input3; + Vector128 v12 = input3 + input1; + Vector128 v13 = input1 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v14 = v13 + v11; + Vector128 v15 = v13 - v11; + Vector128 v16 = v10 + v12; + Vector128 v17 = v12 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v18 = v17 + v16; + Vector128 v19 = v17 - v16; + Vector128 v20 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v18, v14); + Vector128 v21 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v18, v14); + Vector128 v22 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v19, v15); + Vector128 v23 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v19, v15); + Vector128 v24 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5097955791041592f), v20, v6); + Vector128 v25 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5097955791041592f), v20, v6); + Vector128 v26 = Vector128.MultiplyAddEstimate(Vector128.Create(0.6013448869350453f), v22, v8); + Vector128 v27 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.6013448869350453f), v22, v8); + Vector128 v28 = Vector128.MultiplyAddEstimate(Vector128.Create(0.8999762231364156f), v23, v9); + Vector128 v29 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.8999762231364156f), v23, v9); + Vector128 v30 = Vector128.MultiplyAddEstimate(Vector128.Create(2.5629154477415055f), v21, v7); + Vector128 v31 = Vector128.MultiplyAddEstimate(-Vector128.Create(2.5629154477415055f), v21, v7); + + values[0] = v24; + values[1] = v26; + values[2] = v28; + values[3] = v30; + values[4] = v31; + values[5] = v29; + values[6] = v27; + values[7] = v25; + } + + /// + /// Computes independent 16-point inverse transforms without intermediate memory passes. + /// + /// Exactly 16 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Inverse16(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector128 input0 = values[0]; + Vector128 input1 = values[1]; + Vector128 input2 = values[2]; + Vector128 input3 = values[3]; + Vector128 input4 = values[4]; + Vector128 input5 = values[5]; + Vector128 input6 = values[6]; + Vector128 input7 = values[7]; + Vector128 input8 = values[8]; + Vector128 input9 = values[9]; + Vector128 input10 = values[10]; + Vector128 input11 = values[11]; + Vector128 input12 = values[12]; + Vector128 input13 = values[13]; + Vector128 input14 = values[14]; + Vector128 input15 = values[15]; + + Vector128 v0 = input0 + input8; + Vector128 v1 = input0 - input8; + Vector128 v2 = input12 + input4; + Vector128 v3 = input4 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v4 = v3 + v2; + Vector128 v5 = v3 - v2; + Vector128 v6 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v4, v0); + Vector128 v7 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v4, v0); + Vector128 v8 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v5, v1); + Vector128 v9 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v5, v1); + Vector128 v10 = input14 + input10; + Vector128 v11 = input10 + input6; + Vector128 v12 = input6 + input2; + Vector128 v13 = input2 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v14 = v13 + v11; + Vector128 v15 = v13 - v11; + Vector128 v16 = v10 + v12; + Vector128 v17 = v12 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v18 = v17 + v16; + Vector128 v19 = v17 - v16; + Vector128 v20 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v18, v14); + Vector128 v21 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v18, v14); + Vector128 v22 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v19, v15); + Vector128 v23 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v19, v15); + Vector128 v24 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5097955791041592f), v20, v6); + Vector128 v25 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5097955791041592f), v20, v6); + Vector128 v26 = Vector128.MultiplyAddEstimate(Vector128.Create(0.6013448869350453f), v22, v8); + Vector128 v27 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.6013448869350453f), v22, v8); + Vector128 v28 = Vector128.MultiplyAddEstimate(Vector128.Create(0.8999762231364156f), v23, v9); + Vector128 v29 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.8999762231364156f), v23, v9); + Vector128 v30 = Vector128.MultiplyAddEstimate(Vector128.Create(2.5629154477415055f), v21, v7); + Vector128 v31 = Vector128.MultiplyAddEstimate(-Vector128.Create(2.5629154477415055f), v21, v7); + Vector128 v32 = input15 + input13; + Vector128 v33 = input13 + input11; + Vector128 v34 = input11 + input9; + Vector128 v35 = input9 + input7; + Vector128 v36 = input7 + input5; + Vector128 v37 = input5 + input3; + Vector128 v38 = input3 + input1; + Vector128 v39 = input1 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v40 = v39 + v35; + Vector128 v41 = v39 - v35; + Vector128 v42 = v33 + v37; + Vector128 v43 = v37 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v44 = v43 + v42; + Vector128 v45 = v43 - v42; + Vector128 v46 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v44, v40); + Vector128 v47 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v44, v40); + Vector128 v48 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v45, v41); + Vector128 v49 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v45, v41); + Vector128 v50 = v32 + v34; + Vector128 v51 = v34 + v36; + Vector128 v52 = v36 + v38; + Vector128 v53 = v38 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v54 = v53 + v51; + Vector128 v55 = v53 - v51; + Vector128 v56 = v50 + v52; + Vector128 v57 = v52 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v58 = v57 + v56; + Vector128 v59 = v57 - v56; + Vector128 v60 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v58, v54); + Vector128 v61 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v58, v54); + Vector128 v62 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v59, v55); + Vector128 v63 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v59, v55); + Vector128 v64 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5097955791041592f), v60, v46); + Vector128 v65 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5097955791041592f), v60, v46); + Vector128 v66 = Vector128.MultiplyAddEstimate(Vector128.Create(0.6013448869350453f), v62, v48); + Vector128 v67 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.6013448869350453f), v62, v48); + Vector128 v68 = Vector128.MultiplyAddEstimate(Vector128.Create(0.8999762231364156f), v63, v49); + Vector128 v69 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.8999762231364156f), v63, v49); + Vector128 v70 = Vector128.MultiplyAddEstimate(Vector128.Create(2.5629154477415055f), v61, v47); + Vector128 v71 = Vector128.MultiplyAddEstimate(-Vector128.Create(2.5629154477415055f), v61, v47); + Vector128 v72 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5024192861881557f), v64, v24); + Vector128 v73 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5024192861881557f), v64, v24); + Vector128 v74 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5224986149396889f), v66, v26); + Vector128 v75 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5224986149396889f), v66, v26); + Vector128 v76 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5669440348163577f), v68, v28); + Vector128 v77 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5669440348163577f), v68, v28); + Vector128 v78 = Vector128.MultiplyAddEstimate(Vector128.Create(0.6468217833599901f), v70, v30); + Vector128 v79 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.6468217833599901f), v70, v30); + Vector128 v80 = Vector128.MultiplyAddEstimate(Vector128.Create(0.7881546234512502f), v71, v31); + Vector128 v81 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.7881546234512502f), v71, v31); + Vector128 v82 = Vector128.MultiplyAddEstimate(Vector128.Create(1.060677685990347f), v69, v29); + Vector128 v83 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.060677685990347f), v69, v29); + Vector128 v84 = Vector128.MultiplyAddEstimate(Vector128.Create(1.7224470982383342f), v67, v27); + Vector128 v85 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.7224470982383342f), v67, v27); + Vector128 v86 = Vector128.MultiplyAddEstimate(Vector128.Create(5.101148618689155f), v65, v25); + Vector128 v87 = Vector128.MultiplyAddEstimate(-Vector128.Create(5.101148618689155f), v65, v25); + + values[0] = v72; + values[1] = v74; + values[2] = v76; + values[3] = v78; + values[4] = v80; + values[5] = v82; + values[6] = v84; + values[7] = v86; + values[8] = v87; + values[9] = v85; + values[10] = v83; + values[11] = v81; + values[12] = v79; + values[13] = v77; + values[14] = v75; + values[15] = v73; + } + + /// + /// Computes independent 32-point inverse transforms without intermediate memory passes. + /// + /// Exactly 32 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Inverse32(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector128 input0 = values[0]; + Vector128 input1 = values[1]; + Vector128 input2 = values[2]; + Vector128 input3 = values[3]; + Vector128 input4 = values[4]; + Vector128 input5 = values[5]; + Vector128 input6 = values[6]; + Vector128 input7 = values[7]; + Vector128 input8 = values[8]; + Vector128 input9 = values[9]; + Vector128 input10 = values[10]; + Vector128 input11 = values[11]; + Vector128 input12 = values[12]; + Vector128 input13 = values[13]; + Vector128 input14 = values[14]; + Vector128 input15 = values[15]; + Vector128 input16 = values[16]; + Vector128 input17 = values[17]; + Vector128 input18 = values[18]; + Vector128 input19 = values[19]; + Vector128 input20 = values[20]; + Vector128 input21 = values[21]; + Vector128 input22 = values[22]; + Vector128 input23 = values[23]; + Vector128 input24 = values[24]; + Vector128 input25 = values[25]; + Vector128 input26 = values[26]; + Vector128 input27 = values[27]; + Vector128 input28 = values[28]; + Vector128 input29 = values[29]; + Vector128 input30 = values[30]; + Vector128 input31 = values[31]; + + Vector128 v0 = input0 + input16; + Vector128 v1 = input0 - input16; + Vector128 v2 = input24 + input8; + Vector128 v3 = input8 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v4 = v3 + v2; + Vector128 v5 = v3 - v2; + Vector128 v6 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v4, v0); + Vector128 v7 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v4, v0); + Vector128 v8 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v5, v1); + Vector128 v9 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v5, v1); + Vector128 v10 = input28 + input20; + Vector128 v11 = input20 + input12; + Vector128 v12 = input12 + input4; + Vector128 v13 = input4 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v14 = v13 + v11; + Vector128 v15 = v13 - v11; + Vector128 v16 = v10 + v12; + Vector128 v17 = v12 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v18 = v17 + v16; + Vector128 v19 = v17 - v16; + Vector128 v20 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v18, v14); + Vector128 v21 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v18, v14); + Vector128 v22 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v19, v15); + Vector128 v23 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v19, v15); + Vector128 v24 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5097955791041592f), v20, v6); + Vector128 v25 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5097955791041592f), v20, v6); + Vector128 v26 = Vector128.MultiplyAddEstimate(Vector128.Create(0.6013448869350453f), v22, v8); + Vector128 v27 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.6013448869350453f), v22, v8); + Vector128 v28 = Vector128.MultiplyAddEstimate(Vector128.Create(0.8999762231364156f), v23, v9); + Vector128 v29 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.8999762231364156f), v23, v9); + Vector128 v30 = Vector128.MultiplyAddEstimate(Vector128.Create(2.5629154477415055f), v21, v7); + Vector128 v31 = Vector128.MultiplyAddEstimate(-Vector128.Create(2.5629154477415055f), v21, v7); + Vector128 v32 = input30 + input26; + Vector128 v33 = input26 + input22; + Vector128 v34 = input22 + input18; + Vector128 v35 = input18 + input14; + Vector128 v36 = input14 + input10; + Vector128 v37 = input10 + input6; + Vector128 v38 = input6 + input2; + Vector128 v39 = input2 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v40 = v39 + v35; + Vector128 v41 = v39 - v35; + Vector128 v42 = v33 + v37; + Vector128 v43 = v37 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v44 = v43 + v42; + Vector128 v45 = v43 - v42; + Vector128 v46 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v44, v40); + Vector128 v47 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v44, v40); + Vector128 v48 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v45, v41); + Vector128 v49 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v45, v41); + Vector128 v50 = v32 + v34; + Vector128 v51 = v34 + v36; + Vector128 v52 = v36 + v38; + Vector128 v53 = v38 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v54 = v53 + v51; + Vector128 v55 = v53 - v51; + Vector128 v56 = v50 + v52; + Vector128 v57 = v52 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v58 = v57 + v56; + Vector128 v59 = v57 - v56; + Vector128 v60 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v58, v54); + Vector128 v61 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v58, v54); + Vector128 v62 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v59, v55); + Vector128 v63 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v59, v55); + Vector128 v64 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5097955791041592f), v60, v46); + Vector128 v65 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5097955791041592f), v60, v46); + Vector128 v66 = Vector128.MultiplyAddEstimate(Vector128.Create(0.6013448869350453f), v62, v48); + Vector128 v67 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.6013448869350453f), v62, v48); + Vector128 v68 = Vector128.MultiplyAddEstimate(Vector128.Create(0.8999762231364156f), v63, v49); + Vector128 v69 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.8999762231364156f), v63, v49); + Vector128 v70 = Vector128.MultiplyAddEstimate(Vector128.Create(2.5629154477415055f), v61, v47); + Vector128 v71 = Vector128.MultiplyAddEstimate(-Vector128.Create(2.5629154477415055f), v61, v47); + Vector128 v72 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5024192861881557f), v64, v24); + Vector128 v73 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5024192861881557f), v64, v24); + Vector128 v74 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5224986149396889f), v66, v26); + Vector128 v75 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5224986149396889f), v66, v26); + Vector128 v76 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5669440348163577f), v68, v28); + Vector128 v77 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5669440348163577f), v68, v28); + Vector128 v78 = Vector128.MultiplyAddEstimate(Vector128.Create(0.6468217833599901f), v70, v30); + Vector128 v79 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.6468217833599901f), v70, v30); + Vector128 v80 = Vector128.MultiplyAddEstimate(Vector128.Create(0.7881546234512502f), v71, v31); + Vector128 v81 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.7881546234512502f), v71, v31); + Vector128 v82 = Vector128.MultiplyAddEstimate(Vector128.Create(1.060677685990347f), v69, v29); + Vector128 v83 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.060677685990347f), v69, v29); + Vector128 v84 = Vector128.MultiplyAddEstimate(Vector128.Create(1.7224470982383342f), v67, v27); + Vector128 v85 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.7224470982383342f), v67, v27); + Vector128 v86 = Vector128.MultiplyAddEstimate(Vector128.Create(5.101148618689155f), v65, v25); + Vector128 v87 = Vector128.MultiplyAddEstimate(-Vector128.Create(5.101148618689155f), v65, v25); + Vector128 v88 = input31 + input29; + Vector128 v89 = input29 + input27; + Vector128 v90 = input27 + input25; + Vector128 v91 = input25 + input23; + Vector128 v92 = input23 + input21; + Vector128 v93 = input21 + input19; + Vector128 v94 = input19 + input17; + Vector128 v95 = input17 + input15; + Vector128 v96 = input15 + input13; + Vector128 v97 = input13 + input11; + Vector128 v98 = input11 + input9; + Vector128 v99 = input9 + input7; + Vector128 v100 = input7 + input5; + Vector128 v101 = input5 + input3; + Vector128 v102 = input3 + input1; + Vector128 v103 = input1 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v104 = v103 + v95; + Vector128 v105 = v103 - v95; + Vector128 v106 = v91 + v99; + Vector128 v107 = v99 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v108 = v107 + v106; + Vector128 v109 = v107 - v106; + Vector128 v110 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v108, v104); + Vector128 v111 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v108, v104); + Vector128 v112 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v109, v105); + Vector128 v113 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v109, v105); + Vector128 v114 = v89 + v93; + Vector128 v115 = v93 + v97; + Vector128 v116 = v97 + v101; + Vector128 v117 = v101 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v118 = v117 + v115; + Vector128 v119 = v117 - v115; + Vector128 v120 = v114 + v116; + Vector128 v121 = v116 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v122 = v121 + v120; + Vector128 v123 = v121 - v120; + Vector128 v124 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v122, v118); + Vector128 v125 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v122, v118); + Vector128 v126 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v123, v119); + Vector128 v127 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v123, v119); + Vector128 v128 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5097955791041592f), v124, v110); + Vector128 v129 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5097955791041592f), v124, v110); + Vector128 v130 = Vector128.MultiplyAddEstimate(Vector128.Create(0.6013448869350453f), v126, v112); + Vector128 v131 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.6013448869350453f), v126, v112); + Vector128 v132 = Vector128.MultiplyAddEstimate(Vector128.Create(0.8999762231364156f), v127, v113); + Vector128 v133 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.8999762231364156f), v127, v113); + Vector128 v134 = Vector128.MultiplyAddEstimate(Vector128.Create(2.5629154477415055f), v125, v111); + Vector128 v135 = Vector128.MultiplyAddEstimate(-Vector128.Create(2.5629154477415055f), v125, v111); + Vector128 v136 = v88 + v90; + Vector128 v137 = v90 + v92; + Vector128 v138 = v92 + v94; + Vector128 v139 = v94 + v96; + Vector128 v140 = v96 + v98; + Vector128 v141 = v98 + v100; + Vector128 v142 = v100 + v102; + Vector128 v143 = v102 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v144 = v143 + v139; + Vector128 v145 = v143 - v139; + Vector128 v146 = v137 + v141; + Vector128 v147 = v141 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v148 = v147 + v146; + Vector128 v149 = v147 - v146; + Vector128 v150 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v148, v144); + Vector128 v151 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v148, v144); + Vector128 v152 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v149, v145); + Vector128 v153 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v149, v145); + Vector128 v154 = v136 + v138; + Vector128 v155 = v138 + v140; + Vector128 v156 = v140 + v142; + Vector128 v157 = v142 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v158 = v157 + v155; + Vector128 v159 = v157 - v155; + Vector128 v160 = v154 + v156; + Vector128 v161 = v156 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v162 = v161 + v160; + Vector128 v163 = v161 - v160; + Vector128 v164 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v162, v158); + Vector128 v165 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v162, v158); + Vector128 v166 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v163, v159); + Vector128 v167 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v163, v159); + Vector128 v168 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5097955791041592f), v164, v150); + Vector128 v169 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5097955791041592f), v164, v150); + Vector128 v170 = Vector128.MultiplyAddEstimate(Vector128.Create(0.6013448869350453f), v166, v152); + Vector128 v171 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.6013448869350453f), v166, v152); + Vector128 v172 = Vector128.MultiplyAddEstimate(Vector128.Create(0.8999762231364156f), v167, v153); + Vector128 v173 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.8999762231364156f), v167, v153); + Vector128 v174 = Vector128.MultiplyAddEstimate(Vector128.Create(2.5629154477415055f), v165, v151); + Vector128 v175 = Vector128.MultiplyAddEstimate(-Vector128.Create(2.5629154477415055f), v165, v151); + Vector128 v176 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5024192861881557f), v168, v128); + Vector128 v177 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5024192861881557f), v168, v128); + Vector128 v178 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5224986149396889f), v170, v130); + Vector128 v179 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5224986149396889f), v170, v130); + Vector128 v180 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5669440348163577f), v172, v132); + Vector128 v181 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5669440348163577f), v172, v132); + Vector128 v182 = Vector128.MultiplyAddEstimate(Vector128.Create(0.6468217833599901f), v174, v134); + Vector128 v183 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.6468217833599901f), v174, v134); + Vector128 v184 = Vector128.MultiplyAddEstimate(Vector128.Create(0.7881546234512502f), v175, v135); + Vector128 v185 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.7881546234512502f), v175, v135); + Vector128 v186 = Vector128.MultiplyAddEstimate(Vector128.Create(1.060677685990347f), v173, v133); + Vector128 v187 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.060677685990347f), v173, v133); + Vector128 v188 = Vector128.MultiplyAddEstimate(Vector128.Create(1.7224470982383342f), v171, v131); + Vector128 v189 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.7224470982383342f), v171, v131); + Vector128 v190 = Vector128.MultiplyAddEstimate(Vector128.Create(5.101148618689155f), v169, v129); + Vector128 v191 = Vector128.MultiplyAddEstimate(-Vector128.Create(5.101148618689155f), v169, v129); + Vector128 v192 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5006029982351963f), v176, v72); + Vector128 v193 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5006029982351963f), v176, v72); + Vector128 v194 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5054709598975436f), v178, v74); + Vector128 v195 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5054709598975436f), v178, v74); + Vector128 v196 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5154473099226246f), v180, v76); + Vector128 v197 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5154473099226246f), v180, v76); + Vector128 v198 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5310425910897841f), v182, v78); + Vector128 v199 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5310425910897841f), v182, v78); + Vector128 v200 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5531038960344445f), v184, v80); + Vector128 v201 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5531038960344445f), v184, v80); + Vector128 v202 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5829349682061339f), v186, v82); + Vector128 v203 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5829349682061339f), v186, v82); + Vector128 v204 = Vector128.MultiplyAddEstimate(Vector128.Create(0.6225041230356648f), v188, v84); + Vector128 v205 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.6225041230356648f), v188, v84); + Vector128 v206 = Vector128.MultiplyAddEstimate(Vector128.Create(0.6748083414550057f), v190, v86); + Vector128 v207 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.6748083414550057f), v190, v86); + Vector128 v208 = Vector128.MultiplyAddEstimate(Vector128.Create(0.7445362710022986f), v191, v87); + Vector128 v209 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.7445362710022986f), v191, v87); + Vector128 v210 = Vector128.MultiplyAddEstimate(Vector128.Create(0.8393496454155268f), v189, v85); + Vector128 v211 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.8393496454155268f), v189, v85); + Vector128 v212 = Vector128.MultiplyAddEstimate(Vector128.Create(0.9725682378619608f), v187, v83); + Vector128 v213 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.9725682378619608f), v187, v83); + Vector128 v214 = Vector128.MultiplyAddEstimate(Vector128.Create(1.1694399334328847f), v185, v81); + Vector128 v215 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.1694399334328847f), v185, v81); + Vector128 v216 = Vector128.MultiplyAddEstimate(Vector128.Create(1.4841646163141662f), v183, v79); + Vector128 v217 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.4841646163141662f), v183, v79); + Vector128 v218 = Vector128.MultiplyAddEstimate(Vector128.Create(2.057781009953411f), v181, v77); + Vector128 v219 = Vector128.MultiplyAddEstimate(-Vector128.Create(2.057781009953411f), v181, v77); + Vector128 v220 = Vector128.MultiplyAddEstimate(Vector128.Create(3.407608418468719f), v179, v75); + Vector128 v221 = Vector128.MultiplyAddEstimate(-Vector128.Create(3.407608418468719f), v179, v75); + Vector128 v222 = Vector128.MultiplyAddEstimate(Vector128.Create(10.190008123548033f), v177, v73); + Vector128 v223 = Vector128.MultiplyAddEstimate(-Vector128.Create(10.190008123548033f), v177, v73); + + values[0] = v192; + values[1] = v194; + values[2] = v196; + values[3] = v198; + values[4] = v200; + values[5] = v202; + values[6] = v204; + values[7] = v206; + values[8] = v208; + values[9] = v210; + values[10] = v212; + values[11] = v214; + values[12] = v216; + values[13] = v218; + values[14] = v220; + values[15] = v222; + values[16] = v223; + values[17] = v221; + values[18] = v219; + values[19] = v217; + values[20] = v215; + values[21] = v213; + values[22] = v211; + values[23] = v209; + values[24] = v207; + values[25] = v205; + values[26] = v203; + values[27] = v201; + values[28] = v199; + values[29] = v197; + values[30] = v195; + values[31] = v193; + } + + /// + /// Processes complete 4-lane inverse batches along input columns. + /// + /// The number of positions in each transform. + /// The number of independent transforms. + /// The borrowed source matrix. + /// The source row stride, in floats. + /// The borrowed output matrix. + /// The destination row stride, in floats. + /// The first independent transform not already processed by a wider batch. + /// Borrowed storage for two length-by-4 banks. Only the first bank needs initialized inputs. + /// The first independent transform left after the complete batches. + private static int InverseColumns128( + int length, + int count, + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int first, + Span workspace) + { + Span> values = MemoryMarshal.Cast>(workspace[..(length * 4)]); + Span> scratch = MemoryMarshal.Cast>(workspace.Slice(length * 4, length * 4)); + ref float input = ref MemoryMarshal.GetReference(source); + ref float output = ref MemoryMarshal.GetReference(destination); + + for (; first <= count - 4; first += 4) + { + // One vector covers adjacent columns at the same transform position. Physical strides never enter the coefficient-bank layout. + for (int i = 0; i < length; i++) + { + values[i] = Vector128.LoadUnsafe(ref input, (nuint)((i * sourceStride) + first)); + } + + Inverse(values, scratch); + for (int i = 0; i < length; i++) + { + values[i].StoreUnsafe(ref output, (nuint)((i * destinationStride) + first)); + } + } + + return first; + } + + /// + /// Processes complete 4-lane inverse batches along input rows. + /// + /// The number of positions in each transform. + /// The number of independent transforms. + /// The borrowed source matrix. + /// The source row stride, in floats. + /// The borrowed output matrix. + /// The destination row stride, in floats. + /// Whether transform positions become output rows and input rows become output columns. + /// The first independent transform not already processed by a wider batch. + /// Borrowed storage for two length-by-4 banks. Only the first bank needs initialized inputs. + /// The first independent transform left after the complete batches. + private static int InverseRows128( + int length, + int count, + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + bool transposed, + int first, + Span workspace) + { + Span> values = MemoryMarshal.Cast>(workspace[..(length * 4)]); + Span> scratch = MemoryMarshal.Cast>(workspace.Slice(length * 4, length * 4)); + ref float input = ref MemoryMarshal.GetReference(source); + ref float output = ref MemoryMarshal.GetReference(destination); + + for (; first <= count - 4; first += 4) + { + // Each square load contains adjacent samples from independent rows. Transposition puts those rows into SIMD lanes. + // The complete row band is loaded before any same-orientation output is written, so source and destination can be the same matrix. + for (int column = 0; column < length; column += 4) + { + Vector128 row0 = Vector128.LoadUnsafe(ref input, (nuint)(((first + 0) * sourceStride) + column)).AsInt32(); + Vector128 row1 = Vector128.LoadUnsafe(ref input, (nuint)(((first + 1) * sourceStride) + column)).AsInt32(); + Vector128 row2 = Vector128.LoadUnsafe(ref input, (nuint)(((first + 2) * sourceStride) + column)).AsInt32(); + Vector128 row3 = Vector128.LoadUnsafe(ref input, (nuint)(((first + 3) * sourceStride) + column)).AsInt32(); + + SimdUtils.Transpose(ref row0, ref row1, ref row2, ref row3); + values[column + 0] = row0.AsSingle(); + values[column + 1] = row1.AsSingle(); + values[column + 2] = row2.AsSingle(); + values[column + 3] = row3.AsSingle(); + } + + Inverse(values, scratch); + if (transposed) + { + // A complete vector is one contiguous output row. Only logical samples are stored; destination padding remains untouched. + for (int i = 0; i < length; i++) + { + values[i].StoreUnsafe(ref output, (nuint)((i * destinationStride) + first)); + } + } + else + { + // Restore row-major order one register tile at a time. This replaces a separate full-block transpose and copy. + for (int column = 0; column < length; column += 4) + { + Vector128 row0 = values[column + 0].AsInt32(); + Vector128 row1 = values[column + 1].AsInt32(); + Vector128 row2 = values[column + 2].AsInt32(); + Vector128 row3 = values[column + 3].AsInt32(); + + SimdUtils.Transpose(ref row0, ref row1, ref row2, ref row3); + row0.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 0) * destinationStride) + column)); + row1.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 1) * destinationStride) + column)); + row2.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 2) * destinationStride) + column)); + row3.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 3) * destinationStride) + column)); + } + } + } + + return first; + } + + /// + /// Computes independent forward transforms using two nonoverlapping borrowed vector banks. + /// + /// One vector per transform position. Its length is a power of two from one through 256. + /// At least values.Length vectors. Previous contents are not read. + private static void Forward(Span> values, Span> scratch) + { + // Small transforms stay in registers. The one-point transform is an identity and must not copy any trailing samples. + switch (values.Length) + { + case 1: + return; + case 2: + Forward2(values); + return; + case 4: + Forward4(values); + return; + case 8: + Forward8(values); + return; + case 16: + Forward16(values); + return; + case 32: + Forward32(values); + return; + } + + int half = values.Length / 2; + ReadOnlySpan weights = JxlDctScales.GetMultipliers(values.Length); + + // Consume both original halves before reusing their bank as recursive scratch. Neither recursive call can then destroy unread input. + for (int i = 0; i < half; i++) + { + Vector256 first = values[i]; + Vector256 last = values[values.Length - i - 1]; + scratch[i] = first + last; + scratch[half + i] = (first - last) * Vector256.Create(weights[i]); + } + + Forward(scratch[..half], values); + Forward(scratch.Slice(half, half), values); + scratch[half] = Vector256.MultiplyAddEstimate(scratch[half], Vector256.Create(JxlDctScales.Sqrt2), scratch[half + 1]); + + for (int i = 1; i + 1 < half; i++) + { + scratch[half + i] += scratch[half + i + 1]; + } + + for (int i = 0; i < half; i++) + { + values[2 * i] = scratch[i]; + values[(2 * i) + 1] = scratch[half + i]; + } + } + + /// + /// Computes independent 2-point forward transforms without intermediate memory passes. + /// + /// Exactly 2 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Forward2(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector256 input0 = values[0]; + Vector256 input1 = values[1]; + + Vector256 v0 = input0 + input1; + Vector256 v1 = input0 - input1; + + values[0] = v0; + values[1] = v1; + } + + /// + /// Computes independent 4-point forward transforms without intermediate memory passes. + /// + /// Exactly 4 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Forward4(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector256 input0 = values[0]; + Vector256 input1 = values[1]; + Vector256 input2 = values[2]; + Vector256 input3 = values[3]; + + Vector256 v0 = input0 + input3; + Vector256 v1 = (input0 - input3) * Vector256.Create(0.541196100146197f); + Vector256 v2 = input1 + input2; + Vector256 v3 = (input1 - input2) * Vector256.Create(1.3065629648763764f); + Vector256 v4 = v0 + v2; + Vector256 v5 = v0 - v2; + Vector256 v6 = v1 + v3; + Vector256 v7 = v1 - v3; + Vector256 v8 = Vector256.MultiplyAddEstimate(v6, Vector256.Create(JxlDctScales.Sqrt2), v7); + + values[0] = v4; + values[1] = v8; + values[2] = v5; + values[3] = v7; + } + + /// + /// Computes independent 8-point forward transforms without intermediate memory passes. + /// + /// Exactly 8 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Forward8(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector256 input0 = values[0]; + Vector256 input1 = values[1]; + Vector256 input2 = values[2]; + Vector256 input3 = values[3]; + Vector256 input4 = values[4]; + Vector256 input5 = values[5]; + Vector256 input6 = values[6]; + Vector256 input7 = values[7]; + + Vector256 v0 = input0 + input7; + Vector256 v1 = (input0 - input7) * Vector256.Create(0.5097955791041592f); + Vector256 v2 = input1 + input6; + Vector256 v3 = (input1 - input6) * Vector256.Create(0.6013448869350453f); + Vector256 v4 = input2 + input5; + Vector256 v5 = (input2 - input5) * Vector256.Create(0.8999762231364156f); + Vector256 v6 = input3 + input4; + Vector256 v7 = (input3 - input4) * Vector256.Create(2.5629154477415055f); + Vector256 v8 = v0 + v6; + Vector256 v9 = (v0 - v6) * Vector256.Create(0.541196100146197f); + Vector256 v10 = v2 + v4; + Vector256 v11 = (v2 - v4) * Vector256.Create(1.3065629648763764f); + Vector256 v12 = v8 + v10; + Vector256 v13 = v8 - v10; + Vector256 v14 = v9 + v11; + Vector256 v15 = v9 - v11; + Vector256 v16 = Vector256.MultiplyAddEstimate(v14, Vector256.Create(JxlDctScales.Sqrt2), v15); + Vector256 v17 = v1 + v7; + Vector256 v18 = (v1 - v7) * Vector256.Create(0.541196100146197f); + Vector256 v19 = v3 + v5; + Vector256 v20 = (v3 - v5) * Vector256.Create(1.3065629648763764f); + Vector256 v21 = v17 + v19; + Vector256 v22 = v17 - v19; + Vector256 v23 = v18 + v20; + Vector256 v24 = v18 - v20; + Vector256 v25 = Vector256.MultiplyAddEstimate(v23, Vector256.Create(JxlDctScales.Sqrt2), v24); + Vector256 v26 = Vector256.MultiplyAddEstimate(v21, Vector256.Create(JxlDctScales.Sqrt2), v25); + Vector256 v27 = v25 + v22; + Vector256 v28 = v22 + v24; + + values[0] = v12; + values[1] = v26; + values[2] = v16; + values[3] = v27; + values[4] = v13; + values[5] = v28; + values[6] = v15; + values[7] = v24; + } + + /// + /// Computes independent 16-point forward transforms without intermediate memory passes. + /// + /// Exactly 16 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Forward16(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector256 input0 = values[0]; + Vector256 input1 = values[1]; + Vector256 input2 = values[2]; + Vector256 input3 = values[3]; + Vector256 input4 = values[4]; + Vector256 input5 = values[5]; + Vector256 input6 = values[6]; + Vector256 input7 = values[7]; + Vector256 input8 = values[8]; + Vector256 input9 = values[9]; + Vector256 input10 = values[10]; + Vector256 input11 = values[11]; + Vector256 input12 = values[12]; + Vector256 input13 = values[13]; + Vector256 input14 = values[14]; + Vector256 input15 = values[15]; + + Vector256 v0 = input0 + input15; + Vector256 v1 = (input0 - input15) * Vector256.Create(0.5024192861881557f); + Vector256 v2 = input1 + input14; + Vector256 v3 = (input1 - input14) * Vector256.Create(0.5224986149396889f); + Vector256 v4 = input2 + input13; + Vector256 v5 = (input2 - input13) * Vector256.Create(0.5669440348163577f); + Vector256 v6 = input3 + input12; + Vector256 v7 = (input3 - input12) * Vector256.Create(0.6468217833599901f); + Vector256 v8 = input4 + input11; + Vector256 v9 = (input4 - input11) * Vector256.Create(0.7881546234512502f); + Vector256 v10 = input5 + input10; + Vector256 v11 = (input5 - input10) * Vector256.Create(1.060677685990347f); + Vector256 v12 = input6 + input9; + Vector256 v13 = (input6 - input9) * Vector256.Create(1.7224470982383342f); + Vector256 v14 = input7 + input8; + Vector256 v15 = (input7 - input8) * Vector256.Create(5.101148618689155f); + Vector256 v16 = v0 + v14; + Vector256 v17 = (v0 - v14) * Vector256.Create(0.5097955791041592f); + Vector256 v18 = v2 + v12; + Vector256 v19 = (v2 - v12) * Vector256.Create(0.6013448869350453f); + Vector256 v20 = v4 + v10; + Vector256 v21 = (v4 - v10) * Vector256.Create(0.8999762231364156f); + Vector256 v22 = v6 + v8; + Vector256 v23 = (v6 - v8) * Vector256.Create(2.5629154477415055f); + Vector256 v24 = v16 + v22; + Vector256 v25 = (v16 - v22) * Vector256.Create(0.541196100146197f); + Vector256 v26 = v18 + v20; + Vector256 v27 = (v18 - v20) * Vector256.Create(1.3065629648763764f); + Vector256 v28 = v24 + v26; + Vector256 v29 = v24 - v26; + Vector256 v30 = v25 + v27; + Vector256 v31 = v25 - v27; + Vector256 v32 = Vector256.MultiplyAddEstimate(v30, Vector256.Create(JxlDctScales.Sqrt2), v31); + Vector256 v33 = v17 + v23; + Vector256 v34 = (v17 - v23) * Vector256.Create(0.541196100146197f); + Vector256 v35 = v19 + v21; + Vector256 v36 = (v19 - v21) * Vector256.Create(1.3065629648763764f); + Vector256 v37 = v33 + v35; + Vector256 v38 = v33 - v35; + Vector256 v39 = v34 + v36; + Vector256 v40 = v34 - v36; + Vector256 v41 = Vector256.MultiplyAddEstimate(v39, Vector256.Create(JxlDctScales.Sqrt2), v40); + Vector256 v42 = Vector256.MultiplyAddEstimate(v37, Vector256.Create(JxlDctScales.Sqrt2), v41); + Vector256 v43 = v41 + v38; + Vector256 v44 = v38 + v40; + Vector256 v45 = v1 + v15; + Vector256 v46 = (v1 - v15) * Vector256.Create(0.5097955791041592f); + Vector256 v47 = v3 + v13; + Vector256 v48 = (v3 - v13) * Vector256.Create(0.6013448869350453f); + Vector256 v49 = v5 + v11; + Vector256 v50 = (v5 - v11) * Vector256.Create(0.8999762231364156f); + Vector256 v51 = v7 + v9; + Vector256 v52 = (v7 - v9) * Vector256.Create(2.5629154477415055f); + Vector256 v53 = v45 + v51; + Vector256 v54 = (v45 - v51) * Vector256.Create(0.541196100146197f); + Vector256 v55 = v47 + v49; + Vector256 v56 = (v47 - v49) * Vector256.Create(1.3065629648763764f); + Vector256 v57 = v53 + v55; + Vector256 v58 = v53 - v55; + Vector256 v59 = v54 + v56; + Vector256 v60 = v54 - v56; + Vector256 v61 = Vector256.MultiplyAddEstimate(v59, Vector256.Create(JxlDctScales.Sqrt2), v60); + Vector256 v62 = v46 + v52; + Vector256 v63 = (v46 - v52) * Vector256.Create(0.541196100146197f); + Vector256 v64 = v48 + v50; + Vector256 v65 = (v48 - v50) * Vector256.Create(1.3065629648763764f); + Vector256 v66 = v62 + v64; + Vector256 v67 = v62 - v64; + Vector256 v68 = v63 + v65; + Vector256 v69 = v63 - v65; + Vector256 v70 = Vector256.MultiplyAddEstimate(v68, Vector256.Create(JxlDctScales.Sqrt2), v69); + Vector256 v71 = Vector256.MultiplyAddEstimate(v66, Vector256.Create(JxlDctScales.Sqrt2), v70); + Vector256 v72 = v70 + v67; + Vector256 v73 = v67 + v69; + Vector256 v74 = Vector256.MultiplyAddEstimate(v57, Vector256.Create(JxlDctScales.Sqrt2), v71); + Vector256 v75 = v71 + v61; + Vector256 v76 = v61 + v72; + Vector256 v77 = v72 + v58; + Vector256 v78 = v58 + v73; + Vector256 v79 = v73 + v60; + Vector256 v80 = v60 + v69; + + values[0] = v28; + values[1] = v74; + values[2] = v42; + values[3] = v75; + values[4] = v32; + values[5] = v76; + values[6] = v43; + values[7] = v77; + values[8] = v29; + values[9] = v78; + values[10] = v44; + values[11] = v79; + values[12] = v31; + values[13] = v80; + values[14] = v40; + values[15] = v69; + } + + /// + /// Computes independent 32-point forward transforms without intermediate memory passes. + /// + /// Exactly 32 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Forward32(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector256 input0 = values[0]; + Vector256 input1 = values[1]; + Vector256 input2 = values[2]; + Vector256 input3 = values[3]; + Vector256 input4 = values[4]; + Vector256 input5 = values[5]; + Vector256 input6 = values[6]; + Vector256 input7 = values[7]; + Vector256 input8 = values[8]; + Vector256 input9 = values[9]; + Vector256 input10 = values[10]; + Vector256 input11 = values[11]; + Vector256 input12 = values[12]; + Vector256 input13 = values[13]; + Vector256 input14 = values[14]; + Vector256 input15 = values[15]; + Vector256 input16 = values[16]; + Vector256 input17 = values[17]; + Vector256 input18 = values[18]; + Vector256 input19 = values[19]; + Vector256 input20 = values[20]; + Vector256 input21 = values[21]; + Vector256 input22 = values[22]; + Vector256 input23 = values[23]; + Vector256 input24 = values[24]; + Vector256 input25 = values[25]; + Vector256 input26 = values[26]; + Vector256 input27 = values[27]; + Vector256 input28 = values[28]; + Vector256 input29 = values[29]; + Vector256 input30 = values[30]; + Vector256 input31 = values[31]; + + Vector256 v0 = input0 + input31; + Vector256 v1 = (input0 - input31) * Vector256.Create(0.5006029982351963f); + Vector256 v2 = input1 + input30; + Vector256 v3 = (input1 - input30) * Vector256.Create(0.5054709598975436f); + Vector256 v4 = input2 + input29; + Vector256 v5 = (input2 - input29) * Vector256.Create(0.5154473099226246f); + Vector256 v6 = input3 + input28; + Vector256 v7 = (input3 - input28) * Vector256.Create(0.5310425910897841f); + Vector256 v8 = input4 + input27; + Vector256 v9 = (input4 - input27) * Vector256.Create(0.5531038960344445f); + Vector256 v10 = input5 + input26; + Vector256 v11 = (input5 - input26) * Vector256.Create(0.5829349682061339f); + Vector256 v12 = input6 + input25; + Vector256 v13 = (input6 - input25) * Vector256.Create(0.6225041230356648f); + Vector256 v14 = input7 + input24; + Vector256 v15 = (input7 - input24) * Vector256.Create(0.6748083414550057f); + Vector256 v16 = input8 + input23; + Vector256 v17 = (input8 - input23) * Vector256.Create(0.7445362710022986f); + Vector256 v18 = input9 + input22; + Vector256 v19 = (input9 - input22) * Vector256.Create(0.8393496454155268f); + Vector256 v20 = input10 + input21; + Vector256 v21 = (input10 - input21) * Vector256.Create(0.9725682378619608f); + Vector256 v22 = input11 + input20; + Vector256 v23 = (input11 - input20) * Vector256.Create(1.1694399334328847f); + Vector256 v24 = input12 + input19; + Vector256 v25 = (input12 - input19) * Vector256.Create(1.4841646163141662f); + Vector256 v26 = input13 + input18; + Vector256 v27 = (input13 - input18) * Vector256.Create(2.057781009953411f); + Vector256 v28 = input14 + input17; + Vector256 v29 = (input14 - input17) * Vector256.Create(3.407608418468719f); + Vector256 v30 = input15 + input16; + Vector256 v31 = (input15 - input16) * Vector256.Create(10.190008123548033f); + Vector256 v32 = v0 + v30; + Vector256 v33 = (v0 - v30) * Vector256.Create(0.5024192861881557f); + Vector256 v34 = v2 + v28; + Vector256 v35 = (v2 - v28) * Vector256.Create(0.5224986149396889f); + Vector256 v36 = v4 + v26; + Vector256 v37 = (v4 - v26) * Vector256.Create(0.5669440348163577f); + Vector256 v38 = v6 + v24; + Vector256 v39 = (v6 - v24) * Vector256.Create(0.6468217833599901f); + Vector256 v40 = v8 + v22; + Vector256 v41 = (v8 - v22) * Vector256.Create(0.7881546234512502f); + Vector256 v42 = v10 + v20; + Vector256 v43 = (v10 - v20) * Vector256.Create(1.060677685990347f); + Vector256 v44 = v12 + v18; + Vector256 v45 = (v12 - v18) * Vector256.Create(1.7224470982383342f); + Vector256 v46 = v14 + v16; + Vector256 v47 = (v14 - v16) * Vector256.Create(5.101148618689155f); + Vector256 v48 = v32 + v46; + Vector256 v49 = (v32 - v46) * Vector256.Create(0.5097955791041592f); + Vector256 v50 = v34 + v44; + Vector256 v51 = (v34 - v44) * Vector256.Create(0.6013448869350453f); + Vector256 v52 = v36 + v42; + Vector256 v53 = (v36 - v42) * Vector256.Create(0.8999762231364156f); + Vector256 v54 = v38 + v40; + Vector256 v55 = (v38 - v40) * Vector256.Create(2.5629154477415055f); + Vector256 v56 = v48 + v54; + Vector256 v57 = (v48 - v54) * Vector256.Create(0.541196100146197f); + Vector256 v58 = v50 + v52; + Vector256 v59 = (v50 - v52) * Vector256.Create(1.3065629648763764f); + Vector256 v60 = v56 + v58; + Vector256 v61 = v56 - v58; + Vector256 v62 = v57 + v59; + Vector256 v63 = v57 - v59; + Vector256 v64 = Vector256.MultiplyAddEstimate(v62, Vector256.Create(JxlDctScales.Sqrt2), v63); + Vector256 v65 = v49 + v55; + Vector256 v66 = (v49 - v55) * Vector256.Create(0.541196100146197f); + Vector256 v67 = v51 + v53; + Vector256 v68 = (v51 - v53) * Vector256.Create(1.3065629648763764f); + Vector256 v69 = v65 + v67; + Vector256 v70 = v65 - v67; + Vector256 v71 = v66 + v68; + Vector256 v72 = v66 - v68; + Vector256 v73 = Vector256.MultiplyAddEstimate(v71, Vector256.Create(JxlDctScales.Sqrt2), v72); + Vector256 v74 = Vector256.MultiplyAddEstimate(v69, Vector256.Create(JxlDctScales.Sqrt2), v73); + Vector256 v75 = v73 + v70; + Vector256 v76 = v70 + v72; + Vector256 v77 = v33 + v47; + Vector256 v78 = (v33 - v47) * Vector256.Create(0.5097955791041592f); + Vector256 v79 = v35 + v45; + Vector256 v80 = (v35 - v45) * Vector256.Create(0.6013448869350453f); + Vector256 v81 = v37 + v43; + Vector256 v82 = (v37 - v43) * Vector256.Create(0.8999762231364156f); + Vector256 v83 = v39 + v41; + Vector256 v84 = (v39 - v41) * Vector256.Create(2.5629154477415055f); + Vector256 v85 = v77 + v83; + Vector256 v86 = (v77 - v83) * Vector256.Create(0.541196100146197f); + Vector256 v87 = v79 + v81; + Vector256 v88 = (v79 - v81) * Vector256.Create(1.3065629648763764f); + Vector256 v89 = v85 + v87; + Vector256 v90 = v85 - v87; + Vector256 v91 = v86 + v88; + Vector256 v92 = v86 - v88; + Vector256 v93 = Vector256.MultiplyAddEstimate(v91, Vector256.Create(JxlDctScales.Sqrt2), v92); + Vector256 v94 = v78 + v84; + Vector256 v95 = (v78 - v84) * Vector256.Create(0.541196100146197f); + Vector256 v96 = v80 + v82; + Vector256 v97 = (v80 - v82) * Vector256.Create(1.3065629648763764f); + Vector256 v98 = v94 + v96; + Vector256 v99 = v94 - v96; + Vector256 v100 = v95 + v97; + Vector256 v101 = v95 - v97; + Vector256 v102 = Vector256.MultiplyAddEstimate(v100, Vector256.Create(JxlDctScales.Sqrt2), v101); + Vector256 v103 = Vector256.MultiplyAddEstimate(v98, Vector256.Create(JxlDctScales.Sqrt2), v102); + Vector256 v104 = v102 + v99; + Vector256 v105 = v99 + v101; + Vector256 v106 = Vector256.MultiplyAddEstimate(v89, Vector256.Create(JxlDctScales.Sqrt2), v103); + Vector256 v107 = v103 + v93; + Vector256 v108 = v93 + v104; + Vector256 v109 = v104 + v90; + Vector256 v110 = v90 + v105; + Vector256 v111 = v105 + v92; + Vector256 v112 = v92 + v101; + Vector256 v113 = v1 + v31; + Vector256 v114 = (v1 - v31) * Vector256.Create(0.5024192861881557f); + Vector256 v115 = v3 + v29; + Vector256 v116 = (v3 - v29) * Vector256.Create(0.5224986149396889f); + Vector256 v117 = v5 + v27; + Vector256 v118 = (v5 - v27) * Vector256.Create(0.5669440348163577f); + Vector256 v119 = v7 + v25; + Vector256 v120 = (v7 - v25) * Vector256.Create(0.6468217833599901f); + Vector256 v121 = v9 + v23; + Vector256 v122 = (v9 - v23) * Vector256.Create(0.7881546234512502f); + Vector256 v123 = v11 + v21; + Vector256 v124 = (v11 - v21) * Vector256.Create(1.060677685990347f); + Vector256 v125 = v13 + v19; + Vector256 v126 = (v13 - v19) * Vector256.Create(1.7224470982383342f); + Vector256 v127 = v15 + v17; + Vector256 v128 = (v15 - v17) * Vector256.Create(5.101148618689155f); + Vector256 v129 = v113 + v127; + Vector256 v130 = (v113 - v127) * Vector256.Create(0.5097955791041592f); + Vector256 v131 = v115 + v125; + Vector256 v132 = (v115 - v125) * Vector256.Create(0.6013448869350453f); + Vector256 v133 = v117 + v123; + Vector256 v134 = (v117 - v123) * Vector256.Create(0.8999762231364156f); + Vector256 v135 = v119 + v121; + Vector256 v136 = (v119 - v121) * Vector256.Create(2.5629154477415055f); + Vector256 v137 = v129 + v135; + Vector256 v138 = (v129 - v135) * Vector256.Create(0.541196100146197f); + Vector256 v139 = v131 + v133; + Vector256 v140 = (v131 - v133) * Vector256.Create(1.3065629648763764f); + Vector256 v141 = v137 + v139; + Vector256 v142 = v137 - v139; + Vector256 v143 = v138 + v140; + Vector256 v144 = v138 - v140; + Vector256 v145 = Vector256.MultiplyAddEstimate(v143, Vector256.Create(JxlDctScales.Sqrt2), v144); + Vector256 v146 = v130 + v136; + Vector256 v147 = (v130 - v136) * Vector256.Create(0.541196100146197f); + Vector256 v148 = v132 + v134; + Vector256 v149 = (v132 - v134) * Vector256.Create(1.3065629648763764f); + Vector256 v150 = v146 + v148; + Vector256 v151 = v146 - v148; + Vector256 v152 = v147 + v149; + Vector256 v153 = v147 - v149; + Vector256 v154 = Vector256.MultiplyAddEstimate(v152, Vector256.Create(JxlDctScales.Sqrt2), v153); + Vector256 v155 = Vector256.MultiplyAddEstimate(v150, Vector256.Create(JxlDctScales.Sqrt2), v154); + Vector256 v156 = v154 + v151; + Vector256 v157 = v151 + v153; + Vector256 v158 = v114 + v128; + Vector256 v159 = (v114 - v128) * Vector256.Create(0.5097955791041592f); + Vector256 v160 = v116 + v126; + Vector256 v161 = (v116 - v126) * Vector256.Create(0.6013448869350453f); + Vector256 v162 = v118 + v124; + Vector256 v163 = (v118 - v124) * Vector256.Create(0.8999762231364156f); + Vector256 v164 = v120 + v122; + Vector256 v165 = (v120 - v122) * Vector256.Create(2.5629154477415055f); + Vector256 v166 = v158 + v164; + Vector256 v167 = (v158 - v164) * Vector256.Create(0.541196100146197f); + Vector256 v168 = v160 + v162; + Vector256 v169 = (v160 - v162) * Vector256.Create(1.3065629648763764f); + Vector256 v170 = v166 + v168; + Vector256 v171 = v166 - v168; + Vector256 v172 = v167 + v169; + Vector256 v173 = v167 - v169; + Vector256 v174 = Vector256.MultiplyAddEstimate(v172, Vector256.Create(JxlDctScales.Sqrt2), v173); + Vector256 v175 = v159 + v165; + Vector256 v176 = (v159 - v165) * Vector256.Create(0.541196100146197f); + Vector256 v177 = v161 + v163; + Vector256 v178 = (v161 - v163) * Vector256.Create(1.3065629648763764f); + Vector256 v179 = v175 + v177; + Vector256 v180 = v175 - v177; + Vector256 v181 = v176 + v178; + Vector256 v182 = v176 - v178; + Vector256 v183 = Vector256.MultiplyAddEstimate(v181, Vector256.Create(JxlDctScales.Sqrt2), v182); + Vector256 v184 = Vector256.MultiplyAddEstimate(v179, Vector256.Create(JxlDctScales.Sqrt2), v183); + Vector256 v185 = v183 + v180; + Vector256 v186 = v180 + v182; + Vector256 v187 = Vector256.MultiplyAddEstimate(v170, Vector256.Create(JxlDctScales.Sqrt2), v184); + Vector256 v188 = v184 + v174; + Vector256 v189 = v174 + v185; + Vector256 v190 = v185 + v171; + Vector256 v191 = v171 + v186; + Vector256 v192 = v186 + v173; + Vector256 v193 = v173 + v182; + Vector256 v194 = Vector256.MultiplyAddEstimate(v141, Vector256.Create(JxlDctScales.Sqrt2), v187); + Vector256 v195 = v187 + v155; + Vector256 v196 = v155 + v188; + Vector256 v197 = v188 + v145; + Vector256 v198 = v145 + v189; + Vector256 v199 = v189 + v156; + Vector256 v200 = v156 + v190; + Vector256 v201 = v190 + v142; + Vector256 v202 = v142 + v191; + Vector256 v203 = v191 + v157; + Vector256 v204 = v157 + v192; + Vector256 v205 = v192 + v144; + Vector256 v206 = v144 + v193; + Vector256 v207 = v193 + v153; + Vector256 v208 = v153 + v182; + + values[0] = v60; + values[1] = v194; + values[2] = v106; + values[3] = v195; + values[4] = v74; + values[5] = v196; + values[6] = v107; + values[7] = v197; + values[8] = v64; + values[9] = v198; + values[10] = v108; + values[11] = v199; + values[12] = v75; + values[13] = v200; + values[14] = v109; + values[15] = v201; + values[16] = v61; + values[17] = v202; + values[18] = v110; + values[19] = v203; + values[20] = v76; + values[21] = v204; + values[22] = v111; + values[23] = v205; + values[24] = v63; + values[25] = v206; + values[26] = v112; + values[27] = v207; + values[28] = v72; + values[29] = v208; + values[30] = v101; + values[31] = v182; + } + + /// + /// Processes complete 8-lane forward batches along input columns. + /// + /// The number of positions in each transform. + /// The number of independent transforms. + /// The borrowed source matrix. + /// The source row stride, in floats. + /// The borrowed output matrix. + /// The destination row stride, in floats. + /// The first independent transform not already processed by a wider batch. + /// Borrowed storage for two length-by-8 banks. Only the first bank needs initialized inputs. + /// The first independent transform left after the complete batches. + private static int ForwardColumns256( + int length, + int count, + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int first, + Span workspace) + { + Span> values = MemoryMarshal.Cast>(workspace[..(length * 8)]); + Span> scratch = MemoryMarshal.Cast>(workspace.Slice(length * 8, length * 8)); + ref float input = ref MemoryMarshal.GetReference(source); + ref float output = ref MemoryMarshal.GetReference(destination); + Vector256 scale = Vector256.Create(1F / length); + + for (; first <= count - 8; first += 8) + { + // One vector covers adjacent columns at the same transform position. Physical strides never enter the coefficient-bank layout. + for (int i = 0; i < length; i++) + { + values[i] = Vector256.LoadUnsafe(ref input, (nuint)((i * sourceStride) + first)); + } + + Forward(values, scratch); + for (int i = 0; i < length; i++) + { + (values[i] * scale).StoreUnsafe(ref output, (nuint)((i * destinationStride) + first)); + } + } + + return first; + } + + /// + /// Processes complete 8-lane forward batches along input rows. + /// + /// The number of positions in each transform. + /// The number of independent transforms. + /// The borrowed source matrix. + /// The source row stride, in floats. + /// The borrowed output matrix. + /// The destination row stride, in floats. + /// Whether transform positions become output rows and input rows become output columns. + /// The first independent transform not already processed by a wider batch. + /// Borrowed storage for two length-by-8 banks. Only the first bank needs initialized inputs. + /// The first independent transform left after the complete batches. + private static int ForwardRows256( + int length, + int count, + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + bool transposed, + int first, + Span workspace) + { + Span> values = MemoryMarshal.Cast>(workspace[..(length * 8)]); + Span> scratch = MemoryMarshal.Cast>(workspace.Slice(length * 8, length * 8)); + ref float input = ref MemoryMarshal.GetReference(source); + ref float output = ref MemoryMarshal.GetReference(destination); + Vector256 scale = Vector256.Create(1F / length); + + for (; first <= count - 8; first += 8) + { + // Each square load contains adjacent samples from independent rows. Transposition puts those rows into SIMD lanes. + // The complete row band is loaded before any same-orientation output is written, so source and destination can be the same matrix. + for (int column = 0; column < length; column += 8) + { + Vector256 row0 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 0) * sourceStride) + column)).AsInt32(); + Vector256 row1 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 1) * sourceStride) + column)).AsInt32(); + Vector256 row2 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 2) * sourceStride) + column)).AsInt32(); + Vector256 row3 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 3) * sourceStride) + column)).AsInt32(); + Vector256 row4 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 4) * sourceStride) + column)).AsInt32(); + Vector256 row5 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 5) * sourceStride) + column)).AsInt32(); + Vector256 row6 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 6) * sourceStride) + column)).AsInt32(); + Vector256 row7 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 7) * sourceStride) + column)).AsInt32(); + + SimdUtils.Transpose(ref row0, ref row1, ref row2, ref row3, ref row4, ref row5, ref row6, ref row7); + values[column + 0] = row0.AsSingle(); + values[column + 1] = row1.AsSingle(); + values[column + 2] = row2.AsSingle(); + values[column + 3] = row3.AsSingle(); + values[column + 4] = row4.AsSingle(); + values[column + 5] = row5.AsSingle(); + values[column + 6] = row6.AsSingle(); + values[column + 7] = row7.AsSingle(); + } + + Forward(values, scratch); + if (transposed) + { + // A complete vector is one contiguous output row. Only logical samples are stored; destination padding remains untouched. + for (int i = 0; i < length; i++) + { + (values[i] * scale).StoreUnsafe(ref output, (nuint)((i * destinationStride) + first)); + } + } + else + { + // Restore row-major order one register tile at a time. This replaces a separate full-block transpose and copy. + for (int column = 0; column < length; column += 8) + { + Vector256 row0 = (values[column + 0] * scale).AsInt32(); + Vector256 row1 = (values[column + 1] * scale).AsInt32(); + Vector256 row2 = (values[column + 2] * scale).AsInt32(); + Vector256 row3 = (values[column + 3] * scale).AsInt32(); + Vector256 row4 = (values[column + 4] * scale).AsInt32(); + Vector256 row5 = (values[column + 5] * scale).AsInt32(); + Vector256 row6 = (values[column + 6] * scale).AsInt32(); + Vector256 row7 = (values[column + 7] * scale).AsInt32(); + + SimdUtils.Transpose(ref row0, ref row1, ref row2, ref row3, ref row4, ref row5, ref row6, ref row7); + row0.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 0) * destinationStride) + column)); + row1.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 1) * destinationStride) + column)); + row2.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 2) * destinationStride) + column)); + row3.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 3) * destinationStride) + column)); + row4.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 4) * destinationStride) + column)); + row5.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 5) * destinationStride) + column)); + row6.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 6) * destinationStride) + column)); + row7.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 7) * destinationStride) + column)); + } + } + } + + return first; + } + + /// + /// Computes independent inverse transforms using two nonoverlapping borrowed vector banks. + /// + /// One vector per transform position. Its length is a power of two from one through 256. + /// At least values.Length vectors. Previous contents are not read. + private static void Inverse(Span> values, Span> scratch) + { + // Small transforms stay in registers. The one-point transform is an identity and must not copy any trailing samples. + switch (values.Length) + { + case 1: + return; + case 2: + Inverse2(values); + return; + case 4: + Inverse4(values); + return; + case 8: + Inverse8(values); + return; + case 16: + Inverse16(values); + return; + case 32: + Inverse32(values); + return; + } + + int half = values.Length / 2; + // Separate frequency parity before either recursive call. The original bank can then serve as recursive scratch. + for (int i = 0; i < half; i++) + { + scratch[i] = values[2 * i]; + scratch[half + i] = values[(2 * i) + 1]; + } + + Inverse(scratch[..half], values); + + // Descend so every odd-frequency predecessor is read before its own entry changes. + for (int i = half - 1; i > 0; i--) + { + scratch[half + i] += scratch[half + i - 1]; + } + + scratch[half] *= Vector256.Create(JxlDctScales.Sqrt2); + Inverse(scratch.Slice(half, half), values); + ReadOnlySpan weights = JxlDctScales.GetMultipliers(values.Length); + + for (int i = 0; i < half; i++) + { + Vector256 weight = Vector256.Create(weights[i]); + Vector256 even = scratch[i]; + Vector256 odd = scratch[half + i]; + values[i] = Vector256.MultiplyAddEstimate(weight, odd, even); + values[values.Length - i - 1] = Vector256.MultiplyAddEstimate(-weight, odd, even); + } + } + + /// + /// Computes independent 2-point inverse transforms without intermediate memory passes. + /// + /// Exactly 2 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Inverse2(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector256 input0 = values[0]; + Vector256 input1 = values[1]; + + Vector256 v0 = input0 + input1; + Vector256 v1 = input0 - input1; + + values[0] = v0; + values[1] = v1; + } + + /// + /// Computes independent 4-point inverse transforms without intermediate memory passes. + /// + /// Exactly 4 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Inverse4(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector256 input0 = values[0]; + Vector256 input1 = values[1]; + Vector256 input2 = values[2]; + Vector256 input3 = values[3]; + + Vector256 v0 = input0 + input2; + Vector256 v1 = input0 - input2; + Vector256 v2 = input3 + input1; + Vector256 v3 = input1 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v4 = v3 + v2; + Vector256 v5 = v3 - v2; + Vector256 v6 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v4, v0); + Vector256 v7 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v4, v0); + Vector256 v8 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v5, v1); + Vector256 v9 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v5, v1); + + values[0] = v6; + values[1] = v8; + values[2] = v9; + values[3] = v7; + } + + /// + /// Computes independent 8-point inverse transforms without intermediate memory passes. + /// + /// Exactly 8 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Inverse8(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector256 input0 = values[0]; + Vector256 input1 = values[1]; + Vector256 input2 = values[2]; + Vector256 input3 = values[3]; + Vector256 input4 = values[4]; + Vector256 input5 = values[5]; + Vector256 input6 = values[6]; + Vector256 input7 = values[7]; + + Vector256 v0 = input0 + input4; + Vector256 v1 = input0 - input4; + Vector256 v2 = input6 + input2; + Vector256 v3 = input2 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v4 = v3 + v2; + Vector256 v5 = v3 - v2; + Vector256 v6 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v4, v0); + Vector256 v7 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v4, v0); + Vector256 v8 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v5, v1); + Vector256 v9 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v5, v1); + Vector256 v10 = input7 + input5; + Vector256 v11 = input5 + input3; + Vector256 v12 = input3 + input1; + Vector256 v13 = input1 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v14 = v13 + v11; + Vector256 v15 = v13 - v11; + Vector256 v16 = v10 + v12; + Vector256 v17 = v12 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v18 = v17 + v16; + Vector256 v19 = v17 - v16; + Vector256 v20 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v18, v14); + Vector256 v21 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v18, v14); + Vector256 v22 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v19, v15); + Vector256 v23 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v19, v15); + Vector256 v24 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5097955791041592f), v20, v6); + Vector256 v25 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5097955791041592f), v20, v6); + Vector256 v26 = Vector256.MultiplyAddEstimate(Vector256.Create(0.6013448869350453f), v22, v8); + Vector256 v27 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.6013448869350453f), v22, v8); + Vector256 v28 = Vector256.MultiplyAddEstimate(Vector256.Create(0.8999762231364156f), v23, v9); + Vector256 v29 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.8999762231364156f), v23, v9); + Vector256 v30 = Vector256.MultiplyAddEstimate(Vector256.Create(2.5629154477415055f), v21, v7); + Vector256 v31 = Vector256.MultiplyAddEstimate(-Vector256.Create(2.5629154477415055f), v21, v7); + + values[0] = v24; + values[1] = v26; + values[2] = v28; + values[3] = v30; + values[4] = v31; + values[5] = v29; + values[6] = v27; + values[7] = v25; + } + + /// + /// Computes independent 16-point inverse transforms without intermediate memory passes. + /// + /// Exactly 16 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Inverse16(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector256 input0 = values[0]; + Vector256 input1 = values[1]; + Vector256 input2 = values[2]; + Vector256 input3 = values[3]; + Vector256 input4 = values[4]; + Vector256 input5 = values[5]; + Vector256 input6 = values[6]; + Vector256 input7 = values[7]; + Vector256 input8 = values[8]; + Vector256 input9 = values[9]; + Vector256 input10 = values[10]; + Vector256 input11 = values[11]; + Vector256 input12 = values[12]; + Vector256 input13 = values[13]; + Vector256 input14 = values[14]; + Vector256 input15 = values[15]; + + Vector256 v0 = input0 + input8; + Vector256 v1 = input0 - input8; + Vector256 v2 = input12 + input4; + Vector256 v3 = input4 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v4 = v3 + v2; + Vector256 v5 = v3 - v2; + Vector256 v6 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v4, v0); + Vector256 v7 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v4, v0); + Vector256 v8 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v5, v1); + Vector256 v9 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v5, v1); + Vector256 v10 = input14 + input10; + Vector256 v11 = input10 + input6; + Vector256 v12 = input6 + input2; + Vector256 v13 = input2 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v14 = v13 + v11; + Vector256 v15 = v13 - v11; + Vector256 v16 = v10 + v12; + Vector256 v17 = v12 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v18 = v17 + v16; + Vector256 v19 = v17 - v16; + Vector256 v20 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v18, v14); + Vector256 v21 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v18, v14); + Vector256 v22 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v19, v15); + Vector256 v23 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v19, v15); + Vector256 v24 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5097955791041592f), v20, v6); + Vector256 v25 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5097955791041592f), v20, v6); + Vector256 v26 = Vector256.MultiplyAddEstimate(Vector256.Create(0.6013448869350453f), v22, v8); + Vector256 v27 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.6013448869350453f), v22, v8); + Vector256 v28 = Vector256.MultiplyAddEstimate(Vector256.Create(0.8999762231364156f), v23, v9); + Vector256 v29 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.8999762231364156f), v23, v9); + Vector256 v30 = Vector256.MultiplyAddEstimate(Vector256.Create(2.5629154477415055f), v21, v7); + Vector256 v31 = Vector256.MultiplyAddEstimate(-Vector256.Create(2.5629154477415055f), v21, v7); + Vector256 v32 = input15 + input13; + Vector256 v33 = input13 + input11; + Vector256 v34 = input11 + input9; + Vector256 v35 = input9 + input7; + Vector256 v36 = input7 + input5; + Vector256 v37 = input5 + input3; + Vector256 v38 = input3 + input1; + Vector256 v39 = input1 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v40 = v39 + v35; + Vector256 v41 = v39 - v35; + Vector256 v42 = v33 + v37; + Vector256 v43 = v37 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v44 = v43 + v42; + Vector256 v45 = v43 - v42; + Vector256 v46 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v44, v40); + Vector256 v47 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v44, v40); + Vector256 v48 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v45, v41); + Vector256 v49 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v45, v41); + Vector256 v50 = v32 + v34; + Vector256 v51 = v34 + v36; + Vector256 v52 = v36 + v38; + Vector256 v53 = v38 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v54 = v53 + v51; + Vector256 v55 = v53 - v51; + Vector256 v56 = v50 + v52; + Vector256 v57 = v52 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v58 = v57 + v56; + Vector256 v59 = v57 - v56; + Vector256 v60 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v58, v54); + Vector256 v61 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v58, v54); + Vector256 v62 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v59, v55); + Vector256 v63 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v59, v55); + Vector256 v64 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5097955791041592f), v60, v46); + Vector256 v65 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5097955791041592f), v60, v46); + Vector256 v66 = Vector256.MultiplyAddEstimate(Vector256.Create(0.6013448869350453f), v62, v48); + Vector256 v67 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.6013448869350453f), v62, v48); + Vector256 v68 = Vector256.MultiplyAddEstimate(Vector256.Create(0.8999762231364156f), v63, v49); + Vector256 v69 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.8999762231364156f), v63, v49); + Vector256 v70 = Vector256.MultiplyAddEstimate(Vector256.Create(2.5629154477415055f), v61, v47); + Vector256 v71 = Vector256.MultiplyAddEstimate(-Vector256.Create(2.5629154477415055f), v61, v47); + Vector256 v72 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5024192861881557f), v64, v24); + Vector256 v73 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5024192861881557f), v64, v24); + Vector256 v74 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5224986149396889f), v66, v26); + Vector256 v75 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5224986149396889f), v66, v26); + Vector256 v76 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5669440348163577f), v68, v28); + Vector256 v77 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5669440348163577f), v68, v28); + Vector256 v78 = Vector256.MultiplyAddEstimate(Vector256.Create(0.6468217833599901f), v70, v30); + Vector256 v79 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.6468217833599901f), v70, v30); + Vector256 v80 = Vector256.MultiplyAddEstimate(Vector256.Create(0.7881546234512502f), v71, v31); + Vector256 v81 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.7881546234512502f), v71, v31); + Vector256 v82 = Vector256.MultiplyAddEstimate(Vector256.Create(1.060677685990347f), v69, v29); + Vector256 v83 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.060677685990347f), v69, v29); + Vector256 v84 = Vector256.MultiplyAddEstimate(Vector256.Create(1.7224470982383342f), v67, v27); + Vector256 v85 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.7224470982383342f), v67, v27); + Vector256 v86 = Vector256.MultiplyAddEstimate(Vector256.Create(5.101148618689155f), v65, v25); + Vector256 v87 = Vector256.MultiplyAddEstimate(-Vector256.Create(5.101148618689155f), v65, v25); + + values[0] = v72; + values[1] = v74; + values[2] = v76; + values[3] = v78; + values[4] = v80; + values[5] = v82; + values[6] = v84; + values[7] = v86; + values[8] = v87; + values[9] = v85; + values[10] = v83; + values[11] = v81; + values[12] = v79; + values[13] = v77; + values[14] = v75; + values[15] = v73; + } + + /// + /// Computes independent 32-point inverse transforms without intermediate memory passes. + /// + /// Exactly 32 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Inverse32(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector256 input0 = values[0]; + Vector256 input1 = values[1]; + Vector256 input2 = values[2]; + Vector256 input3 = values[3]; + Vector256 input4 = values[4]; + Vector256 input5 = values[5]; + Vector256 input6 = values[6]; + Vector256 input7 = values[7]; + Vector256 input8 = values[8]; + Vector256 input9 = values[9]; + Vector256 input10 = values[10]; + Vector256 input11 = values[11]; + Vector256 input12 = values[12]; + Vector256 input13 = values[13]; + Vector256 input14 = values[14]; + Vector256 input15 = values[15]; + Vector256 input16 = values[16]; + Vector256 input17 = values[17]; + Vector256 input18 = values[18]; + Vector256 input19 = values[19]; + Vector256 input20 = values[20]; + Vector256 input21 = values[21]; + Vector256 input22 = values[22]; + Vector256 input23 = values[23]; + Vector256 input24 = values[24]; + Vector256 input25 = values[25]; + Vector256 input26 = values[26]; + Vector256 input27 = values[27]; + Vector256 input28 = values[28]; + Vector256 input29 = values[29]; + Vector256 input30 = values[30]; + Vector256 input31 = values[31]; + + Vector256 v0 = input0 + input16; + Vector256 v1 = input0 - input16; + Vector256 v2 = input24 + input8; + Vector256 v3 = input8 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v4 = v3 + v2; + Vector256 v5 = v3 - v2; + Vector256 v6 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v4, v0); + Vector256 v7 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v4, v0); + Vector256 v8 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v5, v1); + Vector256 v9 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v5, v1); + Vector256 v10 = input28 + input20; + Vector256 v11 = input20 + input12; + Vector256 v12 = input12 + input4; + Vector256 v13 = input4 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v14 = v13 + v11; + Vector256 v15 = v13 - v11; + Vector256 v16 = v10 + v12; + Vector256 v17 = v12 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v18 = v17 + v16; + Vector256 v19 = v17 - v16; + Vector256 v20 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v18, v14); + Vector256 v21 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v18, v14); + Vector256 v22 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v19, v15); + Vector256 v23 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v19, v15); + Vector256 v24 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5097955791041592f), v20, v6); + Vector256 v25 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5097955791041592f), v20, v6); + Vector256 v26 = Vector256.MultiplyAddEstimate(Vector256.Create(0.6013448869350453f), v22, v8); + Vector256 v27 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.6013448869350453f), v22, v8); + Vector256 v28 = Vector256.MultiplyAddEstimate(Vector256.Create(0.8999762231364156f), v23, v9); + Vector256 v29 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.8999762231364156f), v23, v9); + Vector256 v30 = Vector256.MultiplyAddEstimate(Vector256.Create(2.5629154477415055f), v21, v7); + Vector256 v31 = Vector256.MultiplyAddEstimate(-Vector256.Create(2.5629154477415055f), v21, v7); + Vector256 v32 = input30 + input26; + Vector256 v33 = input26 + input22; + Vector256 v34 = input22 + input18; + Vector256 v35 = input18 + input14; + Vector256 v36 = input14 + input10; + Vector256 v37 = input10 + input6; + Vector256 v38 = input6 + input2; + Vector256 v39 = input2 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v40 = v39 + v35; + Vector256 v41 = v39 - v35; + Vector256 v42 = v33 + v37; + Vector256 v43 = v37 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v44 = v43 + v42; + Vector256 v45 = v43 - v42; + Vector256 v46 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v44, v40); + Vector256 v47 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v44, v40); + Vector256 v48 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v45, v41); + Vector256 v49 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v45, v41); + Vector256 v50 = v32 + v34; + Vector256 v51 = v34 + v36; + Vector256 v52 = v36 + v38; + Vector256 v53 = v38 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v54 = v53 + v51; + Vector256 v55 = v53 - v51; + Vector256 v56 = v50 + v52; + Vector256 v57 = v52 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v58 = v57 + v56; + Vector256 v59 = v57 - v56; + Vector256 v60 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v58, v54); + Vector256 v61 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v58, v54); + Vector256 v62 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v59, v55); + Vector256 v63 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v59, v55); + Vector256 v64 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5097955791041592f), v60, v46); + Vector256 v65 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5097955791041592f), v60, v46); + Vector256 v66 = Vector256.MultiplyAddEstimate(Vector256.Create(0.6013448869350453f), v62, v48); + Vector256 v67 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.6013448869350453f), v62, v48); + Vector256 v68 = Vector256.MultiplyAddEstimate(Vector256.Create(0.8999762231364156f), v63, v49); + Vector256 v69 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.8999762231364156f), v63, v49); + Vector256 v70 = Vector256.MultiplyAddEstimate(Vector256.Create(2.5629154477415055f), v61, v47); + Vector256 v71 = Vector256.MultiplyAddEstimate(-Vector256.Create(2.5629154477415055f), v61, v47); + Vector256 v72 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5024192861881557f), v64, v24); + Vector256 v73 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5024192861881557f), v64, v24); + Vector256 v74 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5224986149396889f), v66, v26); + Vector256 v75 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5224986149396889f), v66, v26); + Vector256 v76 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5669440348163577f), v68, v28); + Vector256 v77 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5669440348163577f), v68, v28); + Vector256 v78 = Vector256.MultiplyAddEstimate(Vector256.Create(0.6468217833599901f), v70, v30); + Vector256 v79 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.6468217833599901f), v70, v30); + Vector256 v80 = Vector256.MultiplyAddEstimate(Vector256.Create(0.7881546234512502f), v71, v31); + Vector256 v81 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.7881546234512502f), v71, v31); + Vector256 v82 = Vector256.MultiplyAddEstimate(Vector256.Create(1.060677685990347f), v69, v29); + Vector256 v83 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.060677685990347f), v69, v29); + Vector256 v84 = Vector256.MultiplyAddEstimate(Vector256.Create(1.7224470982383342f), v67, v27); + Vector256 v85 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.7224470982383342f), v67, v27); + Vector256 v86 = Vector256.MultiplyAddEstimate(Vector256.Create(5.101148618689155f), v65, v25); + Vector256 v87 = Vector256.MultiplyAddEstimate(-Vector256.Create(5.101148618689155f), v65, v25); + Vector256 v88 = input31 + input29; + Vector256 v89 = input29 + input27; + Vector256 v90 = input27 + input25; + Vector256 v91 = input25 + input23; + Vector256 v92 = input23 + input21; + Vector256 v93 = input21 + input19; + Vector256 v94 = input19 + input17; + Vector256 v95 = input17 + input15; + Vector256 v96 = input15 + input13; + Vector256 v97 = input13 + input11; + Vector256 v98 = input11 + input9; + Vector256 v99 = input9 + input7; + Vector256 v100 = input7 + input5; + Vector256 v101 = input5 + input3; + Vector256 v102 = input3 + input1; + Vector256 v103 = input1 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v104 = v103 + v95; + Vector256 v105 = v103 - v95; + Vector256 v106 = v91 + v99; + Vector256 v107 = v99 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v108 = v107 + v106; + Vector256 v109 = v107 - v106; + Vector256 v110 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v108, v104); + Vector256 v111 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v108, v104); + Vector256 v112 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v109, v105); + Vector256 v113 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v109, v105); + Vector256 v114 = v89 + v93; + Vector256 v115 = v93 + v97; + Vector256 v116 = v97 + v101; + Vector256 v117 = v101 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v118 = v117 + v115; + Vector256 v119 = v117 - v115; + Vector256 v120 = v114 + v116; + Vector256 v121 = v116 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v122 = v121 + v120; + Vector256 v123 = v121 - v120; + Vector256 v124 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v122, v118); + Vector256 v125 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v122, v118); + Vector256 v126 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v123, v119); + Vector256 v127 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v123, v119); + Vector256 v128 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5097955791041592f), v124, v110); + Vector256 v129 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5097955791041592f), v124, v110); + Vector256 v130 = Vector256.MultiplyAddEstimate(Vector256.Create(0.6013448869350453f), v126, v112); + Vector256 v131 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.6013448869350453f), v126, v112); + Vector256 v132 = Vector256.MultiplyAddEstimate(Vector256.Create(0.8999762231364156f), v127, v113); + Vector256 v133 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.8999762231364156f), v127, v113); + Vector256 v134 = Vector256.MultiplyAddEstimate(Vector256.Create(2.5629154477415055f), v125, v111); + Vector256 v135 = Vector256.MultiplyAddEstimate(-Vector256.Create(2.5629154477415055f), v125, v111); + Vector256 v136 = v88 + v90; + Vector256 v137 = v90 + v92; + Vector256 v138 = v92 + v94; + Vector256 v139 = v94 + v96; + Vector256 v140 = v96 + v98; + Vector256 v141 = v98 + v100; + Vector256 v142 = v100 + v102; + Vector256 v143 = v102 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v144 = v143 + v139; + Vector256 v145 = v143 - v139; + Vector256 v146 = v137 + v141; + Vector256 v147 = v141 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v148 = v147 + v146; + Vector256 v149 = v147 - v146; + Vector256 v150 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v148, v144); + Vector256 v151 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v148, v144); + Vector256 v152 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v149, v145); + Vector256 v153 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v149, v145); + Vector256 v154 = v136 + v138; + Vector256 v155 = v138 + v140; + Vector256 v156 = v140 + v142; + Vector256 v157 = v142 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v158 = v157 + v155; + Vector256 v159 = v157 - v155; + Vector256 v160 = v154 + v156; + Vector256 v161 = v156 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v162 = v161 + v160; + Vector256 v163 = v161 - v160; + Vector256 v164 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v162, v158); + Vector256 v165 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v162, v158); + Vector256 v166 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v163, v159); + Vector256 v167 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v163, v159); + Vector256 v168 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5097955791041592f), v164, v150); + Vector256 v169 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5097955791041592f), v164, v150); + Vector256 v170 = Vector256.MultiplyAddEstimate(Vector256.Create(0.6013448869350453f), v166, v152); + Vector256 v171 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.6013448869350453f), v166, v152); + Vector256 v172 = Vector256.MultiplyAddEstimate(Vector256.Create(0.8999762231364156f), v167, v153); + Vector256 v173 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.8999762231364156f), v167, v153); + Vector256 v174 = Vector256.MultiplyAddEstimate(Vector256.Create(2.5629154477415055f), v165, v151); + Vector256 v175 = Vector256.MultiplyAddEstimate(-Vector256.Create(2.5629154477415055f), v165, v151); + Vector256 v176 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5024192861881557f), v168, v128); + Vector256 v177 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5024192861881557f), v168, v128); + Vector256 v178 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5224986149396889f), v170, v130); + Vector256 v179 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5224986149396889f), v170, v130); + Vector256 v180 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5669440348163577f), v172, v132); + Vector256 v181 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5669440348163577f), v172, v132); + Vector256 v182 = Vector256.MultiplyAddEstimate(Vector256.Create(0.6468217833599901f), v174, v134); + Vector256 v183 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.6468217833599901f), v174, v134); + Vector256 v184 = Vector256.MultiplyAddEstimate(Vector256.Create(0.7881546234512502f), v175, v135); + Vector256 v185 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.7881546234512502f), v175, v135); + Vector256 v186 = Vector256.MultiplyAddEstimate(Vector256.Create(1.060677685990347f), v173, v133); + Vector256 v187 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.060677685990347f), v173, v133); + Vector256 v188 = Vector256.MultiplyAddEstimate(Vector256.Create(1.7224470982383342f), v171, v131); + Vector256 v189 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.7224470982383342f), v171, v131); + Vector256 v190 = Vector256.MultiplyAddEstimate(Vector256.Create(5.101148618689155f), v169, v129); + Vector256 v191 = Vector256.MultiplyAddEstimate(-Vector256.Create(5.101148618689155f), v169, v129); + Vector256 v192 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5006029982351963f), v176, v72); + Vector256 v193 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5006029982351963f), v176, v72); + Vector256 v194 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5054709598975436f), v178, v74); + Vector256 v195 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5054709598975436f), v178, v74); + Vector256 v196 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5154473099226246f), v180, v76); + Vector256 v197 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5154473099226246f), v180, v76); + Vector256 v198 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5310425910897841f), v182, v78); + Vector256 v199 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5310425910897841f), v182, v78); + Vector256 v200 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5531038960344445f), v184, v80); + Vector256 v201 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5531038960344445f), v184, v80); + Vector256 v202 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5829349682061339f), v186, v82); + Vector256 v203 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5829349682061339f), v186, v82); + Vector256 v204 = Vector256.MultiplyAddEstimate(Vector256.Create(0.6225041230356648f), v188, v84); + Vector256 v205 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.6225041230356648f), v188, v84); + Vector256 v206 = Vector256.MultiplyAddEstimate(Vector256.Create(0.6748083414550057f), v190, v86); + Vector256 v207 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.6748083414550057f), v190, v86); + Vector256 v208 = Vector256.MultiplyAddEstimate(Vector256.Create(0.7445362710022986f), v191, v87); + Vector256 v209 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.7445362710022986f), v191, v87); + Vector256 v210 = Vector256.MultiplyAddEstimate(Vector256.Create(0.8393496454155268f), v189, v85); + Vector256 v211 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.8393496454155268f), v189, v85); + Vector256 v212 = Vector256.MultiplyAddEstimate(Vector256.Create(0.9725682378619608f), v187, v83); + Vector256 v213 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.9725682378619608f), v187, v83); + Vector256 v214 = Vector256.MultiplyAddEstimate(Vector256.Create(1.1694399334328847f), v185, v81); + Vector256 v215 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.1694399334328847f), v185, v81); + Vector256 v216 = Vector256.MultiplyAddEstimate(Vector256.Create(1.4841646163141662f), v183, v79); + Vector256 v217 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.4841646163141662f), v183, v79); + Vector256 v218 = Vector256.MultiplyAddEstimate(Vector256.Create(2.057781009953411f), v181, v77); + Vector256 v219 = Vector256.MultiplyAddEstimate(-Vector256.Create(2.057781009953411f), v181, v77); + Vector256 v220 = Vector256.MultiplyAddEstimate(Vector256.Create(3.407608418468719f), v179, v75); + Vector256 v221 = Vector256.MultiplyAddEstimate(-Vector256.Create(3.407608418468719f), v179, v75); + Vector256 v222 = Vector256.MultiplyAddEstimate(Vector256.Create(10.190008123548033f), v177, v73); + Vector256 v223 = Vector256.MultiplyAddEstimate(-Vector256.Create(10.190008123548033f), v177, v73); + + values[0] = v192; + values[1] = v194; + values[2] = v196; + values[3] = v198; + values[4] = v200; + values[5] = v202; + values[6] = v204; + values[7] = v206; + values[8] = v208; + values[9] = v210; + values[10] = v212; + values[11] = v214; + values[12] = v216; + values[13] = v218; + values[14] = v220; + values[15] = v222; + values[16] = v223; + values[17] = v221; + values[18] = v219; + values[19] = v217; + values[20] = v215; + values[21] = v213; + values[22] = v211; + values[23] = v209; + values[24] = v207; + values[25] = v205; + values[26] = v203; + values[27] = v201; + values[28] = v199; + values[29] = v197; + values[30] = v195; + values[31] = v193; + } + + /// + /// Processes complete 8-lane inverse batches along input columns. + /// + /// The number of positions in each transform. + /// The number of independent transforms. + /// The borrowed source matrix. + /// The source row stride, in floats. + /// The borrowed output matrix. + /// The destination row stride, in floats. + /// The first independent transform not already processed by a wider batch. + /// Borrowed storage for two length-by-8 banks. Only the first bank needs initialized inputs. + /// The first independent transform left after the complete batches. + private static int InverseColumns256( + int length, + int count, + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int first, + Span workspace) + { + Span> values = MemoryMarshal.Cast>(workspace[..(length * 8)]); + Span> scratch = MemoryMarshal.Cast>(workspace.Slice(length * 8, length * 8)); + ref float input = ref MemoryMarshal.GetReference(source); + ref float output = ref MemoryMarshal.GetReference(destination); + + for (; first <= count - 8; first += 8) + { + // One vector covers adjacent columns at the same transform position. Physical strides never enter the coefficient-bank layout. + for (int i = 0; i < length; i++) + { + values[i] = Vector256.LoadUnsafe(ref input, (nuint)((i * sourceStride) + first)); + } + + Inverse(values, scratch); + for (int i = 0; i < length; i++) + { + values[i].StoreUnsafe(ref output, (nuint)((i * destinationStride) + first)); + } + } + + return first; + } + + /// + /// Processes complete 8-lane inverse batches along input rows. + /// + /// The number of positions in each transform. + /// The number of independent transforms. + /// The borrowed source matrix. + /// The source row stride, in floats. + /// The borrowed output matrix. + /// The destination row stride, in floats. + /// Whether transform positions become output rows and input rows become output columns. + /// The first independent transform not already processed by a wider batch. + /// Borrowed storage for two length-by-8 banks. Only the first bank needs initialized inputs. + /// The first independent transform left after the complete batches. + private static int InverseRows256( + int length, + int count, + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + bool transposed, + int first, + Span workspace) + { + Span> values = MemoryMarshal.Cast>(workspace[..(length * 8)]); + Span> scratch = MemoryMarshal.Cast>(workspace.Slice(length * 8, length * 8)); + ref float input = ref MemoryMarshal.GetReference(source); + ref float output = ref MemoryMarshal.GetReference(destination); + + for (; first <= count - 8; first += 8) + { + // Each square load contains adjacent samples from independent rows. Transposition puts those rows into SIMD lanes. + // The complete row band is loaded before any same-orientation output is written, so source and destination can be the same matrix. + for (int column = 0; column < length; column += 8) + { + Vector256 row0 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 0) * sourceStride) + column)).AsInt32(); + Vector256 row1 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 1) * sourceStride) + column)).AsInt32(); + Vector256 row2 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 2) * sourceStride) + column)).AsInt32(); + Vector256 row3 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 3) * sourceStride) + column)).AsInt32(); + Vector256 row4 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 4) * sourceStride) + column)).AsInt32(); + Vector256 row5 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 5) * sourceStride) + column)).AsInt32(); + Vector256 row6 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 6) * sourceStride) + column)).AsInt32(); + Vector256 row7 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 7) * sourceStride) + column)).AsInt32(); + + SimdUtils.Transpose(ref row0, ref row1, ref row2, ref row3, ref row4, ref row5, ref row6, ref row7); + values[column + 0] = row0.AsSingle(); + values[column + 1] = row1.AsSingle(); + values[column + 2] = row2.AsSingle(); + values[column + 3] = row3.AsSingle(); + values[column + 4] = row4.AsSingle(); + values[column + 5] = row5.AsSingle(); + values[column + 6] = row6.AsSingle(); + values[column + 7] = row7.AsSingle(); + } + + Inverse(values, scratch); + if (transposed) + { + // A complete vector is one contiguous output row. Only logical samples are stored; destination padding remains untouched. + for (int i = 0; i < length; i++) + { + values[i].StoreUnsafe(ref output, (nuint)((i * destinationStride) + first)); + } + } + else + { + // Restore row-major order one register tile at a time. This replaces a separate full-block transpose and copy. + for (int column = 0; column < length; column += 8) + { + Vector256 row0 = values[column + 0].AsInt32(); + Vector256 row1 = values[column + 1].AsInt32(); + Vector256 row2 = values[column + 2].AsInt32(); + Vector256 row3 = values[column + 3].AsInt32(); + Vector256 row4 = values[column + 4].AsInt32(); + Vector256 row5 = values[column + 5].AsInt32(); + Vector256 row6 = values[column + 6].AsInt32(); + Vector256 row7 = values[column + 7].AsInt32(); + + SimdUtils.Transpose(ref row0, ref row1, ref row2, ref row3, ref row4, ref row5, ref row6, ref row7); + row0.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 0) * destinationStride) + column)); + row1.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 1) * destinationStride) + column)); + row2.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 2) * destinationStride) + column)); + row3.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 3) * destinationStride) + column)); + row4.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 4) * destinationStride) + column)); + row5.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 5) * destinationStride) + column)); + row6.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 6) * destinationStride) + column)); + row7.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 7) * destinationStride) + column)); + } + } + } + + return first; + } + + /// + /// Computes independent forward transforms using two nonoverlapping borrowed vector banks. + /// + /// One vector per transform position. Its length is a power of two from one through 256. + /// At least values.Length vectors. Previous contents are not read. + private static void Forward(Span> values, Span> scratch) + { + // Small transforms stay in registers. The one-point transform is an identity and must not copy any trailing samples. + switch (values.Length) + { + case 1: + return; + case 2: + Forward2(values); + return; + case 4: + Forward4(values); + return; + case 8: + Forward8(values); + return; + case 16: + Forward16(values); + return; + case 32: + Forward32(values); + return; + } + + int half = values.Length / 2; + ReadOnlySpan weights = JxlDctScales.GetMultipliers(values.Length); + + // Consume both original halves before reusing their bank as recursive scratch. Neither recursive call can then destroy unread input. + for (int i = 0; i < half; i++) + { + Vector512 first = values[i]; + Vector512 last = values[values.Length - i - 1]; + scratch[i] = first + last; + scratch[half + i] = (first - last) * Vector512.Create(weights[i]); + } + + Forward(scratch[..half], values); + Forward(scratch.Slice(half, half), values); + scratch[half] = Vector512.MultiplyAddEstimate(scratch[half], Vector512.Create(JxlDctScales.Sqrt2), scratch[half + 1]); + + for (int i = 1; i + 1 < half; i++) + { + scratch[half + i] += scratch[half + i + 1]; + } + + for (int i = 0; i < half; i++) + { + values[2 * i] = scratch[i]; + values[(2 * i) + 1] = scratch[half + i]; + } + } + + /// + /// Computes independent 2-point forward transforms without intermediate memory passes. + /// + /// Exactly 2 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Forward2(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector512 input0 = values[0]; + Vector512 input1 = values[1]; + + Vector512 v0 = input0 + input1; + Vector512 v1 = input0 - input1; + + values[0] = v0; + values[1] = v1; + } + + /// + /// Computes independent 4-point forward transforms without intermediate memory passes. + /// + /// Exactly 4 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Forward4(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector512 input0 = values[0]; + Vector512 input1 = values[1]; + Vector512 input2 = values[2]; + Vector512 input3 = values[3]; + + Vector512 v0 = input0 + input3; + Vector512 v1 = (input0 - input3) * Vector512.Create(0.541196100146197f); + Vector512 v2 = input1 + input2; + Vector512 v3 = (input1 - input2) * Vector512.Create(1.3065629648763764f); + Vector512 v4 = v0 + v2; + Vector512 v5 = v0 - v2; + Vector512 v6 = v1 + v3; + Vector512 v7 = v1 - v3; + Vector512 v8 = Vector512.MultiplyAddEstimate(v6, Vector512.Create(JxlDctScales.Sqrt2), v7); + + values[0] = v4; + values[1] = v8; + values[2] = v5; + values[3] = v7; + } + + /// + /// Computes independent 8-point forward transforms without intermediate memory passes. + /// + /// Exactly 8 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Forward8(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector512 input0 = values[0]; + Vector512 input1 = values[1]; + Vector512 input2 = values[2]; + Vector512 input3 = values[3]; + Vector512 input4 = values[4]; + Vector512 input5 = values[5]; + Vector512 input6 = values[6]; + Vector512 input7 = values[7]; + + Vector512 v0 = input0 + input7; + Vector512 v1 = (input0 - input7) * Vector512.Create(0.5097955791041592f); + Vector512 v2 = input1 + input6; + Vector512 v3 = (input1 - input6) * Vector512.Create(0.6013448869350453f); + Vector512 v4 = input2 + input5; + Vector512 v5 = (input2 - input5) * Vector512.Create(0.8999762231364156f); + Vector512 v6 = input3 + input4; + Vector512 v7 = (input3 - input4) * Vector512.Create(2.5629154477415055f); + Vector512 v8 = v0 + v6; + Vector512 v9 = (v0 - v6) * Vector512.Create(0.541196100146197f); + Vector512 v10 = v2 + v4; + Vector512 v11 = (v2 - v4) * Vector512.Create(1.3065629648763764f); + Vector512 v12 = v8 + v10; + Vector512 v13 = v8 - v10; + Vector512 v14 = v9 + v11; + Vector512 v15 = v9 - v11; + Vector512 v16 = Vector512.MultiplyAddEstimate(v14, Vector512.Create(JxlDctScales.Sqrt2), v15); + Vector512 v17 = v1 + v7; + Vector512 v18 = (v1 - v7) * Vector512.Create(0.541196100146197f); + Vector512 v19 = v3 + v5; + Vector512 v20 = (v3 - v5) * Vector512.Create(1.3065629648763764f); + Vector512 v21 = v17 + v19; + Vector512 v22 = v17 - v19; + Vector512 v23 = v18 + v20; + Vector512 v24 = v18 - v20; + Vector512 v25 = Vector512.MultiplyAddEstimate(v23, Vector512.Create(JxlDctScales.Sqrt2), v24); + Vector512 v26 = Vector512.MultiplyAddEstimate(v21, Vector512.Create(JxlDctScales.Sqrt2), v25); + Vector512 v27 = v25 + v22; + Vector512 v28 = v22 + v24; + + values[0] = v12; + values[1] = v26; + values[2] = v16; + values[3] = v27; + values[4] = v13; + values[5] = v28; + values[6] = v15; + values[7] = v24; + } + + /// + /// Computes independent 16-point forward transforms without intermediate memory passes. + /// + /// Exactly 16 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Forward16(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector512 input0 = values[0]; + Vector512 input1 = values[1]; + Vector512 input2 = values[2]; + Vector512 input3 = values[3]; + Vector512 input4 = values[4]; + Vector512 input5 = values[5]; + Vector512 input6 = values[6]; + Vector512 input7 = values[7]; + Vector512 input8 = values[8]; + Vector512 input9 = values[9]; + Vector512 input10 = values[10]; + Vector512 input11 = values[11]; + Vector512 input12 = values[12]; + Vector512 input13 = values[13]; + Vector512 input14 = values[14]; + Vector512 input15 = values[15]; + + Vector512 v0 = input0 + input15; + Vector512 v1 = (input0 - input15) * Vector512.Create(0.5024192861881557f); + Vector512 v2 = input1 + input14; + Vector512 v3 = (input1 - input14) * Vector512.Create(0.5224986149396889f); + Vector512 v4 = input2 + input13; + Vector512 v5 = (input2 - input13) * Vector512.Create(0.5669440348163577f); + Vector512 v6 = input3 + input12; + Vector512 v7 = (input3 - input12) * Vector512.Create(0.6468217833599901f); + Vector512 v8 = input4 + input11; + Vector512 v9 = (input4 - input11) * Vector512.Create(0.7881546234512502f); + Vector512 v10 = input5 + input10; + Vector512 v11 = (input5 - input10) * Vector512.Create(1.060677685990347f); + Vector512 v12 = input6 + input9; + Vector512 v13 = (input6 - input9) * Vector512.Create(1.7224470982383342f); + Vector512 v14 = input7 + input8; + Vector512 v15 = (input7 - input8) * Vector512.Create(5.101148618689155f); + Vector512 v16 = v0 + v14; + Vector512 v17 = (v0 - v14) * Vector512.Create(0.5097955791041592f); + Vector512 v18 = v2 + v12; + Vector512 v19 = (v2 - v12) * Vector512.Create(0.6013448869350453f); + Vector512 v20 = v4 + v10; + Vector512 v21 = (v4 - v10) * Vector512.Create(0.8999762231364156f); + Vector512 v22 = v6 + v8; + Vector512 v23 = (v6 - v8) * Vector512.Create(2.5629154477415055f); + Vector512 v24 = v16 + v22; + Vector512 v25 = (v16 - v22) * Vector512.Create(0.541196100146197f); + Vector512 v26 = v18 + v20; + Vector512 v27 = (v18 - v20) * Vector512.Create(1.3065629648763764f); + Vector512 v28 = v24 + v26; + Vector512 v29 = v24 - v26; + Vector512 v30 = v25 + v27; + Vector512 v31 = v25 - v27; + Vector512 v32 = Vector512.MultiplyAddEstimate(v30, Vector512.Create(JxlDctScales.Sqrt2), v31); + Vector512 v33 = v17 + v23; + Vector512 v34 = (v17 - v23) * Vector512.Create(0.541196100146197f); + Vector512 v35 = v19 + v21; + Vector512 v36 = (v19 - v21) * Vector512.Create(1.3065629648763764f); + Vector512 v37 = v33 + v35; + Vector512 v38 = v33 - v35; + Vector512 v39 = v34 + v36; + Vector512 v40 = v34 - v36; + Vector512 v41 = Vector512.MultiplyAddEstimate(v39, Vector512.Create(JxlDctScales.Sqrt2), v40); + Vector512 v42 = Vector512.MultiplyAddEstimate(v37, Vector512.Create(JxlDctScales.Sqrt2), v41); + Vector512 v43 = v41 + v38; + Vector512 v44 = v38 + v40; + Vector512 v45 = v1 + v15; + Vector512 v46 = (v1 - v15) * Vector512.Create(0.5097955791041592f); + Vector512 v47 = v3 + v13; + Vector512 v48 = (v3 - v13) * Vector512.Create(0.6013448869350453f); + Vector512 v49 = v5 + v11; + Vector512 v50 = (v5 - v11) * Vector512.Create(0.8999762231364156f); + Vector512 v51 = v7 + v9; + Vector512 v52 = (v7 - v9) * Vector512.Create(2.5629154477415055f); + Vector512 v53 = v45 + v51; + Vector512 v54 = (v45 - v51) * Vector512.Create(0.541196100146197f); + Vector512 v55 = v47 + v49; + Vector512 v56 = (v47 - v49) * Vector512.Create(1.3065629648763764f); + Vector512 v57 = v53 + v55; + Vector512 v58 = v53 - v55; + Vector512 v59 = v54 + v56; + Vector512 v60 = v54 - v56; + Vector512 v61 = Vector512.MultiplyAddEstimate(v59, Vector512.Create(JxlDctScales.Sqrt2), v60); + Vector512 v62 = v46 + v52; + Vector512 v63 = (v46 - v52) * Vector512.Create(0.541196100146197f); + Vector512 v64 = v48 + v50; + Vector512 v65 = (v48 - v50) * Vector512.Create(1.3065629648763764f); + Vector512 v66 = v62 + v64; + Vector512 v67 = v62 - v64; + Vector512 v68 = v63 + v65; + Vector512 v69 = v63 - v65; + Vector512 v70 = Vector512.MultiplyAddEstimate(v68, Vector512.Create(JxlDctScales.Sqrt2), v69); + Vector512 v71 = Vector512.MultiplyAddEstimate(v66, Vector512.Create(JxlDctScales.Sqrt2), v70); + Vector512 v72 = v70 + v67; + Vector512 v73 = v67 + v69; + Vector512 v74 = Vector512.MultiplyAddEstimate(v57, Vector512.Create(JxlDctScales.Sqrt2), v71); + Vector512 v75 = v71 + v61; + Vector512 v76 = v61 + v72; + Vector512 v77 = v72 + v58; + Vector512 v78 = v58 + v73; + Vector512 v79 = v73 + v60; + Vector512 v80 = v60 + v69; + + values[0] = v28; + values[1] = v74; + values[2] = v42; + values[3] = v75; + values[4] = v32; + values[5] = v76; + values[6] = v43; + values[7] = v77; + values[8] = v29; + values[9] = v78; + values[10] = v44; + values[11] = v79; + values[12] = v31; + values[13] = v80; + values[14] = v40; + values[15] = v69; + } + + /// + /// Computes independent 32-point forward transforms without intermediate memory passes. + /// + /// Exactly 32 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Forward32(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector512 input0 = values[0]; + Vector512 input1 = values[1]; + Vector512 input2 = values[2]; + Vector512 input3 = values[3]; + Vector512 input4 = values[4]; + Vector512 input5 = values[5]; + Vector512 input6 = values[6]; + Vector512 input7 = values[7]; + Vector512 input8 = values[8]; + Vector512 input9 = values[9]; + Vector512 input10 = values[10]; + Vector512 input11 = values[11]; + Vector512 input12 = values[12]; + Vector512 input13 = values[13]; + Vector512 input14 = values[14]; + Vector512 input15 = values[15]; + Vector512 input16 = values[16]; + Vector512 input17 = values[17]; + Vector512 input18 = values[18]; + Vector512 input19 = values[19]; + Vector512 input20 = values[20]; + Vector512 input21 = values[21]; + Vector512 input22 = values[22]; + Vector512 input23 = values[23]; + Vector512 input24 = values[24]; + Vector512 input25 = values[25]; + Vector512 input26 = values[26]; + Vector512 input27 = values[27]; + Vector512 input28 = values[28]; + Vector512 input29 = values[29]; + Vector512 input30 = values[30]; + Vector512 input31 = values[31]; + + Vector512 v0 = input0 + input31; + Vector512 v1 = (input0 - input31) * Vector512.Create(0.5006029982351963f); + Vector512 v2 = input1 + input30; + Vector512 v3 = (input1 - input30) * Vector512.Create(0.5054709598975436f); + Vector512 v4 = input2 + input29; + Vector512 v5 = (input2 - input29) * Vector512.Create(0.5154473099226246f); + Vector512 v6 = input3 + input28; + Vector512 v7 = (input3 - input28) * Vector512.Create(0.5310425910897841f); + Vector512 v8 = input4 + input27; + Vector512 v9 = (input4 - input27) * Vector512.Create(0.5531038960344445f); + Vector512 v10 = input5 + input26; + Vector512 v11 = (input5 - input26) * Vector512.Create(0.5829349682061339f); + Vector512 v12 = input6 + input25; + Vector512 v13 = (input6 - input25) * Vector512.Create(0.6225041230356648f); + Vector512 v14 = input7 + input24; + Vector512 v15 = (input7 - input24) * Vector512.Create(0.6748083414550057f); + Vector512 v16 = input8 + input23; + Vector512 v17 = (input8 - input23) * Vector512.Create(0.7445362710022986f); + Vector512 v18 = input9 + input22; + Vector512 v19 = (input9 - input22) * Vector512.Create(0.8393496454155268f); + Vector512 v20 = input10 + input21; + Vector512 v21 = (input10 - input21) * Vector512.Create(0.9725682378619608f); + Vector512 v22 = input11 + input20; + Vector512 v23 = (input11 - input20) * Vector512.Create(1.1694399334328847f); + Vector512 v24 = input12 + input19; + Vector512 v25 = (input12 - input19) * Vector512.Create(1.4841646163141662f); + Vector512 v26 = input13 + input18; + Vector512 v27 = (input13 - input18) * Vector512.Create(2.057781009953411f); + Vector512 v28 = input14 + input17; + Vector512 v29 = (input14 - input17) * Vector512.Create(3.407608418468719f); + Vector512 v30 = input15 + input16; + Vector512 v31 = (input15 - input16) * Vector512.Create(10.190008123548033f); + Vector512 v32 = v0 + v30; + Vector512 v33 = (v0 - v30) * Vector512.Create(0.5024192861881557f); + Vector512 v34 = v2 + v28; + Vector512 v35 = (v2 - v28) * Vector512.Create(0.5224986149396889f); + Vector512 v36 = v4 + v26; + Vector512 v37 = (v4 - v26) * Vector512.Create(0.5669440348163577f); + Vector512 v38 = v6 + v24; + Vector512 v39 = (v6 - v24) * Vector512.Create(0.6468217833599901f); + Vector512 v40 = v8 + v22; + Vector512 v41 = (v8 - v22) * Vector512.Create(0.7881546234512502f); + Vector512 v42 = v10 + v20; + Vector512 v43 = (v10 - v20) * Vector512.Create(1.060677685990347f); + Vector512 v44 = v12 + v18; + Vector512 v45 = (v12 - v18) * Vector512.Create(1.7224470982383342f); + Vector512 v46 = v14 + v16; + Vector512 v47 = (v14 - v16) * Vector512.Create(5.101148618689155f); + Vector512 v48 = v32 + v46; + Vector512 v49 = (v32 - v46) * Vector512.Create(0.5097955791041592f); + Vector512 v50 = v34 + v44; + Vector512 v51 = (v34 - v44) * Vector512.Create(0.6013448869350453f); + Vector512 v52 = v36 + v42; + Vector512 v53 = (v36 - v42) * Vector512.Create(0.8999762231364156f); + Vector512 v54 = v38 + v40; + Vector512 v55 = (v38 - v40) * Vector512.Create(2.5629154477415055f); + Vector512 v56 = v48 + v54; + Vector512 v57 = (v48 - v54) * Vector512.Create(0.541196100146197f); + Vector512 v58 = v50 + v52; + Vector512 v59 = (v50 - v52) * Vector512.Create(1.3065629648763764f); + Vector512 v60 = v56 + v58; + Vector512 v61 = v56 - v58; + Vector512 v62 = v57 + v59; + Vector512 v63 = v57 - v59; + Vector512 v64 = Vector512.MultiplyAddEstimate(v62, Vector512.Create(JxlDctScales.Sqrt2), v63); + Vector512 v65 = v49 + v55; + Vector512 v66 = (v49 - v55) * Vector512.Create(0.541196100146197f); + Vector512 v67 = v51 + v53; + Vector512 v68 = (v51 - v53) * Vector512.Create(1.3065629648763764f); + Vector512 v69 = v65 + v67; + Vector512 v70 = v65 - v67; + Vector512 v71 = v66 + v68; + Vector512 v72 = v66 - v68; + Vector512 v73 = Vector512.MultiplyAddEstimate(v71, Vector512.Create(JxlDctScales.Sqrt2), v72); + Vector512 v74 = Vector512.MultiplyAddEstimate(v69, Vector512.Create(JxlDctScales.Sqrt2), v73); + Vector512 v75 = v73 + v70; + Vector512 v76 = v70 + v72; + Vector512 v77 = v33 + v47; + Vector512 v78 = (v33 - v47) * Vector512.Create(0.5097955791041592f); + Vector512 v79 = v35 + v45; + Vector512 v80 = (v35 - v45) * Vector512.Create(0.6013448869350453f); + Vector512 v81 = v37 + v43; + Vector512 v82 = (v37 - v43) * Vector512.Create(0.8999762231364156f); + Vector512 v83 = v39 + v41; + Vector512 v84 = (v39 - v41) * Vector512.Create(2.5629154477415055f); + Vector512 v85 = v77 + v83; + Vector512 v86 = (v77 - v83) * Vector512.Create(0.541196100146197f); + Vector512 v87 = v79 + v81; + Vector512 v88 = (v79 - v81) * Vector512.Create(1.3065629648763764f); + Vector512 v89 = v85 + v87; + Vector512 v90 = v85 - v87; + Vector512 v91 = v86 + v88; + Vector512 v92 = v86 - v88; + Vector512 v93 = Vector512.MultiplyAddEstimate(v91, Vector512.Create(JxlDctScales.Sqrt2), v92); + Vector512 v94 = v78 + v84; + Vector512 v95 = (v78 - v84) * Vector512.Create(0.541196100146197f); + Vector512 v96 = v80 + v82; + Vector512 v97 = (v80 - v82) * Vector512.Create(1.3065629648763764f); + Vector512 v98 = v94 + v96; + Vector512 v99 = v94 - v96; + Vector512 v100 = v95 + v97; + Vector512 v101 = v95 - v97; + Vector512 v102 = Vector512.MultiplyAddEstimate(v100, Vector512.Create(JxlDctScales.Sqrt2), v101); + Vector512 v103 = Vector512.MultiplyAddEstimate(v98, Vector512.Create(JxlDctScales.Sqrt2), v102); + Vector512 v104 = v102 + v99; + Vector512 v105 = v99 + v101; + Vector512 v106 = Vector512.MultiplyAddEstimate(v89, Vector512.Create(JxlDctScales.Sqrt2), v103); + Vector512 v107 = v103 + v93; + Vector512 v108 = v93 + v104; + Vector512 v109 = v104 + v90; + Vector512 v110 = v90 + v105; + Vector512 v111 = v105 + v92; + Vector512 v112 = v92 + v101; + Vector512 v113 = v1 + v31; + Vector512 v114 = (v1 - v31) * Vector512.Create(0.5024192861881557f); + Vector512 v115 = v3 + v29; + Vector512 v116 = (v3 - v29) * Vector512.Create(0.5224986149396889f); + Vector512 v117 = v5 + v27; + Vector512 v118 = (v5 - v27) * Vector512.Create(0.5669440348163577f); + Vector512 v119 = v7 + v25; + Vector512 v120 = (v7 - v25) * Vector512.Create(0.6468217833599901f); + Vector512 v121 = v9 + v23; + Vector512 v122 = (v9 - v23) * Vector512.Create(0.7881546234512502f); + Vector512 v123 = v11 + v21; + Vector512 v124 = (v11 - v21) * Vector512.Create(1.060677685990347f); + Vector512 v125 = v13 + v19; + Vector512 v126 = (v13 - v19) * Vector512.Create(1.7224470982383342f); + Vector512 v127 = v15 + v17; + Vector512 v128 = (v15 - v17) * Vector512.Create(5.101148618689155f); + Vector512 v129 = v113 + v127; + Vector512 v130 = (v113 - v127) * Vector512.Create(0.5097955791041592f); + Vector512 v131 = v115 + v125; + Vector512 v132 = (v115 - v125) * Vector512.Create(0.6013448869350453f); + Vector512 v133 = v117 + v123; + Vector512 v134 = (v117 - v123) * Vector512.Create(0.8999762231364156f); + Vector512 v135 = v119 + v121; + Vector512 v136 = (v119 - v121) * Vector512.Create(2.5629154477415055f); + Vector512 v137 = v129 + v135; + Vector512 v138 = (v129 - v135) * Vector512.Create(0.541196100146197f); + Vector512 v139 = v131 + v133; + Vector512 v140 = (v131 - v133) * Vector512.Create(1.3065629648763764f); + Vector512 v141 = v137 + v139; + Vector512 v142 = v137 - v139; + Vector512 v143 = v138 + v140; + Vector512 v144 = v138 - v140; + Vector512 v145 = Vector512.MultiplyAddEstimate(v143, Vector512.Create(JxlDctScales.Sqrt2), v144); + Vector512 v146 = v130 + v136; + Vector512 v147 = (v130 - v136) * Vector512.Create(0.541196100146197f); + Vector512 v148 = v132 + v134; + Vector512 v149 = (v132 - v134) * Vector512.Create(1.3065629648763764f); + Vector512 v150 = v146 + v148; + Vector512 v151 = v146 - v148; + Vector512 v152 = v147 + v149; + Vector512 v153 = v147 - v149; + Vector512 v154 = Vector512.MultiplyAddEstimate(v152, Vector512.Create(JxlDctScales.Sqrt2), v153); + Vector512 v155 = Vector512.MultiplyAddEstimate(v150, Vector512.Create(JxlDctScales.Sqrt2), v154); + Vector512 v156 = v154 + v151; + Vector512 v157 = v151 + v153; + Vector512 v158 = v114 + v128; + Vector512 v159 = (v114 - v128) * Vector512.Create(0.5097955791041592f); + Vector512 v160 = v116 + v126; + Vector512 v161 = (v116 - v126) * Vector512.Create(0.6013448869350453f); + Vector512 v162 = v118 + v124; + Vector512 v163 = (v118 - v124) * Vector512.Create(0.8999762231364156f); + Vector512 v164 = v120 + v122; + Vector512 v165 = (v120 - v122) * Vector512.Create(2.5629154477415055f); + Vector512 v166 = v158 + v164; + Vector512 v167 = (v158 - v164) * Vector512.Create(0.541196100146197f); + Vector512 v168 = v160 + v162; + Vector512 v169 = (v160 - v162) * Vector512.Create(1.3065629648763764f); + Vector512 v170 = v166 + v168; + Vector512 v171 = v166 - v168; + Vector512 v172 = v167 + v169; + Vector512 v173 = v167 - v169; + Vector512 v174 = Vector512.MultiplyAddEstimate(v172, Vector512.Create(JxlDctScales.Sqrt2), v173); + Vector512 v175 = v159 + v165; + Vector512 v176 = (v159 - v165) * Vector512.Create(0.541196100146197f); + Vector512 v177 = v161 + v163; + Vector512 v178 = (v161 - v163) * Vector512.Create(1.3065629648763764f); + Vector512 v179 = v175 + v177; + Vector512 v180 = v175 - v177; + Vector512 v181 = v176 + v178; + Vector512 v182 = v176 - v178; + Vector512 v183 = Vector512.MultiplyAddEstimate(v181, Vector512.Create(JxlDctScales.Sqrt2), v182); + Vector512 v184 = Vector512.MultiplyAddEstimate(v179, Vector512.Create(JxlDctScales.Sqrt2), v183); + Vector512 v185 = v183 + v180; + Vector512 v186 = v180 + v182; + Vector512 v187 = Vector512.MultiplyAddEstimate(v170, Vector512.Create(JxlDctScales.Sqrt2), v184); + Vector512 v188 = v184 + v174; + Vector512 v189 = v174 + v185; + Vector512 v190 = v185 + v171; + Vector512 v191 = v171 + v186; + Vector512 v192 = v186 + v173; + Vector512 v193 = v173 + v182; + Vector512 v194 = Vector512.MultiplyAddEstimate(v141, Vector512.Create(JxlDctScales.Sqrt2), v187); + Vector512 v195 = v187 + v155; + Vector512 v196 = v155 + v188; + Vector512 v197 = v188 + v145; + Vector512 v198 = v145 + v189; + Vector512 v199 = v189 + v156; + Vector512 v200 = v156 + v190; + Vector512 v201 = v190 + v142; + Vector512 v202 = v142 + v191; + Vector512 v203 = v191 + v157; + Vector512 v204 = v157 + v192; + Vector512 v205 = v192 + v144; + Vector512 v206 = v144 + v193; + Vector512 v207 = v193 + v153; + Vector512 v208 = v153 + v182; + + values[0] = v60; + values[1] = v194; + values[2] = v106; + values[3] = v195; + values[4] = v74; + values[5] = v196; + values[6] = v107; + values[7] = v197; + values[8] = v64; + values[9] = v198; + values[10] = v108; + values[11] = v199; + values[12] = v75; + values[13] = v200; + values[14] = v109; + values[15] = v201; + values[16] = v61; + values[17] = v202; + values[18] = v110; + values[19] = v203; + values[20] = v76; + values[21] = v204; + values[22] = v111; + values[23] = v205; + values[24] = v63; + values[25] = v206; + values[26] = v112; + values[27] = v207; + values[28] = v72; + values[29] = v208; + values[30] = v101; + values[31] = v182; + } + + /// + /// Processes complete 16-lane forward batches along input columns. + /// + /// The number of positions in each transform. + /// The number of independent transforms. + /// The borrowed source matrix. + /// The source row stride, in floats. + /// The borrowed output matrix. + /// The destination row stride, in floats. + /// The first independent transform not already processed by a wider batch. + /// Borrowed storage for two length-by-16 banks. Only the first bank needs initialized inputs. + /// The first independent transform left after the complete batches. + private static int ForwardColumns512( + int length, + int count, + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int first, + Span workspace) + { + Span> values = MemoryMarshal.Cast>(workspace[..(length * 16)]); + Span> scratch = MemoryMarshal.Cast>(workspace.Slice(length * 16, length * 16)); + ref float input = ref MemoryMarshal.GetReference(source); + ref float output = ref MemoryMarshal.GetReference(destination); + Vector512 scale = Vector512.Create(1F / length); + + for (; first <= count - 16; first += 16) + { + // One vector covers adjacent columns at the same transform position. Physical strides never enter the coefficient-bank layout. + for (int i = 0; i < length; i++) + { + values[i] = Vector512.LoadUnsafe(ref input, (nuint)((i * sourceStride) + first)); + } + + Forward(values, scratch); + for (int i = 0; i < length; i++) + { + (values[i] * scale).StoreUnsafe(ref output, (nuint)((i * destinationStride) + first)); + } + } + + return first; + } + + /// + /// Processes complete 16-lane forward batches along input rows. + /// + /// The number of positions in each transform. + /// The number of independent transforms. + /// The borrowed source matrix. + /// The source row stride, in floats. + /// The borrowed output matrix. + /// The destination row stride, in floats. + /// Whether transform positions become output rows and input rows become output columns. + /// The first independent transform not already processed by a wider batch. + /// Borrowed storage for two length-by-16 banks. Only the first bank needs initialized inputs. + /// The first independent transform left after the complete batches. + private static int ForwardRows512( + int length, + int count, + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + bool transposed, + int first, + Span workspace) + { + Span> values = MemoryMarshal.Cast>(workspace[..(length * 16)]); + Span> scratch = MemoryMarshal.Cast>(workspace.Slice(length * 16, length * 16)); + ref float input = ref MemoryMarshal.GetReference(source); + ref float output = ref MemoryMarshal.GetReference(destination); + Vector512 scale = Vector512.Create(1F / length); + + for (; first <= count - 16; first += 16) + { + // Each square load contains adjacent samples from independent rows. Transposition puts those rows into SIMD lanes. + // The complete row band is loaded before any same-orientation output is written, so source and destination can be the same matrix. + for (int column = 0; column < length; column += 16) + { + Vector512 row0 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 0) * sourceStride) + column)).AsInt32(); + Vector512 row1 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 1) * sourceStride) + column)).AsInt32(); + Vector512 row2 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 2) * sourceStride) + column)).AsInt32(); + Vector512 row3 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 3) * sourceStride) + column)).AsInt32(); + Vector512 row4 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 4) * sourceStride) + column)).AsInt32(); + Vector512 row5 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 5) * sourceStride) + column)).AsInt32(); + Vector512 row6 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 6) * sourceStride) + column)).AsInt32(); + Vector512 row7 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 7) * sourceStride) + column)).AsInt32(); + Vector512 row8 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 8) * sourceStride) + column)).AsInt32(); + Vector512 row9 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 9) * sourceStride) + column)).AsInt32(); + Vector512 row10 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 10) * sourceStride) + column)).AsInt32(); + Vector512 row11 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 11) * sourceStride) + column)).AsInt32(); + Vector512 row12 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 12) * sourceStride) + column)).AsInt32(); + Vector512 row13 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 13) * sourceStride) + column)).AsInt32(); + Vector512 row14 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 14) * sourceStride) + column)).AsInt32(); + Vector512 row15 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 15) * sourceStride) + column)).AsInt32(); + + SimdUtils.Transpose( + ref row0, + ref row1, + ref row2, + ref row3, + ref row4, + ref row5, + ref row6, + ref row7, + ref row8, + ref row9, + ref row10, + ref row11, + ref row12, + ref row13, + ref row14, + ref row15); + + values[column + 0] = row0.AsSingle(); + values[column + 1] = row1.AsSingle(); + values[column + 2] = row2.AsSingle(); + values[column + 3] = row3.AsSingle(); + values[column + 4] = row4.AsSingle(); + values[column + 5] = row5.AsSingle(); + values[column + 6] = row6.AsSingle(); + values[column + 7] = row7.AsSingle(); + values[column + 8] = row8.AsSingle(); + values[column + 9] = row9.AsSingle(); + values[column + 10] = row10.AsSingle(); + values[column + 11] = row11.AsSingle(); + values[column + 12] = row12.AsSingle(); + values[column + 13] = row13.AsSingle(); + values[column + 14] = row14.AsSingle(); + values[column + 15] = row15.AsSingle(); + } + + Forward(values, scratch); + if (transposed) + { + // A complete vector is one contiguous output row. Only logical samples are stored; destination padding remains untouched. + for (int i = 0; i < length; i++) + { + (values[i] * scale).StoreUnsafe(ref output, (nuint)((i * destinationStride) + first)); + } + } + else + { + // Restore row-major order one register tile at a time. This replaces a separate full-block transpose and copy. + for (int column = 0; column < length; column += 16) + { + Vector512 row0 = (values[column + 0] * scale).AsInt32(); + Vector512 row1 = (values[column + 1] * scale).AsInt32(); + Vector512 row2 = (values[column + 2] * scale).AsInt32(); + Vector512 row3 = (values[column + 3] * scale).AsInt32(); + Vector512 row4 = (values[column + 4] * scale).AsInt32(); + Vector512 row5 = (values[column + 5] * scale).AsInt32(); + Vector512 row6 = (values[column + 6] * scale).AsInt32(); + Vector512 row7 = (values[column + 7] * scale).AsInt32(); + Vector512 row8 = (values[column + 8] * scale).AsInt32(); + Vector512 row9 = (values[column + 9] * scale).AsInt32(); + Vector512 row10 = (values[column + 10] * scale).AsInt32(); + Vector512 row11 = (values[column + 11] * scale).AsInt32(); + Vector512 row12 = (values[column + 12] * scale).AsInt32(); + Vector512 row13 = (values[column + 13] * scale).AsInt32(); + Vector512 row14 = (values[column + 14] * scale).AsInt32(); + Vector512 row15 = (values[column + 15] * scale).AsInt32(); + + SimdUtils.Transpose( + ref row0, + ref row1, + ref row2, + ref row3, + ref row4, + ref row5, + ref row6, + ref row7, + ref row8, + ref row9, + ref row10, + ref row11, + ref row12, + ref row13, + ref row14, + ref row15); + + row0.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 0) * destinationStride) + column)); + row1.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 1) * destinationStride) + column)); + row2.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 2) * destinationStride) + column)); + row3.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 3) * destinationStride) + column)); + row4.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 4) * destinationStride) + column)); + row5.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 5) * destinationStride) + column)); + row6.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 6) * destinationStride) + column)); + row7.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 7) * destinationStride) + column)); + row8.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 8) * destinationStride) + column)); + row9.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 9) * destinationStride) + column)); + row10.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 10) * destinationStride) + column)); + row11.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 11) * destinationStride) + column)); + row12.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 12) * destinationStride) + column)); + row13.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 13) * destinationStride) + column)); + row14.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 14) * destinationStride) + column)); + row15.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 15) * destinationStride) + column)); + } + } + } + + return first; + } + + /// + /// Computes independent inverse transforms using two nonoverlapping borrowed vector banks. + /// + /// One vector per transform position. Its length is a power of two from one through 256. + /// At least values.Length vectors. Previous contents are not read. + private static void Inverse(Span> values, Span> scratch) + { + // Small transforms stay in registers. The one-point transform is an identity and must not copy any trailing samples. + switch (values.Length) + { + case 1: + return; + case 2: + Inverse2(values); + return; + case 4: + Inverse4(values); + return; + case 8: + Inverse8(values); + return; + case 16: + Inverse16(values); + return; + case 32: + Inverse32(values); + return; + } + + int half = values.Length / 2; + // Separate frequency parity before either recursive call. The original bank can then serve as recursive scratch. + for (int i = 0; i < half; i++) + { + scratch[i] = values[2 * i]; + scratch[half + i] = values[(2 * i) + 1]; + } + + Inverse(scratch[..half], values); + + // Descend so every odd-frequency predecessor is read before its own entry changes. + for (int i = half - 1; i > 0; i--) + { + scratch[half + i] += scratch[half + i - 1]; + } + + scratch[half] *= Vector512.Create(JxlDctScales.Sqrt2); + Inverse(scratch.Slice(half, half), values); + ReadOnlySpan weights = JxlDctScales.GetMultipliers(values.Length); + + for (int i = 0; i < half; i++) + { + Vector512 weight = Vector512.Create(weights[i]); + Vector512 even = scratch[i]; + Vector512 odd = scratch[half + i]; + values[i] = Vector512.MultiplyAddEstimate(weight, odd, even); + values[values.Length - i - 1] = Vector512.MultiplyAddEstimate(-weight, odd, even); + } + } + + /// + /// Computes independent 2-point inverse transforms without intermediate memory passes. + /// + /// Exactly 2 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Inverse2(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector512 input0 = values[0]; + Vector512 input1 = values[1]; + + Vector512 v0 = input0 + input1; + Vector512 v1 = input0 - input1; + + values[0] = v0; + values[1] = v1; + } + + /// + /// Computes independent 4-point inverse transforms without intermediate memory passes. + /// + /// Exactly 4 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Inverse4(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector512 input0 = values[0]; + Vector512 input1 = values[1]; + Vector512 input2 = values[2]; + Vector512 input3 = values[3]; + + Vector512 v0 = input0 + input2; + Vector512 v1 = input0 - input2; + Vector512 v2 = input3 + input1; + Vector512 v3 = input1 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v4 = v3 + v2; + Vector512 v5 = v3 - v2; + Vector512 v6 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v4, v0); + Vector512 v7 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v4, v0); + Vector512 v8 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v5, v1); + Vector512 v9 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v5, v1); + + values[0] = v6; + values[1] = v8; + values[2] = v9; + values[3] = v7; + } + + /// + /// Computes independent 8-point inverse transforms without intermediate memory passes. + /// + /// Exactly 8 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Inverse8(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector512 input0 = values[0]; + Vector512 input1 = values[1]; + Vector512 input2 = values[2]; + Vector512 input3 = values[3]; + Vector512 input4 = values[4]; + Vector512 input5 = values[5]; + Vector512 input6 = values[6]; + Vector512 input7 = values[7]; + + Vector512 v0 = input0 + input4; + Vector512 v1 = input0 - input4; + Vector512 v2 = input6 + input2; + Vector512 v3 = input2 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v4 = v3 + v2; + Vector512 v5 = v3 - v2; + Vector512 v6 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v4, v0); + Vector512 v7 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v4, v0); + Vector512 v8 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v5, v1); + Vector512 v9 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v5, v1); + Vector512 v10 = input7 + input5; + Vector512 v11 = input5 + input3; + Vector512 v12 = input3 + input1; + Vector512 v13 = input1 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v14 = v13 + v11; + Vector512 v15 = v13 - v11; + Vector512 v16 = v10 + v12; + Vector512 v17 = v12 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v18 = v17 + v16; + Vector512 v19 = v17 - v16; + Vector512 v20 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v18, v14); + Vector512 v21 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v18, v14); + Vector512 v22 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v19, v15); + Vector512 v23 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v19, v15); + Vector512 v24 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5097955791041592f), v20, v6); + Vector512 v25 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5097955791041592f), v20, v6); + Vector512 v26 = Vector512.MultiplyAddEstimate(Vector512.Create(0.6013448869350453f), v22, v8); + Vector512 v27 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.6013448869350453f), v22, v8); + Vector512 v28 = Vector512.MultiplyAddEstimate(Vector512.Create(0.8999762231364156f), v23, v9); + Vector512 v29 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.8999762231364156f), v23, v9); + Vector512 v30 = Vector512.MultiplyAddEstimate(Vector512.Create(2.5629154477415055f), v21, v7); + Vector512 v31 = Vector512.MultiplyAddEstimate(-Vector512.Create(2.5629154477415055f), v21, v7); + + values[0] = v24; + values[1] = v26; + values[2] = v28; + values[3] = v30; + values[4] = v31; + values[5] = v29; + values[6] = v27; + values[7] = v25; + } + + /// + /// Computes independent 16-point inverse transforms without intermediate memory passes. + /// + /// Exactly 16 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Inverse16(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector512 input0 = values[0]; + Vector512 input1 = values[1]; + Vector512 input2 = values[2]; + Vector512 input3 = values[3]; + Vector512 input4 = values[4]; + Vector512 input5 = values[5]; + Vector512 input6 = values[6]; + Vector512 input7 = values[7]; + Vector512 input8 = values[8]; + Vector512 input9 = values[9]; + Vector512 input10 = values[10]; + Vector512 input11 = values[11]; + Vector512 input12 = values[12]; + Vector512 input13 = values[13]; + Vector512 input14 = values[14]; + Vector512 input15 = values[15]; + + Vector512 v0 = input0 + input8; + Vector512 v1 = input0 - input8; + Vector512 v2 = input12 + input4; + Vector512 v3 = input4 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v4 = v3 + v2; + Vector512 v5 = v3 - v2; + Vector512 v6 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v4, v0); + Vector512 v7 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v4, v0); + Vector512 v8 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v5, v1); + Vector512 v9 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v5, v1); + Vector512 v10 = input14 + input10; + Vector512 v11 = input10 + input6; + Vector512 v12 = input6 + input2; + Vector512 v13 = input2 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v14 = v13 + v11; + Vector512 v15 = v13 - v11; + Vector512 v16 = v10 + v12; + Vector512 v17 = v12 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v18 = v17 + v16; + Vector512 v19 = v17 - v16; + Vector512 v20 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v18, v14); + Vector512 v21 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v18, v14); + Vector512 v22 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v19, v15); + Vector512 v23 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v19, v15); + Vector512 v24 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5097955791041592f), v20, v6); + Vector512 v25 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5097955791041592f), v20, v6); + Vector512 v26 = Vector512.MultiplyAddEstimate(Vector512.Create(0.6013448869350453f), v22, v8); + Vector512 v27 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.6013448869350453f), v22, v8); + Vector512 v28 = Vector512.MultiplyAddEstimate(Vector512.Create(0.8999762231364156f), v23, v9); + Vector512 v29 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.8999762231364156f), v23, v9); + Vector512 v30 = Vector512.MultiplyAddEstimate(Vector512.Create(2.5629154477415055f), v21, v7); + Vector512 v31 = Vector512.MultiplyAddEstimate(-Vector512.Create(2.5629154477415055f), v21, v7); + Vector512 v32 = input15 + input13; + Vector512 v33 = input13 + input11; + Vector512 v34 = input11 + input9; + Vector512 v35 = input9 + input7; + Vector512 v36 = input7 + input5; + Vector512 v37 = input5 + input3; + Vector512 v38 = input3 + input1; + Vector512 v39 = input1 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v40 = v39 + v35; + Vector512 v41 = v39 - v35; + Vector512 v42 = v33 + v37; + Vector512 v43 = v37 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v44 = v43 + v42; + Vector512 v45 = v43 - v42; + Vector512 v46 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v44, v40); + Vector512 v47 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v44, v40); + Vector512 v48 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v45, v41); + Vector512 v49 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v45, v41); + Vector512 v50 = v32 + v34; + Vector512 v51 = v34 + v36; + Vector512 v52 = v36 + v38; + Vector512 v53 = v38 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v54 = v53 + v51; + Vector512 v55 = v53 - v51; + Vector512 v56 = v50 + v52; + Vector512 v57 = v52 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v58 = v57 + v56; + Vector512 v59 = v57 - v56; + Vector512 v60 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v58, v54); + Vector512 v61 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v58, v54); + Vector512 v62 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v59, v55); + Vector512 v63 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v59, v55); + Vector512 v64 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5097955791041592f), v60, v46); + Vector512 v65 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5097955791041592f), v60, v46); + Vector512 v66 = Vector512.MultiplyAddEstimate(Vector512.Create(0.6013448869350453f), v62, v48); + Vector512 v67 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.6013448869350453f), v62, v48); + Vector512 v68 = Vector512.MultiplyAddEstimate(Vector512.Create(0.8999762231364156f), v63, v49); + Vector512 v69 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.8999762231364156f), v63, v49); + Vector512 v70 = Vector512.MultiplyAddEstimate(Vector512.Create(2.5629154477415055f), v61, v47); + Vector512 v71 = Vector512.MultiplyAddEstimate(-Vector512.Create(2.5629154477415055f), v61, v47); + Vector512 v72 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5024192861881557f), v64, v24); + Vector512 v73 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5024192861881557f), v64, v24); + Vector512 v74 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5224986149396889f), v66, v26); + Vector512 v75 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5224986149396889f), v66, v26); + Vector512 v76 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5669440348163577f), v68, v28); + Vector512 v77 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5669440348163577f), v68, v28); + Vector512 v78 = Vector512.MultiplyAddEstimate(Vector512.Create(0.6468217833599901f), v70, v30); + Vector512 v79 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.6468217833599901f), v70, v30); + Vector512 v80 = Vector512.MultiplyAddEstimate(Vector512.Create(0.7881546234512502f), v71, v31); + Vector512 v81 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.7881546234512502f), v71, v31); + Vector512 v82 = Vector512.MultiplyAddEstimate(Vector512.Create(1.060677685990347f), v69, v29); + Vector512 v83 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.060677685990347f), v69, v29); + Vector512 v84 = Vector512.MultiplyAddEstimate(Vector512.Create(1.7224470982383342f), v67, v27); + Vector512 v85 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.7224470982383342f), v67, v27); + Vector512 v86 = Vector512.MultiplyAddEstimate(Vector512.Create(5.101148618689155f), v65, v25); + Vector512 v87 = Vector512.MultiplyAddEstimate(-Vector512.Create(5.101148618689155f), v65, v25); + + values[0] = v72; + values[1] = v74; + values[2] = v76; + values[3] = v78; + values[4] = v80; + values[5] = v82; + values[6] = v84; + values[7] = v86; + values[8] = v87; + values[9] = v85; + values[10] = v83; + values[11] = v81; + values[12] = v79; + values[13] = v77; + values[14] = v75; + values[15] = v73; + } + + /// + /// Computes independent 32-point inverse transforms without intermediate memory passes. + /// + /// Exactly 32 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Inverse32(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector512 input0 = values[0]; + Vector512 input1 = values[1]; + Vector512 input2 = values[2]; + Vector512 input3 = values[3]; + Vector512 input4 = values[4]; + Vector512 input5 = values[5]; + Vector512 input6 = values[6]; + Vector512 input7 = values[7]; + Vector512 input8 = values[8]; + Vector512 input9 = values[9]; + Vector512 input10 = values[10]; + Vector512 input11 = values[11]; + Vector512 input12 = values[12]; + Vector512 input13 = values[13]; + Vector512 input14 = values[14]; + Vector512 input15 = values[15]; + Vector512 input16 = values[16]; + Vector512 input17 = values[17]; + Vector512 input18 = values[18]; + Vector512 input19 = values[19]; + Vector512 input20 = values[20]; + Vector512 input21 = values[21]; + Vector512 input22 = values[22]; + Vector512 input23 = values[23]; + Vector512 input24 = values[24]; + Vector512 input25 = values[25]; + Vector512 input26 = values[26]; + Vector512 input27 = values[27]; + Vector512 input28 = values[28]; + Vector512 input29 = values[29]; + Vector512 input30 = values[30]; + Vector512 input31 = values[31]; + + Vector512 v0 = input0 + input16; + Vector512 v1 = input0 - input16; + Vector512 v2 = input24 + input8; + Vector512 v3 = input8 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v4 = v3 + v2; + Vector512 v5 = v3 - v2; + Vector512 v6 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v4, v0); + Vector512 v7 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v4, v0); + Vector512 v8 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v5, v1); + Vector512 v9 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v5, v1); + Vector512 v10 = input28 + input20; + Vector512 v11 = input20 + input12; + Vector512 v12 = input12 + input4; + Vector512 v13 = input4 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v14 = v13 + v11; + Vector512 v15 = v13 - v11; + Vector512 v16 = v10 + v12; + Vector512 v17 = v12 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v18 = v17 + v16; + Vector512 v19 = v17 - v16; + Vector512 v20 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v18, v14); + Vector512 v21 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v18, v14); + Vector512 v22 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v19, v15); + Vector512 v23 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v19, v15); + Vector512 v24 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5097955791041592f), v20, v6); + Vector512 v25 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5097955791041592f), v20, v6); + Vector512 v26 = Vector512.MultiplyAddEstimate(Vector512.Create(0.6013448869350453f), v22, v8); + Vector512 v27 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.6013448869350453f), v22, v8); + Vector512 v28 = Vector512.MultiplyAddEstimate(Vector512.Create(0.8999762231364156f), v23, v9); + Vector512 v29 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.8999762231364156f), v23, v9); + Vector512 v30 = Vector512.MultiplyAddEstimate(Vector512.Create(2.5629154477415055f), v21, v7); + Vector512 v31 = Vector512.MultiplyAddEstimate(-Vector512.Create(2.5629154477415055f), v21, v7); + Vector512 v32 = input30 + input26; + Vector512 v33 = input26 + input22; + Vector512 v34 = input22 + input18; + Vector512 v35 = input18 + input14; + Vector512 v36 = input14 + input10; + Vector512 v37 = input10 + input6; + Vector512 v38 = input6 + input2; + Vector512 v39 = input2 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v40 = v39 + v35; + Vector512 v41 = v39 - v35; + Vector512 v42 = v33 + v37; + Vector512 v43 = v37 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v44 = v43 + v42; + Vector512 v45 = v43 - v42; + Vector512 v46 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v44, v40); + Vector512 v47 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v44, v40); + Vector512 v48 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v45, v41); + Vector512 v49 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v45, v41); + Vector512 v50 = v32 + v34; + Vector512 v51 = v34 + v36; + Vector512 v52 = v36 + v38; + Vector512 v53 = v38 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v54 = v53 + v51; + Vector512 v55 = v53 - v51; + Vector512 v56 = v50 + v52; + Vector512 v57 = v52 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v58 = v57 + v56; + Vector512 v59 = v57 - v56; + Vector512 v60 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v58, v54); + Vector512 v61 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v58, v54); + Vector512 v62 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v59, v55); + Vector512 v63 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v59, v55); + Vector512 v64 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5097955791041592f), v60, v46); + Vector512 v65 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5097955791041592f), v60, v46); + Vector512 v66 = Vector512.MultiplyAddEstimate(Vector512.Create(0.6013448869350453f), v62, v48); + Vector512 v67 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.6013448869350453f), v62, v48); + Vector512 v68 = Vector512.MultiplyAddEstimate(Vector512.Create(0.8999762231364156f), v63, v49); + Vector512 v69 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.8999762231364156f), v63, v49); + Vector512 v70 = Vector512.MultiplyAddEstimate(Vector512.Create(2.5629154477415055f), v61, v47); + Vector512 v71 = Vector512.MultiplyAddEstimate(-Vector512.Create(2.5629154477415055f), v61, v47); + Vector512 v72 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5024192861881557f), v64, v24); + Vector512 v73 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5024192861881557f), v64, v24); + Vector512 v74 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5224986149396889f), v66, v26); + Vector512 v75 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5224986149396889f), v66, v26); + Vector512 v76 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5669440348163577f), v68, v28); + Vector512 v77 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5669440348163577f), v68, v28); + Vector512 v78 = Vector512.MultiplyAddEstimate(Vector512.Create(0.6468217833599901f), v70, v30); + Vector512 v79 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.6468217833599901f), v70, v30); + Vector512 v80 = Vector512.MultiplyAddEstimate(Vector512.Create(0.7881546234512502f), v71, v31); + Vector512 v81 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.7881546234512502f), v71, v31); + Vector512 v82 = Vector512.MultiplyAddEstimate(Vector512.Create(1.060677685990347f), v69, v29); + Vector512 v83 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.060677685990347f), v69, v29); + Vector512 v84 = Vector512.MultiplyAddEstimate(Vector512.Create(1.7224470982383342f), v67, v27); + Vector512 v85 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.7224470982383342f), v67, v27); + Vector512 v86 = Vector512.MultiplyAddEstimate(Vector512.Create(5.101148618689155f), v65, v25); + Vector512 v87 = Vector512.MultiplyAddEstimate(-Vector512.Create(5.101148618689155f), v65, v25); + Vector512 v88 = input31 + input29; + Vector512 v89 = input29 + input27; + Vector512 v90 = input27 + input25; + Vector512 v91 = input25 + input23; + Vector512 v92 = input23 + input21; + Vector512 v93 = input21 + input19; + Vector512 v94 = input19 + input17; + Vector512 v95 = input17 + input15; + Vector512 v96 = input15 + input13; + Vector512 v97 = input13 + input11; + Vector512 v98 = input11 + input9; + Vector512 v99 = input9 + input7; + Vector512 v100 = input7 + input5; + Vector512 v101 = input5 + input3; + Vector512 v102 = input3 + input1; + Vector512 v103 = input1 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v104 = v103 + v95; + Vector512 v105 = v103 - v95; + Vector512 v106 = v91 + v99; + Vector512 v107 = v99 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v108 = v107 + v106; + Vector512 v109 = v107 - v106; + Vector512 v110 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v108, v104); + Vector512 v111 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v108, v104); + Vector512 v112 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v109, v105); + Vector512 v113 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v109, v105); + Vector512 v114 = v89 + v93; + Vector512 v115 = v93 + v97; + Vector512 v116 = v97 + v101; + Vector512 v117 = v101 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v118 = v117 + v115; + Vector512 v119 = v117 - v115; + Vector512 v120 = v114 + v116; + Vector512 v121 = v116 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v122 = v121 + v120; + Vector512 v123 = v121 - v120; + Vector512 v124 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v122, v118); + Vector512 v125 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v122, v118); + Vector512 v126 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v123, v119); + Vector512 v127 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v123, v119); + Vector512 v128 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5097955791041592f), v124, v110); + Vector512 v129 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5097955791041592f), v124, v110); + Vector512 v130 = Vector512.MultiplyAddEstimate(Vector512.Create(0.6013448869350453f), v126, v112); + Vector512 v131 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.6013448869350453f), v126, v112); + Vector512 v132 = Vector512.MultiplyAddEstimate(Vector512.Create(0.8999762231364156f), v127, v113); + Vector512 v133 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.8999762231364156f), v127, v113); + Vector512 v134 = Vector512.MultiplyAddEstimate(Vector512.Create(2.5629154477415055f), v125, v111); + Vector512 v135 = Vector512.MultiplyAddEstimate(-Vector512.Create(2.5629154477415055f), v125, v111); + Vector512 v136 = v88 + v90; + Vector512 v137 = v90 + v92; + Vector512 v138 = v92 + v94; + Vector512 v139 = v94 + v96; + Vector512 v140 = v96 + v98; + Vector512 v141 = v98 + v100; + Vector512 v142 = v100 + v102; + Vector512 v143 = v102 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v144 = v143 + v139; + Vector512 v145 = v143 - v139; + Vector512 v146 = v137 + v141; + Vector512 v147 = v141 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v148 = v147 + v146; + Vector512 v149 = v147 - v146; + Vector512 v150 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v148, v144); + Vector512 v151 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v148, v144); + Vector512 v152 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v149, v145); + Vector512 v153 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v149, v145); + Vector512 v154 = v136 + v138; + Vector512 v155 = v138 + v140; + Vector512 v156 = v140 + v142; + Vector512 v157 = v142 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v158 = v157 + v155; + Vector512 v159 = v157 - v155; + Vector512 v160 = v154 + v156; + Vector512 v161 = v156 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v162 = v161 + v160; + Vector512 v163 = v161 - v160; + Vector512 v164 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v162, v158); + Vector512 v165 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v162, v158); + Vector512 v166 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v163, v159); + Vector512 v167 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v163, v159); + Vector512 v168 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5097955791041592f), v164, v150); + Vector512 v169 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5097955791041592f), v164, v150); + Vector512 v170 = Vector512.MultiplyAddEstimate(Vector512.Create(0.6013448869350453f), v166, v152); + Vector512 v171 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.6013448869350453f), v166, v152); + Vector512 v172 = Vector512.MultiplyAddEstimate(Vector512.Create(0.8999762231364156f), v167, v153); + Vector512 v173 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.8999762231364156f), v167, v153); + Vector512 v174 = Vector512.MultiplyAddEstimate(Vector512.Create(2.5629154477415055f), v165, v151); + Vector512 v175 = Vector512.MultiplyAddEstimate(-Vector512.Create(2.5629154477415055f), v165, v151); + Vector512 v176 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5024192861881557f), v168, v128); + Vector512 v177 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5024192861881557f), v168, v128); + Vector512 v178 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5224986149396889f), v170, v130); + Vector512 v179 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5224986149396889f), v170, v130); + Vector512 v180 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5669440348163577f), v172, v132); + Vector512 v181 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5669440348163577f), v172, v132); + Vector512 v182 = Vector512.MultiplyAddEstimate(Vector512.Create(0.6468217833599901f), v174, v134); + Vector512 v183 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.6468217833599901f), v174, v134); + Vector512 v184 = Vector512.MultiplyAddEstimate(Vector512.Create(0.7881546234512502f), v175, v135); + Vector512 v185 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.7881546234512502f), v175, v135); + Vector512 v186 = Vector512.MultiplyAddEstimate(Vector512.Create(1.060677685990347f), v173, v133); + Vector512 v187 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.060677685990347f), v173, v133); + Vector512 v188 = Vector512.MultiplyAddEstimate(Vector512.Create(1.7224470982383342f), v171, v131); + Vector512 v189 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.7224470982383342f), v171, v131); + Vector512 v190 = Vector512.MultiplyAddEstimate(Vector512.Create(5.101148618689155f), v169, v129); + Vector512 v191 = Vector512.MultiplyAddEstimate(-Vector512.Create(5.101148618689155f), v169, v129); + Vector512 v192 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5006029982351963f), v176, v72); + Vector512 v193 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5006029982351963f), v176, v72); + Vector512 v194 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5054709598975436f), v178, v74); + Vector512 v195 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5054709598975436f), v178, v74); + Vector512 v196 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5154473099226246f), v180, v76); + Vector512 v197 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5154473099226246f), v180, v76); + Vector512 v198 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5310425910897841f), v182, v78); + Vector512 v199 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5310425910897841f), v182, v78); + Vector512 v200 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5531038960344445f), v184, v80); + Vector512 v201 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5531038960344445f), v184, v80); + Vector512 v202 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5829349682061339f), v186, v82); + Vector512 v203 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5829349682061339f), v186, v82); + Vector512 v204 = Vector512.MultiplyAddEstimate(Vector512.Create(0.6225041230356648f), v188, v84); + Vector512 v205 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.6225041230356648f), v188, v84); + Vector512 v206 = Vector512.MultiplyAddEstimate(Vector512.Create(0.6748083414550057f), v190, v86); + Vector512 v207 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.6748083414550057f), v190, v86); + Vector512 v208 = Vector512.MultiplyAddEstimate(Vector512.Create(0.7445362710022986f), v191, v87); + Vector512 v209 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.7445362710022986f), v191, v87); + Vector512 v210 = Vector512.MultiplyAddEstimate(Vector512.Create(0.8393496454155268f), v189, v85); + Vector512 v211 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.8393496454155268f), v189, v85); + Vector512 v212 = Vector512.MultiplyAddEstimate(Vector512.Create(0.9725682378619608f), v187, v83); + Vector512 v213 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.9725682378619608f), v187, v83); + Vector512 v214 = Vector512.MultiplyAddEstimate(Vector512.Create(1.1694399334328847f), v185, v81); + Vector512 v215 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.1694399334328847f), v185, v81); + Vector512 v216 = Vector512.MultiplyAddEstimate(Vector512.Create(1.4841646163141662f), v183, v79); + Vector512 v217 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.4841646163141662f), v183, v79); + Vector512 v218 = Vector512.MultiplyAddEstimate(Vector512.Create(2.057781009953411f), v181, v77); + Vector512 v219 = Vector512.MultiplyAddEstimate(-Vector512.Create(2.057781009953411f), v181, v77); + Vector512 v220 = Vector512.MultiplyAddEstimate(Vector512.Create(3.407608418468719f), v179, v75); + Vector512 v221 = Vector512.MultiplyAddEstimate(-Vector512.Create(3.407608418468719f), v179, v75); + Vector512 v222 = Vector512.MultiplyAddEstimate(Vector512.Create(10.190008123548033f), v177, v73); + Vector512 v223 = Vector512.MultiplyAddEstimate(-Vector512.Create(10.190008123548033f), v177, v73); + + values[0] = v192; + values[1] = v194; + values[2] = v196; + values[3] = v198; + values[4] = v200; + values[5] = v202; + values[6] = v204; + values[7] = v206; + values[8] = v208; + values[9] = v210; + values[10] = v212; + values[11] = v214; + values[12] = v216; + values[13] = v218; + values[14] = v220; + values[15] = v222; + values[16] = v223; + values[17] = v221; + values[18] = v219; + values[19] = v217; + values[20] = v215; + values[21] = v213; + values[22] = v211; + values[23] = v209; + values[24] = v207; + values[25] = v205; + values[26] = v203; + values[27] = v201; + values[28] = v199; + values[29] = v197; + values[30] = v195; + values[31] = v193; + } + + /// + /// Processes complete 16-lane inverse batches along input columns. + /// + /// The number of positions in each transform. + /// The number of independent transforms. + /// The borrowed source matrix. + /// The source row stride, in floats. + /// The borrowed output matrix. + /// The destination row stride, in floats. + /// The first independent transform not already processed by a wider batch. + /// Borrowed storage for two length-by-16 banks. Only the first bank needs initialized inputs. + /// The first independent transform left after the complete batches. + private static int InverseColumns512( + int length, + int count, + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int first, + Span workspace) + { + Span> values = MemoryMarshal.Cast>(workspace[..(length * 16)]); + Span> scratch = MemoryMarshal.Cast>(workspace.Slice(length * 16, length * 16)); + ref float input = ref MemoryMarshal.GetReference(source); + ref float output = ref MemoryMarshal.GetReference(destination); + + for (; first <= count - 16; first += 16) + { + // One vector covers adjacent columns at the same transform position. Physical strides never enter the coefficient-bank layout. + for (int i = 0; i < length; i++) + { + values[i] = Vector512.LoadUnsafe(ref input, (nuint)((i * sourceStride) + first)); + } + + Inverse(values, scratch); + for (int i = 0; i < length; i++) + { + values[i].StoreUnsafe(ref output, (nuint)((i * destinationStride) + first)); + } + } + + return first; + } + + /// + /// Processes complete 16-lane inverse batches along input rows. + /// + /// The number of positions in each transform. + /// The number of independent transforms. + /// The borrowed source matrix. + /// The source row stride, in floats. + /// The borrowed output matrix. + /// The destination row stride, in floats. + /// Whether transform positions become output rows and input rows become output columns. + /// The first independent transform not already processed by a wider batch. + /// Borrowed storage for two length-by-16 banks. Only the first bank needs initialized inputs. + /// The first independent transform left after the complete batches. + private static int InverseRows512( + int length, + int count, + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + bool transposed, + int first, + Span workspace) + { + Span> values = MemoryMarshal.Cast>(workspace[..(length * 16)]); + Span> scratch = MemoryMarshal.Cast>(workspace.Slice(length * 16, length * 16)); + ref float input = ref MemoryMarshal.GetReference(source); + ref float output = ref MemoryMarshal.GetReference(destination); + + for (; first <= count - 16; first += 16) + { + // Each square load contains adjacent samples from independent rows. Transposition puts those rows into SIMD lanes. + // The complete row band is loaded before any same-orientation output is written, so source and destination can be the same matrix. + for (int column = 0; column < length; column += 16) + { + Vector512 row0 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 0) * sourceStride) + column)).AsInt32(); + Vector512 row1 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 1) * sourceStride) + column)).AsInt32(); + Vector512 row2 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 2) * sourceStride) + column)).AsInt32(); + Vector512 row3 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 3) * sourceStride) + column)).AsInt32(); + Vector512 row4 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 4) * sourceStride) + column)).AsInt32(); + Vector512 row5 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 5) * sourceStride) + column)).AsInt32(); + Vector512 row6 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 6) * sourceStride) + column)).AsInt32(); + Vector512 row7 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 7) * sourceStride) + column)).AsInt32(); + Vector512 row8 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 8) * sourceStride) + column)).AsInt32(); + Vector512 row9 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 9) * sourceStride) + column)).AsInt32(); + Vector512 row10 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 10) * sourceStride) + column)).AsInt32(); + Vector512 row11 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 11) * sourceStride) + column)).AsInt32(); + Vector512 row12 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 12) * sourceStride) + column)).AsInt32(); + Vector512 row13 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 13) * sourceStride) + column)).AsInt32(); + Vector512 row14 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 14) * sourceStride) + column)).AsInt32(); + Vector512 row15 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 15) * sourceStride) + column)).AsInt32(); + + SimdUtils.Transpose( + ref row0, + ref row1, + ref row2, + ref row3, + ref row4, + ref row5, + ref row6, + ref row7, + ref row8, + ref row9, + ref row10, + ref row11, + ref row12, + ref row13, + ref row14, + ref row15); + + values[column + 0] = row0.AsSingle(); + values[column + 1] = row1.AsSingle(); + values[column + 2] = row2.AsSingle(); + values[column + 3] = row3.AsSingle(); + values[column + 4] = row4.AsSingle(); + values[column + 5] = row5.AsSingle(); + values[column + 6] = row6.AsSingle(); + values[column + 7] = row7.AsSingle(); + values[column + 8] = row8.AsSingle(); + values[column + 9] = row9.AsSingle(); + values[column + 10] = row10.AsSingle(); + values[column + 11] = row11.AsSingle(); + values[column + 12] = row12.AsSingle(); + values[column + 13] = row13.AsSingle(); + values[column + 14] = row14.AsSingle(); + values[column + 15] = row15.AsSingle(); + } + + Inverse(values, scratch); + if (transposed) + { + // A complete vector is one contiguous output row. Only logical samples are stored; destination padding remains untouched. + for (int i = 0; i < length; i++) + { + values[i].StoreUnsafe(ref output, (nuint)((i * destinationStride) + first)); + } + } + else + { + // Restore row-major order one register tile at a time. This replaces a separate full-block transpose and copy. + for (int column = 0; column < length; column += 16) + { + Vector512 row0 = values[column + 0].AsInt32(); + Vector512 row1 = values[column + 1].AsInt32(); + Vector512 row2 = values[column + 2].AsInt32(); + Vector512 row3 = values[column + 3].AsInt32(); + Vector512 row4 = values[column + 4].AsInt32(); + Vector512 row5 = values[column + 5].AsInt32(); + Vector512 row6 = values[column + 6].AsInt32(); + Vector512 row7 = values[column + 7].AsInt32(); + Vector512 row8 = values[column + 8].AsInt32(); + Vector512 row9 = values[column + 9].AsInt32(); + Vector512 row10 = values[column + 10].AsInt32(); + Vector512 row11 = values[column + 11].AsInt32(); + Vector512 row12 = values[column + 12].AsInt32(); + Vector512 row13 = values[column + 13].AsInt32(); + Vector512 row14 = values[column + 14].AsInt32(); + Vector512 row15 = values[column + 15].AsInt32(); + + SimdUtils.Transpose( + ref row0, + ref row1, + ref row2, + ref row3, + ref row4, + ref row5, + ref row6, + ref row7, + ref row8, + ref row9, + ref row10, + ref row11, + ref row12, + ref row13, + ref row14, + ref row15); + + row0.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 0) * destinationStride) + column)); + row1.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 1) * destinationStride) + column)); + row2.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 2) * destinationStride) + column)); + row3.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 3) * destinationStride) + column)); + row4.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 4) * destinationStride) + column)); + row5.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 5) * destinationStride) + column)); + row6.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 6) * destinationStride) + column)); + row7.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 7) * destinationStride) + column)); + row8.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 8) * destinationStride) + column)); + row9.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 9) * destinationStride) + column)); + row10.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 10) * destinationStride) + column)); + row11.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 11) * destinationStride) + column)); + row12.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 12) * destinationStride) + column)); + row13.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 13) * destinationStride) + column)); + row14.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 14) * destinationStride) + column)); + row15.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 15) * destinationStride) + column)); + } + } + } + + return first; + } + +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDct.Kernels.Generated.tt b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDct.Kernels.Generated.tt new file mode 100644 index 0000000000..36c91f7f00 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDct.Kernels.Generated.tt @@ -0,0 +1,424 @@ +<# +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#> +<#@ template language="C#" hostspecific="true" #> +<#@ import namespace="System" #> +<#@ import namespace="System.Collections.Generic" #> +<#@ import namespace="System.IO" #> +<#@ import namespace="System.Linq" #> +<#@ import namespace="System.Text" #> +<#@ import namespace="System.Text.RegularExpressions" #> +<#@ output extension=".cs" #> +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; + +internal static partial class JxlDct +{ +<# + // Keep one arithmetic description for every lane width. Only transforms through 32 points are expanded into register expressions. + // Larger transforms reuse two borrowed banks and the same small kernels. This bounds generated code size and register pressure. + // Constants are read from their existing owner, so generation cannot introduce another rounded copy of the coefficient tables. + // Regenerate with: t4 JxlDct.Kernels.Generated.tt -o JxlDct.Kernels.Generated.cs + string scales = File.ReadAllText(this.Host.ResolvePath("JxlDctScales.cs")); + Dictionary multipliers = new Dictionary(); + foreach (int size in new[] { 4, 8, 16, 32 }) + { + string expression = @"Multipliers" + size + @" =>\s*\[(.*?)\];"; + Match match = Regex.Match(scales, expression, RegexOptions.Singleline); + multipliers.Add(size, match.Groups[1].Value.Split(',').Select(x => x.Trim()).Where(x => x.Length != 0).ToArray()); + } + + foreach (int lanes in new[] { 1, 4, 8, 16 }) + { + string type = lanes == 1 ? "float" : "Vector" + (lanes * 32) + ""; + string suffix = lanes == 1 ? "Scalar" : (lanes * 32).ToString(); + string vector = lanes == 1 ? "float" : "Vector" + (lanes * 32); + foreach (bool inverse in new[] { false, true }) + { + string direction = inverse ? "Inverse" : "Forward"; +#> + /// + /// Computes independent <#=inverse ? "inverse" : "forward"#> transforms using two nonoverlapping borrowed vector banks. + /// + /// One vector per transform position. Its length is a power of two from one through 256. + /// At least values.Length vectors. Previous contents are not read. + private static void <#=direction#>(Span<<#=type#>> values, Span<<#=type#>> scratch) + { + // Small transforms stay in registers. The one-point transform is an identity and must not copy any trailing samples. + switch (values.Length) + { + case 1: + return; +<# foreach (int size in new[] { 2, 4, 8, 16, 32 }) { #> + case <#=size#>: + <#=direction#><#=size#>(values); + return; +<# } #> + } + + int half = values.Length / 2; +<# if (inverse) { #> + // Separate frequency parity before either recursive call. The original bank can then serve as recursive scratch. + for (int i = 0; i < half; i++) + { + scratch[i] = values[2 * i]; + scratch[half + i] = values[(2 * i) + 1]; + } + + Inverse(scratch[..half], values); + + // Descend so every odd-frequency predecessor is read before its own entry changes. + for (int i = half - 1; i > 0; i--) + { + scratch[half + i] += scratch[half + i - 1]; + } + + scratch[half] *= <#=Splat(lanes, "JxlDctScales.Sqrt2")#>; + Inverse(scratch.Slice(half, half), values); + ReadOnlySpan weights = JxlDctScales.GetMultipliers(values.Length); + + for (int i = 0; i < half; i++) + { + <#=type#> weight = <#=Splat(lanes, "weights[i]")#>; + <#=type#> even = scratch[i]; + <#=type#> odd = scratch[half + i]; + values[i] = <#=vector#>.MultiplyAddEstimate(weight, odd, even); + values[values.Length - i - 1] = <#=vector#>.MultiplyAddEstimate(-weight, odd, even); + } +<# } else { #> + ReadOnlySpan weights = JxlDctScales.GetMultipliers(values.Length); + + // Consume both original halves before reusing their bank as recursive scratch. Neither recursive call can then destroy unread input. + for (int i = 0; i < half; i++) + { + <#=type#> first = values[i]; + <#=type#> last = values[values.Length - i - 1]; + scratch[i] = first + last; + scratch[half + i] = (first - last) * <#=Splat(lanes, "weights[i]")#>; + } + + Forward(scratch[..half], values); + Forward(scratch.Slice(half, half), values); + scratch[half] = <#=vector#>.MultiplyAddEstimate(scratch[half], <#=Splat(lanes, "JxlDctScales.Sqrt2")#>, scratch[half + 1]); + + for (int i = 1; i + 1 < half; i++) + { + scratch[half + i] += scratch[half + i + 1]; + } + + for (int i = 0; i < half; i++) + { + values[2 * i] = scratch[i]; + values[(2 * i) + 1] = scratch[half + i]; + } +<# } #> + } + +<# + foreach (int size in new[] { 2, 4, 8, 16, 32 }) + { +#> + /// + /// Computes independent <#=size#>-point <#=inverse ? "inverse" : "forward"#> transforms without intermediate memory passes. + /// + /// Exactly <#=size#> position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void <#=direction#><#=size#>(Span<<#=type#>> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. +<# for (int i = 0; i < size; i++) { #> + <#=type#> input<#=i#> = values[<#=i#>]; +<# } #> + +<# + List statements = new List(); + int temporary = 0; + string[] inputs = Enumerable.Range(0, size).Select(i => "input" + i).ToArray(); + string[] results = Expand(inputs, inverse, lanes, multipliers, statements, ref temporary); + foreach (string statement in statements) { WriteLine(" " + statement); } + WriteLine(""); + for (int i = 0; i < size; i++) { WriteLine(" values[" + i + "] = " + results[i] + ";"); } +#> + } + +<# + } + foreach (bool rowBlock in new[] { false, true }) + { + string orientation = rowBlock ? "Rows" : "Columns"; +#> + /// + /// Processes complete <#=lanes#>-lane <#=inverse ? "inverse" : "forward"#> batches along <#=rowBlock ? "input rows" : "input columns"#>. + /// + /// The number of positions in each transform. + /// The number of independent transforms. + /// The borrowed source matrix. + /// The source row stride, in floats. + /// The borrowed output matrix. + /// The destination row stride, in floats. +<# if (rowBlock) { #> + /// Whether transform positions become output rows and input rows become output columns. +<# } #> + /// The first independent transform not already processed by a wider batch. + /// Borrowed storage for two length-by-<#=lanes#> banks. Only the first bank needs initialized inputs. + /// The first independent transform left after the complete batches. + private static int <#=direction#><#=orientation#><#=suffix#>( + int length, + int count, + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, +<# if (rowBlock) { #> + bool transposed, +<# } #> + int first, + Span workspace) + { +<# if (lanes == 1) { #> + Span values = workspace[..length]; + Span scratch = workspace.Slice(length, length); +<# } else { #> + Span<<#=type#>> values = MemoryMarshal.Cast>(workspace[..(length * <#=lanes#>)]); + Span<<#=type#>> scratch = MemoryMarshal.Cast>(workspace.Slice(length * <#=lanes#>, length * <#=lanes#>)); +<# } #> + ref float input = ref MemoryMarshal.GetReference(source); + ref float output = ref MemoryMarshal.GetReference(destination); +<# if (!inverse) { #> + <#=type#> scale = <#=Splat(lanes, "1F / length")#>; +<# } #> + + for (; first <= count - <#=lanes#>; first += <#=lanes#>) + { +<# if (!rowBlock) { #> + // One vector covers adjacent columns at the same transform position. Physical strides never enter the coefficient-bank layout. + for (int i = 0; i < length; i++) + { +<# if (lanes == 1) { #> + values[i] = Unsafe.Add(ref input, (i * sourceStride) + first); +<# } else { #> + values[i] = <#=vector#>.LoadUnsafe(ref input, (nuint)((i * sourceStride) + first)); +<# } #> + } + + <#=direction#>(values, scratch); + for (int i = 0; i < length; i++) + { +<# if (lanes == 1) { #> + Unsafe.Add(ref output, (i * destinationStride) + first) = values[i]<#=inverse ? "" : " * scale"#>; +<# } else { #> + <#=inverse ? "values[i]" : "(values[i] * scale)"#>.StoreUnsafe(ref output, (nuint)((i * destinationStride) + first)); +<# } #> + } +<# } else { #> +<# if (lanes == 1) { #> + source.Slice(first * sourceStride, length).CopyTo(values); +<# } else { #> + // Each square load contains adjacent samples from independent rows. Transposition puts those rows into SIMD lanes. + // The complete row band is loaded before any same-orientation output is written, so source and destination can be the same matrix. + for (int column = 0; column < length; column += <#=lanes#>) + { +<# for (int i = 0; i < lanes; i++) { #> + Vector<#=lanes * 32#> row<#=i#> = <#=vector#>.LoadUnsafe(ref input, (nuint)(((first + <#=i#>) * sourceStride) + column)).AsInt32(); +<# } #> + +<# WriteTransposeCall(lanes, 16); #> +<# for (int i = 0; i < lanes; i++) { #> + values[column + <#=i#>] = row<#=i#>.AsSingle(); +<# } #> + } +<# } #> + + <#=direction#>(values, scratch); + if (transposed) + { + // A complete vector is one contiguous output row. Only logical samples are stored; destination padding remains untouched. + for (int i = 0; i < length; i++) + { +<# if (lanes == 1) { #> + Unsafe.Add(ref output, (i * destinationStride) + first) = values[i]<#=inverse ? "" : " * scale"#>; +<# } else { #> + <#=inverse ? "values[i]" : "(values[i] * scale)"#>.StoreUnsafe(ref output, (nuint)((i * destinationStride) + first)); +<# } #> + } + } + else + { +<# if (lanes == 1) { #> + for (int i = 0; i < length; i++) + { + Unsafe.Add(ref output, (first * destinationStride) + i) = values[i]<#=inverse ? "" : " * scale"#>; + } +<# } else { #> + // Restore row-major order one register tile at a time. This replaces a separate full-block transpose and copy. + for (int column = 0; column < length; column += <#=lanes#>) + { +<# for (int i = 0; i < lanes; i++) { #> + Vector<#=lanes * 32#> row<#=i#> = <#=inverse ? "values[column + " + i + "]" : "(values[column + " + i + "] * scale)"#>.AsInt32(); +<# } #> + +<# WriteTransposeCall(lanes, 20); #> +<# for (int i = 0; i < lanes; i++) { #> + row<#=i#>.AsSingle().StoreUnsafe(ref output, (nuint)(((first + <#=i#>) * destinationStride) + column)); +<# } #> + } +<# } #> + } +<# } #> + } + + return first; + } + +<# + } + } + } +#> +} +<#+ + /// + /// Emits a scalar constant or broadcasts the same float bits to every independent lane. + /// + /// The number of lanes in the generated type. + /// The scalar expression. + /// The typed expression. + private static string Splat(int lanes, string expression) + => lanes == 1 ? expression : "Vector" + (lanes * 32) + ".Create(" + expression + ")"; + + /// + /// Emits one named intermediate so generated butterflies do not duplicate calculations. + /// + /// The arithmetic expression. + /// The number of independent lanes. + /// The statements in execution order. + /// The next local-variable index. + /// The emitted variable name. + private static string Emit(string expression, int lanes, List statements, ref int temporary) + { + string name = "v" + temporary++; + string type = lanes == 1 ? "float" : "Vector" + (lanes * 32) + ""; + statements.Add(type + " " + name + " = " + expression + ";"); + + return name; + } + + /// + /// Expands the Perera-Liu radix-two equations using the existing constant tables and native multiply-add sites. + /// + /// The input variables, in transform order. + /// Whether to emit the inverse transform. + /// The number of independent lanes. + /// The unchanged scalar literals read from JxlDctScales. + /// The generated statements. + /// The next local-variable index. + /// The output variables, in transform order. + private static string[] Expand( + string[] input, + bool inverse, + int lanes, + Dictionary weights, + List statements, + ref int temporary) + { + int count = input.Length; + if (count == 1) + { + return input; + } + + if (count == 2) + { + return new[] + { + Emit(input[0] + " + " + input[1], lanes, statements, ref temporary), + Emit(input[0] + " - " + input[1], lanes, statements, ref temporary) + }; + } + + int half = count / 2; + string[] even = new string[half]; + string[] odd = new string[half]; + string vector = lanes == 1 ? "float" : "Vector" + (lanes * 32); + if (inverse) + { + for (int i = 0; i < half; i++) + { + even[i] = input[2 * i]; + odd[i] = input[(2 * i) + 1]; + } + + even = Expand(even, true, lanes, weights, statements, ref temporary); + for (int i = half - 1; i > 0; i--) + { + odd[i] = Emit(odd[i] + " + " + odd[i - 1], lanes, statements, ref temporary); + } + + odd[0] = Emit(odd[0] + " * " + Splat(lanes, "JxlDctScales.Sqrt2"), lanes, statements, ref temporary); + odd = Expand(odd, true, lanes, weights, statements, ref temporary); + string[] output = new string[count]; + for (int i = 0; i < half; i++) + { + string weight = Splat(lanes, weights[count][i]); + output[i] = Emit(vector + ".MultiplyAddEstimate(" + weight + ", " + odd[i] + ", " + even[i] + ")", lanes, statements, ref temporary); + output[count - i - 1] = Emit( + vector + ".MultiplyAddEstimate(-" + weight + ", " + odd[i] + ", " + even[i] + ")", lanes, statements, ref temporary); + } + + return output; + } + + for (int i = 0; i < half; i++) + { + even[i] = Emit(input[i] + " + " + input[count - i - 1], lanes, statements, ref temporary); + odd[i] = Emit("(" + input[i] + " - " + input[count - i - 1] + ") * " + Splat(lanes, weights[count][i]), lanes, statements, ref temporary); + } + + even = Expand(even, false, lanes, weights, statements, ref temporary); + odd = Expand(odd, false, lanes, weights, statements, ref temporary); + odd[0] = Emit( + vector + ".MultiplyAddEstimate(" + odd[0] + ", " + Splat(lanes, "JxlDctScales.Sqrt2") + ", " + odd[1] + ")", lanes, statements, ref temporary); + for (int i = 1; i + 1 < half; i++) + { + odd[i] = Emit(odd[i] + " + " + odd[i + 1], lanes, statements, ref temporary); + } + + return Enumerable.Range(0, count).Select(i => (i & 1) == 0 ? even[i / 2] : odd[i / 2]).ToArray(); + } + + /// + /// Emits the existing shared integer transpose call. Reinterpretation preserves every floating-point bit. + /// + /// The square tile dimension. + /// The generated indentation. + private void WriteTransposeCall(int lanes, int indent) + { + string prefix = new string(' ', indent); + string[] arguments = Enumerable.Range(0, lanes).Select(i => "ref row" + i).ToArray(); + string call = prefix + "SimdUtils.Transpose(" + string.Join(", ", arguments) + ");"; + if (call.Length <= 160) + { + this.WriteLine(call); + } + else + { + this.WriteLine(prefix + "SimdUtils.Transpose("); + for (int i = 0; i < lanes; i++) + { + this.WriteLine(prefix + " " + arguments[i] + (i + 1 == lanes ? ");" : ",")); + } + + this.WriteLine(""); + } + } +#> diff --git a/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDct.cs b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDct.cs index a99ab81e8d..2d9e0819e4 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDct.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDct.cs @@ -1,305 +1,334 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Numerics; -using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; /// -/// Discrete Cosine Transform with SIMD support. +/// Converts strided sample blocks and canonically ordered JPEG XL DCT coefficients. /// -internal static class JxlDct +/// +/// The coefficient matrix has min(rows, columns) rows and max(rows, columns) columns. A tall or square source therefore has transposed coefficients. +/// Each SIMD lane holds an independent one-dimensional transform. The two axes retain libjxl's operation order and normalization. +/// +internal static partial class JxlDct { /// - /// Creates a new coefficient bundle. + /// Gets the maximum one-dimensional workspace length, in floats, for the enabled SIMD width and supported 256-point transforms. /// - /// Number of items. - /// Coefficient size. - /// A new coefficient bundle. - public static CoefficientBundle CoeffBundle(int n, int sz) => new(n, sz); - - public static void Dct1DCore(int n, int sz, Span mem, Span tmp) + /// + /// The first bank holds the current vectors. The second bank holds the even and odd stages. + /// Recursive calls exchange these banks after their input values are no longer needed. No transform call allocates storage. + /// The lane count follows the same hardware checks as transform dispatch. Scalar execution requires one lane per position. + /// + public static int MaximumWorkingSetLength { - if (n == 2) - { - Vector in1 = new(mem); - Vector in2 = new(mem[sz..]); - (in1 + in2).CopyTo(mem); - (in1 - in2).CopyTo(mem[sz..]); - } - else + get { - CoefficientBundle cb = CoeffBundle(n / 2, sz); - - cb.AddReverse(mem, mem[(n / 2 * sz)..], tmp); - Dct1DCore(n / 2, sz, tmp, tmp[(n * sz)..]); - - cb.SubReverse(mem, mem[(n / 2 * sz)..], tmp[(n / 2 * sz)..]); - cb.Multiply(tmp); - - Dct1DCore(n / 2, sz, tmp[(n / 2 * sz)..], tmp[(n * sz)..]); - cb.B(tmp[(n / 2 * sz)..]); + // Dispatch cannot enter a wider worker when its hardware check is false. Reserve two banks only for the widest enabled worker. + int lanes = Vector512.IsHardwareAccelerated ? Vector512.Count + : Vector256.IsHardwareAccelerated ? Vector256.Count + : Vector128.IsHardwareAccelerated ? Vector128.Count : 1; - CoeffBundle(n, sz).InverseEvenOdd(tmp, mem); + return 2 * JxlAcStrategy.MaximumBlockDimension * lanes; } } - public static void InverseDct1DCore(int n, int sz, Span from, int fromStride, Span to, int toStride, Span tmp) + /// + /// Computes the normalized forward DCT without changing the source pixels. + /// + /// The source height. Both dimensions are powers of two from one through 256. + /// The source width. + /// The borrowed source samples. Source and output storage must not overlap. + /// The physical source row stride, in floats. + /// The contiguous output coefficient matrix in canonical order. + /// Borrowed scratch for one sample block and two banks of max(rows, columns) SIMD vectors. + public static void ComputeScaledDct(int rows, int columns, ReadOnlySpan source, int sourceStride, Span coefficients, Span workspace) { - if (n == 1) - { - from.CopyTo(to); - } - else if (n == 2) + int area = rows * columns; + bool transposed = rows >= columns; + + // The forward order is height, then width. Retain the first-axis matrix in sample order until each complete row band is consumed. + // Transposed final stores would overwrite later bands in the coefficient buffer, so that case uses the existing block-sized scratch region. + Span intermediate = transposed ? workspace[..area] : coefficients[..area]; + Span work = transposed ? workspace[area..] : workspace; + ForwardColumns(rows, columns, source, sourceStride, intermediate, columns, work); + ForwardRows(rows, columns, intermediate, columns, coefficients, transposed ? rows : columns, transposed, work); + } + + /// + /// Restores pixels from a canonical coefficient matrix with the inverse DCT. + /// + /// The restored height. Both dimensions are powers of two from one through 256. + /// The restored width. + /// The contiguous coefficient matrix. Its contents are consumed and can change. + /// The borrowed destination samples. This storage must not overlap the coefficient or scratch storage. + /// The physical destination row stride, in floats. + /// Borrowed scratch for two banks of max(rows, columns) SIMD vectors. + public static void ComputeScaledInverseDct(int rows, int columns, Span coefficients, Span pixels, int pixelsStride, Span workspace) + { + // Inverse axes run in the opposite order: width, then height. The first axis reuses the consumed coefficients. + // Only the final axis writes pixels. Row-band loads finish before their same-orientation stores, so no source clone is needed. + if (rows >= columns) { - Vector in1 = new(from); - Vector in2 = new(from[fromStride..]); - (in1 + in2).CopyTo(to); - (in1 + in2).CopyTo(to[toStride..]); + InverseColumns(columns, rows, coefficients, rows, coefficients, rows, workspace); + InverseRows(columns, rows, coefficients, rows, pixels, pixelsStride, true, workspace); } else { - CoefficientBundle cbDiv2 = CoeffBundle(n / 2, sz); - CoefficientBundle cb = CoeffBundle(n, sz); - - cb.ForwardEvenOdd(from, fromStride, tmp); - InverseDct1DCore(n / 2, sz, tmp, sz, tmp, sz, tmp[(n * sz)..]); - - cbDiv2.BTranspose(tmp[((n / 2) * sz)..]); - InverseDct1DCore(n / 2, sz, tmp[((n / 2) * sz)..], sz, tmp[((n / 2) * sz)..], sz, tmp[(n * sz)..]); - - cb.MultiplyAndAdd(tmp, to, toStride); + InverseRows(rows, columns, coefficients, columns, coefficients, columns, false, workspace); + InverseColumns(rows, columns, coefficients, columns, pixels, pixelsStride, workspace); } } - public static void Dct1DWrapper(int n, int m, bool fit, JxlDctSource from, JxlDctOutput to, int mp, Span tmp) + /// + /// Applies the forward or inverse AFV basis to one four-by-four sample block. + /// + /// Sixteen input samples. The destination must not overlap this span. + /// Sixteen rows of sixteen coefficients. Row j contains input j's contribution to each output position. + /// Storage for sixteen output samples. Later samples remain unchanged. + public static void TransformAfv4x4(ReadOnlySpan source, ReadOnlySpan basis, Span destination) { - CoefficientBundle cb = CoeffBundle(n, m); + ref float output = ref MemoryMarshal.GetReference(destination); - for (int i = 0; i < mp; i += m) + // A lane identifies one output position. Accumulate inputs in their serialized basis order, as libjxl does. + // The basis is shared constant data. Borrow its existing arrays directly; no matrix packing or reduction buffer is needed. + if (Vector512.IsHardwareAccelerated) { - cb.LoadFromBlock(from, i, tmp); - Dct1DCore(n, m, tmp, tmp[(n * m)..]); - cb.StoreToBlockAndScale(tmp, ref to, i); - - if (fit) + for (int i = 0; i < 16; i += Vector512.Count) { - return; + Vector512 result = Vector512.Zero; + + for (int j = 0; j < 16; j++) + { + ref float row = ref MemoryMarshal.GetArrayDataReference(basis[j]); + Vector512 weights = Vector512.LoadUnsafe(ref row, (uint)i); + result = Vector512.MultiplyAddEstimate(Vector512.Create(source[j]), weights, result); + } + + result.StoreUnsafe(ref output, (uint)i); } } - } - - public static void InverseDct1DWrapper(int n, int m, bool fit, JxlDctSource from, JxlDctOutput to, int mp, Span tmp) - { - for (int i = 0; i < mp; i += m) + else if (Vector256.IsHardwareAccelerated) { - InverseDct1DCore(n, m, from.Address(0, i), from.Stride, to.Address(0, i), to.Stride, tmp); - - if (fit) + for (int i = 0; i < 16; i += Vector256.Count) { - return; + Vector256 result = Vector256.Zero; + + for (int j = 0; j < 16; j++) + { + ref float row = ref MemoryMarshal.GetArrayDataReference(basis[j]); + Vector256 weights = Vector256.LoadUnsafe(ref row, (uint)i); + result = Vector256.MultiplyAddEstimate(Vector256.Create(source[j]), weights, result); + } + + result.StoreUnsafe(ref output, (uint)i); } } - } + else if (Vector128.IsHardwareAccelerated) + { + for (int i = 0; i < 16; i += Vector128.Count) + { + Vector128 result = Vector128.Zero; - public static void Dct1DCapped(int n, int m, int l, JxlDctSource from, JxlDctOutput to, Span tmp) - { - bool fit = m <= l; - Dct1DWrapper(n, m, fit, from, to, m, tmp); - } + for (int j = 0; j < 16; j++) + { + ref float row = ref MemoryMarshal.GetArrayDataReference(basis[j]); + Vector128 weights = Vector128.LoadUnsafe(ref row, (uint)i); + result = Vector128.MultiplyAddEstimate(Vector128.Create(source[j]), weights, result); + } - public static void InverseDct1DCapped(int n, int m, int l, JxlDctSource from, JxlDctOutput to, Span tmp) - { - bool fit = m <= l; - InverseDct1DWrapper(n, m, fit, from, to, m, tmp); - } + result.StoreUnsafe(ref output, (uint)i); + } + } + else + { + for (int i = 0; i < 16; i++) + { + float result = 0; - public static void Dct1D(int n, int m, JxlDctSource from, JxlDctOutput to, Span tmp) - { - int lanes = Vector.Count; - Dct1DCapped(n, m, lanes, from, to, tmp); - } + for (int j = 0; j < 16; j++) + { + result = float.MultiplyAddEstimate(source[j], basis[j][i], result); + } - public static void InverseDct1D(int n, int m, JxlDctSource source, JxlDctOutput output, Span tmp) - { - int lanes = Vector.Count; - InverseDct1DCapped(n, m, lanes, source, output, tmp); + destination[i] = result; + } + } } - public static void ComputeScaledDct(int rows, int columns, JxlDctSource from, Span to, Span scratchSpace) + /// + /// Selects complete SIMD batches once for each independent axis. Remaining one- or two-value batches use scalar arithmetic. + /// + /// The number of positions along each one-dimensional transform. + /// The number of independent transforms. + /// The borrowed input matrix. + /// The input row stride, in floats. + /// The borrowed output matrix. + /// The output row stride, in floats. + /// The shared two-bank transform workspace. + private static void ForwardColumns( + int length, + int count, + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + Span workspace) { - Span block = scratchSpace; - Span tmp = scratchSpace[(rows * columns)..]; + int first = 0; - if (rows < columns) + if (Vector512.IsHardwareAccelerated && count - first >= Vector512.Count) { - Dct1D(rows, columns, from, new JxlDctOutput(block, columns), tmp); - SimdUtils.Transpose(block, columns, to, rows, rows, columns); - Dct1D(columns, rows, new JxlDctSource(to, rows), new JxlDctOutput(block, rows), tmp); - SimdUtils.Transpose(block, rows, to, columns, columns, rows); + first = ForwardColumns512(length, count, source, sourceStride, destination, destinationStride, first, workspace); } - else + + if (Vector256.IsHardwareAccelerated && count - first >= Vector256.Count) { - Dct1D(rows, columns, from, new JxlDctOutput(to, columns), tmp); - SimdUtils.Transpose(to, columns, block, rows, rows, columns); - Dct1D(columns, rows, new JxlDctSource(block, rows), new JxlDctOutput(to, rows), tmp); + first = ForwardColumns256(length, count, source, sourceStride, destination, destinationStride, first, workspace); } - } - - public static void ComputeScaledInverseDct(int rows, int columns, Span from, JxlDctOutput to, Span scratchSpace) - { - Span block = scratchSpace; - Span tmp = scratchSpace[(rows * columns)..]; - if (rows < columns) + if (Vector128.IsHardwareAccelerated && count - first >= Vector128.Count) { - SimdUtils.Transpose(from, columns, block, rows, rows, columns); - InverseDct1D(columns, rows, new JxlDctSource(block, rows), new JxlDctOutput(from, rows), tmp); - SimdUtils.Transpose(from, rows, block, columns, columns, rows); - InverseDct1D(rows, columns, new JxlDctSource(block, columns), to, tmp); + first = ForwardColumns128(length, count, source, sourceStride, destination, destinationStride, first, workspace); } - else + + if (first < count) { - InverseDct1D(columns, rows, new JxlDctSource(from, rows), new JxlDctOutput(block, rows), tmp); - SimdUtils.Transpose(block, rows, from, columns, columns, rows); - InverseDct1D(rows, columns, new JxlDctSource(from, columns), to, tmp); + ForwardColumnsScalar(length, count, source, sourceStride, destination, destinationStride, first, workspace); } } /// - /// Core methods for the Discrete Cosine Transform (DCT). + /// Selects complete SIMD batches once for each independent axis. Remaining one- or two-value batches use scalar arithmetic. /// - public readonly struct CoefficientBundle(int n, int sz) + /// The number of independent transforms. + /// The number of positions along each one-dimensional transform. + /// The borrowed input matrix. + /// The input row stride, in floats. + /// The borrowed output matrix. + /// The output row stride, in floats. + /// Whether transformed positions become destination rows and independent input rows become destination columns. + /// The shared two-bank transform workspace. + private static void ForwardRows( + int count, + int length, + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + bool transposed, + Span workspace) { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void AddReverse(Span aIn1, Span aIn2, Span aOut) + int first = 0; + + if (Vector512.IsHardwareAccelerated && count - first >= Vector512.Count && length >= Vector512.Count) { - for (int i = 0; i < n; i++) - { - Vector in1 = new(aIn1[(i * sz)..]); - Vector in2 = new(aIn2[((n - i - 1) * sz)..]); - (in1 + in2).CopyTo(aOut[(i * sz)..]); - } + first = ForwardRows512(length, count, source, sourceStride, destination, destinationStride, transposed, first, workspace); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void SubReverse(Span aIn1, Span aIn2, Span aOut) + if (Vector256.IsHardwareAccelerated && count - first >= Vector256.Count && length >= Vector256.Count) { - for (int i = 0; i < n; i++) - { - Vector in1 = new(aIn1[(i * sz)..]); - Vector in2 = new(aIn2[((n - i - 1) * sz)..]); - (in1 - in2).CopyTo(aOut[(i * sz)..]); - } + first = ForwardRows256(length, count, source, sourceStride, destination, destinationStride, transposed, first, workspace); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void B(Span coeff) + if (Vector128.IsHardwareAccelerated && count - first >= Vector128.Count && length >= Vector128.Count) { - Vector sqrt2 = new(JxlDctScales.Sqrt2); - Vector in10 = new(coeff); - Vector in20 = new(coeff[sz..]); - ((in10 * sqrt2) + in20).CopyTo(coeff); - - for (int i = 1; i + 1 < n; i++) - { - Vector in1 = new(coeff[(i * sz)..]); - Vector in2 = new(coeff[((i + 1) * sz)..]); - (in1 + in2).CopyTo(coeff[(i * sz)..]); - } + first = ForwardRows128(length, count, source, sourceStride, destination, destinationStride, transposed, first, workspace); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void BTranspose(Span coeff) + if (first < count) { - for (int i = n - 1; i > 0; i--) - { - Vector in1 = new(coeff[(i * sz)..]); - Vector in2 = new(coeff[((i - 1) * sz)..]); - (in1 + in2).CopyTo(coeff[(i * sz)..]); - } - - Vector sqrt2 = new(JxlDctScales.Sqrt2); - Vector in1x = new(coeff); - (in1x * sqrt2).CopyTo(coeff); + ForwardRowsScalar(length, count, source, sourceStride, destination, destinationStride, transposed, first, workspace); } + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void InverseEvenOdd(Span aIn, Span aOut) - { - for (int i = 0; i < n / 2; i++) - { - new Vector(aIn[(i * sz)..]).CopyTo(aOut[((2 * i) * sz)..]); - } + /// + /// Selects complete SIMD batches once for each independent axis. Remaining one- or two-value batches use scalar arithmetic. + /// + /// The number of positions along each one-dimensional transform. + /// The number of independent transforms. + /// The borrowed input matrix. + /// The input row stride, in floats. + /// The borrowed output matrix. + /// The output row stride, in floats. + /// The shared two-bank transform workspace. + private static void InverseColumns( + int length, + int count, + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + Span workspace) + { + int first = 0; - for (int i = n / 2; i < n; i++) - { - new Vector(aIn[(i * sz)..]).CopyTo(aOut[(((2 * (i - (n / 2))) + 1) * sz)..]); - } + if (Vector512.IsHardwareAccelerated && count - first >= Vector512.Count) + { + first = InverseColumns512(length, count, source, sourceStride, destination, destinationStride, first, workspace); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void ForwardEvenOdd(Span aIn, int aInStride, Span aOut) + if (Vector256.IsHardwareAccelerated && count - first >= Vector256.Count) { - for (int i = 0; i < n / 2; i++) - { - new Vector(aIn[(2 * i * aInStride)..]).CopyTo(aOut[(i * sz)..]); - } - - for (int i = n / 2; i < n; i++) - { - new Vector(aIn[(((2 * (i - (n / 2))) + 1) * aInStride)..]).CopyTo(aOut[(i * sz)..]); - } + first = InverseColumns256(length, count, source, sourceStride, destination, destinationStride, first, workspace); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Multiply(Span coeff) + if (Vector128.IsHardwareAccelerated && count - first >= Vector128.Count) { - ReadOnlySpan multipliers = JxlDctScales.GetMultipliers(n); + first = InverseColumns128(length, count, source, sourceStride, destination, destinationStride, first, workspace); + } - for (int i = 0; i < n / 2; i++) - { - Vector in1 = new(coeff[(((n / 2) + i) * sz)..]); - Vector mul = new(multipliers[i]); - (in1 * mul).CopyTo(coeff[((n / (2 + i)) * sz)..]); - } + if (first < count) + { + InverseColumnsScalar(length, count, source, sourceStride, destination, destinationStride, first, workspace); } + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void MultiplyAndAdd(Span coeff, Span output, int outStride) + /// + /// Selects complete SIMD batches once for each independent axis. Remaining one- or two-value batches use scalar arithmetic. + /// + /// The number of independent transforms. + /// The number of positions along each one-dimensional transform. + /// The borrowed input matrix. + /// The input row stride, in floats. + /// The borrowed output matrix. + /// The output row stride, in floats. + /// Whether transformed positions become destination rows and independent input rows become destination columns. + /// The shared two-bank transform workspace. + private static void InverseRows( + int count, + int length, + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + bool transposed, + Span workspace) + { + int first = 0; + + if (Vector512.IsHardwareAccelerated && count - first >= Vector512.Count && length >= Vector512.Count) { - ReadOnlySpan multipliers = JxlDctScales.GetMultipliers(n); + first = InverseRows512(length, count, source, sourceStride, destination, destinationStride, transposed, first, workspace); + } - for (int i = 0; i < n / 2; i++) - { - Vector mul = new(multipliers[i]); - Vector in1 = new(coeff[(i * sz)..]); - Vector in2 = new(coeff[((n / (2 + i)) * sz)..]); - Vector out1 = (mul * in2) * in1; - Vector out2 = -(mul * in2) + in1; - out1.CopyTo(output[(i * outStride)..]); - out2.CopyTo(output[((n - i - 1) * outStride)..]); - } + if (Vector256.IsHardwareAccelerated && count - first >= Vector256.Count && length >= Vector256.Count) + { + first = InverseRows256(length, count, source, sourceStride, destination, destinationStride, transposed, first, workspace); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void LoadFromBlock(in JxlDctSource input, int offset, Span coeff) + if (Vector128.IsHardwareAccelerated && count - first >= Vector128.Count && length >= Vector128.Count) { - for (int i = 0; i < n; i++) - { - input.LoadPart(i, offset).CopyTo(coeff[(i * sz)..]); - } + first = InverseRows128(length, count, source, sourceStride, destination, destinationStride, transposed, first, workspace); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void StoreToBlockAndScale(Span coeff, ref JxlDctOutput output, int offset) + if (first < count) { - Vector mul = new(1.0f / n); - for (int i = 0; i < n; i++) - { - output.StorePart(mul * new Vector(coeff[(i * sz)..]), i, offset); - } + InverseRowsScalar(length, count, source, sourceStride, destination, destinationStride, transposed, first, workspace); } } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctOutput.cs b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctOutput.cs deleted file mode 100644 index 5a1d5c9581..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctOutput.cs +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.Intrinsics; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; - -/// -/// Output DCT block. -/// -internal ref struct JxlDctOutput(Span data, int stride) -{ - /// - /// Raw block data. - /// - public Span Data = data; - - /// - /// Stride size. - /// - public readonly int Stride = stride; - - /// - /// Returns the span to the start of a row and offset. - /// - /// The row index. - /// The offset. - /// - /// Span for that row & offset. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly Span Address(int row, int i) => this.Data[((row * this.Stride) + i)..]; - - /// - /// Writes a single value to the block at the row and offset. - /// - /// The value to write. - /// The row index. - /// The offset. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Write(float value, int row, int i) => this.Data[(row * this.Stride) + i] = value; - - /// - /// Stores the vector into the data at the specified row and offset. - /// - /// The vector to write. - /// The row index. - /// The offset. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly void StorePart(Vector value, int row, int index) => value.CopyTo(this.Address(row, index)); - - /// - /// Stores the vector into the data at the specified row and offset. - /// - /// The vector to write. - /// The row index. - /// The offset. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly void StorePart256(Vector256 value, int row, int index) => value.CopyTo(this.Address(row, index)); - - /// - /// Stores the vector into the data at the specified row and offset. - /// - /// The vector to write. - /// The row index. - /// The offset. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly void StorePart128(Vector128 value, int row, int index) => value.CopyTo(this.Address(row, index)); -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctSource.cs b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctSource.cs deleted file mode 100644 index ba99dd5f67..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctSource.cs +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.Intrinsics; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; - -/// -/// Source DCT block. -/// -internal readonly ref struct JxlDctSource(Span data, int stride) -{ - /// - /// Raw block data. - /// - public readonly Span Data = data; - - /// - /// Stride size. - /// - public readonly int Stride = stride; - - /// - /// Returns the span to the start of a row and offset. - /// - /// The row index. - /// The offset. - /// - /// Span for that row & offset. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Span Address(int row, int i) => this.Data[((row * this.Stride) + i)..]; - - /// - /// Returns the coefficient at the row and offset. - /// - /// The row index. - /// The offset. - /// - /// Coefficient at that row and offset. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public float Read(int row, int i) => this.Data[(row * this.Stride) + i]; - - /// - /// Loads a vector at the specified row and offset. - /// - /// The row index. - /// The offset. - /// - /// Vector at that row and offset. - /// - public Vector LoadPart(int row, int i) => new(this.Address(row, i)); - - /// - /// Loads a vector at the specified row and offset. - /// - /// The row index. - /// The offset. - /// - /// Vector at that row and offset. - /// - public Vector256 LoadPart256(int row, int i) => Vector256.Create(this.Address(row, i)); - - /// - /// Loads a vector at the specified row and offset. - /// - /// The row index. - /// The offset. - /// - /// Vector at that row and offset. - /// - public Vector128 LoadPart128(int row, int i) => Vector128.Create(this.Address(row, i)); -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlTransformsDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlTransformsDecoder.cs index f537b15bf3..eec56bc2c5 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlTransformsDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlTransformsDecoder.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Numerics; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; @@ -305,6 +304,21 @@ internal static class JxlTransformsDecoder ], ]; + /// + /// Computes the low-frequency rectangle of a larger transform from its DC samples. + /// + /// The full spatial transform height. + /// The full spatial transform width. + /// The number of low-frequency rows. + /// The number of low-frequency columns. + /// The small transform height in DC samples. + /// The small transform width in DC samples. + /// The borrowed DC sample rows. + /// The physical input row stride, in floats. + /// The borrowed low-frequency coefficient rows. + /// The physical output row stride, in floats. + /// Borrowed storage for the small canonical coefficient matrix. + /// Borrowed transform workspace, separate from the input and block. public static void ReinterpretingDct( int dctRows, int dctCols, @@ -319,8 +333,9 @@ public static void ReinterpretingDct( Span block, Span scratchSpace) { - JxlDct.ComputeScaledDct(rows, cols, new JxlDctSource(input, inputStride), block, scratchSpace); + JxlDct.ComputeScaledDct(rows, cols, input, inputStride, block, scratchSpace); + // Each factor belongs to one frequency axis. Canonical coefficient orientation exchanges the axes for tall and square blocks. if (rows < cols) { ReadOnlySpan sp1 = JxlDctScales.GetResampleScales(rows, dctRows); @@ -328,9 +343,11 @@ public static void ReinterpretingDct( for (int y = 0; y < lfRows; y++) { + float rowScale = sp1[y]; + for (int x = 0; x < lfCols; x++) { - output[(y * outputStride) + x] = block[(y * cols) + x] * sp1[y] * sp2[x]; + output[(y * outputStride) + x] = block[(y * cols) + x] * rowScale * sp2[x]; } } } @@ -341,16 +358,26 @@ public static void ReinterpretingDct( for (int y = 0; y < lfCols; y++) { + float rowScale = sp1[y]; + for (int x = 0; x < lfRows; x++) { - output[(y * outputStride) + x] = block[(y * rows) + x] * sp1[y] * sp2[y]; + output[(y * outputStride) + x] = block[(y * rows) + x] * rowScale * sp2[x]; } } } } + /// + /// Restores two-by-two sample cells from their average and three signed differences. + /// + /// The active square side: two, four or eight samples. + /// The borrowed eight-column coefficient matrix. It can be the output allocation. + /// The physical destination row stride, in floats. + /// The borrowed restored sample rows. public static void Idct2TopBlock(int s, Span block, int strideOut, Span output) { + // All source cells must be consumed before output changes their layout. One eight-by-eight temporary permits in-place composition. Span temp = stackalloc float[JxlFrameDimensions.DctBlockSize]; int num2x2 = s >> 1; @@ -370,10 +397,11 @@ public static void Idct2TopBlock(int s, Span block, int strideOut, Span block, int strideOut, Span coeffs, Span pixels) - { - if (!Vector.IsHardwareAccelerated || Vector.Count > 16) - { - // Vector.Count can be > 16 on some CPUs, - // e.g. some ARM SVE2 processors can reach 2048 bits - for (int i = 0; i < 16; i++) - { - float pixel = 0f; - - for (int j = 0; j < 16; j++) - { - float cf = coeffs[j]; - float basis = AfvBasis4x4[j][i]; - pixel = (cf * basis) + pixel; - } - - pixels[i] = pixel; - } - } - else - { - for (int i = 0; i < 16; i += Vector.Count) - { - Vector pixel = Vector.Zero; - - for (int j = 0; j < 16; j++) - { - Vector cf = Vector.Create(coeffs[j]); - Vector basis = Vector.Create(AfvBasis4x4[j].AsSpan(i)); - pixel = (cf * basis) + pixel; - } - - pixel.CopyTo(pixels[i..]); - } - } - } - + /// + /// Converts the three AFV component transforms to pixels. + /// + /// The horizontal and vertical AFV quadrant bits. + /// The borrowed source eight-by-eight coefficient matrix. + /// The borrowed eight-by-eight destination sample rows. + /// The physical sample row stride, in floats. public static void AfvTransformToPixels(int afvKind, Span coefficients, Span pixels, int pixelsStride) { Span scratchSpace = stackalloc float[4 * 8 * 4]; // 128 floats, 512 bytes @@ -468,7 +465,7 @@ public static void AfvTransformToPixels(int afvKind, Span coefficients, S iy28 = 0; Span block = stackalloc float[4 * 8]; // Don't zero-init - AfvIdct4x4(coeff, block); + JxlDct.TransformAfv4x4(coeff, AfvBasis4x4, block); int afvX4 = afvX * 4; @@ -486,7 +483,7 @@ public static void AfvTransformToPixels(int afvKind, Span coefficients, S } } - // IDCT4x4 in (odd, even) positions. + // The separate DCT4x4 occupies odd columns of the even coefficient rows. The AFV basis occupies their even columns. block[0] = dcs[1]; for (int iy4 = 0; iy4 < 16; iy4 += 4) { @@ -505,12 +502,7 @@ public static void AfvTransformToPixels(int afvKind, Span coefficients, S iy28 = 0; - JxlDct.ComputeScaledInverseDct( - 4, - 4, - block, - new JxlDctOutput(pixels[((afvY * 4 * pixelsStride) + (afvX == 1 ? 0 : 4))..], pixelsStride), - scratchSpace); + JxlDct.ComputeScaledInverseDct(4, 4, block, pixels[((afvY * 4 * pixelsStride) + (afvX == 1 ? 0 : 4))..], pixelsStride, scratchSpace); block[0] = dcs[2]; @@ -532,12 +524,7 @@ public static void AfvTransformToPixels(int afvKind, Span coefficients, S iy8 = 0; - JxlDct.ComputeScaledInverseDct( - 4, - 8, - block, - new JxlDctOutput(pixels[((afvY == 1 ? 0 : 4) * pixelsStride)..], pixelsStride), - scratchSpace); + JxlDct.ComputeScaledInverseDct(4, 8, block, pixels[((afvY == 1 ? 0 : 4) * pixelsStride)..], pixelsStride, scratchSpace); } public static void TransformToPixels(JxlAcStrategyType strategy, Span coefficients, Span pixels, int pixelsStride, Span scratchSpace) @@ -635,12 +622,7 @@ public static void TransformToPixels(JxlAcStrategyType strategy, Span coe } } - JxlDct.ComputeScaledInverseDct( - 8, - 4, - block, - new JxlDctOutput(pixels[(x * 4)..], pixelsStride), - scratchSpace); + JxlDct.ComputeScaledInverseDct(8, 4, block, pixels[(x * 4)..], pixelsStride, scratchSpace); } break; @@ -676,12 +658,7 @@ public static void TransformToPixels(JxlAcStrategyType strategy, Span coe } } - JxlDct.ComputeScaledInverseDct( - 4, - 8, - block, - new JxlDctOutput(pixels[(y * 4 * pixelsStride)..], pixelsStride), - scratchSpace); + JxlDct.ComputeScaledInverseDct(4, 8, block, pixels[(y * 4 * pixelsStride)..], pixelsStride, scratchSpace); } break; @@ -722,12 +699,7 @@ public static void TransformToPixels(JxlAcStrategyType strategy, Span coe } } - JxlDct.ComputeScaledInverseDct( - 4, - 4, - block, - new JxlDctOutput(pixels[((y * 4 * pixelsStride) + (x * 4))..], pixelsStride), - scratchSpace); + JxlDct.ComputeScaledInverseDct(4, 4, block, pixels[((y * 4 * pixelsStride) + (x * 4))..], pixelsStride, scratchSpace); } } @@ -756,55 +728,55 @@ public static void TransformToPixels(JxlAcStrategyType strategy, Span coe case JxlAcStrategyType.DCT16X16: { - JxlDct.ComputeScaledInverseDct(16, 16, coefficients, new JxlDctOutput(pixels, pixelsStride), scratchSpace); + JxlDct.ComputeScaledInverseDct(16, 16, coefficients, pixels, pixelsStride, scratchSpace); break; } case JxlAcStrategyType.DCT16X8: { - JxlDct.ComputeScaledInverseDct(16, 8, coefficients, new JxlDctOutput(pixels, pixelsStride), scratchSpace); + JxlDct.ComputeScaledInverseDct(16, 8, coefficients, pixels, pixelsStride, scratchSpace); break; } case JxlAcStrategyType.DCT8X16: { - JxlDct.ComputeScaledInverseDct(8, 16, coefficients, new JxlDctOutput(pixels, pixelsStride), scratchSpace); + JxlDct.ComputeScaledInverseDct(8, 16, coefficients, pixels, pixelsStride, scratchSpace); break; } case JxlAcStrategyType.DCT32X8: { - JxlDct.ComputeScaledInverseDct(32, 8, coefficients, new JxlDctOutput(pixels, pixelsStride), scratchSpace); + JxlDct.ComputeScaledInverseDct(32, 8, coefficients, pixels, pixelsStride, scratchSpace); break; } case JxlAcStrategyType.DCT8X32: { - JxlDct.ComputeScaledInverseDct(8, 32, coefficients, new JxlDctOutput(pixels, pixelsStride), scratchSpace); + JxlDct.ComputeScaledInverseDct(8, 32, coefficients, pixels, pixelsStride, scratchSpace); break; } case JxlAcStrategyType.DCT32X16: { - JxlDct.ComputeScaledInverseDct(32, 16, coefficients, new JxlDctOutput(pixels, pixelsStride), scratchSpace); + JxlDct.ComputeScaledInverseDct(32, 16, coefficients, pixels, pixelsStride, scratchSpace); break; } case JxlAcStrategyType.DCT16X32: { - JxlDct.ComputeScaledInverseDct(16, 32, coefficients, new JxlDctOutput(pixels, pixelsStride), scratchSpace); + JxlDct.ComputeScaledInverseDct(16, 32, coefficients, pixels, pixelsStride, scratchSpace); break; } case JxlAcStrategyType.DCT32X32: { - JxlDct.ComputeScaledInverseDct(32, 32, coefficients, new JxlDctOutput(pixels, pixelsStride), scratchSpace); + JxlDct.ComputeScaledInverseDct(32, 32, coefficients, pixels, pixelsStride, scratchSpace); break; } case JxlAcStrategyType.DCT: { - JxlDct.ComputeScaledInverseDct(8, 8, coefficients, new JxlDctOutput(pixels, pixelsStride), scratchSpace); + JxlDct.ComputeScaledInverseDct(8, 8, coefficients, pixels, pixelsStride, scratchSpace); break; } @@ -834,55 +806,55 @@ public static void TransformToPixels(JxlAcStrategyType strategy, Span coe case JxlAcStrategyType.DCT64X32: { - JxlDct.ComputeScaledInverseDct(64, 32, coefficients, new JxlDctOutput(pixels, pixelsStride), scratchSpace); + JxlDct.ComputeScaledInverseDct(64, 32, coefficients, pixels, pixelsStride, scratchSpace); break; } case JxlAcStrategyType.DCT32X64: { - JxlDct.ComputeScaledInverseDct(32, 64, coefficients, new JxlDctOutput(pixels, pixelsStride), scratchSpace); + JxlDct.ComputeScaledInverseDct(32, 64, coefficients, pixels, pixelsStride, scratchSpace); break; } case JxlAcStrategyType.DCT64X64: { - JxlDct.ComputeScaledInverseDct(64, 64, coefficients, new JxlDctOutput(pixels, pixelsStride), scratchSpace); + JxlDct.ComputeScaledInverseDct(64, 64, coefficients, pixels, pixelsStride, scratchSpace); break; } case JxlAcStrategyType.DCT128X64: { - JxlDct.ComputeScaledInverseDct(128, 64, coefficients, new JxlDctOutput(pixels, pixelsStride), scratchSpace); + JxlDct.ComputeScaledInverseDct(128, 64, coefficients, pixels, pixelsStride, scratchSpace); break; } case JxlAcStrategyType.DCT64X128: { - JxlDct.ComputeScaledInverseDct(64, 128, coefficients, new JxlDctOutput(pixels, pixelsStride), scratchSpace); + JxlDct.ComputeScaledInverseDct(64, 128, coefficients, pixels, pixelsStride, scratchSpace); break; } case JxlAcStrategyType.DCT128X128: { - JxlDct.ComputeScaledInverseDct(128, 128, coefficients, new JxlDctOutput(pixels, pixelsStride), scratchSpace); + JxlDct.ComputeScaledInverseDct(128, 128, coefficients, pixels, pixelsStride, scratchSpace); break; } case JxlAcStrategyType.DCT256X128: { - JxlDct.ComputeScaledInverseDct(256, 128, coefficients, new JxlDctOutput(pixels, pixelsStride), scratchSpace); + JxlDct.ComputeScaledInverseDct(256, 128, coefficients, pixels, pixelsStride, scratchSpace); break; } case JxlAcStrategyType.DCT128X256: { - JxlDct.ComputeScaledInverseDct(128, 256, coefficients, new JxlDctOutput(pixels, pixelsStride), scratchSpace); + JxlDct.ComputeScaledInverseDct(128, 256, coefficients, pixels, pixelsStride, scratchSpace); break; } case JxlAcStrategyType.DCT256X256: { - JxlDct.ComputeScaledInverseDct(256, 256, coefficients, new JxlDctOutput(pixels, pixelsStride), scratchSpace); + JxlDct.ComputeScaledInverseDct(256, 256, coefficients, pixels, pixelsStride, scratchSpace); break; } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlChromaFromLumaEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlChromaFromLumaEncoder.cs index 8eb9e9f25a..14bebb77e8 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlChromaFromLumaEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlChromaFromLumaEncoder.cs @@ -6,6 +6,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; @@ -134,7 +135,7 @@ public static void ComputeTile( { int widthBlocks = opsinRect.Width / JxlFrameDimensions.BlockDimensions; const float distanceMultiplierAC = 1e-9f; - int dctScratchSize = 3 * (JxlSimdUtils.MaxVectorSize / sizeof(float)) * JxlAcStrategy.MaximumBlockDimension; + int dctScratchSize = JxlDct.MaximumWorkingSetLength; int y0 = rect.Y0(); int x0 = rect.X0(); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlAcStrategyHeuristics.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlAcStrategyHeuristics.cs index efc8f470f3..2fd7152783 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlAcStrategyHeuristics.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlAcStrategyHeuristics.cs @@ -4,6 +4,7 @@ using System.Buffers; using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; @@ -51,9 +52,9 @@ public JxlAcStrategyHeuristics(Configuration configuration, JxlCompressionParame if (!this.useDct8Only) { // Counts are float elements, not bytes. Three maximum-area regions hold the candidate's channel coefficients. - // A fourth holds reconstruction error. The remaining two regions and three SIMD-width strips serve the forward/inverse transforms. + // A fourth holds reconstruction error. The remaining regions serve block storage and two banks of one-dimensional transform vectors. // Each maximum-area region accommodates a 256 by 256 transform. Rounded coefficients remain float vectors in the entropy calculation. - int dctScratchLength = 3 * (JxlSimdUtils.MaxVectorSize / sizeof(float)) * JxlAcStrategy.MaximumBlockDimension; + int dctScratchLength = JxlDct.MaximumWorkingSetLength; int scratchLength = (6 * JxlAcStrategy.MaximumCoefficientArea) + dctScratchLength; IMemoryOwner memory = configuration.MemoryAllocator.Allocate(scratchLength); this.memoryOwner = memory; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlCflHeuristics.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlCflHeuristics.cs index 922fd4e4f1..0760173ca0 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlCflHeuristics.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlCflHeuristics.cs @@ -4,6 +4,7 @@ using System.Buffers; using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.ChromaFromLuma; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; @@ -64,7 +65,7 @@ private static int ScratchLength { // Transform blocks can cover up to 256 by 256 pixels. A color tile covers 64 by 64 pixels, independently of that maximum. // Regions follow ComputeTile's order: three transform blocks; four tile coefficient arrays; three DC arrays; transform scratch. - int dctScratchLength = 3 * (JxlSimdUtils.MaxVectorSize / sizeof(float)) * JxlAcStrategy.MaximumBlockDimension; + int dctScratchLength = JxlDct.MaximumWorkingSetLength; int dcScratchLength = 3 * JxlAcStrategy.MaximumCoefficientBlocks * JxlAcStrategy.MaximumCoefficientBlocks; return (JxlAcStrategy.MaximumCoefficientArea * 3) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs index 2060290354..fc101c3ea2 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs @@ -3,7 +3,6 @@ using System.Buffers; using System.Numerics.Tensors; -using System.Runtime.Intrinsics; using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; @@ -72,79 +71,60 @@ internal static class JxlGroupEncoder private static readonly DenseMatrix Mul1 = new(Mul1Data); private static readonly DenseMatrix Mul2 = new(Mul2Data); + /// + /// Quantizes one transform's AC plane using its four frequency-quadrant thresholds. + /// + /// The prepared frame quantizer and inverse matrices. + /// The XYB channel index. + /// The channel's inverse matrix multiplier. + /// The transform strategy. + /// The canonical coefficient width, in 8-sample blocks. + /// The canonical coefficient height, in 8-sample blocks. + /// The four quadrant thresholds, adjusted in place for larger chroma blocks. + /// The float coefficient plane. + /// The positive block quantizer. + /// The caller-owned integer coefficient plane. public static void QuantizeBlockAc( JxlQuantizer quantizer, - int c, - float qmMultiplier, - JxlAcStrategyType quantKind, - int width, - int height, + int channelIndex, + float matrixMultiplier, + JxlAcStrategyType strategy, + int blocksWide, + int blocksHigh, Span thresholds, - Span blockIn, + Span coefficients, int quant, - Span blockOut) + Span destination) { - ReadOnlySpan qm = quantizer.InverseDequantMatrix(quantKind, c); - float quantAc = quantizer.Scale * quant; + ReadOnlySpan inverseMatrix = quantizer.InverseDequantMatrix(strategy, channelIndex); + float scale = (quantizer.Scale * quant) * matrixMultiplier; + int blockCount = blocksWide * blocksHigh; - if (c != 1 && width * height >= 4) + if (channelIndex != 1 && blockCount >= 4) { - float sub = 0.00744f * width * width; - - for (int i = 0; i < 4; i++) + // The native chroma heuristic uses covered area, not width squared. It never lowers a threshold below one half. + float reduction = 0.00744F * blockCount; + for (int quadrant = 0; quadrant < 4; quadrant++) { - thresholds[i] -= sub; - - if (thresholds[i] < 0.5f) - { - thresholds[i] = 0.5f; - } + thresholds[quadrant] = MathF.Max(thresholds[quadrant] - reduction, 0.5F); } } - // We need 8 floats at once; reference software - // uses a fixed-size of 8 items in the vector (HWY_CAPPED(float, 8)). - // TODO: can we update this to use Vector? - Vector256 quantV = Vector256.Create(quantAc * qmMultiplier); - - int yGeTarget = height * 8 / 2; - int xGeTarget = width * 8 / 2; - - for (int y = 0; y < height * 8; y++) + int rowLength = blocksWide * JxlFrameDimensions.BlockDimensions; + int rowCount = blocksHigh * JxlFrameDimensions.BlockDimensions; + for (int y = 0; y < rowCount; y++) { - int yFix = (y >= yGeTarget ? 1 : 0) * 2; - int off = (y * 8) + width; - - for (int x = 0; x < width * 8; x += 8) - { - Vector256 threshold = Vector256.Zero; - - if (width == 1) - { - // 🐛: in reference software, it seems like - // this: - // Load(kMask + x) - // has a potential for a program crash. - // https://github.com/libjxl/libjxl/blob/ef67fde2ec16d52e644c5a0969230b9a85e3eb31/lib/jxl/enc_group.cc#L85 - // x can be > 0 - // and there's 8 items to load, with kMask itself having 8 items - // So if x is 1 or higher and it tries loading kMask[1..], then there's only - // 7 items remaining - kaboom 💥 - // So we're not implementing this. Seems like a bug. - throw new InvalidOperationException("width=1 not supported"); - } - else - { - threshold = Vector256.Create(thresholds[yFix + (x >= xGeTarget ? 1 : 0)]); - } - - Vector256 q = Vector256.Create(qm[(off + x)..]) * quantV; - Vector256 input = Vector256.Create(blockIn[(off + x)..]); - Vector256 value = q * input; - Vector256 nonZeroMask = Vector256.GreaterThanOrEqual(Vector256.Abs(value), threshold); - Vector256 v = Vector256.ConvertToInt32Native(Vector256.ConditionalSelect(nonZeroMask, Vector256.Round(value), Vector256.Zero)); - v.CopyTo(blockOut[(off + x)..]); - } + int offset = y * rowLength; + int quadrant = y < rowCount / 2 ? 0 : 2; + + // Each half-row has one threshold. The quantizer keeps narrow rows in one register and traverses wider constant intervals. + JxlQuantizer.Quantize( + coefficients.Slice(offset, rowLength), + inverseMatrix.Slice(offset, rowLength), + scale, + thresholds[quadrant], + thresholds[quadrant + 1], + destination.Slice(offset, rowLength)); } } @@ -484,7 +464,7 @@ public static void ComputeCoefficients( JxlPlane fullQuantField = encoderState.RawQuantField; JxlCompressParameters cparams = encoderState.CompressParameters; - int dctScratchSize = 3 * (JxlSimdUtils.MaxVectorSize / sizeof(float)) * JxlAcStrategy.MaximumBlockDimension; + int dctScratchSize = JxlDct.MaximumWorkingSetLength; using IMemoryOwner mem = configuration.MemoryAllocator.Allocate(3 * JxlAcStrategy.MaximumCoefficientArea); using IMemoryOwner fmem = configuration.MemoryAllocator.Allocate((5 * JxlAcStrategy.MaximumCoefficientArea) + dctScratchSize); @@ -599,8 +579,8 @@ public static void ComputeCoefficients( encoderState.Shared.Quantizer, errorDiffusion, acs.Strategy, - widthBlocks, - heightBlocks, + xBlocks, + yBlocks, JxlQuantizer.DefaultQuantBias, ref quantAc, coeffsIn, diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlTransformsEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlTransformsEncoder.cs index 32fbaed684..8b54a606b5 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlTransformsEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlTransformsEncoder.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Numerics; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; @@ -310,6 +309,20 @@ internal static class JxlTransformsEncoder ], ]; + /// + /// Restores DC samples from the rescaled low-frequency rectangle. + /// + /// The full spatial transform height. + /// The full spatial transform width. + /// The number of low-frequency rows. + /// The number of low-frequency columns. + /// The small transform height in DC samples. + /// The small transform width in DC samples. + /// The borrowed low-frequency coefficients in canonical orientation. + /// The physical input row stride, in floats. + /// The borrowed restored DC samples. + /// The physical output row stride, in floats. + /// Borrowed storage for a 32-by-32 coefficient region followed by the transform workspace. public static void ReinterpretingInverseDct( int dctRows, int dctCols, @@ -323,6 +336,7 @@ public static void ReinterpretingInverseDct( int outputStride, Span scratch) { + // Each factor belongs to one frequency axis. Canonical coefficient orientation exchanges the axes for tall and square blocks. if (rows < cols) { ReadOnlySpan sp1 = JxlDctScales.GetResampleScales(dctRows, rows); @@ -332,10 +346,11 @@ public static void ReinterpretingInverseDct( { int yCols = y * cols; int yInputStride = y * inputStride; + float rowScale = sp1[y]; for (int x = 0; x < lfCols; x++) { - scratch[yCols + x] = input[yInputStride + x] * sp1[y] * sp2[y]; + scratch[yCols + x] = input[yInputStride + x] * rowScale * sp2[x]; } } } @@ -348,27 +363,34 @@ public static void ReinterpretingInverseDct( { int yRows = y * rows; int yInputStride = y * inputStride; + float rowScale = sp1[y]; for (int x = 0; x < lfRows; x++) { - scratch[yRows + x] = input[yInputStride + x] * sp1[y] * sp2[y]; + scratch[yRows + x] = input[yInputStride + x] * rowScale * sp2[x]; } } } Span scratchSpace = scratch[(MaxBlocks * MaxBlocks)..]; - JxlDct.ComputeScaledInverseDct(rows, cols, scratch, new JxlDctOutput(output, outputStride), scratchSpace); + JxlDct.ComputeScaledInverseDct(rows, cols, scratch, output, outputStride, scratchSpace); } + /// + /// Splits two-by-two sample cells into their average and three signed differences. + /// + /// The active square side: two, four or eight samples. + /// The borrowed source sample rows. It can be the output allocation. + /// The physical source row stride, in floats. + /// The destination eight-column coefficient matrix. public static void Dct2TopBlock(int s, Span block, int stride, Span output) { + // All source cells must be consumed before output changes their layout. One eight-by-eight temporary permits in-place composition. Span temp = stackalloc float[JxlFrameDimensions.DctBlockSize]; int num2x2 = s >>> 1; // Logical right shift (no sign-extend) for (int y = 0; y < num2x2; y++) { - // TODO: convert repeated arithmetic (e.g. y * DctBlockSize) - // into variables? for (int x = 0; x < num2x2; x++) { float c00 = block[(y * 2 * stride) + (x * 2)]; @@ -376,20 +398,21 @@ public static void Dct2TopBlock(int s, Span block, int stride, Span block, int stride, Span coeffs, Span pixels) - { - if (!Vector.IsHardwareAccelerated || Vector.Count > 16) - { - // Vector.Count can be > 16 on some CPUs, - // e.g. some ARM SVE2 processors can reach 2048 bits - for (int i = 0; i < 16; i++) - { - float scalar = 0f; - - for (int j = 0; j < 16; j++) - { - float px = pixels[j]; - float basis = AfvBasisTranspose4x4[j][i]; - scalar = (px * basis) + scalar; - } - - coeffs[i] = scalar; - } - } - else - { - for (int i = 0; i < 16; i += Vector.Count) - { - Vector scalar = Vector.Zero; - - for (int j = 0; j < 16; j++) - { - Vector px = Vector.Create(pixels[j]); - Vector basis = Vector.Create(AfvBasisTranspose4x4[j].AsSpan(i)); - scalar = (px * basis) + scalar; - } - - scalar.CopyTo(pixels[i..]); - } - } - } - + /// + /// Converts pixels to the three AFV component transforms. + /// + /// The horizontal and vertical AFV quadrant bits. + /// The borrowed eight-by-eight source sample rows. + /// The physical sample row stride, in floats. + /// The destination eight-by-eight coefficient matrix. private static void AfvTransformFromPixels(int afvKind, Span pixels, int pixelsStride, Span coefficients) { Span scratchSpace = stackalloc float[4 * 8 * 5]; @@ -460,7 +452,8 @@ private static void AfvTransformFromPixels(int afvKind, Span pixels, int } Span coeff = stackalloc float[4 * 4]; - AfvDct4x4(block, coeff); + // The AFV basis fills even/even coefficients. DCT4x4 fills even/odd coefficients, and DCT4x8 fills the odd rows. + JxlDct.TransformAfv4x4(block, AfvBasisTranspose4x4, coeff); for (int iy = 0; iy < 4; iy++) { @@ -470,27 +463,17 @@ private static void AfvTransformFromPixels(int afvKind, Span pixels, int } } - JxlDct.ComputeScaledDct( - 4, - 4, - new JxlDctSource(pixels[((afvY * 4 * pixelsStride) + (afvX == 1 ? 0 : 4))..], pixelsStride), - block, - scratchSpace); + JxlDct.ComputeScaledDct(4, 4, pixels[((afvY * 4 * pixelsStride) + (afvX == 1 ? 0 : 4))..], pixelsStride, block, scratchSpace); for (int iy = 0; iy < 4; iy++) { - for (int ix = 0; ix < 8; ix++) + for (int ix = 0; ix < 4; ix++) { coefficients[(iy * 2 * 8) + (ix * 2) + 1] = block[(iy * 4) + ix]; } } - JxlDct.ComputeScaledDct( - 4, - 8, - new JxlDctSource(pixels[((afvY == 1 ? 0 : 4) * pixelsStride)..], pixelsStride), - block, - scratchSpace); + JxlDct.ComputeScaledDct(4, 8, pixels[((afvY == 1 ? 0 : 4) * pixelsStride)..], pixelsStride, block, scratchSpace); for (int iy = 0; iy < 4; iy++) { @@ -569,12 +552,7 @@ public static void TransformFromPixels(JxlAcStrategyType strategy, Span p for (int x = 0; x < 2; x++) { - JxlDct.ComputeScaledDct( - 8, - 4, - new JxlDctSource(pixels[(x * 4)..], pixelsStride), - block, - scratchSpace); + JxlDct.ComputeScaledDct(8, 4, pixels[(x * 4)..], pixelsStride, block, scratchSpace); for (int iy = 0; iy < 4; iy++) { @@ -601,12 +579,7 @@ public static void TransformFromPixels(JxlAcStrategyType strategy, Span p for (int y = 0; y < 2; y++) { - JxlDct.ComputeScaledDct( - 4, - 8, - new JxlDctSource(pixels[(y * 4 * pixelsStride)..], pixelsStride), - block, - scratchSpace); + JxlDct.ComputeScaledDct(4, 8, pixels[(y * 4 * pixelsStride)..], pixelsStride, block, scratchSpace); for (int iy = 0; iy < 4; iy++) { @@ -634,12 +607,7 @@ public static void TransformFromPixels(JxlAcStrategyType strategy, Span p { for (int x = 0; x < 2; x++) { - JxlDct.ComputeScaledDct( - 4, - 4, - new JxlDctSource(pixels[((y * 4 * pixelsStride) + (x * 4))..], pixelsStride), - block, - scratchSpace); + JxlDct.ComputeScaledDct(4, 4, pixels[((y * 4 * pixelsStride) + (x * 4))..], pixelsStride, block, scratchSpace); for (int iy = 0; iy < 4; iy++) { @@ -675,100 +643,55 @@ public static void TransformFromPixels(JxlAcStrategyType strategy, Span p case JxlAcStrategyType.DCT16X16: { - JxlDct.ComputeScaledDct( - 16, - 16, - new JxlDctSource(pixels, pixelsStride), - coefficients, - scratchSpace); + JxlDct.ComputeScaledDct(16, 16, pixels, pixelsStride, coefficients, scratchSpace); break; } case JxlAcStrategyType.DCT16X8: { - JxlDct.ComputeScaledDct( - 16, - 8, - new JxlDctSource(pixels, pixelsStride), - coefficients, - scratchSpace); + JxlDct.ComputeScaledDct(16, 8, pixels, pixelsStride, coefficients, scratchSpace); break; } case JxlAcStrategyType.DCT8X16: { - JxlDct.ComputeScaledDct( - 8, - 16, - new JxlDctSource(pixels, pixelsStride), - coefficients, - scratchSpace); + JxlDct.ComputeScaledDct(8, 16, pixels, pixelsStride, coefficients, scratchSpace); break; } case JxlAcStrategyType.DCT32X8: { - JxlDct.ComputeScaledDct( - 32, - 8, - new JxlDctSource(pixels, pixelsStride), - coefficients, - scratchSpace); + JxlDct.ComputeScaledDct(32, 8, pixels, pixelsStride, coefficients, scratchSpace); break; } case JxlAcStrategyType.DCT8X32: { - JxlDct.ComputeScaledDct( - 8, - 32, - new JxlDctSource(pixels, pixelsStride), - coefficients, - scratchSpace); + JxlDct.ComputeScaledDct(8, 32, pixels, pixelsStride, coefficients, scratchSpace); break; } case JxlAcStrategyType.DCT32X16: { - JxlDct.ComputeScaledDct( - 32, - 16, - new JxlDctSource(pixels, pixelsStride), - coefficients, - scratchSpace); + JxlDct.ComputeScaledDct(32, 16, pixels, pixelsStride, coefficients, scratchSpace); break; } case JxlAcStrategyType.DCT16X32: { - JxlDct.ComputeScaledDct( - 16, - 32, - new JxlDctSource(pixels, pixelsStride), - coefficients, - scratchSpace); + JxlDct.ComputeScaledDct(16, 32, pixels, pixelsStride, coefficients, scratchSpace); break; } case JxlAcStrategyType.DCT32X32: { - JxlDct.ComputeScaledDct( - 32, - 32, - new JxlDctSource(pixels, pixelsStride), - coefficients, - scratchSpace); + JxlDct.ComputeScaledDct(32, 32, pixels, pixelsStride, coefficients, scratchSpace); break; } case JxlAcStrategyType.DCT: { - JxlDct.ComputeScaledDct( - 8, - 8, - new JxlDctSource(pixels, pixelsStride), - coefficients, - scratchSpace); + JxlDct.ComputeScaledDct(8, 8, pixels, pixelsStride, coefficients, scratchSpace); break; } @@ -798,100 +721,55 @@ public static void TransformFromPixels(JxlAcStrategyType strategy, Span p case JxlAcStrategyType.DCT64X64: { - JxlDct.ComputeScaledDct( - 64, - 64, - new JxlDctSource(pixels, pixelsStride), - coefficients, - scratchSpace); + JxlDct.ComputeScaledDct(64, 64, pixels, pixelsStride, coefficients, scratchSpace); break; } case JxlAcStrategyType.DCT64X32: { - JxlDct.ComputeScaledDct( - 64, - 32, - new JxlDctSource(pixels, pixelsStride), - coefficients, - scratchSpace); + JxlDct.ComputeScaledDct(64, 32, pixels, pixelsStride, coefficients, scratchSpace); break; } case JxlAcStrategyType.DCT32X64: { - JxlDct.ComputeScaledDct( - 32, - 64, - new JxlDctSource(pixels, pixelsStride), - coefficients, - scratchSpace); + JxlDct.ComputeScaledDct(32, 64, pixels, pixelsStride, coefficients, scratchSpace); break; } case JxlAcStrategyType.DCT128X128: { - JxlDct.ComputeScaledDct( - 128, - 128, - new JxlDctSource(pixels, pixelsStride), - coefficients, - scratchSpace); + JxlDct.ComputeScaledDct(128, 128, pixels, pixelsStride, coefficients, scratchSpace); break; } case JxlAcStrategyType.DCT128X64: { - JxlDct.ComputeScaledDct( - 128, - 64, - new JxlDctSource(pixels, pixelsStride), - coefficients, - scratchSpace); + JxlDct.ComputeScaledDct(128, 64, pixels, pixelsStride, coefficients, scratchSpace); break; } case JxlAcStrategyType.DCT64X128: { - JxlDct.ComputeScaledDct( - 64, - 128, - new JxlDctSource(pixels, pixelsStride), - coefficients, - scratchSpace); + JxlDct.ComputeScaledDct(64, 128, pixels, pixelsStride, coefficients, scratchSpace); break; } case JxlAcStrategyType.DCT256X256: { - JxlDct.ComputeScaledDct( - 256, - 256, - new JxlDctSource(pixels, pixelsStride), - coefficients, - scratchSpace); + JxlDct.ComputeScaledDct(256, 256, pixels, pixelsStride, coefficients, scratchSpace); break; } case JxlAcStrategyType.DCT256X128: { - JxlDct.ComputeScaledDct( - 256, - 128, - new JxlDctSource(pixels, pixelsStride), - coefficients, - scratchSpace); + JxlDct.ComputeScaledDct(256, 128, pixels, pixelsStride, coefficients, scratchSpace); break; } case JxlAcStrategyType.DCT128X256: { - JxlDct.ComputeScaledDct( - 128, - 256, - new JxlDctSource(pixels, pixelsStride), - coefficients, - scratchSpace); + JxlDct.ComputeScaledDct(128, 256, pixels, pixelsStride, coefficients, scratchSpace); break; } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlDequantMatrices.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlDequantMatrices.cs index 40fbbfe817..14751840b5 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlDequantMatrices.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlDequantMatrices.cs @@ -92,9 +92,9 @@ public JxlDequantMatrices() int num = numBlocks * JxlFrameDimensions.DctBlockSize; int i3 = 3 * i; - for (int c = 0; c < 3; c++) + for (int channelIndex = 0; channelIndex < 3; channelIndex++) { - offsets[i3 + c] = pos + (c * num); + offsets[i3 + channelIndex] = pos + (channelIndex * num); } pos += 3 * num; @@ -102,9 +102,9 @@ public JxlDequantMatrices() for (int i = 0; i < JxlAcStrategy.NumberOfValidStrategies; i++) { - for (int c = 0; c < 3; c++) + for (int channelIndex = 0; channelIndex < 3; channelIndex++) { - this.tableOffsets[(i * 3) + c] = offsets[((int)JxlQuantizerConstants.AcStrategyToQuantTableMap[i] * 3) + c]; + this.tableOffsets[(i * 3) + channelIndex] = offsets[((int)JxlQuantizerConstants.AcStrategyToQuantTableMap[i] * 3) + channelIndex]; } } } @@ -169,7 +169,7 @@ public static JxlQuantizerEncoding[] GetLibrary() JxlQuantWeights.Dct4x8, JxlQuantWeights.Afv, JxlQuantWeights.Dct64x64, - JxlQuantWeights.Dct32x32, + JxlQuantWeights.Dct32x64, JxlQuantWeights.Dct128x128, JxlQuantWeights.Dct64x128, JxlQuantWeights.Dct256x256, @@ -187,36 +187,36 @@ static void Verify(int expected, JxlQuantTable actual) } /// - /// Returns a matrix for the specified kind of quantizer and index. + /// Returns the dequantization matrix for a transform and color channel. /// - /// Quantizer kind - /// Index + /// The AC transform type. + /// The XYB channel index. /// Matrix - public Span GetMatrix(JxlAcStrategyType quantKind, int c) + public Span GetMatrix(JxlAcStrategyType transformType, int channelIndex) { - DebugGuard.MustBeGreaterThan((1 << (int)quantKind) & this.computedMask, 0, nameof(quantKind)); - return this.table.Span[this.tableOffsets[((int)quantKind * 3) + c]..]; + DebugGuard.MustBeGreaterThan((1 << (int)transformType) & this.computedMask, 0, nameof(transformType)); + return this.table.Span[this.tableOffsets[((int)transformType * 3) + channelIndex]..]; } /// - /// Returns an inverse matrix for the specified kind of quantizer and index. + /// Returns the inverse dequantization matrix for a transform and color channel. /// - /// Quantizer kind - /// Index + /// The AC transform type. + /// The XYB channel index. /// Inverse matrix - public Span GetInverseMatrix(JxlAcStrategyType quantKind, int c) + public Span GetInverseMatrix(JxlAcStrategyType transformType, int channelIndex) { - DebugGuard.MustBeGreaterThan((1 << (int)quantKind) & this.computedMask, 0, nameof(quantKind)); - return this.inverseTable.Span[this.tableOffsets[((int)quantKind * 3) + c]..]; + DebugGuard.MustBeGreaterThan((1 << (int)transformType) & this.computedMask, 0, nameof(transformType)); + return this.inverseTable.Span[this.tableOffsets[((int)transformType * 3) + channelIndex]..]; } /// - /// Returns a DC quant for index c. + /// Returns the DC quantization multiplier for a channel. /// - /// The DC quantizer index. - /// DC quant for index . + /// The DC quantizer index. + /// DC quant for index . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public float GetDcQuant(int c) => this.dcQuant[c]; + public float GetDcQuant(int channelIndex) => this.dcQuant[channelIndex]; /// /// Returns all DC quantizers. See also . @@ -225,23 +225,23 @@ public Span GetInverseMatrix(JxlAcStrategyType quantKind, int c) public Span GetDcQuants() => this.dcQuant; /// - /// Returns an inverse DC quant for index c. + /// Returns the inverse DC quantization multiplier for a channel. /// - /// The inverse DC quantizer index. - /// Inverse DC quant for index . + /// The inverse DC quantizer index. + /// Inverse DC quant for index . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public float GetInverseDcQuant(int c) => this.inverseDcQuant[c]; + public float GetInverseDcQuant(int channelIndex) => this.inverseDcQuant[channelIndex]; /// /// Applies the specified DC quantizer. /// - /// DC quantizer to apply to the dequantization matrices. - public void SetDcQuant(InlineArray3 dc) + /// DC quantizer to apply to the dequantization matrices. + public void SetDcQuant(InlineArray3 dcQuantizationValues) { - for (int c = 0; c < 3; c++) + for (int channelIndex = 0; channelIndex < 3; channelIndex++) { - this.dcQuant[c] = 1f / dc[c]; - this.inverseDcQuant[c] = dc[c]; + this.dcQuant[channelIndex] = 1f / dcQuantizationValues[channelIndex]; + this.inverseDcQuant[channelIndex] = dcQuantizationValues[channelIndex]; } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantWeights.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantWeights.cs index ddbae3a08d..f2cf945535 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantWeights.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantWeights.cs @@ -5,7 +5,9 @@ using System.Numerics; using System.Numerics.Tensors; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; @@ -149,7 +151,7 @@ internal static class JxlQuantWeights -0.3f, ] ], - 7)); + 8)); /// /// DCT8x16 quantizer encoding. @@ -559,18 +561,6 @@ internal static class JxlQuantWeights 12.97166202570235f, ]; - private static Vector128 Gather(ReadOnlySpan data, Vector128 indices) - { - Vector128 result = Vector128.Zero; - - for (int i = 0; i < 4; i++) - { - result = result.WithElement(i, data[indices[i]]); - } - - return result; - } - public static void GetQuantWeightsDCT2(float[][] dct2Weights, Span weights) { for (int c = 0; c < 3; c++) @@ -634,39 +624,121 @@ public static void GetQuantWeightsIdentity(float[][] idWeights, Span weig } } - public static float Interpolate(float pos, float max, Span array, int len) + /// + /// Interpolates geometrically between adjacent quantization weights. + /// + /// The position in sample intervals, below sampleValues.Length minus one. + /// The ordered sample values. + /// The precomputed base-two logarithms of adjacent sample ratios. + /// The interpolated weight. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Interpolate(float scaledPosition, ReadOnlySpan sampleValues, ReadOnlySpan logRatios) { - float scaledPos = pos * (len - 1) / max; - int idx = (int)scaledPos; - float a = array[idx]; - float b = array[idx + 1]; - return a * MathF.Pow(b / a, scaledPos - idx); + int sampleIndex = (int)scaledPosition; + float a = sampleValues[sampleIndex]; + float fraction = scaledPosition - sampleIndex; + float logarithm = logRatios[sampleIndex]; + return fraction == 0F || logarithm == 0F ? a : a * float.Exp2(logarithm * fraction); } + /// + /// Converts a signed distance-band delta to its positive multiplicative factor. + /// + /// The encoded delta. + /// One plus a positive delta, or the reciprocal of one minus a nonpositive delta. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float Mult(float v) => v > 0f - ? 1.0f + v - : 1.0f / (1.0f - v); + public static float Mult(float v) => v > 0f ? 1.0f + v : 1.0f / (1.0f - v); - public static Vector128 InterpolateVec(Vector128 scaledPos, ReadOnlySpan array) + /// + /// Interpolates four quantization weights from sample-space positions. + /// + /// Nonnegative sample-space positions below sampleValues.Length minus one. + /// The ordered sample values. + /// The precomputed base-two logarithms of adjacent sample ratios. + /// The four interpolated weights. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Interpolate(Vector128 scaledPositions, ReadOnlySpan sampleValues, ReadOnlySpan logRatios) { - Vector128 idx = Vector128.ConvertToInt32(scaledPos); - Vector128 frac = scaledPos - Vector128.ConvertToSingle(idx); + // Positions are nonnegative and below the final band. Native truncation cannot produce an out-of-range gather index. + Vector128 idx = Vector128.ConvertToInt32Native(scaledPositions); + Vector128 frac = scaledPositions - Vector128.ConvertToSingle(idx); + Vector128 a = Vector128_.Gather(ref Unsafe.As(ref MemoryMarshal.GetReference(sampleValues)), idx).AsSingle(); + Vector128 logarithm = Vector128_.Gather(ref Unsafe.As(ref MemoryMarshal.GetReference(logRatios)), idx).AsSingle(); + Vector128 exact = Vector128.Equals(frac, Vector128.Zero) | Vector128.Equals(logarithm, Vector128.Zero); + + // Each lane uses its own interval and exponent. Shared Exp2 is approximate; restore exact interval starts and constant intervals. + return Vector128.ConditionalSelect(exact, a, a * Vector128_.Exp2(logarithm * frac)); + } - Vector128 a = Gather(array, idx); - Vector128 b = Gather(array[1..], idx); + /// + /// Interpolates eight quantization weights from sample-space positions. + /// + /// Nonnegative sample-space positions below sampleValues.Length minus one. + /// The ordered sample values. + /// The precomputed base-two logarithms of adjacent sample ratios. + /// The eight interpolated weights. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Interpolate(Vector256 scaledPositions, ReadOnlySpan sampleValues, ReadOnlySpan logRatios) + { + Vector256 idx = Vector256.ConvertToInt32Native(scaledPositions); + Vector256 frac = scaledPositions - Vector256.ConvertToSingle(idx); + Vector256 a = Vector256_.Gather(ref Unsafe.As(ref MemoryMarshal.GetReference(sampleValues)), idx).AsSingle(); + Vector256 logarithm = Vector256_.Gather(ref Unsafe.As(ref MemoryMarshal.GetReference(logRatios)), idx).AsSingle(); + Vector256 exact = Vector256.Equals(frac, Vector256.Zero) | Vector256.Equals(logarithm, Vector256.Zero); + return Vector256.ConditionalSelect(exact, a, a * Vector256_.Exp2(logarithm * frac)); + } - return a * JxlSimdUtils.FastPowf(b / a, frac); + /// + /// Interpolates sixteen quantization weights from sample-space positions on a supported SIMD target. + /// + /// Nonnegative sample-space positions below sampleValues.Length minus one. + /// The ordered sample values. + /// The precomputed base-two logarithms of adjacent sample ratios. + /// The sixteen interpolated weights. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Interpolate(Vector512 scaledPositions, ReadOnlySpan sampleValues, ReadOnlySpan logRatios) + { + Vector512 idx = Vector512.ConvertToInt32Native(scaledPositions); + Vector512 frac = scaledPositions - Vector512.ConvertToSingle(idx); + Vector512 a = Vector512_.Gather(ref Unsafe.As(ref MemoryMarshal.GetReference(sampleValues)), idx).AsSingle(); + Vector512 logarithm = Vector512_.Gather(ref Unsafe.As(ref MemoryMarshal.GetReference(logRatios)), idx).AsSingle(); + Vector512 exact = Vector512.Equals(frac, Vector512.Zero) | Vector512.Equals(logarithm, Vector512.Zero); + return Vector512.ConditionalSelect(exact, a, a * Vector512_.Exp2(logarithm * frac)); } - public static bool GetQuantWeights( - int rows, - int cols, - float[][] distanceBands, - int numBands, - Span output) + /// + /// Precomputes the logarithm used by every position in each geometric interval. + /// + /// The positive cumulative band values. + /// Storage for bands.Length minus one adjacent logarithms. + private static void PrepareLogRatios(ReadOnlySpan bands, Span logRatios) + { + for (int i = 0; i < logRatios.Length; i++) + { + logRatios[i] = bands[i + 1] / bands[i]; + } + + TensorPrimitives.Log2(logRatios, logRatios); + } + + /// + /// Builds quantization weights from the distance bands for each color channel. + /// + /// The number of coefficient rows. + /// The number of coefficients per row. + /// The distance-band parameters for each channel. + /// The number of bands per channel. + /// The destination weights, in channel, row, and column order. + /// Whether the cumulative bands satisfy the native minimum weight. + public static bool GetQuantWeights(int rowCount, int columnCount, float[][] distanceBands, int bandCount, Span output) { Span bands = stackalloc float[JxlQuantizerConstants.MaxDistanceBands]; + Span logStorage = stackalloc float[JxlQuantizerConstants.MaxDistanceBands - 1]; + Span logRatios = logStorage[..(bandCount - 1)]; + int channelSize = rowCount * columnCount; + float positionScale = (bandCount - 1) / (JxlDctScales.Sqrt2 + 1e-6f); + float columnStep = positionScale / (columnCount - 1); + float rowStep = positionScale / (rowCount - 1); for (int c = 0; c < 3; c++) { @@ -677,9 +749,9 @@ public static bool GetQuantWeights( return false; } - for (int i = 1; i < numBands; i++) + for (int i = 1; i < bandCount; i++) { - bands[i] = bands[i - 1] * distanceBands[c][i]; + bands[i] = bands[i - 1] * Mult(distanceBands[c][i]); if (bands[i] < AlmostZero) { @@ -687,31 +759,63 @@ public static bool GetQuantWeights( } } - float scale = (numBands - 1) / (JxlDctScales.Sqrt2 + 1e-6f); - float rcpCol = scale / (cols - 1); - float rcpRow = scale / (rows - 1); + Span channel = output.Slice(c * channelSize, channelSize); + if (bandCount == 1) + { + channel.Fill(bands[0]); + continue; + } + + // These two bounded stack tables serve every row of this channel. No per-coefficient logarithm or division is needed. + // The epsilon in positionScale keeps distances below bandCount-1, so truncation selects an existing adjacent interval. + // Interior values use shared Exp2 precision, which can differ from libjxl's separate FastPowf approximation. + ReadOnlySpan sampleValues = bands[..bandCount]; + PrepareLogRatios(sampleValues, logRatios); - for (int y = 0; y < rows; y++) + for (int y = 0; y < rowCount; y++) { - float dy = y * rcpRow; + float dy = y * rowStep; float dy2 = dy * dy; + Span row = channel.Slice(y * columnCount, columnCount); + ref float destination = ref MemoryMarshal.GetReference(row); + int x = 0; - for (int x = 0; x < cols; x += 4) + // Lanes hold consecutive coefficient columns. Each store covers only complete registers within this logical row. + if (Vector512.IsHardwareAccelerated) { - Vector128 dx = - Vector128.Create((float)x, x + 1, x + 2, x + 3) - * Vector128.Create(rcpCol); + for (; x <= columnCount - Vector512.Count; x += Vector512.Count) + { + Vector512 dx = (Vector512.Indices + Vector512.Create((float)x)) * Vector512.Create(columnStep); + Vector512 distance = Vector512.Sqrt(Vector512.MultiplyAddEstimate(dx, dx, Vector512.Create(dy2))); + Interpolate(distance, sampleValues, logRatios).StoreUnsafe(ref destination, (uint)x); + } + } - Vector128 scaledDistance = - Vector128.Sqrt( - (dx * dx) + Vector128.Create(dy2)); + if (Vector256.IsHardwareAccelerated) + { + for (; x <= columnCount - Vector256.Count; x += Vector256.Count) + { + Vector256 dx = (Vector256.Indices + Vector256.Create((float)x)) * Vector256.Create(columnStep); + Vector256 distance = Vector256.Sqrt(Vector256.MultiplyAddEstimate(dx, dx, Vector256.Create(dy2))); + Interpolate(distance, sampleValues, logRatios).StoreUnsafe(ref destination, (uint)x); + } + } - Vector128 weight = - numBands == 1 - ? Vector128.Create(bands[0]) - : InterpolateVec(scaledDistance, bands); + if (Vector128.IsHardwareAccelerated) + { + for (; x <= columnCount - Vector128.Count; x += Vector128.Count) + { + Vector128 dx = (Vector128.Indices + Vector128.Create((float)x)) * Vector128.Create(columnStep); + Vector128 distance = Vector128.Sqrt(Vector128.MultiplyAddEstimate(dx, dx, Vector128.Create(dy2))); + Interpolate(distance, sampleValues, logRatios).StoreUnsafe(ref destination, (uint)x); + } + } - weight.CopyTo(output[((c * cols * rows) + (y * cols) + x)..]); + for (; x < columnCount; x++) + { + float dx = x * columnStep; + float distance = MathF.Sqrt(float.MultiplyAddEstimate(dx, dx, dy2)); + row[x] = Interpolate(distance, sampleValues, logRatios); } } } @@ -719,7 +823,23 @@ public static bool GetQuantWeights( return true; } - public static bool ComputeQuantTable(JxlQuantizerEncoding encoding, Span table, Span inverseTable, int tableNum, JxlQuantTable kind, ref int pos) + /// + /// Builds forward and inverse quantization tables for one transform. + /// + /// The quantization weight parameters. + /// The destination forward weights. + /// The destination inverse weights. + /// The table index. This implementation does not yet use it. + /// The transform table kind. + /// The starting destination offset, advanced past the written weights. + /// Whether the encoding supports the selected transform. + public static bool ComputeQuantTable( + JxlQuantizerEncoding encoding, + Span quantizationTable, + Span inverseQuantizationTable, + int tableIndex, + JxlQuantTable kind, + ref int writeOffset) { const int n = JxlFrameDimensions.BlockDimensions; @@ -871,6 +991,7 @@ public static bool ComputeQuantTable(JxlQuantizerEncoding encoding, Span const float hi = 12.97166202570235f - lo + 1e-6f; Span bands = [0, 0, 0, 0]; + Span logRatios = stackalloc float[3]; for (int c = 0; c < 3; c++) { @@ -891,6 +1012,8 @@ public static bool ComputeQuantTable(JxlQuantizerEncoding encoding, Span } } + PrepareLogRatios(bands, logRatios); + int start = c * 64; void SetWeight(int x, int y, float value, Span weights) => weights[start + (y * 8) + x] = value; @@ -913,7 +1036,8 @@ public static bool ComputeQuantTable(JxlQuantizerEncoding encoding, Span continue; } - float interpolatedVal = Interpolate(AfvFrequencies[(y * 4) + x] - lo, hi, bands, 4); + float position = (AfvFrequencies[(y * 4) + x] - lo) * 3 / hi; + float interpolatedVal = Interpolate(position, bands, logRatios); SetWeight(2 * x, 2 * y, interpolatedVal, weights); } @@ -952,7 +1076,7 @@ public static bool ComputeQuantTable(JxlQuantizerEncoding encoding, Span } } - int prevPos = pos; + int prevPos = writeOffset; // Don't zero-init Span invVal = stackalloc float[64]; @@ -973,11 +1097,11 @@ public static bool ComputeQuantTable(JxlQuantizerEncoding encoding, Span val.Fill(1.0f); TensorPrimitives.Divide(val, invVal, val); - val.CopyTo(table.Slice(pos + i, 64)); - invVal.CopyTo(inverseTable.Slice(pos + i, 64)); + val.CopyTo(quantizationTable.Slice(writeOffset + i, 64)); + invVal.CopyTo(inverseQuantizationTable.Slice(writeOffset + i, 64)); } - pos += 3 * num; + writeOffset += 3 * num; int xs = JxlDequantMatrices.RequiredSizeX[quantSizeTable]; int ys = JxlDequantMatrices.RequiredSizeY[quantSizeTable]; @@ -990,7 +1114,7 @@ public static bool ComputeQuantTable(JxlQuantizerEncoding encoding, Span { for (int x = 0; x < xs; x++) { - inverseTable[prevPos + (c * ys * xs * JxlFrameDimensions.DctBlockSize) + (y * JxlFrameDimensions.BlockDimensions * xs) + x] = 0; + inverseQuantizationTable[prevPos + (c * ys * xs * JxlFrameDimensions.DctBlockSize) + (y * JxlFrameDimensions.BlockDimensions * xs) + x] = 0; } } } @@ -1023,20 +1147,38 @@ public static bool DecodeDctParameters(JxlBitReader reader, JxlDctQuantWeightPar return true; } - public static bool Decode(Configuration configuration, JxlBitReader br, JxlQuantizerEncoding encoding, int requiredSizeX, int requiredSizeY, int idx, JxlModularFrameDecoder modularFrameDecoder) + /// + /// Reads the quantization weight parameters for one table. + /// + /// The configuration used by the modular decoder. + /// The source bit reader. + /// The destination quantization parameters. + /// The required horizontal extent in blocks. + /// The required vertical extent in blocks. + /// The index that identifies the table in the modular stream. + /// The decoder for raw quantization tables. + /// Whether the parameters were read successfully. + public static bool Decode( + Configuration configuration, + JxlBitReader reader, + JxlQuantizerEncoding encoding, + int requiredSizeX, + int requiredSizeY, + int quantizationTableIndex, + JxlModularFrameDecoder modularFrameDecoder) { int requiredSize = requiredSizeX * requiredSizeY; requiredSizeX *= JxlFrameDimensions.BlockDimensions; requiredSizeY *= JxlFrameDimensions.BlockDimensions; - int mode = (int)br.ReadBits32(JxlQuantizerConstants.Log2NumQuantModes); + int mode = (int)reader.ReadBits32(JxlQuantizerConstants.Log2NumQuantModes); switch ((JxlQuantMode)mode) { case JxlQuantMode.Library: { - encoding.Predefined = (byte)br.ReadBits32(CeilLog2NumPredefinedTables); + encoding.Predefined = (byte)reader.ReadBits32(CeilLog2NumPredefinedTables); if (encoding.Predefined >= NumPredefinedTables) { @@ -1057,7 +1199,7 @@ public static bool Decode(Configuration configuration, JxlBitReader br, JxlQuant { for (int i = 0; i < 3; i++) { - if (!JxlF16Coder.Read(br, ref encoding.IdWeights![c][i])) + if (!JxlF16Coder.Read(reader, ref encoding.IdWeights![c][i])) { return false; } @@ -1085,7 +1227,7 @@ public static bool Decode(Configuration configuration, JxlBitReader br, JxlQuant { for (int i = 0; i < 6; i++) { - if (!JxlF16Coder.Read(br, ref encoding.Dct2Weights![c][i])) + if (!JxlF16Coder.Read(reader, ref encoding.Dct2Weights![c][i])) { return false; } @@ -1111,7 +1253,7 @@ public static bool Decode(Configuration configuration, JxlBitReader br, JxlQuant for (int c = 0; c < 3; c++) { - if (!JxlF16Coder.Read(br, ref encoding.Dct4x8Multipliers![c])) + if (!JxlF16Coder.Read(reader, ref encoding.Dct4x8Multipliers![c])) { return false; } @@ -1122,7 +1264,7 @@ public static bool Decode(Configuration configuration, JxlBitReader br, JxlQuant } } - if (!DecodeDctParameters(br, encoding.DctParameters!)) + if (!DecodeDctParameters(reader, encoding.DctParameters!)) { return false; } @@ -1141,7 +1283,7 @@ public static bool Decode(Configuration configuration, JxlBitReader br, JxlQuant { for (int i = 0; i < 2; i++) { - if (!JxlF16Coder.Read(br, ref encoding.Dct4Multipliers![c][i])) + if (!JxlF16Coder.Read(reader, ref encoding.Dct4Multipliers![c][i])) { return false; } @@ -1153,7 +1295,7 @@ public static bool Decode(Configuration configuration, JxlBitReader br, JxlQuant } } - if (!DecodeDctParameters(br, encoding.DctParameters!)) + if (!DecodeDctParameters(reader, encoding.DctParameters!)) { return false; } @@ -1172,7 +1314,7 @@ public static bool Decode(Configuration configuration, JxlBitReader br, JxlQuant { for (int i = 0; i < 9; i++) { - if (!JxlF16Coder.Read(br, ref encoding.AfvWeights![c][i])) + if (!JxlF16Coder.Read(reader, ref encoding.AfvWeights![c][i])) { return false; } @@ -1184,12 +1326,12 @@ public static bool Decode(Configuration configuration, JxlBitReader br, JxlQuant } } - if (!DecodeDctParameters(br, encoding.DctParameters!)) + if (!DecodeDctParameters(reader, encoding.DctParameters!)) { return false; } - if (!DecodeDctParameters(br, encoding.DctParametersAfv4x4!)) + if (!DecodeDctParameters(reader, encoding.DctParametersAfv4x4!)) { return false; } @@ -1199,7 +1341,7 @@ public static bool Decode(Configuration configuration, JxlBitReader br, JxlQuant case JxlQuantMode.Dct: { - if (!DecodeDctParameters(br, encoding.DctParameters!)) + if (!DecodeDctParameters(reader, encoding.DctParameters!)) { return false; } @@ -1212,7 +1354,14 @@ public static bool Decode(Configuration configuration, JxlBitReader br, JxlQuant // Set mode early, to avoid mem-leak. encoding.Mode = JxlQuantMode.Raw; - if (!JxlModularFrameDecoder.DecodeQuantTable(configuration, requiredSizeX, requiredSizeY, br, encoding, idx, modularFrameDecoder)) + if (!JxlModularFrameDecoder.DecodeQuantTable( + configuration, + requiredSizeX, + requiredSizeY, + reader, + encoding, + quantizationTableIndex, + modularFrameDecoder)) { return false; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.Quantization.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.Quantization.cs new file mode 100644 index 0000000000..a73b710192 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.Quantization.cs @@ -0,0 +1,173 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; + +internal sealed partial class JxlQuantizer +{ + /// + /// Quantizes one coefficient row with a separate zero threshold for each half. + /// + /// The logical float coefficients in frequency order. + /// The corresponding inverse dequantization weights. + /// The block quantizer scale including the channel matrix multiplier. + /// The minimum absolute weighted magnitude retained in the first half. + /// The minimum absolute weighted magnitude retained in the second half. + /// The caller-owned integer coefficient row. + public static void Quantize( + ReadOnlySpan coefficients, + ReadOnlySpan inverseMatrix, + float scale, + float leftThreshold, + float rightThreshold, + Span destination) + { + QuantizationOperator operation = new(scale); + + // An 8-column row crosses its quadrant boundary inside a 256-bit register. A 16-column row does the same at 512 bits. + // Build the two constant threshold halves once. All input loads and the integer store cover exactly the logical row. + if (Vector512.IsHardwareAccelerated && coefficients.Length == Vector512.Count) + { + Vector512 thresholds = Vector512.Create(Vector256.Create(leftThreshold), Vector256.Create(rightThreshold)); + Vector512 value = operation.Invoke(Vector512.Create(coefficients), Vector512.Create(inverseMatrix), thresholds); + Vector512_.ConvertToInt32RoundToEven(value).CopyTo(destination); + return; + } + + if (Vector256.IsHardwareAccelerated && coefficients.Length == Vector256.Count) + { + Vector256 thresholds = Vector256.Create(Vector128.Create(leftThreshold), Vector128.Create(rightThreshold)); + Vector256 value = operation.Invoke(Vector256.Create(coefficients), Vector256.Create(inverseMatrix), thresholds); + Vector256_.ConvertToInt32RoundToEven(value).CopyTo(destination); + return; + } + + // Larger row halves align with complete registers. Keep their threshold constant through the loop instead of comparing lane indices. + int half = coefficients.Length / 2; + QuantizeInterval(coefficients[..half], inverseMatrix[..half], operation, leftThreshold, destination[..half]); + QuantizeInterval(coefficients[half..], inverseMatrix[half..], operation, rightThreshold, destination[half..]); + } + + /// + /// Quantizes a contiguous frequency interval whose zero threshold is constant. + /// + /// The logical coefficients in one row half. + /// The matching inverse matrix weights. + /// The row's prepared scale operation. + /// The interval's zero threshold. + /// The matching integer output interval. + private static void QuantizeInterval( + ReadOnlySpan coefficients, + ReadOnlySpan inverseMatrix, + QuantizationOperator operation, + float threshold, + Span destination) + { + ref float input = ref MemoryMarshal.GetReference(coefficients); + ref float weights = ref MemoryMarshal.GetReference(inverseMatrix); + ref int output = ref MemoryMarshal.GetReference(destination); + int i = 0; + + // One lane is one coefficient. The existing conversion helpers round to nearest, with ties to even, + // so no separate rounded-float buffer or rounding instruction is required before integer conversion. + if (Vector512.IsHardwareAccelerated) + { + nuint count = (coefficients.Length - i).Vector512Count(); + Vector512 thresholds = Vector512.Create(threshold); + + for (nuint block = 0; block < count; block++, i += Vector512.Count) + { + Vector512 value = operation.Invoke(Vector512.LoadUnsafe(ref input, (uint)i), Vector512.LoadUnsafe(ref weights, (uint)i), thresholds); + Vector512_.ConvertToInt32RoundToEven(value).StoreUnsafe(ref output, (uint)i); + } + } + + if (Vector256.IsHardwareAccelerated) + { + nuint count = (coefficients.Length - i).Vector256Count(); + Vector256 thresholds = Vector256.Create(threshold); + + for (nuint block = 0; block < count; block++, i += Vector256.Count) + { + Vector256 value = operation.Invoke(Vector256.LoadUnsafe(ref input, (uint)i), Vector256.LoadUnsafe(ref weights, (uint)i), thresholds); + Vector256_.ConvertToInt32RoundToEven(value).StoreUnsafe(ref output, (uint)i); + } + } + + if (Vector128.IsHardwareAccelerated) + { + nuint count = (coefficients.Length - i).Vector128Count(); + Vector128 thresholds = Vector128.Create(threshold); + + for (nuint block = 0; block < count; block++, i += Vector128.Count) + { + Vector128 value = operation.Invoke(Vector128.LoadUnsafe(ref input, (uint)i), Vector128.LoadUnsafe(ref weights, (uint)i), thresholds); + Vector128_.ConvertToInt32RoundToEven(value).StoreUnsafe(ref output, (uint)i); + } + } + + // Narrow intervals and final coefficients retain the same product order, threshold comparison and midpoint rule. + for (; i < coefficients.Length; i++) + { + float value = operation.Invoke(Unsafe.Add(ref input, i), Unsafe.Add(ref weights, i), threshold); + Unsafe.Add(ref output, i) = (int)MathF.Round(value, MidpointRounding.ToEven); + } + } + + /// + /// Applies inverse matrix scaling and the encoded zero threshold before integer rounding. + /// + private readonly struct QuantizationOperator : TensorPrimitives_.ITernaryOperator + { + private readonly float scale; + + /// + /// Initializes a new instance of the struct. + /// + /// The block quantizer and channel multiplier. + public QuantizationOperator(float scale) + { + this.scale = scale; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float Invoke(float first, float second, float third) + { + float value = (second * this.scale) * first; + return MathF.Abs(value) >= third ? value : 0F; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector128 Invoke(Vector128 first, Vector128 second, Vector128 third) + { + // Keep the native (matrix * scale) * coefficient order. Selection also clears unordered NaN comparisons. + Vector128 value = (second * Vector128.Create(this.scale)) * first; + return Vector128.ConditionalSelect(Vector128.GreaterThanOrEqual(Vector128.Abs(value), third), value, Vector128.Zero); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector256 Invoke(Vector256 first, Vector256 second, Vector256 third) + { + // Keep the native (matrix * scale) * coefficient order. Selection also clears unordered NaN comparisons. + Vector256 value = (second * Vector256.Create(this.scale)) * first; + return Vector256.ConditionalSelect(Vector256.GreaterThanOrEqual(Vector256.Abs(value), third), value, Vector256.Zero); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector512 Invoke(Vector512 first, Vector512 second, Vector512 third) + { + // Keep the native (matrix * scale) * coefficient order. Selection also clears unordered NaN comparisons. + Vector512 value = (second * Vector512.Create(this.scale)) * first; + return Vector512.ConditionalSelect(Vector512.GreaterThanOrEqual(Vector512.Abs(value), third), value, Vector512.Zero); + } + } +} diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index adb03fedfa..51e542bdcc 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -52,6 +52,15 @@ + + True + True + JxlDct.Kernels.Generated.tt + + + TextTemplatingFileGenerator + JxlDct.Kernels.Generated.cs + True True diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/DctTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/DctTests.cs new file mode 100644 index 0000000000..811faca73a --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/DctTests.cs @@ -0,0 +1,348 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; +using SixLabors.ImageSharp.Tests.TestUtilities; + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; + +/// +/// Tests exact DCT basis cases, coefficient orientation and the small transform contracts used by JPEG XL. +/// +public class DctTests +{ + /// + /// Checks one-point identity and the independent two-point sum/difference equations without allowing trailing-span copies. + /// + [Fact] + public void SmallDct_PreservesIdentityAndDistinctPairOutputs() + { + static void RunTest() + { + float[] workspace = new float[16]; + float[] single = [6F, 123F]; + float[] coefficients = [123F, 123F, 123F]; + float[] pixels = [123F, 123F, 123F]; + JxlDct.ComputeScaledDct(1, 1, single, 1, coefficients.AsSpan(1), workspace); + Assert.Equal(6F, coefficients[1]); + Assert.Equal(123F, coefficients[0]); + Assert.Equal(123F, coefficients[2]); + JxlDct.ComputeScaledInverseDct(1, 1, coefficients.AsSpan(1), pixels.AsSpan(1), 1, workspace); + Assert.Equal(6F, pixels[1]); + Assert.Equal(123F, pixels[0]); + Assert.Equal(123F, pixels[2]); + + ReadOnlySpan<(int Rows, int Columns)> shapes = [(1, 2), (2, 1)]; + foreach ((int rows, int columns) in shapes) + { + float[] pair = [6F, 2F]; + coefficients.AsSpan().Fill(123F); + JxlDct.ComputeScaledDct(rows, columns, pair, columns, coefficients, workspace); + Assert.Equal(4F, coefficients[0]); + Assert.Equal(2F, coefficients[1]); + Assert.Equal(123F, coefficients[2]); + + // Supply independent frequency values. A paired forward/inverse defect must not be able to cancel itself. + coefficients[0] = 4F; + coefficients[1] = 2F; + pixels.AsSpan().Fill(123F); + JxlDct.ComputeScaledInverseDct(rows, columns, coefficients, pixels, columns, workspace); + Assert.Equal(6F, pixels[0]); + Assert.Equal(2F, pixels[1]); + Assert.Equal(123F, pixels[2]); + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } + + /// + /// Checks an exact non-DC cosine basis at every supported length, both axis orientations, real strides and SIMD/scalar dispatch. + /// + [Fact] + public void Dct_HalfFrequencyBasisPreservesAxesAndBounds() + { + static void RunTest() + { + ReadOnlySpan lengths = [2, 4, 8, 16, 32, 64, 128, 256]; + ReadOnlySpan orientations = [false, true]; + const int prefix = 3; + const float sentinel = 123F; + foreach (int length in lengths) + { + foreach (bool horizontal in orientations) + { + int rows = horizontal ? 32 : length; + int columns = horizontal ? length : 32; + int sourceStride = columns + 5; + int destinationStride = columns + 7; + int area = rows * columns; + float[] source = new float[prefix + (rows * sourceStride) + 3]; + float[] coefficients = new float[prefix + area + 3]; + float[] destination = new float[prefix + (rows * destinationStride) + 3]; + source.AsSpan().Fill(sentinel); + coefficients.AsSpan().Fill(sentinel); + destination.AsSpan().Fill(sentinel); + + // sqrt(2)*cos((sample+1/2)*pi/2) is exactly the repeating sign pattern [1,-1,-1,1]. + // Its normalized DCT has one unit coefficient at frequency length/2, with the other axis at DC. + for (int y = 0; y < rows; y++) + { + for (int x = 0; x < columns; x++) + { + int position = (horizontal ? x : y) & 3; + source[prefix + (y * sourceStride) + x] = position is 0 or 3 ? 1F : -1F; + } + } + + // The narrow axis caps row-band lanes. Include the existing block region and two banks, but no hidden padding allowance. + int lanes = Math.Min(16, Math.Min(rows, columns)); + int workspaceLength = area + (2 * Math.Max(rows, columns) * lanes); + float[] workspace = new float[workspaceLength + 2]; + workspace.AsSpan().Fill(sentinel); + Span work = workspace.AsSpan(1, workspaceLength); + Span coefficientBlock = coefficients.AsSpan(prefix); + JxlDct.ComputeScaledDct(rows, columns, source.AsSpan(prefix), sourceStride, coefficientBlock, work); + int frequencyY = horizontal ? 0 : length / 2; + int frequencyX = horizontal ? length / 2 : 0; + int index = rows >= columns ? (frequencyX * rows) + frequencyY : (frequencyY * columns) + frequencyX; + Assert.Equal(1F, coefficientBlock[index]); + coefficientBlock[index] = 0; + Assert.True(coefficientBlock[..area].IndexOfAnyExcept(0F) < 0); + Assert.True(coefficients.AsSpan(0, prefix).IndexOfAnyExcept(sentinel) < 0); + Assert.True(coefficients.AsSpan(prefix + area).IndexOfAnyExcept(sentinel) < 0); + + coefficientBlock[..area].Clear(); + coefficientBlock[index] = 1F; + JxlDct.ComputeScaledInverseDct(rows, columns, coefficientBlock, destination.AsSpan(prefix), destinationStride, work); + for (int i = 0; i < destination.Length; i++) + { + int relative = i - prefix; + int y = relative / destinationStride; + int x = relative % destinationStride; + int position = (horizontal ? x : y) & 3; + float expected = relative >= 0 && y < rows && x < columns ? position is 0 or 3 ? 1F : -1F : sentinel; + Assert.Equal(expected, destination[i]); + } + + // Forward conversion must leave its source and padding unchanged, even when later row-band stores reuse the output matrix. + for (int i = 0; i < source.Length; i++) + { + int relative = i - prefix; + int y = relative / sourceStride; + int x = relative % sourceStride; + int position = (horizontal ? x : y) & 3; + float expected = relative >= 0 && y < rows && x < columns ? position is 0 or 3 ? 1F : -1F : sentinel; + Assert.Equal(expected, source[i]); + } + + Assert.Equal(sentinel, workspace[0]); + Assert.Equal(sentinel, workspace[^1]); + } + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } + + /// + /// Checks a nonzero odd-frequency basis independently in each direction, including the signed weighted combination. + /// + [Fact] + public void Dct_OddFrequencyUsesBothWeightedHalves() + { + static void RunTest() + { + ReadOnlySpan orientations = [false, true]; + ReadOnlySpan samples = [0F, 1F, -1F, 0F]; + + // These are the four samples of sqrt(2)*cos(3*pi*(sample+1/2)/4), rounded once to float. + ReadOnlySpan basis = [0.541196100146197F, -1.3065629648763764F, 1.3065629648763764F, -0.541196100146197F]; + foreach (bool horizontal in orientations) + { + int rows = horizontal ? 32 : 4; + int columns = horizontal ? 4 : 32; + int stride = columns + 3; + float[] source = new float[rows * stride]; + for (int y = 0; y < rows; y++) + { + for (int x = 0; x < columns; x++) + { + source[(y * stride) + x] = samples[horizontal ? x : y]; + } + } + + float[] coefficients = new float[rows * columns]; + float[] workspace = new float[512]; + JxlDct.ComputeScaledDct(rows, columns, source, stride, coefficients, workspace); + + // Frequency three of [0,1,-1,0] is -sqrt(2)*cos(pi/8)/2. The other axis is DC. + Assert.Equal(-0.6532814824381882F, coefficients[3 * 32]); + coefficients.AsSpan().Clear(); + coefficients[3 * 32] = 1F; + float[] restored = new float[rows * stride]; + JxlDct.ComputeScaledInverseDct(rows, columns, coefficients, restored, stride, workspace); + for (int y = 0; y < rows; y++) + { + for (int x = 0; x < columns; x++) + { + Assert.Equal(basis[horizontal ? x : y], restored[(y * stride) + x]); + } + } + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } + + /// + /// Checks separate horizontal and vertical resampling factors with an exact two-point frequency basis. + /// + [Fact] + public void ReinterpretingDct_UsesIndependentFrequencyAxes() + { + static void RunTest() + { + float[] source = [1F, -1F, 123F, 1F, -1F, 123F, 1F, -1F, 123F, 1F, -1F, 123F]; + float[] output = new float[14]; + output.AsSpan().Fill(123F); + float[] coefficients = new float[8]; + float[] workspace = new float[1056]; + JxlTransformsDecoder.ReinterpretingDct(32, 16, 4, 2, 4, 2, source, 3, output, 7, coefficients, workspace); + + // The nonzero frequency is on the two-sample axis. The four-sample axis is DC, whose scale is exactly one. + Assert.Equal(1.108937353592731823F, output[7]); + Assert.Equal(123F, output[4]); + Assert.Equal(123F, output[11]); + + output.AsSpan().Clear(); + output[7] = 1; + float[] restored = new float[12]; + restored.AsSpan().Fill(123F); + JxlTransformsEncoder.ReinterpretingInverseDct(32, 16, 4, 2, 4, 2, output, 7, restored, 3, workspace); + for (int y = 0; y < 4; y++) + { + Assert.Equal(0.901764195028874394F, restored[y * 3]); + Assert.Equal(-0.901764195028874394F, restored[(y * 3) + 1]); + Assert.Equal(123F, restored[(y * 3) + 2]); + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } + + /// + /// Checks independent Haar equations and the complete AFV component placement without floating-point tolerances. + /// + [Fact] + public void SmallTransforms_PreserveHaarSignsAndAfvDc() + { + static void RunTest() + { + float[] source = new float[88]; + source.AsSpan().Fill(123F); + source[0] = 1; + source[1] = 2; + source[11] = 3; + source[12] = 4; + float[] coefficients = new float[64]; + JxlTransformsEncoder.Dct2TopBlock(2, source, 11, coefficients); + Assert.Equal(2.5F, coefficients[0]); + Assert.Equal(-0.5F, coefficients[1]); + Assert.Equal(-1F, coefficients[8]); + Assert.Equal(0F, coefficients[9]); + + coefficients.AsSpan().Clear(); + coefficients[0] = 2.5F; + coefficients[1] = -0.5F; + coefficients[8] = -1F; + float[] restored = new float[88]; + restored.AsSpan().Fill(123F); + JxlTransformsDecoder.Idct2TopBlock(2, coefficients, 11, restored); + Assert.Equal(1F, restored[0]); + Assert.Equal(2F, restored[1]); + Assert.Equal(3F, restored[11]); + Assert.Equal(4F, restored[12]); + Assert.Equal(123F, restored[2]); + Assert.Equal(123F, restored[13]); + + // Repeated [1,2;3,4] cells have fixed first-stage Haar quadrants. Later stages only reduce their constant average quadrant. + for (int y = 0; y < 8; y++) + { + for (int x = 0; x < 8; x++) + { + source[(y * 11) + x] = 1 + (x & 1) + (2 * (y & 1)); + } + } + + float[] haarWorkspace = new float[256]; + JxlTransformsEncoder.TransformFromPixels(JxlAcStrategyType.DCT2X2, source, 11, coefficients, haarWorkspace); + for (int y = 0; y < 8; y++) + { + for (int x = 0; x < 8; x++) + { + float expected = x == 0 && y == 0 ? 2.5F : x >= 4 && y < 4 ? -0.5F : x < 4 && y >= 4 ? -1F : 0F; + Assert.Equal(expected, coefficients[(y * 8) + x]); + } + } + + restored.AsSpan().Fill(123F); + JxlTransformsDecoder.TransformToPixels(JxlAcStrategyType.DCT2X2, coefficients, restored, 11, haarWorkspace); + for (int y = 0; y < 8; y++) + { + for (int x = 0; x < 8; x++) + { + Assert.Equal(1F + (x & 1) + (2 * (y & 1)), restored[(y * 11) + x]); + } + + Assert.True(restored.AsSpan((y * 11) + 8, 3).IndexOfAnyExcept(123F) < 0); + } + + ReadOnlySpan strategies = + [ + JxlAcStrategyType.AFV0, JxlAcStrategyType.AFV1, JxlAcStrategyType.AFV2, JxlAcStrategyType.AFV3 + ]; + + foreach (JxlAcStrategyType strategy in strategies) + { + for (int y = 0; y < 8; y++) + { + source.AsSpan(y * 11, 8).Fill(1F); + } + + coefficients.AsSpan().Fill(123F); + float[] workspace = new float[256]; + JxlTransformsEncoder.TransformFromPixels(strategy, source, 11, coefficients, workspace); + Assert.Equal(1F, coefficients[0]); + Assert.Equal(1F, source[0]); + Assert.Equal(1F, source[7]); + + // A unit DC coefficient independently restores a constant block in all three AFV regions and each quadrant orientation. + coefficients.AsSpan().Clear(); + coefficients[0] = 1; + restored.AsSpan().Fill(123F); + JxlTransformsDecoder.TransformToPixels(strategy, coefficients, restored, 11, workspace); + for (int y = 0; y < 8; y++) + { + Assert.True(restored.AsSpan(y * 11, 8).IndexOfAnyExcept(1F) < 0); + Assert.True(restored.AsSpan((y * 11) + 8, 3).IndexOfAnyExcept(123F) < 0); + } + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/QuantizationTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/QuantizationTests.cs index e36c8df0f1..b1c59c7279 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/QuantizationTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/QuantizationTests.cs @@ -8,6 +8,120 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; public class QuantizationTests { + /// + /// Checks the native default band count and the rectangular transform's predefined table selection. + /// + [Fact] + public void DefaultWeights_MatchNativeBandAndTableMetadata() + { + Assert.Equal(8, JxlQuantWeights.Dct32x32.DctParameters!.NumDistanceBands); + Assert.Same(JxlQuantWeights.Dct32x64, JxlDequantMatrices.Library[(int)JxlQuantTable.DCT32X64]); + } + + /// + /// Checks native signed band multipliers, exact constant intervals, independent channel layout, and all SIMD tails. + /// + [Fact] + public void DistanceWeights_ApplyMultipliersAndPreserveExactIntervalValues() + { + static void RunTest() + { + // Mult(0)=1 keeps every interval constant. This exercises the real interpolation path rather than the single-band fill. + float[][] constantBands = [[2F, 0F, 0F], [4F, 0F, 0F], [8F, 0F, 0F]]; + float[][] varyingBands = [[2F, 1F, -1F], [4F, 1F, -1F], [8F, 1F, -1F]]; + ReadOnlySpan columns = [3, 4, 7, 8, 15, 16, 17, 31, 32, 33]; + + foreach (int columnCount in columns) + { + const int rowCount = 3; + int channelSize = rowCount * columnCount; + float[] storage = new float[(3 * channelSize) + 2]; + storage.AsSpan().Fill(-77F); + Span weights = storage.AsSpan(1, 3 * channelSize); + Assert.True(JxlQuantWeights.GetQuantWeights(rowCount, columnCount, constantBands, 3, weights)); + + for (int c = 0; c < 3; c++) + { + for (int i = 0; i < channelSize; i++) + { + Assert.Equal(constantBands[c][0], weights[(c * channelSize) + i]); + } + } + + Assert.Equal(-77F, storage[0]); + Assert.Equal(-77F, storage[^1]); + + // Mult(1)=2 and Mult(-1)=1/2 produce positive cumulative bands [a,2a,a]. Raw multiplication would reject them. + Assert.True(JxlQuantWeights.GetQuantWeights(rowCount, columnCount, varyingBands, 3, weights)); + for (int c = 0; c < 3; c++) + { + Assert.Equal(varyingBands[c][0], weights[c * channelSize]); + } + + Assert.Equal(-77F, storage[0]); + Assert.Equal(-77F, storage[^1]); + } + + // Scaled positions zero and one are the exact starts of successive geometric intervals. + ReadOnlySpan samples = [2F, 8F, 4F]; + ReadOnlySpan logarithms = [2F, -1F]; + Assert.Equal(2F, JxlQuantWeights.Interpolate(0F, samples, logarithms)); + Assert.Equal(8F, JxlQuantWeights.Interpolate(1F, samples, logarithms)); + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } + + /// + /// Checks half-row thresholds and ties-to-even integer rounding for narrow and full-width coefficient rows. + /// + [Fact] + public void Quantize_UsesQuadrantThresholdsAndEvenMidpoints() + { + static void RunTest() + { + ReadOnlySpan samples = [-3.5F, -2.5F, -1.5F, -1.25F, -1.125F, -0.5F, 0F, 0.5F, 1.125F, 1.25F, 1.5F, 2.5F, 3.5F]; + ReadOnlySpan lowThreshold = [-4, -2, -2, -1, -1, 0, 0, 0, 1, 1, 2, 2, 4]; + ReadOnlySpan highThreshold = [-4, -2, -2, -1, 0, 0, 0, 0, 0, 1, 2, 2, 4]; + ReadOnlySpan widths = [8, 16, 32, 64, 128, 256]; + + foreach (int width in widths) + { + float[] source = new float[width + 1]; + float[] matrix = new float[width + 1]; + int[] output = new int[width + 2]; + matrix.AsSpan().Fill(2F); + output.AsSpan().Fill(-777); + + for (int i = 0; i < width; i++) + { + source[i] = samples[i % samples.Length]; + } + + source[width] = float.NaN; + matrix[width] = float.NaN; + + // Matrix 2 and scale 0.5 leave sample magnitudes unchanged. The two tables state rounding and cutoff results independently. + JxlQuantizer.Quantize(source.AsSpan(0, width), matrix, 0.5F, 0.5F, 1.25F, output.AsSpan(1, width)); + + for (int i = 0; i < width; i++) + { + int expected = i < width / 2 ? lowThreshold[i % samples.Length] : highThreshold[i % samples.Length]; + Assert.Equal(expected, output[1 + i]); + } + + Assert.Equal(-777, output[0]); + Assert.Equal(-777, output[^1]); + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } + /// /// Checks numeric conversion, zero/unit bias, matrix scaling and correlation for both coefficient storage widths. /// From 94391de4043bd9c2504b017975bf691c3edc86ae Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 29 Oct 2026 19:44:52 +0400 Subject: [PATCH 296/328] Add fast SIMD tests --- .../Formats/Jxl/Processing/FastSimdTests.cs | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 tests/ImageSharp.Tests/Formats/Jxl/Processing/FastSimdTests.cs diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/FastSimdTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/FastSimdTests.cs new file mode 100644 index 0000000000..562b36c0a1 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/FastSimdTests.cs @@ -0,0 +1,124 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Formats.Jxl.Cms.TransferFunctions; +using SixLabors.ImageSharp.Formats.Jxl.Processing; + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; + +/// +/// Round-trip tests for high-performance, SIMD-accelerated +/// routines. +/// +public class FastSimdTests +{ + /// + /// Ensures that + /// works correctly and is precise. + /// + [Fact] + public void TestFastPow2() + { + const int numberOfTrials = 1 << 23; + Rng rng = new(1); + + for (int i = 0; i < numberOfTrials; i++) + { + float f = rng.UniformF(-100, 100); + + // Result of FastPow2f (faster, lower precision) + Vector vectorized = JxlSimdUtils.FastPow2f(Vector.Create(f)); + + // Result of MathF.Pow (precise) + float scalar = MathF.Pow(2, f); + + // Precision difference + float error = MathF.Abs(scalar - vectorized.ToScalar()) / scalar; + + // Precision should be normal. + Assert.True(error < 3.1E-6); + } + } + + /// + /// Ensures that + /// works correctly and is precise. + /// + [Fact] + public void TestFastPow() + { + const int numberOfTrials = 1 << 23; + Rng rng = new(1); + + for (int i = 0; i < numberOfTrials; i++) + { + // Generate a random base and exponent + float @base = rng.UniformF(1e-3f, 1e3f); + float exponent = rng.UniformF(-10, 10); + + // Result of FastPowf (faster, lower precision) + Vector vectorized = JxlSimdUtils.FastPowf(Vector.Create(@base), Vector.Create(exponent)); + + // Result of MathF.Pow (precise) + float scalar = MathF.Pow(@base, exponent); + + // Precision difference + float error = MathF.Abs(scalar - vectorized.ToScalar()) / scalar; + + // Precision should be normal. + Assert.True(error < 3e-5); + } + } + + /// + /// Ensures that + /// works correctly and is precise. + /// + [Fact] + public void TestFastErf() + { + const int numberOfTrials = 1 << 23; + Rng rng = new(1); + + for (int i = 0; i < numberOfTrials; i++) + { + float f = rng.UniformF(-5f, 5f); + + // Result of FastPowf (faster, lower precision) + Vector vectorized = JxlSimdUtils.FastErff(Vector.Create(f)); + + // Result of a more precise error function. (.NET doesn't + // support an std::erf equivalent) + float scalar = JxlSimdUtils.FastErff(f); + + // Precision difference + float error = MathF.Abs(scalar - vectorized.ToScalar()) / scalar; + + // Precision should be normal. + Assert.True(error < 7e-4); + } + } + + /// + /// Tests the SIMD BT.709 transfer function against a + /// scalar version. + /// + [Fact] + public void TestFastBt709EncodedFromDisplay() + { + const int numberOfTrials = 1 << 23; + Rng rng = new(1); + + for (int i = 0; i < numberOfTrials; i++) + { + float f = rng.UniformF(0, 1f); + + float simd = JxlBt709TransferFunction.EncodedFromDisplay(Vector.Create(f)).ToScalar(); + float scalar = JxlBt709TransferFunction.EncodedFromDisplay(f); + + float absoluteError = MathF.Abs(scalar - simd); + Assert.True(absoluteError < 2e-6f); + } + } +} From 2843bc33b02f2adf7276cc179bb5a9fc5fd57b09 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Oct 2026 02:16:13 +1000 Subject: [PATCH 297/328] Correct JPEG XL learned-tree histogram cost with shared SIMD operators --- .../Modular/Encoding/JxlMaEncoder.cs | 135 +++++++++++++++--- .../Jxl/Processing/EntropyCoderTests.cs | 53 +++++++ 2 files changed, 172 insertions(+), 16 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaEncoder.cs index 115136b1bb..4ef0e2eb06 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaEncoder.cs @@ -6,6 +6,8 @@ using System.Numerics.Tensors; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; @@ -26,30 +28,131 @@ internal enum IntersectionType : byte private static int Padded(int x) => JxlMath.RoundUpTo(x, Vector.Count); - public static float EstimateBits(Span counts, int numSymbols) + /// + /// Estimates the ANS bit cost of a histogram with the codec's minimum nonzero symbol probability. + /// + /// The nonnegative occurrence counts. Storage after numSymbols is not part of the histogram. + /// The logical number of histogram entries. + /// The estimated cost in bits. Empty and single-symbol histograms have zero cost. + public static float EstimateBits(ReadOnlySpan counts, int numSymbols) { - int total = TensorPrimitives.Sum((ReadOnlySpan)counts.Slice(0, numSymbols)); + ReadOnlySpan histogram = counts[..numSymbols]; + int total = TensorPrimitives.Sum(histogram); + HistogramBitCostOperator operation = new(total); + ref int input = ref MemoryMarshal.GetReference(histogram); + float cost = 0F; + int i = 0; + + // Convert integer counts numerically. Keep the single-symbol comparison in integer lanes so counts above 2^24 cannot compare equal by rounding. + if (Vector512.IsHardwareAccelerated) + { + nuint count = (histogram.Length - i).Vector512Count(); + Vector512 totalCounts = Vector512.Create(total); + Vector512 accumulated = Vector512.Zero; + + for (nuint block = 0; block < count; block++, i += Vector512.Count) + { + Vector512 occurrences = Vector512.LoadUnsafe(ref input, (uint)i); + Vector512 bits = operation.Invoke(Vector512.ConvertToSingle(occurrences)); + bits = Vector512.ConditionalSelect(Vector512.Equals(occurrences, totalCounts).AsSingle(), Vector512.Zero, bits); + accumulated += bits; + } + + cost += Vector512.Sum(accumulated); + } + + if (Vector256.IsHardwareAccelerated) + { + nuint count = (histogram.Length - i).Vector256Count(); + Vector256 totalCounts = Vector256.Create(total); + Vector256 accumulated = Vector256.Zero; + + for (nuint block = 0; block < count; block++, i += Vector256.Count) + { + Vector256 occurrences = Vector256.LoadUnsafe(ref input, (uint)i); + Vector256 bits = operation.Invoke(Vector256.ConvertToSingle(occurrences)); + bits = Vector256.ConditionalSelect(Vector256.Equals(occurrences, totalCounts).AsSingle(), Vector256.Zero, bits); + accumulated += bits; + } + + cost += Vector256.Sum(accumulated); + } + + if (Vector128.IsHardwareAccelerated) + { + nuint count = (histogram.Length - i).Vector128Count(); + Vector128 totalCounts = Vector128.Create(total); + Vector128 accumulated = Vector128.Zero; + + for (nuint block = 0; block < count; block++, i += Vector128.Count) + { + Vector128 occurrences = Vector128.LoadUnsafe(ref input, (uint)i); + Vector128 bits = operation.Invoke(Vector128.ConvertToSingle(occurrences)); + bits = Vector128.ConditionalSelect(Vector128.Equals(occurrences, totalCounts).AsSingle(), Vector128.Zero, bits); + accumulated += bits; + } - Vector minprob = Vector.Create(1.0f / JxlAnsConstants.AnsTableSize); - Vector inverseTotal = Vector.Create(1.0f / total); - Vector bitsLanes = Vector.Zero; + cost += Vector128.Sum(accumulated); + } - for (int i = 0; i < numSymbols; i += Vector.Count) + // The native single-symbol rule also covers total zero: all its counts are zero and contribute zero. + // Scalar tails require no padding and use the same probability floor as complete registers. + for (; i < histogram.Length; i++) { - Vector countsIv = new(counts[i..]); - Vector countsFv = Vector.ConvertToSingle(countsIv); - Vector probs = countsFv * inverseTotal; - Vector mprobs = probs * minprob; + int value = Unsafe.Add(ref input, i); + cost += value == total ? 0F : operation.Invoke(value); + } + + return cost; + } + + /// + /// Calculates -count * log2(max(count / total, 1 / ANS table size)) for one histogram entry. + /// + /// The caller handles a count equal to the histogram total before reduction, including the all-zero case. + private readonly struct HistogramBitCostOperator : TensorPrimitives_.IUnaryOperator + { + private const float MinimumProbability = 1F / JxlAnsConstants.AnsTableSize; - Vector nbps = Vector.ConditionalSelect( - Vector.Equals(countsIv, Vector.Create(total)), - Vector.Zero, - Vector.Log2(mprobs)); + private readonly float inverseTotal; - bitsLanes -= countsFv * nbps; + /// + /// Initializes a new instance of the struct. + /// + /// The histogram's total occurrence count. + public HistogramBitCostOperator(int total) => this.inverseTotal = 1F / total; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float Invoke(float value) + => -(value * MathF.Log2(MathF.Max(value * this.inverseTotal, MinimumProbability))); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector128 Invoke(Vector128 value) + { + // ANS cannot assign a nonzero symbol a probability below one table entry. Flooring probabilities also keeps empty bins away from log2(0). + Vector128 probability = Vector128.Max(value * Vector128.Create(this.inverseTotal), Vector128.Create(MinimumProbability)); + return -(value * Vector128.Log2(probability)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector256 Invoke(Vector256 value) + { + // ANS cannot assign a nonzero symbol a probability below one table entry. Flooring probabilities also keeps empty bins away from log2(0). + Vector256 probability = Vector256.Max(value * Vector256.Create(this.inverseTotal), Vector256.Create(MinimumProbability)); + return -(value * Vector256.Log2(probability)); } - return Vector.Sum(bitsLanes); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector512 Invoke(Vector512 value) + { + // ANS cannot assign a nonzero symbol a probability below one table entry. Flooring probabilities also keeps empty bins away from log2(0). + Vector512 probability = Vector512.Max(value * Vector512.Create(this.inverseTotal), Vector512.Create(MinimumProbability)); + return -(value * Vector512.Log2(probability)); + } } public static void MakeSplitNode(int pos, int property, int splitValue, JxlPredictor leftPredictor, int leftOffset, JxlPredictor rightPredictor, int rightOffset, Tree tree) diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/EntropyCoderTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/EntropyCoderTests.cs index f141a75602..4c43985967 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/EntropyCoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/EntropyCoderTests.cs @@ -1,11 +1,13 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding; using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using SixLabors.ImageSharp.Tests.TestUtilities; @@ -14,6 +16,57 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; public class EntropyCoderTests { + /// + /// Checks ANS cost for exact binary probabilities, zero bins and logical histogram lengths under every feature mode. + /// + [Fact] + public void HistogramBitCost_UsesProbabilityFloorAndLogicalCount() + { + static void RunTest() + { + ReadOnlySpan widths = [1, 2, 4, 8, 16, 32, 64]; + foreach (int width in widths) + { + int[] counts = new int[width + 1]; + counts.AsSpan(0, width).Fill(4); + counts[width] = int.MaxValue; + + // Equal frequencies require log2(width) bits per occurrence. Powers of two make this expectation exact. + float expected = 4 * width * (int)BitOperations.Log2((uint)width); + Assert.Equal(expected, JxlMaEncoder.EstimateBits(counts, width)); + + counts.AsSpan(0, width).Clear(); + Assert.Equal(0F, JxlMaEncoder.EstimateBits(counts, width)); + counts[width - 1] = 7; + Assert.Equal(0F, JxlMaEncoder.EstimateBits(counts, width)); + Assert.Equal(int.MaxValue, counts[width]); + } + + ReadOnlySpan lengths = [3, 7, 15, 17, 31, 33, 65]; + foreach (int length in lengths) + { + int[] counts = new int[length + 1]; + counts[0] = 1; + counts[length - 1] = 1; + counts[length] = int.MaxValue; + + // Two equally likely symbols require one bit each. Empty bins must contribute zero, including scalar tail bins. + Assert.Equal(2F, JxlMaEncoder.EstimateBits(counts, length)); + } + + // ANS has 4096 table entries, so the probability floor limits this estimate to 12 bits per occurrence. + // With 8192 equally likely symbols, unconstrained Shannon entropy would incorrectly return 13 bits each. + int[] rareSymbols = new int[8192]; + rareSymbols.AsSpan().Fill(1); + Assert.Equal(98304F, JxlMaEncoder.EstimateBits(rareSymbols, rareSymbols.Length)); + Assert.Equal(0F, JxlMaEncoder.EstimateBits([], 0)); + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } + /// /// Checks terminal zero-bit and one-bit Huffman symbols through the encoded table and bounded bit reader. /// From 21a1e15e6b8935d7b415707a0d0a92ad17e2732b Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Oct 2026 02:52:14 +1000 Subject: [PATCH 298/328] Correct JPEG XL modular sample ownership and reuse tree-search storage --- .../Modular/Encoding/JxlMaEncoder.cs | 658 +++++++--- .../Encoding/JxlModularEncodingEncoder.cs | 390 ++++-- .../Encoding/JxlPropertyDecisionNode.cs | 12 +- .../Modular/Encoding/JxlTreeSamples.cs | 1129 +++++++---------- .../Jxl/Processing/EntropyCoderTests.cs | 189 +++ 5 files changed, 1401 insertions(+), 977 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaEncoder.cs index 4ef0e2eb06..672b4f207e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaEncoder.cs @@ -12,7 +12,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; -using StaticPropertyRange = System.Runtime.CompilerServices.InlineArray2>; +using StaticPropertyRange = SixLabors.ImageSharp.InlineArray2>; using Tree = System.Collections.Generic.List; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding; @@ -215,24 +215,41 @@ public static IntersectionType BoxIntersects(StaticPropertyRange needle, StaticP return partial ? IntersectionType.Partial : IntersectionType.Inside; } - public static void SplitTreeSamples(bool s, JxlTreeSamples samples, int begin, int pos, int end, int prop, int value) + /// + /// Partitions samples around a property value at the selected split index. + /// + /// Whether to read the static property table. + /// The samples to partition. + /// The inclusive start of the sample range. + /// The boundary between the two sample partitions. + /// The exclusive end of the sample range. + /// The property used for the split. + /// The largest property value in the first partition. + public static void SplitTreeSamples( + bool useStaticProperty, + scoped ref JxlTreeSamples samples, + int begin, + int splitIndex, + int end, + int propertyIndex, + int splitValue) { int beginPos = begin; - int endPos = pos; + int endPos = splitIndex; do { - while (beginPos < pos && samples.GetProperty(s, prop, beginPos) <= value) + while (beginPos < splitIndex && samples.GetProperty(useStaticProperty, propertyIndex, beginPos) <= splitValue) { beginPos++; } - while (endPos < end && samples.GetProperty(s, prop, endPos) > value) + while (endPos < end && samples.GetProperty(useStaticProperty, propertyIndex, endPos) > splitValue) { endPos++; } - if (beginPos < pos && endPos < end) + if (beginPos < splitIndex && endPos < end) { samples.Swap(beginPos, endPos); } @@ -240,30 +257,38 @@ public static void SplitTreeSamples(bool s, JxlTreeSamples samples, int begin, i beginPos++; endPos++; } - while (beginPos < pos && endPos < end); + while (beginPos < splitIndex && endPos < end); } - // Simple overload so we don't have to type out CollectionsMarshal.AsSpan all the time. - public static void CollectExtraBitsIncrease(bool s, JxlTreeSamples treeSamples, List residualTokens, List countIncrease, List extraBitsIncrease, int begin, int end, int propertyIndex, int maxSymbols) - => CollectExtraBitsIncrease( - s, - treeSamples, - residualTokens, - CollectionsMarshal.AsSpan(countIncrease), - CollectionsMarshal.AsSpan(extraBitsIncrease), - begin, - end, - propertyIndex, - maxSymbols); - - public static void CollectExtraBitsIncrease(bool s, JxlTreeSamples treeSamples, List residualTokens, Span countIncrease, Span extraBitsIncrease, int begin, int end, int propertyIndex, int maxSymbols) + /// + /// Accumulates symbol counts and extra-bit costs for a candidate property. + /// + /// Whether to read the static property table. + /// The samples and their occurrence counts. + /// The encoded residual for each sample. + /// The destination occurrence counts, grouped by property value and symbol. + /// The destination extra-bit costs for each property value. + /// The inclusive start of the sample range. + /// The exclusive end of the sample range. + /// The candidate property index. + /// The number of symbol entries reserved for each property value. + public static void CollectExtraBitsIncrease( + bool useStaticProperty, + scoped in JxlTreeSamples treeSamples, + scoped ReadOnlySpan residualTokens, + Span countIncrease, + Span extraBitsIncrease, + int begin, + int end, + int propertyIndex, + int maxSymbols) { for (int i2 = begin; i2 < end; i2++) { JxlResidualToken rt = residualTokens[i2]; int cnt = treeSamples.GetCount(i2); - int p = treeSamples.GetProperty(s, propertyIndex, i2); + int p = treeSamples.GetProperty(useStaticProperty, propertyIndex, i2); int sym = rt.Token; int ebi = rt.NumberOfBits * cnt; @@ -272,7 +297,24 @@ public static void CollectExtraBitsIncrease(bool s, JxlTreeSamples treeSamples, } } - public static unsafe void FindBestSplit(JxlTreeSamples treeSamples, float threshold, List mulInfo, StaticPropertyRange initialStaticPropertyRange, float fastDecodeMultiplier, Tree tree) + /// + /// Chooses prediction-tree splits with one reusable allocator-owned counter workspace. + /// + /// The configuration that owns search storage. + /// The prepared sample owner, partitioned in place as splits are accepted. + /// The minimum bit saving required to split a node. + /// The forced multiplier ranges. + /// The channel/group ranges covered by the root node. + /// The allowed cost ratio for simpler predictors. + /// The root leaf and destination tree nodes. + public static void FindBestSplit( + Configuration configuration, + scoped ref JxlTreeSamples treeSamples, + float threshold, + List mulInfo, + StaticPropertyRange initialStaticPropertyRange, + float fastDecodeMultiplier, + Tree tree) { Stack nodes = []; nodes.Push(new(0, 0, treeSamples.NumberOfDistinctSamples, initialStaticPropertyRange)); @@ -280,28 +322,60 @@ public static unsafe void FindBestSplit(JxlTreeSamples treeSamples, float thresh int numPred = treeSamples.NumberOfPredictors; int numProp = treeSamples.NumberOfProperties; - Span totalExtraBits = stackalloc int[numPred]; + int maximumPropertySize = 0; + for (int property = 0; property < numProp; property++) + { + maximumPropertySize = Math.Max(maximumPropertySize, treeSamples.CountPropertyValues(property)); + } + + // Sample residuals use HybridUint(4,1,2): even UInt32.MaxValue produces token 239, inside the existing 256-symbol ANS alphabet. + // Reserve that bounded alphabet once. Individual nodes still use their smaller active stride and never resize these buffers. + int symbolCapacity = Padded(JxlAnsConstants.AnsMaxAlphabetSize); + int counterLength = checked(((numPred + 2) * symbolCapacity) + (maximumPropertySize * (symbolCapacity + 1))); + using IMemoryOwner counterOwner = configuration.MemoryAllocator.Allocate(counterLength); + using IMemoryOwner costOwner = configuration.MemoryAllocator.Allocate(2 * maximumPropertySize); + using IMemoryOwner bitOwner = configuration.MemoryAllocator.Allocate(maximumPropertySize + numPred); + Span counters = counterOwner.Memory.Span; + Span costs = costOwner.Memory.Span; + Span bitCounts = bitOwner.Memory.Span; + int offset = numPred * symbolCapacity; + Span counts = counters[..offset]; + Span aboveCounts = counters.Slice(offset, symbolCapacity); + offset += symbolCapacity; + Span belowCounts = counters.Slice(offset, symbolCapacity); + offset += symbolCapacity; + Span countsIncrease = counters.Slice(offset, maximumPropertySize * symbolCapacity); + offset += countsIncrease.Length; + Span propertyValueUsedCount = counters.Slice(offset, maximumPropertySize); + Span extraBitsIncrease = bitCounts[..maximumPropertySize]; + Span totalExtraBits = bitCounts[maximumPropertySize..]; + Span leftCosts = costs[..maximumPropertySize]; + Span rightCosts = costs[maximumPropertySize..]; + + // Extra-bit totals use Int64: many valid 65535-count records can exceed an Int32 bit total. + // Allocation contents are unspecified. The incremental buffers are cleared here, then drained after each predictor's scan. + // Parent histograms, property usage, child histograms and candidate costs are initialized to their logical extent before each use. + countsIncrease.Clear(); + extraBitsIncrease.Clear(); while (nodes.Count > 0) { - NodeInfo last = nodes.Peek(); + NodeInfo last = nodes.Pop(); int pos = last.Pos; int begin = last.Begin; int end = last.End; StaticPropertyRange staticPropertyRange = last.StaticPropertyRange; - _ = nodes.Pop(); - if (begin == end) { continue; } - SplitInfo bestSplitStaticConstant = default; - SplitInfo bestSplitStatic = default; - SplitInfo bestSplitNonStatic = default; - SplitInfo bestSplitNoWeightedPrediction = default; + SplitInfo bestSplitStaticConstant = new(); + SplitInfo bestSplitStatic = new(); + SplitInfo bestSplitNonStatic = new(); + SplitInfo bestSplitNoWeightedPrediction = new(); if (begin > end) { @@ -313,6 +387,7 @@ public static unsafe void FindBestSplit(JxlTreeSamples treeSamples, float thresh throw new InvalidOperationException("End index out of range"); } + JxlPredictor currentPredictor = tree[pos].Predictor; int maxSymbols = 0; for (int pred = 0; pred < numPred; pred++) @@ -326,12 +401,12 @@ public static unsafe void FindBestSplit(JxlTreeSamples treeSamples, float thresh maxSymbols = Padded(maxSymbols); - int[] counts = ArrayPool.Shared.Rent(maxSymbols * numPred); + counts[..(maxSymbols * numPred)].Clear(); for (int pred = 0; pred < numPred; pred++) { - int extraBits = 0; - List rtokens = treeSamples.GetResidualTokensForPrediction(pred); + long extraBits = 0; + ReadOnlySpan rtokens = treeSamples.GetResidualTokensForPrediction(pred); for (int i = begin; i < end; i++) { @@ -349,13 +424,13 @@ public static unsafe void FindBestSplit(JxlTreeSamples treeSamples, float thresh float baseBits = 0; { - int pred = treeSamples.FindPredictorIndex(tree[pos].Predictor); - baseBits = EstimateBits(counts.AsSpan(pred * maxSymbols), maxSymbols) + totalExtraBits[pred]; + int pred = treeSamples.FindPredictorIndex(currentPredictor); + baseBits = EstimateBits(counts[(pred * maxSymbols)..], maxSymbols) + totalExtraBits[pred]; } ref SplitInfo best = ref bestSplitNonStatic; - SplitInfo forcedSplit = default; + SplitInfo forcedSplit = new(); foreach (JxlModularMultiplierInfo mmi in mulInfo) { @@ -379,7 +454,7 @@ public static unsafe void FindBestSplit(JxlTreeSamples treeSamples, float thresh forcedSplit.Value = treeSamples.QuantizeStaticProperty(axis, val); forcedSplit.Property = axis; forcedSplit.LeftCost = forcedSplit.RightCost = (baseBits / 2) - threshold; - forcedSplit.LeftPredictor = forcedSplit.RightPredictor = CollectionsMarshal.AsSpan(tree)[pos].Predictor; + forcedSplit.LeftPredictor = forcedSplit.RightPredictor = currentPredictor; best = ref forcedSplit; best.Position = begin; @@ -417,32 +492,13 @@ public static unsafe void FindBestSplit(JxlTreeSamples treeSamples, float thresh if (!Unsafe.AreSame(ref best, ref forcedSplit)) { - List countsIncrease = []; - List extraBitsIncrease = []; - - List leftCosts = []; - List rightCosts = []; - - int[] aboveCounts = ArrayPool.Shared.Rent(maxSymbols); - int[] belowCounts = ArrayPool.Shared.Rent(maxSymbols); - float changePredictionPenalty = 800.0f / (100.0f + threshold); for (int prop = 0; prop < numProp && baseBits > threshold; prop++) { - leftCosts.Clear(); - rightCosts.Clear(); - int propertySize = treeSamples.CountPropertyValues(prop); - if (extraBitsIncrease.Count < propertySize) - { - countsIncrease.Grow(0, propertySize * maxSymbols); - extraBitsIncrease.Grow(0, propertySize); - } - - int[] propertyValueUsedCount = ArrayPool.Shared.Rent(propertySize); - propertyValueUsedCount.AsSpan().Clear(); + propertyValueUsedCount[..propertySize].Clear(); int firstUsed = propertySize; int lastUsed = 0; @@ -454,7 +510,7 @@ public static unsafe void FindBestSplit(JxlTreeSamples treeSamples, float thresh int p = treeSamples.GetProperty(true, prop, i); propertyValueUsedCount[p]++; lastUsed = Math.Max(lastUsed, p); - firstUsed = Math.Max(firstUsed, p); + firstUsed = Math.Min(firstUsed, p); } } else @@ -466,30 +522,42 @@ public static unsafe void FindBestSplit(JxlTreeSamples treeSamples, float thresh int p = treeSamples.GetProperty(false, prop_idx, i); propertyValueUsedCount[p]++; lastUsed = Math.Max(lastUsed, p); - firstUsed = Math.Max(firstUsed, p); + firstUsed = Math.Min(firstUsed, p); } } - leftCosts.Grow(default, lastUsed - firstUsed); - rightCosts.Grow(default, lastUsed - firstUsed); + // CostInfo's constructor supplies maximum candidate costs. Default/cleared structs would incorrectly start at zero. + CostInfo initialCost = new(); + leftCosts[..(lastUsed - firstUsed)].Fill(initialCost); + rightCosts[..(lastUsed - firstUsed)].Fill(initialCost); for (int pred = 0; pred < numPred; pred++) { - List rtokens = treeSamples.GetResidualTokensForPrediction(pred); + ReadOnlySpan rtokens = treeSamples.GetResidualTokensForPrediction(pred); if (prop < treeSamples.NumberOfStaticProperties) { - CollectExtraBitsIncrease(true, treeSamples, rtokens, countsIncrease, extraBitsIncrease, begin, end, prop, maxSymbols); + CollectExtraBitsIncrease(true, in treeSamples, rtokens, countsIncrease, extraBitsIncrease, begin, end, prop, maxSymbols); } else { - CollectExtraBitsIncrease(false, treeSamples, rtokens, countsIncrease, extraBitsIncrease, begin, end, prop - treeSamples.NumberOfStaticProperties, maxSymbols); + CollectExtraBitsIncrease( + false, + in treeSamples, + rtokens, + countsIncrease, + extraBitsIncrease, + begin, + end, + prop - treeSamples.NumberOfStaticProperties, + maxSymbols); + } - counts.AsSpan().Slice(pred * maxSymbols, maxSymbols).CopyTo(aboveCounts); - belowCounts.AsSpan().Slice(0, maxSymbols).Clear(); + counts.Slice(pred * maxSymbols, maxSymbols).CopyTo(aboveCounts); + belowCounts[..maxSymbols].Clear(); - int extraBitsBelow = 0; + long extraBitsBelow = 0; for (int i = firstUsed; i < lastUsed; i++) { @@ -501,15 +569,10 @@ public static unsafe void FindBestSplit(JxlTreeSamples treeSamples, float thresh extraBitsBelow += extraBitsIncrease[i]; extraBitsIncrease[i] = 0; - for (int sym = 0; sym < maxSymbols; sym++) - { - aboveCounts[sym] -= countsIncrease[(i * maxSymbols) + sym]; - belowCounts[sym] += countsIncrease[(i * maxSymbols) + sym]; - countsIncrease[(i * maxSymbols) + sym] = 0; - } + TransferHistogramCounts(aboveCounts[..maxSymbols], belowCounts[..maxSymbols], countsIncrease.Slice(i * maxSymbols, maxSymbols)); - float rightCost = EstimateBits(aboveCounts.AsSpan(), maxSymbols) + totalExtraBits[pred] - extraBitsBelow; - float leftCost = EstimateBits(belowCounts.AsSpan(), maxSymbols) + extraBitsBelow; + float rightCost = EstimateBits(aboveCounts, maxSymbols) + totalExtraBits[pred] - extraBitsBelow; + float leftCost = EstimateBits(belowCounts, maxSymbols) + extraBitsBelow; if (extraBitsBelow > totalExtraBits[pred]) { @@ -518,8 +581,8 @@ public static unsafe void FindBestSplit(JxlTreeSamples treeSamples, float thresh float penalty = 0; - if (treeSamples.PredictorFromIndex(pred) != tree[pos].Predictor && - tree[pos].Predictor != JxlPredictor.Weighted) + if (treeSamples.PredictorFromIndex(pred) != currentPredictor && + currentPredictor != JxlPredictor.Weighted) { penalty = changePredictionPenalty; } @@ -552,6 +615,10 @@ public static unsafe void FindBestSplit(JxlTreeSamples treeSamples, float thresh leftCosts[i - firstUsed] = cost; } } + + // No split moves the highest property bucket below the boundary. Its increments still belong only to this predictor. + extraBitsIncrease[lastUsed] = 0; + countsIncrease.Slice(lastUsed * maxSymbols, maxSymbols).Clear(); } int split = begin; @@ -574,31 +641,23 @@ public static unsafe void FindBestSplit(JxlTreeSamples treeSamples, float thresh bool zeroEntropySide = rightCost == 0 || leftCost == 0; - // Using pointer specifically for this variable instead of - // ref, as we can't assign a ref variable conditionally. - SplitInfo* referenceToBest = - treeSamples.PropertyFromIndex(prop) < JxlPredictorFacts.StaticProperties - ? (zeroEntropySide ? &bestSplitStaticConstant : &bestSplitStatic) - : (usesWeightedPrediction ? &bestSplitNonStatic : &bestSplitNoWeightedPrediction); + // Update the selected candidate in its owning slot. Conditional references require no pointer or unsafe scope. + ref SplitInfo referenceToBest = ref (treeSamples.PropertyFromIndex(prop) < JxlPredictorFacts.StaticProperties + ? ref (zeroEntropySide ? ref bestSplitStaticConstant : ref bestSplitStatic) + : ref (usesWeightedPrediction ? ref bestSplitNonStatic : ref bestSplitNoWeightedPrediction)); - if (leftCost + rightCost < referenceToBest->Cost) + if (leftCost + rightCost < referenceToBest.Cost) { - referenceToBest->Property = prop; - referenceToBest->Value = i; - referenceToBest->Position = split; - referenceToBest->LeftCost = leftCost; - referenceToBest->LeftPredictor = leftCosts[i - firstUsed].Predictor; - referenceToBest->RightCost = rightCost; - referenceToBest->RightPredictor = rightCosts[i - firstUsed].Predictor; + referenceToBest.Property = prop; + referenceToBest.Value = i; + referenceToBest.Position = split; + referenceToBest.LeftCost = leftCost; + referenceToBest.LeftPredictor = leftCosts[i - firstUsed].Predictor; + referenceToBest.RightCost = rightCost; + referenceToBest.RightPredictor = rightCosts[i - firstUsed].Predictor; } } - extraBitsIncrease[lastUsed] = 0; - - for (int sym = 0; sym < maxSymbols; sym++) - { - countsIncrease[(lastUsed * maxSymbols) + sym] = 0; - } } if (bestSplitNoWeightedPrediction.Cost + threshold < baseBits && @@ -618,8 +677,6 @@ public static unsafe void FindBestSplit(JxlTreeSamples treeSamples, float thresh best = ref bestSplitStaticConstant; } - ArrayPool.Shared.Return(aboveCounts); - ArrayPool.Shared.Return(belowCounts); } if (best.Cost + threshold < baseBits) @@ -631,11 +688,11 @@ public static unsafe void FindBestSplit(JxlTreeSamples treeSamples, float thresh if (best.Property < treeSamples.NumberOfStaticProperties) { - SplitTreeSamples(true, treeSamples, begin, best.Position, end, best.Property, best.Value); + SplitTreeSamples(true, ref treeSamples, begin, best.Position, end, best.Property, best.Value); } else { - SplitTreeSamples(false, treeSamples, begin, best.Position, end, best.Property - treeSamples.NumberOfStaticProperties, best.Value); + SplitTreeSamples(false, ref treeSamples, begin, best.Position, end, best.Property - treeSamples.NumberOfStaticProperties, best.Value); } StaticPropertyRange newStaticPropertyRange = staticPropertyRange; @@ -676,11 +733,27 @@ public static unsafe void FindBestSplit(JxlTreeSamples treeSamples, float thresh nodes.Push(new NodeInfo(tree[pos].LeftChild, best.Position, end, newStaticPropertyRange)); } - ArrayPool.Shared.Return(counts); } } - public static void ComputeBestTree(JxlTreeSamples treeSamples, float threshold, List mulInfo, StaticPropertyRange staticPropertyRange, float fastDecodeMultiplier, Tree tree) + /// + /// Initializes the root leaf and searches its prepared training samples for useful splits. + /// + /// The configuration that owns tree-search storage. + /// The sample owner, partitioned in place as splits are accepted. + /// The minimum bit saving required to split a node. + /// The forced multiplier ranges. + /// The channel/group ranges of the complete sample set. + /// The allowed cost ratio for simpler predictors. + /// The destination nodes. + public static void ComputeBestTree( + Configuration configuration, + scoped ref JxlTreeSamples treeSamples, + float threshold, + List mulInfo, + StaticPropertyRange staticPropertyRange, + float fastDecodeMultiplier, + Tree tree) { JxlPropertyDecisionNode node = new() { @@ -694,38 +767,50 @@ public static void ComputeBestTree(JxlTreeSamples treeSamples, float threshold, if (treeSamples.NumberOfProperties >= 64) { - throw new InvalidOperationException($"Too many properties: {treeSamples.NumberOfPredictors}"); + throw new InvalidOperationException($"Too many properties: {treeSamples.NumberOfProperties}"); } - FindBestSplit(treeSamples, threshold, mulInfo, staticPropertyRange, fastDecodeMultiplier, tree); + FindBestSplit(configuration, ref treeSamples, threshold, mulInfo, staticPropertyRange, fastDecodeMultiplier, tree); } - public static List QuantizeHistogram(ReadOnlySpan histogram, int numChunks) + /// + /// Selects distinct cumulative-count thresholds for approximately equal-frequency property buckets. + /// + /// The nonnegative training counts. + /// The requested number of buckets. + /// Caller-owned storage for at most numChunks minus one thresholds. + /// The number of written thresholds. Empty or all-zero histograms produce none. + public static int QuantizeHistogram(ReadOnlySpan histogram, int numChunks, Span thresholds) { if (histogram.Length == 0 || numChunks == 0) { - return []; + return 0; } int sum = TensorPrimitives.Sum(histogram); - if (sum == 0) { - return []; + return 0; } - List thresholds = []; - long cumulativeSum = 0; long threshold = 1; + int written = 0; + // Integer cross-products retain exact bucket boundaries without rounding cumulative probabilities to float. + // One occupied histogram entry can cross several quantiles; retain that entry only once. for (int i = 0; i < histogram.Length; i++) { cumulativeSum += histogram[i]; if (cumulativeSum * numChunks >= threshold * sum) { - thresholds.Add(i); + // The final endpoint collects the complete histogram; native removes it after construction. + // Excluding it here keeps the caller's required capacity at numChunks-1, without a temporary slot or copy. + if (cumulativeSum != sum) + { + thresholds[written++] = i; + } while (cumulativeSum * numChunks >= threshold * sum) { @@ -734,46 +819,49 @@ public static List QuantizeHistogram(ReadOnlySpan histogram, int numCh } } - if (thresholds.Count > numChunks) - { - throw new InvalidOperationException("Too many thresholds"); - } - - thresholds.RemoveAt(thresholds.Count - 1); - - return thresholds; + return written; } - public static List QuantizeSamples(ReadOnlySpan samples, int numChunks) + /// + /// Builds property thresholds from a bounded histogram of the borrowed training samples. + /// + /// The signed training values. Their storage remains unchanged. + /// The requested number of property buckets. + /// Caller-owned storage for at most numChunks minus one thresholds. + /// The number of written thresholds. + public static int QuantizeSamples(ReadOnlySpan samples, int numChunks, Span thresholds) { const int range = 512; if (samples.Length == 0) { - return []; + return 0; } - int min = Math.Clamp(TensorPrimitives.Min(samples), -range, range); - - Span counts = stackalloc int[2048].Slice(0, (2 * range) + 1); + int minimum = Math.Clamp(TensorPrimitives.Min(samples), -range, range); + Span counts = stackalloc int[(2 * range) + 1]; + counts.Clear(); - for (int i = 0; i < samples.Length; i++) + // Native bounds only this training histogram to [-512,512]. It does not clip the source pixels or stored residuals. + // Shift by the bounded minimum so sparse signed values use the same cumulative-quantile rule as static properties. + foreach (int sample in samples) { - int s = samples[i]; - int sampleOffset = Math.Clamp(s, -range, range) - min; - counts[sampleOffset]++; + counts[Math.Clamp(sample, -range, range) - minimum]++; } - List thresholds = QuantizeHistogram(counts, numChunks); - - // For fast processing via TensorPrimitives - Span thresholdsSpan = CollectionsMarshal.AsSpan(thresholds); - TensorPrimitives.Add(thresholdsSpan, min, thresholdsSpan); - - return thresholds; + int written = QuantizeHistogram(counts, numChunks, thresholds); + TensorPrimitives.Add(thresholds[..written], minimum, thresholds[..written]); + return written; } - public static void QuantMap(Span from, Span to, int numPegs, int bias) + /// + /// Maps bounded property values to the bucket selected by strictly-greater tree decisions. + /// + /// The ordered split thresholds. + /// Caller-owned mapping storage. + /// The number of property values to map. + /// The value subtracted from each mapping index. + public static void QuantMap(ReadOnlySpan from, Span to, int numPegs, int bias) { int mapped = 0; @@ -788,34 +876,100 @@ public static void QuantMap(Span from, Span to, int numPegs, int bias) } } - public static void CollectPixelSamples(Configuration configuration, JxlModularImage image, JxlModularOptions options, int groupId, Span groupPixelCount, Span channelPixelCount, List pixelSamples, List diffSamples) + /// + /// Counts exact geometric training selections and prepares group/channel occurrence histograms without reading pixels. + /// + /// The borrowed modular channels. + /// The resolved sampling and maximum-channel settings. + /// The group index and deterministic RNG seed. + /// The initialized group histogram, updated once during preparation. + /// The initialized channel histogram, shared across groups. + /// The required capacity of each raw pixel/difference sample plane. + public static int CountPixelSamples( + JxlModularImage image, + JxlModularOptions options, + int groupId, + Span groupPixelCount, + Span channelPixelCount) { + // Native CollectPixelSamples treats zero repeats as no training samples and leaves both histograms unchanged. if (options.NumberOfRepeats == 0) { - throw new InvalidOperationException("No repeats"); + return 0; } - // Power of 2 size is unknown. - Span alignedGroupPixelCount = stackalloc int[groupPixelCount.Length <= groupId ? groupId + 1 : groupPixelCount.Length]; - groupPixelCount.CopyTo(alignedGroupPixelCount); + long totalPixels = 0; + for (int c = 0; c < image.Channels.Count; c++) + { + JxlModularChannel channel = image.Channels[c]; + if (c >= image.MetaChannels && (channel.Width > options.MaxChannelSize || channel.Height > options.MaxChannelSize)) + { + break; + } + + // Difference samples need a horizontal neighbor. These are the same eligible channels visited during collection. + if (channel.Width <= 1 || channel.Height == 0) + { + continue; + } - Span alignedChannelPixelCount = stackalloc int[channelPixelCount.Length < image.Channels.Count ? image.Channels.Count : channelPixelCount.Length]; - channelPixelCount.CopyTo(alignedGroupPixelCount); + // These are working-size estimates at the reservation boundary, not arithmetic in the sample loop. + // The plane's checked allocation already bounds one channel's area. Combining channels/groups still needs checked accounting. + int area = channel.Width * channel.Height; + groupPixelCount[groupId] = checked(groupPixelCount[groupId] + area); + channelPixelCount[c] = checked(channelPixelCount[c] + area); + totalPixels += area; + } Rng rng = new((ulong)groupId); + float fraction = MathF.Min(options.NumberOfRepeats * 0.1F, 0.99F); + // Make prepares 1/log(1-p); the distribution's constructor accepts that prepared value, not p itself. + Rng.GeometricDistribution distribution = Rng.GeometricDistribution.Make(fraction); + long position = rng.Geometric(in distribution); + long count = 0; + + // Flatten only the logical samples. Geometric gaps cross rows and eligible channel boundaries without touching image storage. + // Collection starts from the same seed and repeats these gaps, so its two output planes need no growth or copies. + while (position < totalPixels) + { + count++; + position += rng.Geometric(in distribution) + 1L; + } - float fraction = MathF.Min(options.NumberOfRepeats * 0.1f, 0.99f); - Rng.GeometricDistribution dist = new(fraction); - - int totalPixels = 0; - List channelIds = []; + return checked((int)count); + } - int i; - for (i = 0; i < image.Channels.Count; i++) + /// + /// Collects raw pixel and neighbor-difference training samples into the exact reserved output planes. + /// + /// The borrowed modular channels. + /// The resolved settings used during sample counting. + /// The group index and deterministic RNG seed. + /// Caller-owned storage sized by CountPixelSamples. + /// Caller-owned storage with the same capacity as pixelSamples. + /// The number of written samples in each plane. + public static int CollectPixelSamples( + JxlModularImage image, + JxlModularOptions options, + int groupId, + Span pixelSamples, + Span diffSamples) + { + if (options.NumberOfRepeats == 0) { - JxlModularChannel channel = image.Channels[i]; + return 0; + } + + Rng rng = new((ulong)groupId); + float fraction = MathF.Min(options.NumberOfRepeats * 0.1F, 0.99F); + Rng.GeometricDistribution distribution = Rng.GeometricDistribution.Make(fraction); + long position = rng.Geometric(in distribution); + int written = 0; - if (i >= image.MetaChannels && (channel.Width > options.MaxChannelSize || channel.Height > options.MaxChannelSize)) + for (int c = 0; c < image.Channels.Count; c++) + { + JxlModularChannel channel = image.Channels[c]; + if (c >= image.MetaChannels && (channel.Width > options.MaxChannelSize || channel.Height > options.MaxChannelSize)) { break; } @@ -825,56 +979,106 @@ public static void CollectPixelSamples(Configuration configuration, JxlModularIm continue; } - channelIds.Add(i); + long area = (long)channel.Width * channel.Height; + if (position < area) + { + // Borrow this selected channel once. Row padding does not affect logical positions or RNG advancement. + Span samples = channel.GetRow(0); + int stride = channel.Plane.PixelsPerRow; + int row = 0; + long rowStart = 0; - groupPixelCount[groupId] += channel.Width * channel.Height; - channelPixelCount[i] += channel.Width * channel.Height; - totalPixels += channel.Width * channel.Height; - } + while (position < area) + { + // Nearby samples stay in this row. Divide only when a geometric gap crosses a row boundary. + if (position >= rowStart + channel.Width) + { + row = (int)(position / channel.Width); + rowStart = (long)row * channel.Width; + } - if (channelIds.Count == 0) - { - throw new InvalidOperationException("No channel IDs"); + int x = (int)(position - rowStart); + int offset = (row * stride) + x; + int previous = x == 0 ? offset + 1 : offset - 1; + pixelSamples[written] = samples[offset]; + diffSamples[written] = samples[offset] - samples[previous]; + written++; + position += rng.Geometric(in distribution) + 1L; + } + } + + position -= area; } - pixelSamples.Grow(0, (int)(pixelSamples.Count + (fraction * totalPixels))); - diffSamples.Grow(0, (int)(diffSamples.Count + (fraction * totalPixels))); + return written; + } - i = 0; - int y = 0; - int x = 0; + /// + /// Moves one property's symbol counts from the above-threshold histogram to the below-threshold histogram. + /// + /// The remaining parent counts. + /// The accumulated lower-partition counts, with the same length as above. + /// The disjoint property bucket, zeroed after its counts move. + private static void TransferHistogramCounts(Span above, Span below, Span increase) + { + ref int upper = ref MemoryMarshal.GetReference(above); + ref int lower = ref MemoryMarshal.GetReference(below); + ref int bucket = ref MemoryMarshal.GetReference(increase); + int i = 0; - void Advance(uint amount) + // Each lane represents one symbol. Load its three counts before storing either partition or clearing the consumed bucket. + // Fusing the three updates avoids separate add, subtract and clear passes over every candidate bucket. + if (Vector512.IsHardwareAccelerated) { - x += (int)amount; + nuint count = (above.Length - i).Vector512Count(); - while (x >= image.Channels[channelIds[i]].Width) + for (nuint block = 0; block < count; block++, i += Vector512.Count) { - x -= image.Channels[channelIds[i]].Width; - y++; + Vector512 value = Vector512.LoadUnsafe(ref bucket, (uint)i); + Vector512 from = Vector512.LoadUnsafe(ref upper, (uint)i); + Vector512 to = Vector512.LoadUnsafe(ref lower, (uint)i); + (from - value).StoreUnsafe(ref upper, (uint)i); + (to + value).StoreUnsafe(ref lower, (uint)i); + Vector512.Zero.StoreUnsafe(ref bucket, (uint)i); + } + } - if (y == image.Channels[channelIds[i]].Height) - { - i++; - y = 0; + if (Vector256.IsHardwareAccelerated) + { + nuint count = (above.Length - i).Vector256Count(); - if (i >= channelIds.Count) - { - return; - } - } + for (nuint block = 0; block < count; block++, i += Vector256.Count) + { + Vector256 value = Vector256.LoadUnsafe(ref bucket, (uint)i); + Vector256 from = Vector256.LoadUnsafe(ref upper, (uint)i); + Vector256 to = Vector256.LoadUnsafe(ref lower, (uint)i); + (from - value).StoreUnsafe(ref upper, (uint)i); + (to + value).StoreUnsafe(ref lower, (uint)i); + Vector256.Zero.StoreUnsafe(ref bucket, (uint)i); } } - Advance(rng.Geometric(in dist)); - - for (; i < channelIds.Count; Advance(rng.Geometric(in dist) + 1)) + if (Vector128.IsHardwareAccelerated) { - Span row = image.Channels[channelIds[i]].GetRow(y); - pixelSamples.Add(row[x]); + nuint count = (above.Length - i).Vector128Count(); - int xp = x == 0 ? 1 : x - 1; - diffSamples.Add(row[x] - row[xp]); + for (nuint block = 0; block < count; block++, i += Vector128.Count) + { + Vector128 value = Vector128.LoadUnsafe(ref bucket, (uint)i); + Vector128 from = Vector128.LoadUnsafe(ref upper, (uint)i); + Vector128 to = Vector128.LoadUnsafe(ref lower, (uint)i); + (from - value).StoreUnsafe(ref upper, (uint)i); + (to + value).StoreUnsafe(ref lower, (uint)i); + Vector128.Zero.StoreUnsafe(ref bucket, (uint)i); + } + } + + for (; i < above.Length; i++) + { + int value = Unsafe.Add(ref bucket, i); + Unsafe.Add(ref upper, i) -= value; + Unsafe.Add(ref lower, i) += value; + Unsafe.Add(ref bucket, i) = 0; } } @@ -934,35 +1138,97 @@ public static void TokenizeTree(Tree tree, List tokens, Tree decoderTr } } - private readonly record struct NodeInfo(int Pos, int Begin, int End, StaticPropertyRange StaticPropertyRange); + /// + /// Stores one pending node's sample interval and static-property bounds. + /// + private readonly struct NodeInfo + { + /// Initializes a new instance of the struct. + /// The destination node index. + /// The first retained sample in the node. + /// The first retained sample after the node. + /// The node's half-open channel and group ranges. + public NodeInfo(int pos, int begin, int end, StaticPropertyRange staticPropertyRange) + { + this.Pos = pos; + this.Begin = begin; + this.End = end; + this.StaticPropertyRange = staticPropertyRange; + } + + /// Gets the destination node index. + public int Pos { get; } + + /// Gets the first retained sample in the node. + public int Begin { get; } + + /// Gets the first retained sample after the node. + public int End { get; } + + /// Gets the half-open channel and group ranges. + public StaticPropertyRange StaticPropertyRange { get; } + } - private struct SplitInfo() + /// + /// Stores one candidate split and the estimated costs of its lower and upper partitions. + /// + private struct SplitInfo { + /// Initializes a new instance of the struct with no selected split. + public SplitInfo() + { + this = default; + this.LeftCost = float.MaxValue; + this.RightCost = float.MaxValue; + } + + /// Gets or sets the selected property's index. public int Property { get; set; } + /// Gets or sets the quantized split value. public int Value { get; set; } + /// Gets or sets the first retained sample above the split. public int Position { get; set; } - public float LeftCost { get; set; } = float.MaxValue; + /// Gets or sets the estimated bit cost at or below the split value. + public float LeftCost { get; set; } - public float RightCost { get; set; } = float.MaxValue; + /// Gets or sets the estimated bit cost above the split value. + public float RightCost { get; set; } - public JxlPredictor LeftPredictor { get; set; } = JxlPredictor.Zero; + /// Gets or sets the predictor at or below the split value. + public JxlPredictor LeftPredictor { get; set; } - public JxlPredictor RightPredictor { get; set; } = JxlPredictor.Zero; + /// Gets or sets the predictor above the split value. + public JxlPredictor RightPredictor { get; set; } + /// Gets the combined estimated bit cost. public readonly float Cost => this.LeftCost + this.RightCost; } - private struct CostInfo() + /// + /// Stores the best predictor cost for one side of a possible split. + /// + private struct CostInfo { - public float Cost { get; set; } = float.MaxValue; + /// Initializes a new instance of the struct with no selected predictor. + public CostInfo() + { + this = default; + this.Cost = float.MaxValue; + } + + /// Gets or sets the estimated bit cost. + public float Cost { get; set; } + /// Gets or sets the predictor preference penalty. public float ExtraCost { get; set; } + /// Gets or sets the selected predictor. public JxlPredictor Predictor { get; set; } + /// Gets the estimated bit cost plus the predictor preference penalty. public readonly float TotalCost => this.Cost + this.ExtraCost; } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncodingEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncodingEncoder.cs index e8123f36c1..9647f096fb 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncodingEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncodingEncoder.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Runtime.CompilerServices; +using System.Buffers; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; @@ -9,8 +9,9 @@ using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using SixLabors.ImageSharp.Memory; using FlatTree = System.Collections.Generic.List; -using StaticPropertyRange = System.Runtime.CompilerServices.InlineArray2>; +using StaticPropertyRange = SixLabors.ImageSharp.InlineArray2>; using Tree = System.Collections.Generic.List; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding; @@ -77,9 +78,16 @@ public static InlineArray3 PredictorColor(JxlPredictor p) return result; } - public static Tree MakeFixedTree(int property, Span cutoffs, JxlPredictor pred, int numPixels, int bitDepth) + /// Builds a fixed predictor tree, with fewer splits for small channel populations. + /// The context property used for each split. + /// The ordered split thresholds. + /// The predictor used by the leaves. + /// The complete pixel population, including all selected channels. + /// The source sample precision. + /// The fixed prediction tree. + public static Tree MakeFixedTree(int property, Span cutoffs, JxlPredictor pred, long numPixels, int bitDepth) { - int logPx = JxlMath.CeilLog2Nonzero(numPixels); + int logPx = (int)JxlMath.CeilLog2Nonzero(numPixels); int minGap = 0; if (logPx < 14) @@ -140,9 +148,9 @@ public static void GatherTreeData( int groupId, JxlModularHeader wpHeader, JxlModularOptions options, - JxlTreeSamples treeSamples, + ref JxlTreeSamples treeSamples, JxlModularState wpState, - ref int totalPixels) + ref long totalPixels) { JxlModularChannel channel = image.Channels[channelIndex]; @@ -152,27 +160,8 @@ public static void GatherTreeData( Span properties = stackalloc int[JxlMaConstants.NumNonrefProperties + (JxlContextPrediction.ExtraPropertiesPerChannel * options.MaxProperties)]; - double pixelFraction = Math.Min(1.0f, options.NumberOfRepeats); - - if (pixelFraction > 0) - { - pixelFraction = Math.Max(pixelFraction, Math.Min(1.0, 1024.0 / (channel.Width * channel.Height))); - } - - ulong threshold = (ulong)((ulong.MaxValue >> 32) * pixelFraction); - - ulong s1 = 0x94D049BB133111EBuL; - ulong s2 = 0xBF58476D1CE4E5B9uL; - - bool UseSample() - { - ulong bits = s2 + s1; - s1 = s2; - s1 ^= s1 << 23; - s1 ^= s2 ^ (s1 >> 18) ^ (s2 >> 5); - s2 = s1; - return (bits >> 32) <= threshold; - } + ulong threshold = SamplingThreshold(channel.Width, channel.Height, options.NumberOfRepeats); + Rng samplingRng = new(0); Span channelSamples = channel.GetRow(0); int pixelsPerRow = channel.Plane.PixelsPerRow; @@ -181,18 +170,19 @@ bool UseSample() int referenceStride = references.Plane.PixelsPerRow; Span weightedStorage = wpState.Reset(wpHeader, channel.Width); - treeSamples.PrepareForSamples((int)(pixelFraction * channel.Height * channel.Width) + 64); bool haveMultiplePredictors = treeSamples.NumberOfPredictors != 1; // Pass spans into the local operation. Its captured state contains no borrowed spans. void ComputeSample( - Span p, + scoped Span p, int x, int y, - Span properties, - Span predictionStorage, - ReadOnlySpan referenceProperties, - ref int totalPixels) + scoped Span properties, + scoped Span predictionStorage, + scoped ReadOnlySpan referenceProperties, + ref long totalPixels, + ref JxlTreeSamples treeSamples, + ref Rng samplingRng) { Span pred = stackalloc int[JxlPredictorFacts.ModularPredictorsAlignment].Slice(0, JxlPredictorFacts.ModularPredictors); @@ -229,7 +219,7 @@ void ComputeSample( totalPixels++; - if (UseSample()) + if ((samplingRng.Next() >> 32) <= threshold) { treeSamples.AddSample(p[x], properties, pred); } @@ -249,7 +239,17 @@ void ComputeSample( { for (int x = 0; x < 2; x++) { - ComputeSample(p, x, y, properties, weightedStorage, referenceStorage.Slice(x * referenceStride, references.Width), ref totalPixels); + ComputeSample( + p, + x, + y, + properties, + weightedStorage, + referenceStorage.Slice(x * referenceStride, references.Width), + ref totalPixels, + ref treeSamples, + ref samplingRng); + } for (int x = 2; x < channel.Width - 2; x++) @@ -287,7 +287,7 @@ void ComputeSample( totalPixels++; - if (UseSample()) + if ((samplingRng.Next() >> 32) <= threshold) { treeSamples.AddSample(p[x], properties, pred); } @@ -297,20 +297,54 @@ void ComputeSample( for (int x = channel.Width - 2; x < channel.Width; x++) { - ComputeSample(p, x, y, properties, weightedStorage, referenceStorage.Slice(x * referenceStride, references.Width), ref totalPixels); + ComputeSample( + p, + x, + y, + properties, + weightedStorage, + referenceStorage.Slice(x * referenceStride, references.Width), + ref totalPixels, + ref treeSamples, + ref samplingRng); + } } else { for (int x = 0; x < channel.Width; x++) { - ComputeSample(p, x, y, properties, weightedStorage, referenceStorage.Slice(x * referenceStride, references.Width), ref totalPixels); + ComputeSample( + p, + x, + y, + properties, + weightedStorage, + referenceStorage.Slice(x * referenceStride, references.Width), + ref totalPixels, + ref treeSamples, + ref samplingRng); + } } } } - public static Tree LearnTree(JxlTreeSamples treeSamples, int totalPixels, JxlModularOptions options, List? info = null, StaticPropertyRange staticPropertyRange = default) + /// Learns the tree from the retained records, then releases collection-only deduplication storage. + /// The configuration used for tree-search scratch. + /// The prepared sample owner, partitioned in place during tree search. + /// The population used to normalize the sampling cost. + /// The tree-search options. + /// The forced multiplier splits. + /// The channel and group ranges. + /// The learned prediction tree. + public static Tree LearnTree( + Configuration configuration, + ref JxlTreeSamples treeSamples, + long totalPixels, + JxlModularOptions options, + List info, + StaticPropertyRange staticPropertyRange) { Tree tree = []; @@ -341,7 +375,15 @@ public static Tree LearnTree(JxlTreeSamples treeSamples, int totalPixels, JxlMod treeSamples.AllSamplesDone(); - JxlMaEncoder.ComputeBestTree(treeSamples, options.SplittingHeuristicsNodeThreshold * requiredCost, info, staticPropertyRange, options.FastDecodeMultiplier, tree); + JxlMaEncoder.ComputeBestTree( + configuration, + ref treeSamples, + options.SplittingHeuristicsNodeThreshold * requiredCost, + info, + staticPropertyRange, + options.FastDecodeMultiplier, + tree); + return tree; } @@ -616,7 +658,13 @@ public static void EncodeModularChannelMAANS( } } - public static Tree PredefinedTree(JxlTreeKind treeKind, int totalPixels, int bitDepth, int prevProp) + /// Selects a predefined modular prediction tree. + /// The requested predefined tree. + /// The complete pixel population, including all selected channels. + /// The source sample precision. + /// The number of reference properties available from earlier channels. + /// The selected prediction tree. + public static Tree PredefinedTree(JxlTreeKind treeKind, long totalPixels, int bitDepth, int prevProp) { switch (treeKind) { @@ -706,129 +754,216 @@ public static Tree PredefinedTree(JxlTreeKind treeKind, int totalPixels, int bit /// The matching sampling and prediction options. /// The first image index to include. /// The first image index to exclude. - /// The optional learned multiplier information. + /// The forced multiplier ranges, or an empty list when none are required. /// The learned prediction tree. public static Tree LearnTree( Configuration configuration, - JxlModularImage[] images, - Span options, + ReadOnlySpan images, + ReadOnlySpan options, int start, int stop, - List? multiplierInfo = null) + List multiplierInfo) { - multiplierInfo ??= []; - - JxlTreeSamples samples = new(); - samples.SetPredictor(options[start].Predictor, options[start].WpTreeMode); - samples.SetProperties(options[start].SplittingHeuristicsProperties, options[start].WpTreeMode); + JxlTreeSamples samples = new(configuration); + try + { + samples.SetPredictor(options[start].Predictor, options[start].WpTreeMode); + samples.SetProperties(CollectionsMarshal.AsSpan(options[start].SplittingHeuristicsProperties), options[start].WpTreeMode); - int maxC = 0; - int maximumWidth = 0; + int maxC = 0; + int maximumWidth = 0; - List pixelSamples = []; - List diffSamples = []; - List groupPixelCount = []; - List channelPixelCount = []; + int acceptedSamples = 0; + int previousPixels = -1; + ulong previousThreshold = 0; + int previousAccepted = 0; - for (int i = start; i < stop; i++) - { - maxC = Math.Max(images[i].Channels.Count, maxC); - - for (int c = 0; c < images[i].Channels.Count; c++) + for (int i = start; i < stop; i++) { - JxlModularChannel channel = images[i].Channels[c]; + maxC = Math.Max(images[i].Channels.Count, maxC); - if (c >= images[i].MetaChannels && (channel.Width > options[i].MaxChannelSize || channel.Height > options[i].MaxChannelSize)) + for (int c = 0; c < images[i].Channels.Count; c++) { - break; - } - - maximumWidth = Math.Max(maximumWidth, channel.Width); - } - - JxlMaEncoder.CollectPixelSamples(configuration, images[i], options[i], i, groupPixelCount, channelPixelCount, pixelSamples, diffSamples); - } + JxlModularChannel channel = images[i].Channels[c]; - // StaticPropRange range; - // range[0] = { { 0, max_c } }; - // range[1] = { { start, stop } }; - StaticPropertyRange range = default; - InlineArray2 currRange = default; - currRange[0] = 0; - currRange[1] = maxC; - range[0] = currRange; - currRange[0] = start; - currRange[1] = stop; - range[1] = currRange; + if (c >= images[i].MetaChannels && (channel.Width > options[i].MaxChannelSize || channel.Height > options[i].MaxChannelSize)) + { + break; + } - samples.PreQuantizeProperties(configuration, range, multiplierInfo, groupPixelCount, channelPixelCount, pixelSamples, diffSamples, options[start].MaxPropertyValues); + maximumWidth = Math.Max(maximumWidth, channel.Width); + if (images[i].Width == 0 || images[i].Height == 0 || channel.Width == 0 || channel.Height == 0) + { + continue; + } - int totalPixels = 0; + int pixels = checked(channel.Width * channel.Height); + ulong threshold = SamplingThreshold(channel.Width, channel.Height, options[i].NumberOfRepeats); + if (pixels != previousPixels || threshold != previousThreshold) + { + previousAccepted = CountAcceptedSamples(pixels, threshold); + previousPixels = pixels; + previousThreshold = threshold; + } - for (int i = 0; i < images[start].Channels.Count; i++) - { - if (i >= images[start].MetaChannels && - (images[start].Channels[i].Width > options[start].MaxChannelSize || - images[start].Channels[i].Height > options[start].MaxChannelSize)) - { - break; + acceptedSamples = checked(acceptedSamples + previousAccepted); + } } - totalPixels += images[start].Channels[i].Width * images[start].Channels[i].Height; - } - - totalPixels = Math.Max(totalPixels, 1); - - JxlModularHeader wpHeader = new(); - using JxlModularState weightedState = new(configuration, wpHeader, maximumWidth, 1); - - for (int i = start; i < stop; i++) - { - int numChannels = images[i].Channels.Count; - - if (images[i].Width == 0 || images[i].Height == 0 || numChannels < 1) + // Count the actual deterministic selections before allocating sample planes. Repeated pixel counts and thresholds + // reuse the preceding count. A fraction of one uses the channel pixel count without an RNG counting pass. + samples.PrepareForSamples(acceptedSamples); + using IMemoryOwner histogramOwner = configuration.MemoryAllocator.Allocate(checked(stop + maxC), AllocationOptions.Clean); + Span histogramStorage = histogramOwner.Memory.Span; + Span groupPixelCount = histogramStorage[..stop]; + Span channelPixelCount = histogramStorage[stop..]; + int rawSampleCount = 0; + for (int i = start; i < stop; i++) { - continue; + rawSampleCount = checked(rawSampleCount + JxlMaEncoder.CountPixelSamples(images[i], options[i], i, groupPixelCount, channelPixelCount)); } - if (images[i].IsInvalid) + // Pixel and residual training values have the same selection count. Keep both in one owned allocation. + using IMemoryOwner trainingOwner = configuration.MemoryAllocator.Allocate(checked(2 * rawSampleCount)); + Span trainingStorage = trainingOwner.Memory.Span; + Span pixelSamples = trainingStorage[..rawSampleCount]; + Span diffSamples = trainingStorage[rawSampleCount..]; + int sampleOffset = 0; + for (int i = start; i < stop; i++) { - throw new InvalidOperationException("Invalid image"); + sampleOffset += JxlMaEncoder.CollectPixelSamples(images[i], options[i], i, pixelSamples[sampleOffset..], diffSamples[sampleOffset..]); } - if (options[i].TreeKind != JxlTreeKind.Learn) + StaticPropertyRange range = default; + InlineArray2 currRange = default; + currRange[0] = 0; + currRange[1] = maxC; + range[0] = currRange; + currRange[0] = start; + currRange[1] = stop; + range[1] = currRange; + + samples.PreQuantizeProperties( + range, + CollectionsMarshal.AsSpan(multiplierInfo), + groupPixelCount, + channelPixelCount, + pixelSamples, + diffSamples, + options[start].MaxPropertyValues); + + // Individual planes fit Int32 storage. Their combined population can exceed Int32 when sampling is sparse. + long totalPixels = 0; + + for (int i = 0; i < images[start].Channels.Count; i++) { - throw new InvalidOperationException("Tree type must be Learn"); + if (i >= images[start].MetaChannels && + (images[start].Channels[i].Width > options[start].MaxChannelSize || + images[start].Channels[i].Height > options[start].MaxChannelSize)) + { + break; + } + + totalPixels += images[start].Channels[i].Width * images[start].Channels[i].Height; } - JxlBundle.Init(wpHeader); + totalPixels = Math.Max(totalPixels, 1); - if (JxlContextPrediction.IsWeightedPredictor(options[i].Predictor)) - { - JxlContextPrediction.SetPredictorMode(options[i].WpMode, wpHeader); - } + JxlModularHeader wpHeader = new(); + using JxlModularState weightedState = new(configuration, wpHeader, maximumWidth, 1); - for (int c = 0; c < numChannels; c++) + for (int i = start; i < stop; i++) { - if (c >= images[i].MetaChannels && - (images[i].Channels[c].Width > options[i].MaxChannelSize || - images[i].Channels[c].Height > options[i].MaxChannelSize)) + int numChannels = images[i].Channels.Count; + + if (images[i].Width == 0 || images[i].Height == 0 || numChannels < 1) { - break; + continue; } - if (images[i].Channels[c].Width == 0 || images[i].Channels[c].Height == 0) + if (images[i].IsInvalid) { - continue; // skip empty channels + throw new InvalidOperationException("Invalid image"); } - GatherTreeData(configuration, images[i], c, i, wpHeader, options[i], samples, weightedState, ref totalPixels); + if (options[i].TreeKind != JxlTreeKind.Learn) + { + throw new InvalidOperationException("Tree type must be Learn"); + } + + JxlBundle.Init(wpHeader); + + if (JxlContextPrediction.IsWeightedPredictor(options[i].Predictor)) + { + JxlContextPrediction.SetPredictorMode(options[i].WpMode, wpHeader); + } + + for (int c = 0; c < numChannels; c++) + { + if (c >= images[i].MetaChannels && + (images[i].Channels[c].Width > options[i].MaxChannelSize || + images[i].Channels[c].Height > options[i].MaxChannelSize)) + { + break; + } + + if (images[i].Channels[c].Width == 0 || images[i].Channels[c].Height == 0) + { + continue; // skip empty channels + } + + GatherTreeData(configuration, images[i], c, i, wpHeader, options[i], ref samples, weightedState, ref totalPixels); + } } + + Tree tree = LearnTree(configuration, ref samples, totalPixels, options[start], multiplierInfo, range); + + return tree; + } + finally + { + samples.Dispose(); + } + } + + /// Computes the native acceptance threshold shared by sample counting and collection. + /// The channel width. + /// The channel height. + /// The requested sampling fraction. + /// The inclusive threshold for the high 32 RNG bits. + private static ulong SamplingThreshold(int width, int height, float repeats) + { + double fraction = Math.Min(1F, repeats); + if (fraction > 0) + { + fraction = Math.Max(fraction, Math.Min(1D, 1024D / ((long)width * height))); } - Tree tree = LearnTree(samples, totalPixels, options[start], multiplierInfo, range); + return (ulong)(uint.MaxValue * fraction); + } - return tree; + /// Counts the exact accepted samples without reading or copying channel samples. + /// The number of channel pixels visited by collection. + /// The inclusive native acceptance threshold. + /// The capacity required before collecting this channel. + private static int CountAcceptedSamples(int pixelCount, ulong threshold) + { + if (threshold == uint.MaxValue) + { + return pixelCount; + } + + Rng rng = new(0); + int accepted = 0; + for (int i = 0; i < pixelCount; i++) + { + if ((rng.Next() >> 32) <= threshold) + { + accepted++; + } + } + + return accepted; } /// @@ -988,12 +1123,15 @@ public static void ModularGenericCompress(Configuration configuration, JxlModula if (modularOptions.TreeKind == JxlTreeKind.Learn) { - tree = LearnTree(configuration, image, options, 0, 1); + // Borrow the single image and its resolved options. Tree preparation reads these references only during this call. + ReadOnlySpan images = MemoryMarshal.CreateReadOnlySpan(ref image, 1); + ReadOnlySpan treeOptions = MemoryMarshal.CreateReadOnlySpan(ref modularOptions, 1); + tree = LearnTree(configuration, images, treeOptions, 0, 1, []); } else { // It's a predefined tree. - int totalPixels = 0; + long totalPixels = 0; for (int i = 0; i < numChannels; i++) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlPropertyDecisionNode.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlPropertyDecisionNode.cs index b0fda7efd9..83b1443d45 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlPropertyDecisionNode.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlPropertyDecisionNode.cs @@ -34,13 +34,21 @@ public JxlPropertyDecisionNode() public static JxlPropertyDecisionNode Leaf(JxlPredictor predictor, long offset = 0, int multiplier = 1) => new(-1, 0, 0, 0, predictor, offset, multiplier); - public static JxlPropertyDecisionNode Split(int p, int splitValue, int leftChild, int rightChild = -1) + /// + /// Creates a decision node that splits samples by a property value. + /// + /// The property tested by this node. + /// The threshold. Larger property values go to the left child; other values go to the right child. + /// The index of the left child. + /// The index of the right child, or minus one to use the next node after the left child. + /// The decision node. + public static JxlPropertyDecisionNode Split(int propertyIndex, int splitValue, int leftChild, int rightChild = -1) { if (rightChild == -1) { rightChild = leftChild + 1; } - return new(p, splitValue, leftChild, rightChild, JxlPredictor.Zero, 0, 1); + return new(propertyIndex, splitValue, leftChild, rightChild, JxlPredictor.Zero, 0, 1); } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlTreeSamples.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlTreeSamples.cs index 1d25122266..b39f69e65c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlTreeSamples.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlTreeSamples.cs @@ -1,884 +1,707 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; +using System.Numerics; using System.Numerics.Tensors; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; -using StaticPropertyRange = System.Runtime.CompilerServices.InlineArray2>; +using SixLabors.ImageSharp.Memory; +using StaticPropertyRange = SixLabors.ImageSharp.InlineArray2>; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding; /// -/// Contains all the necessary data needed to build a tree. +/// Owns the sampled residuals and quantized properties used to learn one modular prediction tree. /// -internal sealed class JxlTreeSamples +/// +/// Preparation allocates each numeric buffer once. Separate predictor and property planes retain contiguous tree-search reads. +/// The borrowed spans remain valid until disposal. Pass this owner by reference; copying it would copy ownership and logical counts. +/// +internal ref struct JxlTreeSamples { - /// - /// This deduplication table entry marks an unused entry. - /// private const int DeduplicationEntryUnused = -1; - private const int PropertyRange = 511; + private const int PropertyMapSize = (2 * PropertyRange) + 1; + + private readonly MemoryAllocator allocator; + private InlineArray16 predictors; + private IMemoryOwner? propertyIdsOwner; + private IMemoryOwner? quantizationOwner; + private IMemoryOwner? residualsOwner; + private IMemoryOwner? countsOwner; + private IMemoryOwner? propertiesOwner; + private IMemoryOwner? deduplicationOwner; + + // Each predictor/property occupies one capacity-sized plane. Only the first NumberOfDistinctSamples entries are samples. + private Span propertiesToUse; + private Span thresholdCounts; + private Span compactProperties; + private Span propertyMapping; + private Span residuals; + private Span sampleCounts; + private Span staticProperties; + private Span properties; + private Span deduplicationTable; + private int sampleCapacity; + private int thresholdCapacity; /// - /// Residual information: token and number of extra bits per predictor. - /// - private List> residuals = []; - - /// - /// Number of occurrences of each sample. - /// - private readonly List sampleCounts = []; - - /// - /// Quantized static property values. - /// - private InlineArray2> staticProperties; - - /// - /// Property values, quantized to at most 256 distinct values. - /// - private readonly List> properties = []; - - /// - /// Decompactification info for . - /// - private readonly List> compactProperties = []; - - /// - /// List of properties to use. - /// - private List propertiesToUse = []; - - /// - /// List of predictors to use. + /// Creates an empty owner. Select predictors and properties before preparing quantization and sample storage. /// - private List predictors = []; - - /// - /// Mapping property value -> quantized property value. - /// - private InlineArray2> staticPropertyMapping; - - private readonly List> propertyMapping = []; - - /// - /// Table for deduplication. - /// - private List deduplicationTable = []; + /// The configuration that owns all numeric storage. + public JxlTreeSamples(Configuration configuration) + { + this = default; + this.allocator = configuration.MemoryAllocator; + } - /// - /// Gets a value indicating whether there are any residual samples. - /// - public bool HasSamples => this.residuals.Count > 0 && this.residuals[0].Count > 0; + /// Gets whether at least one distinct sample was inserted. + public readonly bool HasSamples => this.NumberOfDistinctSamples != 0; - /// - /// Gets the total number of distinct samples. - /// - public int NumberOfDistinctSamples => this.sampleCounts.Count; + /// Gets the number of retained sample records. + public int NumberOfDistinctSamples { get; private set; } - /// - /// Gets the total number of samples. - /// + /// Gets the number of inserted samples, including merged duplicates. public int NumberOfSamples { get; private set; } - /// - /// Gets the number of quantized static property values. - /// + /// Gets the number of selected static properties. public int NumberOfStaticProperties { get; private set; } - /// - /// Gets the total number of predictors. - /// - public int NumberOfPredictors => this.predictors.Count; - - /// - /// Gets the total number of properties. - /// - public int NumberOfProperties => this.propertiesToUse.Count; - - /// - /// Returns a List of residual tokens for the specified kind of prediction. - /// - /// The type of prediction. - /// Residual tokens corresponding to the prediction specified by the parameter . - public List GetResidualTokensForPrediction(int prediction) => this.residuals[prediction]; - - /// - /// Returns a reference to the residual token. - /// - /// Kind of prediction. - /// Index of the residual token within that prediction. - /// The residual token for prediction indexed . - public ref JxlResidualToken GetResidualToken(int pred, int i) => ref CollectionsMarshal.AsSpan(this.residuals[pred])[i]; - - /// - /// Returns a token of the residual for prediction index . - /// - /// The kind of prediction. - /// Index of the residual token within that prediction. - /// For residual token whose prediction is and index is , returns its token coefficient. - public int GetToken(int prediction, int index) => this.residuals[prediction][index].Token; - - /// - /// Returns the number of occurrences for sample . - /// - /// The index of the sample. - /// Number of times appears. - public int GetCount(int i) => this.sampleCounts[i]; - - /// - /// Finds the index of the predictor . - /// - /// The predictor to find the index for. - /// Index of the predictor in the predictors storage. - /// Thrown when the predictor can't be found. - public int FindPredictorIndex(JxlPredictor predictor) + /// Gets the number of selected predictors. + public int NumberOfPredictors { get; private set; } + + /// Gets the number of selected properties. + public readonly int NumberOfProperties => this.propertiesToUse.Length; + + /// Returns the logical residual plane for one selected predictor. + /// The selected predictor index. + /// The retained residual tokens, without unused capacity. + public readonly ReadOnlySpan GetResidualTokensForPrediction(int prediction) + => this.residuals.Slice(prediction * this.sampleCapacity, this.NumberOfDistinctSamples); + + /// Returns one retained residual token. + /// The selected predictor index. + /// The retained sample index. + /// The token and extra-bit count. + public readonly ref JxlResidualToken GetResidualToken(int prediction, int sample) + => ref this.residuals[(prediction * this.sampleCapacity) + sample]; + + /// Returns the entropy symbol for one predictor and sample. + /// The selected predictor index. + /// The retained sample index. + /// The residual symbol. + public readonly int GetToken(int prediction, int sample) => this.residuals[(prediction * this.sampleCapacity) + sample].Token; + + /// Returns the occurrence count for one retained sample. + /// The retained sample index. + /// The count, saturated at 65535 for each retained record. + public readonly int GetCount(int sample) => this.sampleCounts[sample]; + + /// Finds a selected predictor. + /// The predictor to find. + /// Its index in the selected predictor set. + public readonly int FindPredictorIndex(JxlPredictor predictor) { - ReadOnlySpan span = CollectionsMarshal.AsSpan(this.predictors); - - int index = span.IndexOf(predictor); - - if (index < 0) + for (int i = 0; i < this.NumberOfPredictors; i++) { - // Should not happen. - throw new InvalidOperationException("Cannot find the index of the predictor"); + if (this.predictors[i] == predictor) + { + return i; + } } - return index; + throw new InvalidOperationException("The predictor was not selected."); } - /// - /// Finds the index of the property . - /// - /// The property to find the index for. - /// Index of the property in the properties storage. - /// Thrown when the property isn't valid. - public int FindPropertyIndex(int property) + /// Finds a selected property. + /// The original property index. + /// Its index in the selected property set. + public readonly int FindPropertyIndex(int property) { - ReadOnlySpan span = CollectionsMarshal.AsSpan(this.propertiesToUse); - - int index = span.IndexOf(property); - - if (index != this.propertiesToUse[^1]) - { - // Should not happen. - throw new InvalidOperationException("Invalid property"); - } - + int index = this.propertiesToUse.IndexOf(property); + DebugGuard.MustBeGreaterThanOrEqualTo(index, 0, nameof(property)); return index; } - /// - /// Returns the number of properties for a . - /// - /// Index of the property. - /// Number of property values for property with index . - public int CountPropertyValues(int propertyIndex) => this.compactProperties[propertyIndex].Count + 1; + /// Returns the number of quantization intervals for a selected property. + /// The selected property index. + /// The threshold count plus one. + public readonly int CountPropertyValues(int propertyIndex) => this.thresholdCounts[propertyIndex] + 1; - /// - /// Returns the value of a property. - /// - /// Prefer a static property? - /// The index of the properties table. - /// The index of the property. - /// - /// Property for index within table . - /// + /// Reads one quantized static or dynamic property. + /// Whether to read a static property plane. + /// The index within the static or dynamic property planes. + /// The retained sample index. + /// The quantization interval. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int GetProperty(bool useStaticProperty, int propertyIndex, int i) - { - if (useStaticProperty) - { - return this.staticProperties[propertyIndex][i]; - } - else - { - return this.properties[propertyIndex][i]; - } - } - - /// - /// Returns the dequantized property. - /// - /// Index of the target property. - /// Property quantizer. - /// The dequantized property. - /// Thrown when the quant is out of range. - public int UnquantizeProperty(int propertyIndex, int quant) + public readonly int GetProperty(bool useStaticProperty, int propertyIndex, int sampleIndex) + => useStaticProperty + ? this.staticProperties[(propertyIndex * this.sampleCapacity) + sampleIndex] + : this.properties[(propertyIndex * this.sampleCapacity) + sampleIndex]; + + /// Returns the original threshold for a quantized split. + /// The selected property index. + /// The threshold index. + /// The original property threshold. + public readonly int UnquantizeProperty(int propertyIndex, int quant) + => this.compactProperties[(propertyIndex * this.thresholdCapacity) + quant]; + + /// Returns the predictor at a selected index. + /// The selected predictor index. + /// The predictor. + public readonly JxlPredictor PredictorFromIndex(int index) => this.predictors[index]; + + /// Returns the original property at a selected index. + /// The selected property index. + /// The original property index. + public readonly int PropertyFromIndex(int index) => this.propertiesToUse[index]; + + /// Releases the deduplication table after collection. Sample planes remain available for tree search. + public void AllSamplesDone() { - List compactProperties = this.compactProperties[propertyIndex]; - - if (quant >= compactProperties.Count) - { - throw new InvalidOperationException("Quant is out of range"); - } - - return compactProperties[quant]; - } - - /// - /// Returns a predictor for index . - /// - /// The index of the predictor. - /// Predictor at index . - public JxlPredictor PredictorFromIndex(int index) - { - DebugGuard.MustBeLessThan(index, this.predictors.Count, nameof(index)); - - return this.predictors[index]; - } - - /// - /// Returns a property for index . - /// - /// The index of the property. - /// Property at index . - public int PropertyFromIndex(int index) - { - DebugGuard.MustBeLessThan(index, this.propertiesToUse.Count, nameof(index)); - - return this.propertiesToUse[index]; - } - - /// - /// Invoked after processing samples completed. - /// - public void AllSamplesDone() => this.deduplicationTable = []; - - /// - /// Returns the quantized property value of . - /// - /// Index of the property. - /// Value to quantize. - /// Quantized value. - public int QuantizeProperty(int property, int v) - { - DebugGuard.MustBeGreaterThanOrEqualTo(property, this.NumberOfStaticProperties, nameof(property)); - - v = Math.Clamp(v, -PropertyRange, PropertyRange) + PropertyRange; - - return this.propertyMapping[property][v]; - } - - /// - /// Returns the quantized static property value of . - /// - /// Index of the static property. - /// Value to quantize. - /// Quantized value. - public int QuantizeStaticProperty(int property, int v) - { - DebugGuard.MustBeGreaterThanOrEqualTo(property, this.NumberOfStaticProperties, nameof(property)); - - v = Math.Clamp(v, -PropertyRange, PropertyRange) + PropertyRange; - - return this.staticPropertyMapping[property][v]; + this.deduplicationTable = default; + this.deduplicationOwner?.Dispose(); + this.deduplicationOwner = null; } + /// Maps a dynamic property to its quantization interval. + /// The selected property index, including static properties. + /// The original property value. + /// The quantization interval. + public readonly int QuantizeProperty(int property, int value) + => this.propertyMapping[(property * PropertyMapSize) + Math.Clamp(value, -PropertyRange, PropertyRange) + PropertyRange]; + + /// Maps a static property to its quantization interval. + /// The selected static property index. + /// The original property value. + /// The quantization interval. + public readonly int QuantizeStaticProperty(int property, int value) + => this.propertyMapping[(property * PropertyMapSize) + Math.Clamp(value, -PropertyRange, PropertyRange) + PropertyRange]; + + /// Selects the predictors before sample storage is prepared. + /// The requested predictor or predictor search set. + /// The weighted-prediction selection mode. public void SetPredictor(JxlPredictor predictor, JxlTreeMode wpTreeMode) { + this.NumberOfPredictors = 0; if (wpTreeMode == JxlTreeMode.WpOnly) { - this.predictors = [JxlPredictor.Weighted]; - - // equivalent of residuals.resize(1) - if (this.residuals.Count > 1) - { - this.residuals = [this.residuals[0]]; - } - else if (this.residuals.Count < 1) - { - this.residuals = [[]]; - } - + this.predictors[this.NumberOfPredictors++] = JxlPredictor.Weighted; return; } if (wpTreeMode == JxlTreeMode.NoWp && predictor == JxlPredictor.Weighted) { - throw new InvalidOperationException("Invalid predictor settings"); + throw new InvalidOperationException("Weighted prediction is excluded by the tree mode."); } if (predictor == JxlPredictor.Variable) { for (int i = 0; i < JxlPredictorFacts.ModularPredictors; i++) { - this.predictors.Add((JxlPredictor)i); + this.predictors[this.NumberOfPredictors++] = (JxlPredictor)i; } - Span predictorsSpan = CollectionsMarshal.AsSpan(this.predictors); - RuntimeUtility.Swap(ref predictorsSpan[0], ref predictorsSpan[(int)JxlPredictor.Weighted]); - RuntimeUtility.Swap(ref predictorsSpan[1], ref predictorsSpan[(int)JxlPredictor.Gradient]); + RuntimeUtility.Swap(ref this.predictors[0], ref this.predictors[(int)JxlPredictor.Weighted]); + RuntimeUtility.Swap(ref this.predictors[1], ref this.predictors[(int)JxlPredictor.Gradient]); } else if (predictor == JxlPredictor.Best) { - this.predictors = [JxlPredictor.Weighted, JxlPredictor.Gradient]; + this.predictors[this.NumberOfPredictors++] = JxlPredictor.Weighted; + this.predictors[this.NumberOfPredictors++] = JxlPredictor.Gradient; } else { - this.predictors = [predictor]; + this.predictors[this.NumberOfPredictors++] = predictor; } if (wpTreeMode == JxlTreeMode.NoWp) { - // delete all weighted predictors - _ = this.predictors.RemoveAll(p => p == JxlPredictor.Weighted); - } - - this.residuals.Grow([], this.predictors.Count); - } - - public void SetProperties(List properties, JxlTreeMode wpTreeMode) - { - this.propertiesToUse = properties; - - if (wpTreeMode == JxlTreeMode.WpOnly) - { - this.propertiesToUse = [JxlMaConstants.WpProp]; - } - else if (wpTreeMode == JxlTreeMode.GradientOnly) - { - this.propertiesToUse = [JxlMaConstants.GradientProp]; - } - else if (wpTreeMode == JxlTreeMode.NoWp) - { - // delete all weighted properties, tree mode - // says "no weighted predictors" - _ = this.propertiesToUse.RemoveAll(x => x == JxlMaConstants.WpProp); - } - - if (this.propertiesToUse.Count == 0) - { - // could happen in default tree mode or when properties parameter - // is empty - throw new InvalidOperationException("Invalid property set configuration"); - } - - this.NumberOfStaticProperties = 0; - - for (int i = 0; i < this.propertiesToUse.Count; ++i) - { - int prop = this.propertiesToUse[i]; - - if (prop < JxlPredictorFacts.StaticProperties) + int count = 0; + for (int i = 0; i < this.NumberOfPredictors; i++) { - if (i != prop) + if (this.predictors[i] != JxlPredictor.Weighted) { - throw new InvalidOperationException("Index is not equal to the property"); + this.predictors[count++] = this.predictors[i]; } - - this.NumberOfStaticProperties++; } - } - this.properties.Resize(this.propertiesToUse.Count - this.NumberOfStaticProperties); + this.NumberOfPredictors = count; + } } - public void InitializeTable(int logSize) + /// Copies the selected property IDs. Tree modes do not modify the caller's options. + /// The original property IDs. + /// The weighted-prediction selection mode. + public void SetProperties(scoped ReadOnlySpan selectedProperties, JxlTreeMode wpTreeMode) { - int size = 1 << logSize; - - if (this.deduplicationTable.Count == size) + int capacity = wpTreeMode is JxlTreeMode.WpOnly or JxlTreeMode.GradientOnly ? 1 : selectedProperties.Length; + this.propertyIdsOwner = this.allocator.Allocate(capacity); + Span destination = this.propertyIdsOwner.Memory.Span; + int count = 0; + if (wpTreeMode is JxlTreeMode.WpOnly or JxlTreeMode.GradientOnly) { - return; + destination[count++] = wpTreeMode == JxlTreeMode.WpOnly ? JxlMaConstants.WpProp : JxlMaConstants.GradientProp; } - - this.deduplicationTable.Resize(size, DeduplicationEntryUnused); - - for (int i = 0; i < this.NumberOfDistinctSamples; i++) + else { - if (this.sampleCounts[i] != ushort.MaxValue) + foreach (int property in selectedProperties) { - this.AddToTable(i); + if (wpTreeMode != JxlTreeMode.NoWp || property != JxlMaConstants.WpProp) + { + destination[count++] = property; + } } } - } - - public void AddToTableAndMerge(int a) - { - int pos1 = Hash1(a); - int pos2 = Hash2(a); - if (this.deduplicationTable[pos1] != DeduplicationEntryUnused && this.IsSameSample(a, this.deduplicationTable[pos1])) + if (count == 0) { - if (this.sampleCounts[a] != 1) - { - throw new InvalidOperationException("Sample count must be 1"); - } - - this.sampleCounts[this.deduplicationTable[pos1]]++; - - if (this.sampleCounts[this.deduplicationTable[pos1]] == ushort.MaxValue) - { - this.deduplicationTable[pos1] = DeduplicationEntryUnused; - } - - return; + throw new InvalidOperationException("The selected property set is empty."); } - if (this.deduplicationTable[pos2] != DeduplicationEntryUnused && this.IsSameSample(a, this.deduplicationTable[pos2])) + this.propertiesToUse = destination[..count]; + this.NumberOfStaticProperties = 0; + for (int i = 0; i < count; i++) { - if (this.sampleCounts[a] != 1) + if (destination[i] < JxlPredictorFacts.StaticProperties) { - throw new InvalidOperationException("Sample count must be 1"); - } - - this.sampleCounts[this.deduplicationTable[pos2]]++; - - if (this.sampleCounts[this.deduplicationTable[pos2]] == ushort.MaxValue) - { - this.deduplicationTable[pos2] = DeduplicationEntryUnused; + DebugGuard.IsTrue(i == destination[i], "Static properties must form the initial property prefix."); + this.NumberOfStaticProperties++; } - - return; } - - this.AddToTable(a); } - public void AddToTable(int a) + /// Allocates sample planes and the deduplication table for the exact accepted sample count. + /// The number of samples selected by the prepared RNG sequence. + public void PrepareForSamples(int sampleCount) { - int pos1 = Hash1(a); - int pos2 = Hash2(a); - - if (this.deduplicationTable[pos1] == DeduplicationEntryUnused) - { - this.deduplicationTable[pos1] = a; - } - else if (this.deduplicationTable[pos2] == DeduplicationEntryUnused) - { - this.deduplicationTable[pos2] = a; - } + this.sampleCapacity = sampleCount; + this.residualsOwner = this.allocator.Allocate(checked(sampleCount * this.NumberOfPredictors)); + this.residuals = this.residualsOwner.Memory.Span; + this.countsOwner = this.allocator.Allocate(checked(sampleCount * (1 + this.NumberOfStaticProperties))); + Span counts = this.countsOwner.Memory.Span; + this.sampleCounts = counts[..sampleCount]; + this.staticProperties = counts[sampleCount..]; + this.propertiesOwner = this.allocator.Allocate(checked(sampleCount * (this.NumberOfProperties - this.NumberOfStaticProperties))); + this.properties = this.propertiesOwner.Memory.Span; + + // Native uses two candidate buckets and removes saturated records. Keep the same power-of-two load bound. + // One accepted sample can add at most one distinct record; no insertion can outgrow these prepared planes. + uint buckets = BitOperations.RoundUpToPowerOf2((uint)Math.Max(1L, ((long)sampleCount * 3) / 2)); + this.deduplicationOwner = this.allocator.Allocate(checked((int)buckets)); + this.deduplicationTable = this.deduplicationOwner.Memory.Span; + this.deduplicationTable.Fill(DeduplicationEntryUnused); } - public void PrepareForSamples(int extraNumSamples) + /// Inserts one selected sample, or merges its occurrence into a matching record. + /// The source sample value. + /// The original static and dynamic context properties. + /// Predictions indexed by the modular predictor enum. + public void AddSample(int pixel, scoped ReadOnlySpan sampleProperties, scoped ReadOnlySpan predictions) { - foreach (List residual in this.residuals) + int sample = this.NumberOfDistinctSamples; + JxlAnsHybridUIntConfiguration coding = new(4, 1, 2); + for (int i = 0; i < this.NumberOfPredictors; i++) { - residual.Grow(default, residual.Count + extraNumSamples); + int residual = unchecked(pixel - predictions[(int)this.predictors[i]]); + coding.Encode(JxlPackSigned.PackUnsigned(residual), out uint token, out uint bitCount, out _); + this.residuals[(i * this.sampleCapacity) + sample] = new((int)token, (int)bitCount); } for (int i = 0; i < this.NumberOfStaticProperties; i++) { - this.staticProperties[i].Grow(0, this.staticProperties[i].Count + extraNumSamples); + this.staticProperties[(i * this.sampleCapacity) + sample] = this.QuantizeStaticProperty(i, sampleProperties[i]); } - foreach (List prop in this.properties) + for (int i = this.NumberOfStaticProperties; i < this.NumberOfProperties; i++) { - prop.Grow((byte)0, prop.Count + extraNumSamples); + int property = this.QuantizeProperty(i, sampleProperties[this.propertiesToUse[i]]); + this.properties[((i - this.NumberOfStaticProperties) * this.sampleCapacity) + sample] = (byte)property; } - int totalNumSamples = extraNumSamples + this.sampleCounts.Count; - int nextSize = JxlMath.CeilLog2Nonzero(totalNumSamples * 3 / 2); - - this.InitializeTable(nextSize); - } - - public int Hash1(int a) - { - const ulong constant = 0x1e35a7bd; - - ulong h = constant; - - foreach (List r in this.residuals) - { - h = (h * constant) + (ulong)r[a].Token; - h = (h * constant) + (ulong)r[a].NumberOfBits; - } - - for (int i = 0; i < this.NumberOfStaticProperties; i++) - { - h = (h * constant) + (ulong)this.staticProperties[i][a]; - } - - foreach (List property in this.properties) + this.sampleCounts[sample] = 1; + this.NumberOfSamples++; + if (!this.AddToTableAndMerge(sample)) { - h = (h * constant) + property[a]; + this.NumberOfDistinctSamples++; } - - return (int)((h >> 16) & (ulong)(this.deduplicationTable.Count - 1)); } - public int Hash2(int a) + /// Probes the native two-bucket table and commits a new record only if neither bucket matches. + /// The tentative record at the end of the logical sample planes. + /// Whether a previous record absorbed the sample. + private bool AddToTableAndMerge(int sample) { - const ulong constant = 0x1e35a7bd1e35a7bd; - - ulong h = constant; - - for (int i = 0; i < this.NumberOfStaticProperties; i++) + int first = this.Hash1(sample); + int second = this.Hash2(sample); + int match = this.deduplicationTable[first]; + int bucket = first; + if (match == DeduplicationEntryUnused || !this.IsSameSample(sample, match)) { - h = (h * constant) ^ (ulong)this.staticProperties[i][a]; - } + match = this.deduplicationTable[second]; + bucket = second; + if (match == DeduplicationEntryUnused || !this.IsSameSample(sample, match)) + { + if (this.deduplicationTable[first] == DeduplicationEntryUnused) + { + this.deduplicationTable[first] = sample; + } + else if (this.deduplicationTable[second] == DeduplicationEntryUnused) + { + this.deduplicationTable[second] = sample; + } - foreach (List property in this.properties) - { - h = (h * constant) ^ property[a]; + return false; + } } - foreach (List r in this.residuals) + if (++this.sampleCounts[match] == ushort.MaxValue) { - h = (h * constant) ^ (ulong)r[a].Token; - h = (h * constant) ^ (ulong)r[a].NumberOfBits; + this.deduplicationTable[bucket] = DeduplicationEntryUnused; } - return (int)((h >> 16) & (ulong)(this.deduplicationTable.Count - 1)); + return true; } - public bool IsSameSample(int a, int b) + /// Computes the first native sample hash over residuals, static properties, then dynamic properties. + /// The retained or tentative record. + /// The first candidate bucket. + private readonly int Hash1(int sample) { - foreach (List r in this.residuals) + const ulong multiplier = 0x1e35a7bd; + ulong hash = multiplier; + unchecked { - if (r[a].Token != r[b].Token) - { - return false; - } - - if (r[a].NumberOfBits != r[b].NumberOfBits) + for (int i = 0; i < this.NumberOfPredictors; i++) { - return false; + JxlResidualToken residual = this.residuals[(i * this.sampleCapacity) + sample]; + hash = (hash * multiplier) + (ulong)residual.Token; + hash = (hash * multiplier) + (ulong)residual.NumberOfBits; } - } - for (int i = 0; i < this.NumberOfStaticProperties; ++i) - { - if (this.staticProperties[i][a] != this.staticProperties[i][b]) + for (int i = 0; i < this.NumberOfStaticProperties; i++) { - return false; + hash = (hash * multiplier) + (ulong)this.staticProperties[(i * this.sampleCapacity) + sample]; } - } - foreach (List p in this.properties) - { - if (p[a] != p[b]) + for (int i = 0; i < this.NumberOfProperties - this.NumberOfStaticProperties; i++) { - return false; + hash = (hash * multiplier) + this.properties[(i * this.sampleCapacity) + sample]; } } - return false; + return (int)((hash >> 16) & (ulong)(this.deduplicationTable.Length - 1)); } - public void AddSample(int pixel, Span properties, Span predictions) + /// Computes the second native hash with properties first and XOR instead of addition. + /// The retained or tentative record. + /// The second candidate bucket. + private readonly int Hash2(int sample) { - for (int i = 0; i < this.predictors.Count; i++) + const ulong multiplier = 0x1e35a7bd1e35a7bd; + ulong hash = multiplier; + unchecked { - int v = pixel - predictions[(int)this.predictors[i]]; - new JxlAnsHybridUIntConfiguration(4, 1, 2).Encode(JxlPackSigned.PackUnsigned(v), out uint tok, out uint nbits, out uint bits); - - if (tok >= 256) + for (int i = 0; i < this.NumberOfStaticProperties; i++) { - throw new InvalidOperationException("Token is too large"); + hash = (hash * multiplier) ^ (ulong)this.staticProperties[(i * this.sampleCapacity) + sample]; } - if (nbits >= 256) + for (int i = 0; i < this.NumberOfProperties - this.NumberOfStaticProperties; i++) { - throw new InvalidOperationException("Number of bits is too large"); + hash = (hash * multiplier) ^ this.properties[(i * this.sampleCapacity) + sample]; } - JxlResidualToken token = new((int)tok, (int)nbits); - this.residuals[i].Add(token); - } - - for (int i = 0; i < this.NumberOfStaticProperties; ++i) - { - this.staticProperties[i].Add(this.QuantizeStaticProperty(i, properties[i])); - } - - for (int i = this.NumberOfStaticProperties; i < this.propertiesToUse.Count; i++) - { - this.properties[i - this.NumberOfStaticProperties].Add(unchecked((byte)this.QuantizeProperty(i, properties[this.propertiesToUse[i]]))); + for (int i = 0; i < this.NumberOfPredictors; i++) + { + JxlResidualToken residual = this.residuals[(i * this.sampleCapacity) + sample]; + hash = (hash * multiplier) ^ (ulong)residual.Token; + hash = (hash * multiplier) ^ (ulong)residual.NumberOfBits; + } } - this.sampleCounts.Add(1); - this.NumberOfSamples++; - - this.AddToTableAndMerge(this.sampleCounts.Count - 1); + return (int)((hash >> 16) & (ulong)(this.deduplicationTable.Length - 1)); + } - foreach (List residual in this.residuals) + /// Compares every selected residual and quantized property without allocating a sample key. + /// The first record. + /// The second record. + /// Whether all fields match. + private readonly bool IsSameSample(int first, int second) + { + for (int i = 0; i < this.NumberOfPredictors; i++) { - // remove last item from List - residual.RemoveAt(residual.Count - 1); + int offset = i * this.sampleCapacity; + JxlResidualToken a = this.residuals[offset + first]; + JxlResidualToken b = this.residuals[offset + second]; + if (a.Token != b.Token || a.NumberOfBits != b.NumberOfBits) + { + return false; + } } for (int i = 0; i < this.NumberOfStaticProperties; i++) { - // ditto - this.staticProperties[i].RemoveAt(this.staticProperties[i].Count - 1); + int offset = i * this.sampleCapacity; + if (this.staticProperties[offset + first] != this.staticProperties[offset + second]) + { + return false; + } } - foreach (List property in this.properties) + for (int i = 0; i < this.NumberOfProperties - this.NumberOfStaticProperties; i++) { - // ditto - property.RemoveAt(property.Count - 1); + int offset = i * this.sampleCapacity; + if (this.properties[offset + first] != this.properties[offset + second]) + { + return false; + } } - // ditto - this.sampleCounts.RemoveAt(this.sampleCounts.Count - 1); + return true; } - public void Swap(int a, int b) + /// Swaps two retained records across all sample planes during tree partitioning. + /// The first retained record. + /// The second retained record. + public void Swap(int first, int second) { - if (a == b) - { - return; - } - - foreach (List r in this.residuals) + for (int i = 0; i < this.NumberOfPredictors; i++) { - // Get a Span for this List so we can get a ref to its items - // for use in RuntimeUtility.Swap (tuple-swap is slightly slower) - Span sp = CollectionsMarshal.AsSpan(r); - - RuntimeUtility.Swap(ref sp[a], ref sp[b]); + int offset = i * this.sampleCapacity; + RuntimeUtility.Swap(ref this.residuals[offset + first], ref this.residuals[offset + second]); } for (int i = 0; i < this.NumberOfStaticProperties; i++) { - // Ditto - Span sp = CollectionsMarshal.AsSpan(this.staticProperties[i]); - - RuntimeUtility.Swap(ref sp[a], ref sp[b]); + int offset = i * this.sampleCapacity; + RuntimeUtility.Swap(ref this.staticProperties[offset + first], ref this.staticProperties[offset + second]); } - foreach (List p in this.properties) + for (int i = 0; i < this.NumberOfProperties - this.NumberOfStaticProperties; i++) { - // Ditto - Span sp = CollectionsMarshal.AsSpan(p); - - RuntimeUtility.Swap(ref sp[a], ref sp[b]); + int offset = i * this.sampleCapacity; + RuntimeUtility.Swap(ref this.properties[offset + first], ref this.properties[offset + second]); } - // Ditto - Span sampleCounts = CollectionsMarshal.AsSpan(this.sampleCounts); - - RuntimeUtility.Swap(ref sampleCounts[a], ref sampleCounts[b]); + RuntimeUtility.Swap(ref this.sampleCounts[first], ref this.sampleCounts[second]); } + /// Builds thresholds and complete lookup maps before any sample is inserted. + /// The channel and group ranges represented by this tree. + /// Forced multiplier splits. + /// The number of eligible pixels in each group. + /// The number of eligible pixels in each channel. + /// Owned preparation samples. Absolute thresholds can replace these values with their magnitudes. + /// Owned residual preparation samples, with the same destructive absolute-value rule. + /// The requested maximum number of quantization intervals. public void PreQuantizeProperties( - Configuration configuration, StaticPropertyRange range, - List multiplierInfo, - List groupPixelCount, - List channelPixelCount, - List pixelSamples, - List diffSamples, + scoped ReadOnlySpan multiplierInfo, + scoped ReadOnlySpan groupPixelCount, + scoped ReadOnlySpan channelPixelCount, + scoped Span pixelSamples, + scoped Span diffSamples, int maxPropertyValues) { - List groupMultiplierThresholds = []; - List channelMultiplierThresholds = []; + // Forced static splits can exceed the ordinary quantile count. Weighted thresholds use at most 55 fixed entries. + this.thresholdCapacity = Math.Max(55, Math.Max(maxPropertyValues - 1, checked(2 * multiplierInfo.Length))); + int thresholdLength = checked(this.NumberOfProperties * this.thresholdCapacity); + int mapLength = checked(this.NumberOfProperties * PropertyMapSize); + this.quantizationOwner = this.allocator.Allocate(checked(this.NumberOfProperties + thresholdLength + mapLength)); + Span storage = this.quantizationOwner.Memory.Span; + this.thresholdCounts = storage[..this.NumberOfProperties]; + this.compactProperties = storage.Slice(this.NumberOfProperties, thresholdLength); + this.propertyMapping = storage[(this.NumberOfProperties + thresholdLength)..]; - foreach (JxlModularMultiplierInfo v in multiplierInfo) + // Four cached distributions avoid rescanning pixel/residual samples for each property that uses the same thresholds. + // Two additional rows retain sorted, unique forced channel/group thresholds. This scratch expires before collection. + using IMemoryOwner scratchOwner = this.allocator.Allocate(checked(6 * this.thresholdCapacity)); + Span scratch = scratchOwner.Memory.Span; + Span channelThresholds = scratch.Slice(0, this.thresholdCapacity); + Span groupThresholds = scratch.Slice(this.thresholdCapacity, this.thresholdCapacity); + int channelCount = 0; + int groupCount = 0; + foreach (JxlModularMultiplierInfo multiplier in multiplierInfo) { - if (v.Range[0][0] != range[0][0]) + if (multiplier.Range[0][0] != range[0][0]) { - channelMultiplierThresholds.Add(v.Range[0][0] - 1); + channelThresholds[channelCount++] = checked((int)multiplier.Range[0][0]) - 1; } - if (v.Range[0][1] != range[0][1]) + if (multiplier.Range[0][1] != range[0][1]) { - channelMultiplierThresholds.Add(v.Range[0][1] - 1); + channelThresholds[channelCount++] = checked((int)multiplier.Range[0][1]) - 1; } - if (v.Range[1][0] != range[1][0]) + if (multiplier.Range[1][0] != range[1][0]) { - groupMultiplierThresholds.Add(v.Range[1][0] - 1); + groupThresholds[groupCount++] = checked((int)multiplier.Range[1][0]) - 1; } - if (v.Range[1][1] != range[1][1]) + if (multiplier.Range[1][1] != range[1][1]) { - groupMultiplierThresholds.Add(v.Range[1][1] - 1); + groupThresholds[groupCount++] = checked((int)multiplier.Range[1][1]) - 1; } } - channelMultiplierThresholds.Sort(); - channelMultiplierThresholds.Resize(0, channelMultiplierThresholds.Distinct().Count()); - groupMultiplierThresholds.Sort(); - groupMultiplierThresholds.Resize(0, groupMultiplierThresholds.Distinct().Count()); - - this.compactProperties.Resize([], this.propertiesToUse.Count); - - List QuantizeChannel() + // Each forced boundary appears once, even when adjacent multiplier ranges share it. + channelThresholds = channelThresholds[..channelCount]; + groupThresholds = groupThresholds[..groupCount]; + channelThresholds.Sort(); + groupThresholds.Sort(); + for (int kind = 0; kind < 2; kind++) { - if (channelMultiplierThresholds.Count > 0) + Span thresholds = kind == 0 ? channelThresholds : groupThresholds; + int unique = 0; + foreach (int threshold in thresholds) { - return channelMultiplierThresholds; + if (unique == 0 || thresholds[unique - 1] != threshold) + { + thresholds[unique++] = threshold; + } } - return JxlMaEncoder.QuantizeHistogram( - CollectionsMarshal.AsSpan(groupPixelCount), - maxPropertyValues); - } - - List QuantizeGroupId() - { - if (groupMultiplierThresholds.Count > 0) + if (kind == 0) { - return groupMultiplierThresholds; + channelThresholds = thresholds[..unique]; } - - return JxlMaEncoder.QuantizeHistogram( - CollectionsMarshal.AsSpan(groupPixelCount), - maxPropertyValues); - } - - List QuantizeCoordinate() - { - List quantized = new(maxPropertyValues - 1); - - for (int i = 0; i + 1 < maxPropertyValues; i++) + else { - quantized[i] = ((i + 1) * 256 / maxPropertyValues) - 1; + groupThresholds = thresholds[..unique]; } - - return quantized; } - List absPixelThresholds = []; - List pixelThresholds = []; - - List QuantizePixelProperty() + InlineArray4 cachedCounts = default; + InlineArray4 required = default; + foreach (int property in this.propertiesToUse) { - if (pixelThresholds.Count == 0) + if (property > 3 && property != JxlMaConstants.WpProp) { - pixelThresholds = JxlMaEncoder.QuantizeSamples( - CollectionsMarshal.AsSpan(pixelSamples), - maxPropertyValues); + required[DistributionIndex(property)] = true; } - - return pixelThresholds; } - List QuantizeAbsolutePixelProperty() + // Compute ordinary thresholds before converting the same owned input to magnitudes, as native enc_ma.cc requires. + for (int kind = 0; kind < 2; kind++) { - if (absPixelThresholds.Count == 0) + Span samples = kind == 0 ? pixelSamples : diffSamples; + int normal = 2 * kind; + if (required[normal] || required[normal + 1]) { - _ = QuantizePixelProperty(); // compute the non-abs thresholds - - Span pixelSamplesSpan = CollectionsMarshal.AsSpan(pixelSamples); - TensorPrimitives.Abs(pixelSamplesSpan, pixelSamplesSpan); - - absPixelThresholds = JxlMaEncoder.QuantizeSamples(pixelSamplesSpan, maxPropertyValues); + Span thresholds = scratch.Slice((normal + 2) * this.thresholdCapacity, this.thresholdCapacity); + cachedCounts[normal] = JxlMaEncoder.QuantizeSamples(samples, maxPropertyValues, thresholds); } - return absPixelThresholds; - } - - List absoluteDiffThresholds = []; - List diffThresholds = []; - - List QuantizeDiffProperty() - { - if (diffThresholds.Count == 0) + if (required[normal + 1]) { - diffThresholds = JxlMaEncoder.QuantizeSamples( - CollectionsMarshal.AsSpan(diffSamples), - maxPropertyValues); + TensorPrimitives.Abs(samples, samples); + Span thresholds = scratch.Slice((normal + 3) * this.thresholdCapacity, this.thresholdCapacity); + cachedCounts[normal + 1] = JxlMaEncoder.QuantizeSamples(samples, maxPropertyValues, thresholds); } - - return diffThresholds; } - List QuantizeAbsoluteDiffProperty() + for (int i = 0; i < this.NumberOfProperties; i++) { - if (absoluteDiffThresholds.Count == 0) + int property = this.propertiesToUse[i]; + Span thresholds = this.compactProperties.Slice(i * this.thresholdCapacity, this.thresholdCapacity); + int count; + if (property is 0 or 1) { - _ = QuantizeDiffProperty(); - - Span diffSamplesSpan = CollectionsMarshal.AsSpan(diffSamples); - TensorPrimitives.Abs(diffSamplesSpan, diffSamplesSpan); - - absoluteDiffThresholds = JxlMaEncoder.QuantizeSamples(diffSamplesSpan, maxPropertyValues); + ReadOnlySpan forced = property == 0 ? channelThresholds : groupThresholds; + if (!forced.IsEmpty) + { + forced.CopyTo(thresholds); + count = forced.Length; + } + else + { + ReadOnlySpan histogram = property == 0 ? channelPixelCount : groupPixelCount; + count = JxlMaEncoder.QuantizeHistogram(histogram, maxPropertyValues, thresholds); + } } - - return absoluteDiffThresholds; - } - - List QuantizeWeightedPrediction() - { - // TODO: static ReadOnlySpan ... => [...]? - if (maxPropertyValues < 32) + else if (property is 2 or 3) { - return [-127, -63, -31, -15, -7, -3, -1, 0, 1, - 3, 7, 15, 31, 63, 127]; + count = maxPropertyValues - 1; + for (int j = 0; j < count; j++) + { + thresholds[j] = ((j + 1) * 256 / maxPropertyValues) - 1; + } } - else if (maxPropertyValues < 64) + else if (property == JxlMaConstants.WpProp) { - return [-255, -191, -127, -95, -63, -47, -31, -23, - -15, -11, -7, -5, -3, -1, 0, 1, - 3, 5, 7, 11, 15, 23, 31, 47, - 63, 95, 127, 191, 255]; + ReadOnlySpan weighted = WeightedThresholds(maxPropertyValues); + weighted.CopyTo(thresholds); + count = weighted.Length; } else { - return [-255, -223, -191, -159, -127, -111, -95, -79, -63, -55, -47, - -39, -31, -27, -23, -19, -15, -13, -11, -9, -7, -6, - -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, - 6, 7, 9, 11, 13, 15, 19, 23, 27, 31, 39, - 47, 55, 63, 79, 95, 111, 127, 159, 191, 223, 255]; + int distribution = DistributionIndex(property); + count = cachedCounts[distribution]; + scratch.Slice((distribution + 2) * this.thresholdCapacity, count).CopyTo(thresholds); } + + this.thresholdCounts[i] = count; + JxlMaEncoder.QuantMap(thresholds[..count], this.propertyMapping.Slice(i * PropertyMapSize, PropertyMapSize), PropertyMapSize, PropertyRange); } + } - this.propertyMapping.Resize(0, this.propertiesToUse.Count - this.NumberOfStaticProperties); + /// Selects the native pixel, absolute-pixel, residual, or absolute-residual threshold distribution. + /// The original non-static property ID. + /// Zero for pixel values, one for magnitudes, two for residuals, or three for residual magnitudes. + private static int DistributionIndex(int property) + { + int referenceProperty = property - JxlMaConstants.NumNonrefProperties; + if (property is 6 or 7 or 8 || (referenceProperty >= 0 && referenceProperty % 4 == 1)) + { + return 0; + } - for (int i = 0; i < this.propertiesToUse.Count; i++) + if (property is 4 or 5 || (referenceProperty >= 0 && referenceProperty % 4 == 0)) { - if (this.propertiesToUse[i] == 0) - { - this.compactProperties[i] = QuantizeChannel(); - } - else if (this.propertiesToUse[i] == 1) - { - this.compactProperties[i] = QuantizeGroupId(); - } - else if (this.propertiesToUse[i] is 2 or 3) - { - this.compactProperties[i] = QuantizeCoordinate(); - } - else if (this.propertiesToUse[i] == 6 || this.propertiesToUse[i] == 7 || - this.propertiesToUse[i] == 8 || - (this.propertiesToUse[i] >= JxlMaConstants.NumNonrefProperties && (this.propertiesToUse[i] - JxlMaConstants.NumNonrefProperties) % 4 == 1)) - { - this.compactProperties[i] = QuantizePixelProperty(); - } - else if (this.propertiesToUse[i] == 4 || this.propertiesToUse[i] == 5 || - (this.propertiesToUse[i] >= JxlMaConstants.NumNonrefProperties && (this.propertiesToUse[i] - JxlMaConstants.NumNonrefProperties) % 4 == 0)) - { - this.compactProperties[i] = QuantizeAbsolutePixelProperty(); - } - else if (this.propertiesToUse[i] >= JxlMaConstants.NumNonrefProperties && (this.propertiesToUse[i] - JxlMaConstants.NumNonrefProperties) % 4 == 2) - { - this.compactProperties[i] = QuantizeAbsoluteDiffProperty(); - } - else if (this.propertiesToUse[i] == JxlMaConstants.WpProp) - { - this.compactProperties[i] = QuantizeWeightedPrediction(); - } - else - { - this.compactProperties[i] = QuantizeDiffProperty(); - } + return 1; + } + + return referenceProperty >= 0 && referenceProperty % 4 == 2 ? 3 : 2; + } + + /// Returns the native fixed weighted-prediction thresholds for the requested quantization resolution. + /// The requested interval count. + /// The immutable signed error thresholds. + private static ReadOnlySpan WeightedThresholds(int maxPropertyValues) + { + if (maxPropertyValues < 32) + { + return [-127, -63, -31, -15, -7, -3, -1, 0, 1, 3, 7, 15, 31, 63, 127]; + } + else if (maxPropertyValues < 64) + { + return [-255, -191, -127, -95, -63, -47, -31, -23, -15, -11, -7, -5, -3, -1, 0, 1, 3, 5, 7, 11, 15, 23, 31, 47, 63, 95, 127, 191, 255]; + } + else + { + return [-255, -223, -191, -159, -127, -111, -95, -79, -63, -55, -47, + -39, -31, -27, -23, -19, -15, -13, -11, -9, -7, -6, + -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, + 6, 7, 9, 11, 13, 15, 19, 23, 27, 31, 39, + 47, 55, 63, 79, 95, 111, 127, 159, 191, 223, 255]; - if (i < this.NumberOfStaticProperties) - { - JxlMaEncoder.QuantMap( - CollectionsMarshal.AsSpan(this.compactProperties[i]), - CollectionsMarshal.AsSpan(this.staticPropertyMapping[i]), - (PropertyRange * 2) + 1, - PropertyRange); - } - else - { - JxlMaEncoder.QuantMap( - CollectionsMarshal.AsSpan(this.compactProperties[i]), - CollectionsMarshal.AsSpan(this.propertyMapping[i - this.NumberOfStaticProperties]), - (PropertyRange * 2) + 1, - PropertyRange); - } } } + + /// Releases every allocation, including partially prepared state after an exception. + public void Dispose() + { + this.AllSamplesDone(); + this.propertyIdsOwner?.Dispose(); + this.quantizationOwner?.Dispose(); + this.residualsOwner?.Dispose(); + this.countsOwner?.Dispose(); + this.propertiesOwner?.Dispose(); + this = default; + } } diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/EntropyCoderTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/EntropyCoderTests.cs index 4c43985967..9b91c57df4 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/EntropyCoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/EntropyCoderTests.cs @@ -10,12 +10,73 @@ using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding; using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using SixLabors.ImageSharp.Tests.Memory; using SixLabors.ImageSharp.Tests.TestUtilities; namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; public class EntropyCoderTests { + /// + /// Checks an actual learned split with dirty allocator storage and confirms that search and sample buffers are returned. + /// + [Fact] + public void TreeSearch_SplitsDistinctChannelsAndReturnsOwnedStorage() + { + static void RunTest() + { + Configuration configuration = TestEnvironment.Configuration.Clone(); + TestMemoryAllocator allocator = new(); + allocator.EnableNonThreadSafeLogging(); + configuration.MemoryAllocator = allocator; + JxlTreeSamples samples = new(configuration); + + try + { + samples.SetPredictor(JxlPredictor.Zero, JxlTreeMode.Default); + samples.SetProperties([0, 1], JxlTreeMode.Default); + samples.PrepareForSamples(4); + InlineArray2> range = default; + range[0][1] = 2; + range[1][1] = 1; + samples.PreQuantizeProperties(range, [], [4], [2, 2], [], [], 2); + Span properties = [0, 0]; + Span predictions = stackalloc int[JxlPredictorFacts.ModularPredictors]; + predictions.Clear(); + samples.AddSample(0, properties, predictions); + samples.AddSample(0, properties, predictions); + properties[0] = 1; + samples.AddSample(16, properties, predictions); + samples.AddSample(16, properties, predictions); + samples.AllSamplesDone(); + long sampleBytes = allocator.LiveBufferBytes; + + // Each channel has one repeated token. Splitting at channel 0 removes four entropy bits, with payload bits unchanged. + // Residual 16 encodes to token 24, so the real search also traverses complete 512-bit histogram batches. + List tree = []; + JxlMaEncoder.ComputeBestTree(configuration, ref samples, 0.5F, [], range, 1F, tree); + Assert.Equal(3, tree.Count); + Assert.Equal(0, tree[0].Property); + Assert.Equal(0, tree[0].SplitValue); + Assert.Equal(-1, tree[tree[0].LeftChild].Property); + Assert.Equal(-1, tree[tree[0].RightChild].Property); + Assert.Equal(JxlPredictor.Zero, tree[tree[0].LeftChild].Predictor); + Assert.Equal(JxlPredictor.Zero, tree[tree[0].RightChild].Predictor); + Assert.Equal(sampleBytes, allocator.LiveBufferBytes); + } + finally + { + samples.Dispose(); + } + + Assert.Equal(0L, allocator.LiveBufferBytes); + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } + /// /// Checks ANS cost for exact binary probabilities, zero bins and logical histogram lengths under every feature mode. /// @@ -67,6 +128,134 @@ static void RunTest() HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); } + /// + /// Checks sample insertion, independent predictor planes, static and dynamic maps, duplicate counts, and in-place partition swaps. + /// + [Fact] + public void TreeSamples_RetainNewRecordsAndMergeOnlyEqualQuantizedSamples() + { + JxlTreeSamples samples = new(TestEnvironment.Configuration); + try + { + samples.SetPredictor(JxlPredictor.Best, JxlTreeMode.Default); + Span selected = [0, 1, 6]; + samples.SetProperties(selected, JxlTreeMode.Default); + selected[2] = JxlMaConstants.WpProp; + samples.PrepareForSamples(5); + + // Two equal channel counts split at channel 0. Group counts [0,0,1,1] instead split at group 2. + // Pixel training values [-8,8] split at -8. Each exact threshold stays in the lower interval. + InlineArray2> range = default; + range[0][1] = 2; + range[1][1] = 4; + Span pixels = [-8, 8]; + samples.PreQuantizeProperties(range, [], [0, 0, 1, 1], [1, 1], pixels, [], 2); + Assert.Equal(6, samples.PropertyFromIndex(2)); + Assert.Equal(0, samples.QuantizeStaticProperty(0, 0)); + Assert.Equal(1, samples.QuantizeStaticProperty(0, 1)); + Assert.Equal(0, samples.QuantizeStaticProperty(1, 2)); + Assert.Equal(1, samples.QuantizeStaticProperty(1, 3)); + Assert.Equal(0, samples.QuantizeProperty(2, -8)); + Assert.Equal(1, samples.QuantizeProperty(2, -7)); + Assert.Equal(2, samples.FindPropertyIndex(6)); + + Span properties = [0, 0, 0, 0, 0, 0, -8]; + Span predictions = stackalloc int[JxlPredictorFacts.ModularPredictors]; + predictions[(int)JxlPredictor.Weighted] = 0; + predictions[(int)JxlPredictor.Gradient] = 1; + samples.AddSample(2, properties, predictions); + samples.AddSample(2, properties, predictions); + + // Residuals 2 and 1 encode directly as tokens 4 and 2. Changing only Gradient must retain a different sample. + predictions[(int)JxlPredictor.Gradient] = 0; + samples.AddSample(2, properties, predictions); + predictions[(int)JxlPredictor.Gradient] = 1; + properties[0] = 1; + samples.AddSample(2, properties, predictions); + properties[0] = 0; + properties[6] = 8; + samples.AddSample(2, properties, predictions); + + Assert.Equal(5, samples.NumberOfSamples); + Assert.Equal(4, samples.NumberOfDistinctSamples); + Assert.Equal(2, samples.GetCount(0)); + Assert.Equal(1, samples.GetCount(1)); + Assert.Equal(4, samples.GetResidualTokensForPrediction(0).Length); + Assert.Equal(4, samples.GetToken(0, 0)); + Assert.Equal(2, samples.GetToken(1, 0)); + Assert.Equal(4, samples.GetToken(1, 1)); + Assert.Equal(1, samples.GetProperty(true, 0, 2)); + Assert.Equal(1, samples.GetProperty(false, 0, 3)); + + samples.AllSamplesDone(); + samples.Swap(0, 3); + Assert.Equal(1, samples.GetCount(0)); + Assert.Equal(2, samples.GetCount(3)); + Assert.Equal(1, samples.GetProperty(false, 0, 0)); + Assert.Equal(0, samples.GetProperty(false, 0, 3)); + } + finally + { + samples.Dispose(); + } + } + + /// + /// Checks native count saturation: the next matching sample starts a new record after the first count reaches 65535. + /// + [Fact] + public void TreeSamples_SaturatedCountsStartANewRecord() + { + JxlTreeSamples samples = new(TestEnvironment.Configuration); + try + { + samples.SetPredictor(JxlPredictor.Zero, JxlTreeMode.Default); + samples.SetProperties([2], JxlTreeMode.Default); + samples.PrepareForSamples(ushort.MaxValue + 1); + samples.PreQuantizeProperties(default, [], [], [], [], [], 2); + ReadOnlySpan properties = [0, 0, 0]; + ReadOnlySpan predictions = [0]; + for (int i = 0; i <= ushort.MaxValue; i++) + { + samples.AddSample(1, properties, predictions); + } + + Assert.Equal(ushort.MaxValue + 1, samples.NumberOfSamples); + Assert.Equal(2, samples.NumberOfDistinctSamples); + Assert.Equal(ushort.MaxValue, samples.GetCount(0)); + Assert.Equal(1, samples.GetCount(1)); + } + finally + { + samples.Dispose(); + } + } + + /// + /// Checks that excluding weighted prediction changes the owned selection without modifying caller property IDs. + /// + [Fact] + public void TreeSamples_NoWeightedModeDoesNotChangeOptions() + { + Span selected = [0, 1, JxlMaConstants.WpProp, 6]; + JxlTreeSamples samples = new(TestEnvironment.Configuration); + try + { + samples.SetPredictor(JxlPredictor.Best, JxlTreeMode.NoWp); + samples.SetProperties(selected, JxlTreeMode.NoWp); + Assert.Equal(1, samples.NumberOfPredictors); + Assert.Equal(JxlPredictor.Gradient, samples.PredictorFromIndex(0)); + Assert.Equal(3, samples.NumberOfProperties); + Assert.Equal(6, samples.PropertyFromIndex(2)); + Assert.Equal(JxlMaConstants.WpProp, selected[2]); + Assert.Equal(6, selected[3]); + } + finally + { + samples.Dispose(); + } + } + /// /// Checks terminal zero-bit and one-bit Huffman symbols through the encoded table and bounded bit reader. /// From f6abb7407305cd9785f783fef736f84c643b7ecb Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Oct 2026 02:52:36 +1000 Subject: [PATCH 299/328] Correct JPEG XL strategy estimates and canonical coefficient ordering --- .../Processing/AcStrategy/JxlAcStrategy.cs | 15 +- .../AcStrategy/JxlAcStrategyConfiguration.cs | 53 +- .../JxlAcStrategyEncoder.Entropy.cs | 659 ++++++++++++++++++ .../AcStrategy/JxlAcStrategyEncoder.cs | 292 +++----- .../Processing/JxlForwardCoefficientOrder.cs | 27 +- .../Jxl/Processing/CoefficientOrderTests.cs | 105 +++ 6 files changed, 924 insertions(+), 227 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyEncoder.Entropy.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategy.cs b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategy.cs index 78e97ed16c..83a5759e62 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategy.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategy.cs @@ -91,14 +91,21 @@ public JxlAcStrategy(int rawStrategy) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsRawStrategyValid(int rawStrategy) => rawStrategy is < NumberOfValidStrategies and >= 0; + /// + /// Visits the canonical coefficient rectangle in alternating diagonal order, with its low-frequency block rectangle first. + /// + /// The validated spatial transform dimensions. + /// Whether output maps coefficient offsets to order positions, rather than order positions to coefficient offsets. + /// Storage for the transform's complete coefficient area. private static void CoefficientOrderAndLookup(JxlAcStrategy strategy, bool isLookup, Span output) { - // TODO: CoefficientLayout int cx = strategy.CoveredBlocksX; int cy = strategy.CoveredBlocksY; - JxlForwardCoefficientOrder.CoefficientLayout(ref cx, ref cy); + // Coefficients use the short dimension as rows and the long dimension as columns, independently of the spatial block orientation. + JxlForwardCoefficientOrder.CoefficientLayout(ref cy, ref cx); + // cx and cy count eight-pixel blocks. Visit a square at the longer extent, retaining only diagonals that fit the aspect ratio. int xs = cx / cy; int xsm = xs - 1; int xss = JxlMath.CeilLog2Nonzero(xs); @@ -111,9 +118,9 @@ private static void CoefficientOrderAndLookup(JxlAcStrategy strategy, bool isLoo int x = j; int y = i - j; - if ((i & 1) == 0) + // Odd diagonals advance toward increasing row indices. The same direction rule applies in the second triangular half. + if ((i & 1) != 0) { - // swap (x, y) = (y, x); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyConfiguration.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyConfiguration.cs index 44dd37336a..75beaf5eba 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyConfiguration.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyConfiguration.cs @@ -2,49 +2,82 @@ // Licensed under the Six Labors Split License. using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AcStrategy; +/// +/// Retains the frame-owned source and mask memory used by sequential transform-strategy estimates. +/// +/// The frame-owned prepared matrices. This view does not dispose them. internal sealed class JxlAcStrategyConfiguration(JxlDequantMatrices dequant) { + /// + /// Borrows the first source row and subsequent storage for each XYB channel. The frame must retain these allocations during strategy selection. + /// [SuppressMessage( "StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "when an InlineArray is a property its values cannot be mutated")] public InlineArray3> SourceRows; + /// + /// Gets or sets the borrowed prepared quantization matrices. + /// public JxlDequantMatrices Dequant { get; set; } = dequant; + /// + /// Gets or sets the quantization field from its first block row. + /// public Memory QuantFieldRow { get; set; } + /// + /// Gets or sets the quantization-field stride, in eight-pixel blocks. + /// public int QuantFieldStride { get; set; } + /// + /// Gets or sets the borrowed block-resolution masking field. + /// public Memory MaskingFieldRow { get; set; } + /// + /// Gets or sets the block-resolution mask stride, in floats. + /// public int MaskingFieldStride { get; set; } + /// + /// Gets or sets the borrowed pixel-resolution mask from its logical origin. + /// public Memory Masking1x1FieldRow { get; set; } + /// + /// Gets or sets the pixel-mask row stride, in floats. + /// public int Masking1x1FieldStride { get; set; } + /// + /// Gets or sets the logical pixel-mask width, excluding physical row padding. + /// public int Mask1x1Width { get; set; } + /// + /// Gets or sets the physical source row stride, in floats. + /// public int SourceStride { get; set; } + /// + /// Gets or sets the weight applied to masked reconstruction loss. + /// public float InfoLossMultiplier { get; set; } + /// + /// Gets or sets the weight applied to square roots of absolute rounded coefficients. + /// public float CostDelta { get; set; } + /// + /// Gets or sets the weight applied to the estimated nonzero-count coding cost. + /// public float ZerosMultiplier { get; set; } - - public ref float Pixel(int c, int x, int y) - => ref this.SourceRows[c].Span[(y * this.SourceStride) + x]; - - public Span PixelSpan(int c, int x, int y) - => this.SourceRows[c].Span[((y * this.SourceStride) + x)..]; - - public ref float Quant(int bx, int by) - => ref this.QuantFieldRow.Span[(by * this.QuantFieldStride) + bx]; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyEncoder.Entropy.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyEncoder.Entropy.cs new file mode 100644 index 0000000000..35323cfc01 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyEncoder.Entropy.cs @@ -0,0 +1,659 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AcStrategy; + +internal static partial class JxlAcStrategyEncoder +{ + /// + /// The native red-green loss weight pow(8.2, 8), narrowed once from double to float: bits 0x4B9BF4A0. + /// + private const float XChannelLossMultiplier = 20441408F; + + /// + /// The native blue loss weight pow(1.03, 8), narrowed once from double to float: bits 0x3FA22586. + /// + private const float BChannelLossMultiplier = 1.2667701244354248F; + + /// + /// Estimates coefficient coding cost and masked reconstruction loss for one candidate transform. + /// + /// The candidate strategy. Its pixel extent fits the source and the prepared matrix storage. + /// The candidate's fixed coding-cost multiplier. + /// The source pixel column of the candidate's top-left corner. + /// The source pixel row of the candidate's top-left corner. + /// The borrowed source, quantization, mask and matrix storage, with their physical strides. + /// The X, Y and B color-correlation factors. The Y factor is zero. + /// Three packed coefficient regions, each large enough for this strategy's sample count. + /// One maximum-area error region followed by the existing transform workspace. + /// The estimated coding cost plus information loss. + /// True after the estimate is complete. Storage and strategy validity are established by the search owner. + public static bool EstimateEntropy( + JxlAcStrategy acs, + float entropyMultiplier, + int x, + int y, + JxlAcStrategyConfiguration config, + Span cmapFactors, + Span block, + Span fullScratchSpace, + out float entropy) + { + int blocksX = acs.CoveredBlocksX; + int blocksY = acs.CoveredBlocksY; + int numBlocks = blocksX * blocksY; + int size = numBlocks * JxlFrameDimensions.DctBlockSize; + Span error = fullScratchSpace[..size]; + Span transformScratch = fullScratchSpace[JxlAcStrategy.MaximumCoefficientArea..]; + + // Borrow once before candidate processing. Neither the frame-owned source nor the prepared matrices change during the estimate. + // Independent physical strides belong to their own fields; reconstructed error below is a tightly packed candidate rectangle. + ReadOnlySpan sourceX = config.SourceRows[0].Span; + ReadOnlySpan sourceY = config.SourceRows[1].Span; + ReadOnlySpan sourceB = config.SourceRows[2].Span; + ReadOnlySpan quantField = config.QuantFieldRow.Span; + ReadOnlySpan mask = config.Masking1x1FieldRow.Span; + int sourceOffset = (y * config.SourceStride) + x; + JxlTransformsEncoder.TransformFromPixels(acs.Strategy, sourceX[sourceOffset..], config.SourceStride, block[..size], transformScratch); + JxlTransformsEncoder.TransformFromPixels(acs.Strategy, sourceY[sourceOffset..], config.SourceStride, block.Slice(size, size), transformScratch); + JxlTransformsEncoder.TransformFromPixels(acs.Strategy, sourceB[sourceOffset..], config.SourceStride, block.Slice(2 * size, size), transformScratch); + + int quantX = x / JxlFrameDimensions.BlockDimensions; + int quantY = y / JxlFrameDimensions.BlockDimensions; + int quantOffset = (quantY * config.QuantFieldStride) + quantX; + float quantNorm; + if (numBlocks == 1) + { + quantNorm = quantField[quantOffset]; + } + else if (numBlocks == 2) + { + // Native selection uses the larger field value for the smallest rectangular transforms, rather than the general sixteenth norm. + int next = blocksY == 2 ? quantOffset + config.QuantFieldStride : quantOffset + 1; + quantNorm = MathF.Max(quantField[quantOffset], quantField[next]); + } + else + { + float powers = 0; + for (int iy = 0; iy < blocksY; iy++) + { + ReadOnlySpan row = quantField.Slice(quantOffset + (iy * config.QuantFieldStride), blocksX); + for (int ix = 0; ix < blocksX; ix++) + { + float value = row[ix]; + value *= value; + value *= value; + value *= value; + powers += value * value; + } + } + + quantNorm = MathF.Pow(powers / numBlocks, 1F / 16F); + } + + entropy = 0; + Vector256 loss = Vector256.Zero; + ReadOnlySpan luminance = block.Slice(size, size); + for (int c = 0; c < 3; c++) + { + ReadOnlySpan coefficients = block.Slice(c * size, size); + ReadOnlySpan inverseMatrix = config.Dequant.GetInverseMatrix(acs.Strategy, c)[..size]; + ReadOnlySpan matrix = config.Dequant.GetMatrix(acs.Strategy, c)[..size]; + EntropyOperator operation = new(quantNorm, cmapFactors[c]); + float coefficientCost = ComputeErrors(coefficients, luminance, inverseMatrix, matrix, error, operation, out int nonzero); + + // All three coefficient regions are complete before reconstruction starts. Output uses only the first region; + // the Y coefficients remain available for both color-correlation subtractions until the last channel is processed. + JxlTransformsDecoder.TransformToPixels(acs.Strategy, error, block[..size], blocksX * JxlFrameDimensions.BlockDimensions, transformScratch); + LossOperator lossOperation = new(c == 0 ? 12F : c == 1 ? 0F : 4F); + Vector256 channelLoss = ComputeLoss( + block[..size], + blocksX * JxlFrameDimensions.BlockDimensions, + blocksY * JxlFrameDimensions.BlockDimensions, + mask, + config.Masking1x1FieldStride, + config.Mask1x1Width, + x, + y, + lossOperation); + + float channelMultiplier = c == 0 ? XChannelLossMultiplier : c == 1 ? 1F : BChannelLossMultiplier; + loss += channelLoss * Vector256.Create(channelMultiplier); + entropy += config.CostDelta * coefficientCost; + int bits = JxlMath.CeilLog2Nonzero(nonzero + 1) + 1; + entropy += config.ZerosMultiplier * (JxlMath.CeilLog2Nonzero(bits + 17) + bits); + + if (c == 0 && numBlocks >= 2) + { + // Bound the extra red-green penalty at four. A minimum of three would instead over-penalize every small rectangle. + float weight = 1F + MathF.Min(3F, numBlocks / 8F); + entropy *= weight; + loss *= Vector256.Create(weight); + } + } + + // The eighth norm is a mean per pixel, not per block column. Every block contains 64 samples, including all channels' weighted errors. + float informationLoss = MathF.Pow(Vector256.Sum(loss) / size, 1F / 8F) * size / quantNorm; + entropy *= entropyMultiplier; + entropy += config.InfoLossMultiplier * informationLoss; + + return true; + } + + /// + /// Writes every dequantized coefficient error while accumulating its coding cost and nonzero count in the same traversal. + /// + /// One channel's logical coefficient region. + /// The corresponding Y coefficients, retained until all channels have been read. + /// The forward quantization weights for this channel. + /// The inverse quantization weights for restoring coefficient errors. + /// The existing error region, with one output per coefficient. + /// The candidate's quantization scale and channel-correlation factor. + /// The exact number of rounded nonzero coefficients. + /// The sum of square roots of absolute rounded coefficients. + private static float ComputeErrors( + ReadOnlySpan coefficients, + ReadOnlySpan luminance, + ReadOnlySpan inverseMatrix, + ReadOnlySpan matrix, + Span error, + EntropyOperator operation, + out int nonzero) + { + ref float input = ref MemoryMarshal.GetReference(coefficients); + ref float inputY = ref MemoryMarshal.GetReference(luminance); + ref float inverseWeights = ref MemoryMarshal.GetReference(inverseMatrix); + ref float weights = ref MemoryMarshal.GetReference(matrix); + ref float output = ref MemoryMarshal.GetReference(error); + float entropy = 0; + float count = 0; + int i = 0; + + // Each lane describes one coefficient. Four inputs, an error output, and two reductions share these loads; + // separate tensor calls would require intermediate storage and extra full passes over the candidate. + // Valid coefficient areas are multiples of 64, so the widest enabled register consumes the complete region without narrower follow-up passes. + if (Vector512.IsHardwareAccelerated) + { + Vector512 costs = Vector512.Zero; + Vector512 counts = Vector512.Zero; + for (; i <= coefficients.Length - Vector512.Count; i += Vector512.Count) + { + Vector512 value = operation.Invoke( + Vector512.LoadUnsafe(ref input, (uint)i), + Vector512.LoadUnsafe(ref inputY, (uint)i), + Vector512.LoadUnsafe(ref inverseWeights, (uint)i), + Vector512.LoadUnsafe(ref weights, (uint)i), + out Vector512 cost, + out Vector512 used); + + value.StoreUnsafe(ref output, (uint)i); + costs += cost; + counts += used; + } + + entropy += Vector512.Sum(costs); + count += Vector512.Sum(counts); + } + + else if (Vector256.IsHardwareAccelerated) + { + Vector256 costs = Vector256.Zero; + Vector256 counts = Vector256.Zero; + for (; i <= coefficients.Length - Vector256.Count; i += Vector256.Count) + { + Vector256 value = operation.Invoke( + Vector256.LoadUnsafe(ref input, (uint)i), + Vector256.LoadUnsafe(ref inputY, (uint)i), + Vector256.LoadUnsafe(ref inverseWeights, (uint)i), + Vector256.LoadUnsafe(ref weights, (uint)i), + out Vector256 cost, + out Vector256 used); + + value.StoreUnsafe(ref output, (uint)i); + costs += cost; + counts += used; + } + + entropy += Vector256.Sum(costs); + count += Vector256.Sum(counts); + } + + else if (Vector128.IsHardwareAccelerated) + { + Vector128 costs = Vector128.Zero; + Vector128 counts = Vector128.Zero; + for (; i <= coefficients.Length - Vector128.Count; i += Vector128.Count) + { + Vector128 value = operation.Invoke( + Vector128.LoadUnsafe(ref input, (uint)i), + Vector128.LoadUnsafe(ref inputY, (uint)i), + Vector128.LoadUnsafe(ref inverseWeights, (uint)i), + Vector128.LoadUnsafe(ref weights, (uint)i), + out Vector128 cost, + out Vector128 used); + + value.StoreUnsafe(ref output, (uint)i); + costs += cost; + counts += used; + } + + entropy += Vector128.Sum(costs); + count += Vector128.Sum(counts); + } + + for (; i < coefficients.Length; i++) + { + Unsafe.Add(ref output, i) = operation.Invoke( + Unsafe.Add(ref input, i), + Unsafe.Add(ref inputY, i), + Unsafe.Add(ref inverseWeights, i), + Unsafe.Add(ref weights, i), + out float cost, + out float used); + + entropy += cost; + count += used; + } + + // At most 65,536 coefficients contribute ones. Float represents every possible count exactly, so this conversion loses no bits. + nonzero = (int)count; + + return entropy; + } + + /// + /// Measures masked pixel error with the existing whole-eight-sample edge policy and fixed eight-lane accumulation order. + /// + /// The tightly packed reconstructed candidate error. + /// The candidate width in pixels, a multiple of eight. + /// The candidate height in pixels, a multiple of eight. + /// The borrowed pixel mask from its logical origin. + /// The mask row stride, in floats. + /// The logical mask width. Incomplete eight-sample groups do not contribute. + /// The candidate's first mask column. + /// The candidate's first mask row. + /// The channel-specific mask offset and eighth-power calculation. + /// Eight loss accumulators, before the channel multiplier is applied. + private static Vector256 ComputeLoss( + ReadOnlySpan pixels, + int width, + int height, + ReadOnlySpan mask, + int maskStride, + int maskWidth, + int x, + int y, + LossOperator operation) + { + if (Vector512.IsHardwareAccelerated) + { + return ComputeLoss512(pixels, width, height, mask, maskStride, maskWidth, x, y, operation); + } + + if (Vector256.IsHardwareAccelerated) + { + return ComputeLoss256(pixels, width, height, mask, maskStride, maskWidth, x, y, operation); + } + + if (Vector128.IsHardwareAccelerated) + { + return ComputeLoss128(pixels, width, height, mask, maskStride, maskWidth, x, y, operation); + } + + return ComputeLossScalar(pixels, width, height, mask, maskStride, maskWidth, x, y, operation); + } + + /// + private static Vector256 ComputeLoss512( + ReadOnlySpan pixels, + int width, + int height, + ReadOnlySpan mask, + int maskStride, + int maskWidth, + int x, + int y, + LossOperator operation) + { + ref float input = ref MemoryMarshal.GetReference(pixels); + ref float masks = ref MemoryMarshal.GetReference(mask); + Vector256 loss = Vector256.Zero; + + // Keep block-column order outside the eight image rows, as in the native capped-eight loss calculation. + for (int blockY = 0; blockY < height; blockY += 8) + { + for (int blockX = 0; blockX < width; blockX += 8) + { + int maskX = x + blockX; + if (maskX + 8 > maskWidth) + { + // This is the existing mask boundary, not permission to read physical row padding. It also handles an absent mask without a load. + continue; + } + + // Two independent eight-pixel rows fill one 512-bit register. Fold the two results into the same eight lanes in row order. + for (int dy = 0; dy < 8; dy += 2) + { + int row = blockY + dy; + int pixelOffset = (row * width) + blockX; + int maskOffset = ((y + row) * maskStride) + maskX; + Vector512 values = Vector512.Create( + Vector256.LoadUnsafe(ref input, (uint)pixelOffset), + Vector256.LoadUnsafe(ref input, (uint)(pixelOffset + width))); + + Vector512 weights = Vector512.Create( + Vector256.LoadUnsafe(ref masks, (uint)maskOffset), + Vector256.LoadUnsafe(ref masks, (uint)(maskOffset + maskStride))); + + Vector512 result = operation.Invoke(values, weights); + loss += result.GetLower(); + loss += result.GetUpper(); + } + } + } + + return loss; + } + + /// + private static Vector256 ComputeLoss256( + ReadOnlySpan pixels, + int width, + int height, + ReadOnlySpan mask, + int maskStride, + int maskWidth, + int x, + int y, + LossOperator operation) + { + ref float input = ref MemoryMarshal.GetReference(pixels); + ref float masks = ref MemoryMarshal.GetReference(mask); + Vector256 loss = Vector256.Zero; + + // Keep block-column order outside the eight image rows, as in the native capped-eight loss calculation. + for (int blockY = 0; blockY < height; blockY += 8) + { + for (int blockX = 0; blockX < width; blockX += 8) + { + int maskX = x + blockX; + if (maskX + 8 > maskWidth) + { + // This is the existing mask boundary, not permission to read physical row padding. It also handles an absent mask without a load. + continue; + } + + for (int dy = 0; dy < 8; dy++) + { + int row = blockY + dy; + Vector256 values = Vector256.LoadUnsafe(ref input, (uint)((row * width) + blockX)); + Vector256 weights = Vector256.LoadUnsafe(ref masks, (uint)(((y + row) * maskStride) + maskX)); + loss += operation.Invoke(values, weights); + } + } + } + + return loss; + } + + /// + private static Vector256 ComputeLoss128( + ReadOnlySpan pixels, + int width, + int height, + ReadOnlySpan mask, + int maskStride, + int maskWidth, + int x, + int y, + LossOperator operation) + { + ref float input = ref MemoryMarshal.GetReference(pixels); + ref float masks = ref MemoryMarshal.GetReference(mask); + Vector128 low = Vector128.Zero; + Vector128 high = Vector128.Zero; + + // Keep block-column order outside the eight image rows, as in the native capped-eight loss calculation. + for (int blockY = 0; blockY < height; blockY += 8) + { + for (int blockX = 0; blockX < width; blockX += 8) + { + int maskX = x + blockX; + if (maskX + 8 > maskWidth) + { + // This is the existing mask boundary, not permission to read physical row padding. It also handles an absent mask without a load. + continue; + } + + for (int dy = 0; dy < 8; dy++) + { + int row = blockY + dy; + int pixelOffset = (row * width) + blockX; + int maskOffset = ((y + row) * maskStride) + maskX; + low += operation.Invoke(Vector128.LoadUnsafe(ref input, (uint)pixelOffset), Vector128.LoadUnsafe(ref masks, (uint)maskOffset)); + high += operation.Invoke( + Vector128.LoadUnsafe(ref input, (uint)(pixelOffset + 4)), + Vector128.LoadUnsafe(ref masks, (uint)(maskOffset + 4))); + } + } + } + + return Vector256.Create(low, high); + } + + /// + private static Vector256 ComputeLossScalar( + ReadOnlySpan pixels, + int width, + int height, + ReadOnlySpan mask, + int maskStride, + int maskWidth, + int x, + int y, + LossOperator operation) + { + ref float input = ref MemoryMarshal.GetReference(pixels); + ref float masks = ref MemoryMarshal.GetReference(mask); + InlineArray8 storage = default; + Span loss = storage; + + // Keep block-column order outside the eight image rows, as in the native capped-eight loss calculation. + for (int blockY = 0; blockY < height; blockY += 8) + { + for (int blockX = 0; blockX < width; blockX += 8) + { + int maskX = x + blockX; + if (maskX + 8 > maskWidth) + { + // This is the existing mask boundary, not permission to read physical row padding. It also handles an absent mask without a load. + continue; + } + + for (int dy = 0; dy < 8; dy++) + { + int row = blockY + dy; + int pixelOffset = (row * width) + blockX; + int maskOffset = ((y + row) * maskStride) + maskX; + for (int dx = 0; dx < 8; dx++) + { + loss[dx] += operation.Invoke(Unsafe.Add(ref input, pixelOffset + dx), Unsafe.Add(ref masks, maskOffset + dx)); + } + } + } + } + + return Vector256.Create(loss); + } + + /// + /// Computes one coefficient's error and coding statistics without changing the estimator's plain nearest-even rounding rule. + /// + private readonly struct EntropyOperator + { + /// + /// The candidate's quantization-field norm. + /// + private readonly float quantization; + + /// + /// The channel's factor for subtracting the Y coefficient. + /// + private readonly float correlation; + + /// + /// Initializes the invariant values shared by every coefficient in one channel. + /// + /// The positive candidate quantization norm. + /// The channel's color-correlation factor. + public EntropyOperator(float quantization, float correlation) + { + this.quantization = quantization; + this.correlation = correlation; + } + + /// + /// Returns the dequantized rounding error and the two statistics for the same coefficient. + /// + /// The channel coefficient. + /// The corresponding Y coefficient. + /// The forward quantization weight. + /// The inverse quantization weight. + /// The square root of the absolute rounded coefficient. + /// One for a nonzero rounded coefficient; otherwise zero. + /// The restored quantization error. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float Invoke(float input, float luminance, float inverseMatrix, float matrix, out float cost, out float nonzero) + { + // Preserve the native multiply/subtract grouping. Fusing the subtraction could move a coefficient across its rounding midpoint. + float value = (input - (luminance * this.correlation)) * (inverseMatrix * this.quantization); + float rounded = MathF.Round(value, MidpointRounding.ToEven); + cost = MathF.Sqrt(MathF.Abs(rounded)); + nonzero = rounded == 0 ? 0F : 1F; + + return matrix * (value - rounded); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector128 Invoke( + Vector128 input, + Vector128 luminance, + Vector128 inverseMatrix, + Vector128 matrix, + out Vector128 cost, + out Vector128 nonzero) + { + // Preserve the native multiply/subtract grouping. Fusing the subtraction could move a coefficient across its rounding midpoint. + Vector128 value = (input - (luminance * Vector128.Create(this.correlation))) * (inverseMatrix * Vector128.Create(this.quantization)); + Vector128 rounded = Vector128.Round(value); + cost = Vector128.Sqrt(Vector128.Abs(rounded)); + nonzero = Vector128.ConditionalSelect(Vector128.Equals(rounded, Vector128.Zero), Vector128.Zero, Vector128.One); + + return matrix * (value - rounded); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector256 Invoke( + Vector256 input, + Vector256 luminance, + Vector256 inverseMatrix, + Vector256 matrix, + out Vector256 cost, + out Vector256 nonzero) + { + // Preserve the native multiply/subtract grouping. Fusing the subtraction could move a coefficient across its rounding midpoint. + Vector256 value = (input - (luminance * Vector256.Create(this.correlation))) * (inverseMatrix * Vector256.Create(this.quantization)); + Vector256 rounded = Vector256.Round(value); + cost = Vector256.Sqrt(Vector256.Abs(rounded)); + nonzero = Vector256.ConditionalSelect(Vector256.Equals(rounded, Vector256.Zero), Vector256.Zero, Vector256.One); + + return matrix * (value - rounded); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector512 Invoke( + Vector512 input, + Vector512 luminance, + Vector512 inverseMatrix, + Vector512 matrix, + out Vector512 cost, + out Vector512 nonzero) + { + // Preserve the native multiply/subtract grouping. Fusing the subtraction could move a coefficient across its rounding midpoint. + Vector512 value = (input - (luminance * Vector512.Create(this.correlation))) * (inverseMatrix * Vector512.Create(this.quantization)); + Vector512 rounded = Vector512.Round(value); + cost = Vector512.Sqrt(Vector512.Abs(rounded)); + nonzero = Vector512.ConditionalSelect(Vector512.Equals(rounded, Vector512.Zero), Vector512.Zero, Vector512.One); + + return matrix * (value - rounded); + } + } + + /// + /// Computes the eighth power of a pixel's error after adding the channel offset to its perceptual mask. + /// + private readonly struct LossOperator : TensorPrimitives_.IBinaryOperator + { + /// + /// The native channel offset, in mask units: twelve for X, zero for Y and four for B. + /// + private readonly float offset; + + /// + /// Initializes the mask offset shared by one channel's pixels. + /// + /// The additive channel offset in mask units. + public LossOperator(float offset) => this.offset = offset; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float Invoke(float first, float second) + { + float value = first * (second + this.offset); + value *= value; + value *= value; + return value * value; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector128 Invoke(Vector128 first, Vector128 second) + { + Vector128 value = first * (second + Vector128.Create(this.offset)); + value *= value; + value *= value; + return value * value; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector256 Invoke(Vector256 first, Vector256 second) + { + Vector256 value = first * (second + Vector256.Create(this.offset)); + value *= value; + value *= value; + return value * value; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector512 Invoke(Vector512 first, Vector512 second) + { + Vector512 value = first * (second + Vector512.Create(this.offset)); + value *= value; + value *= value; + return value * value; + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyEncoder.cs index 9b56e51e8d..97c45394af 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyEncoder.cs @@ -1,16 +1,13 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.Intrinsics; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AcStrategy; -internal static class JxlAcStrategyEncoder +internal static partial class JxlAcStrategyEncoder { private static readonly byte[][] TypeColors = [ @@ -273,183 +270,6 @@ public static bool MultiBlockTransformCrossesVerticalBoundary(JxlAcStrategyImage return false; } - public static bool EstimateEntropy( - JxlAcStrategy acs, - float entropyMultiplier, - int x, - int y, - JxlAcStrategyConfiguration config, - Span cmapFactors, - Span block, - Span fullScratchSpace, - out float entropy) - { - entropy = 0; - Span memory = fullScratchSpace; - Span scratchSpace = fullScratchSpace[JxlAcStrategy.MaximumCoefficientArea..]; - int size = (1 << acs.Log2CoveredBlocks) * JxlFrameDimensions.DctBlockSize; - - for (int c = 0; c < 3; c++) - { - Span blockC = block[(size * c)..]; - JxlTransformsEncoder.TransformFromPixels( - acs.Strategy, - config.PixelSpan(c, x, y), - config.SourceStride, - blockC, - scratchSpace); - } - - int numBlocks = acs.CoveredBlocksX * acs.CoveredBlocksY; - float quantNorm16 = 0; - - if (numBlocks == 1) - { - // There's only one 8x8. We don't need aggregation of values. - quantNorm16 = config.Quant(x / 8, y / 8); - } - else if (numBlocks == 2) - { - if (acs.CoveredBlocksY == 2) - { - quantNorm16 = Math.Max(config.Quant(x / 8, y / 8), config.Quant(x / 8, (y / 8) + 1)); - } - else - { - quantNorm16 = Math.Max(config.Quant(x / 8, y / 8), config.Quant((x / 8) + 1, y / 8)); - } - } - else - { - // Load quantizer field value, calculate empirical heuristic on masking field - // for weighting the information loss. Information loss manifests - // itself as ringing, and masking could hide it. - for (int iy = 0; iy < acs.CoveredBlocksY; iy++) - { - int yPos = (y / 8) + iy; - - for (int ix = 0; ix < acs.CoveredBlocksX; ix++) - { - float qval = config.Quant((x / 8) + ix, yPos); - qval *= qval; - qval *= qval; - qval *= qval; - quantNorm16 += qval * qval; - } - } - - quantNorm16 /= numBlocks; - quantNorm16 = MathF.Pow(quantNorm16, 1.0f / 16.0f); - } - - // Computation of entropy. - // We have to process 8 coeffs at once. Not sure if we can use - // Vector here... - Vector256 quant = Vector256.Create(quantNorm16); - Vector256 loss = Vector256.Zero; - for (int c = 0; c < 3; c++) - { - Span inverseMatrix = config.Dequant.GetInverseMatrix(acs.Strategy, c); - Span matrix = config.Dequant.GetMatrix(acs.Strategy, c); - Vector256 cmapFactor = Vector256.Create(cmapFactors[c]); - - Vector256 entropyV = Vector256.Zero; - Vector256 numZerosV = Vector256.Zero; - - for (int i = 0; i < (numBlocks * JxlFrameDimensions.DctBlockSize) - Vector.Count; i += Vector.Count) - { - Vector256 input = Vector256.Create(block[((c * size) + i)..]); - Vector256 inY = Vector256.Create(block[(size + i)..]) * cmapFactor; - Vector256 im = Vector256.Create(inverseMatrix[i..]); - Vector256 value = (input - inY) * (im * quant); - Vector256 rvalue = Vector256.Round(value); - Vector256 diff = value - rvalue; - Vector256 m = Vector256.Create(matrix[i..]); - - (m * diff).CopyTo(memory[i..]); - - Vector256 q = Vector256.Abs(rvalue); - Vector256 qIsZero = Vector256.Equals(q, Vector256.Zero); - - entropyV = Vector256.Sqrt(q) + entropyV; - numZerosV += Vector256.ConditionalSelect(qIsZero, Vector256.Zero, Vector256.One); - } - - { - Span maskULut = [12.0f, 0.0f, 4.0f]; - Vector256 maskUOffset = Vector256.Create(maskULut[c]); - Vector256 lossC = Vector256.Zero; - - JxlTransformsDecoder.TransformToPixels( - acs.Strategy, - memory, - block, - acs.CoveredBlocksX * 8, - scratchSpace); - - for (int iy = 0; iy < acs.CoveredBlocksY; iy++) - { - for (int ix = 0; ix < acs.CoveredBlocksX; ix++) - { - for (int dy = 0; dy < JxlFrameDimensions.BlockDimensions; ++dy) - { - // This loop here is expected to be vector size-aligned. - // No need for <= N - VectorSize. - for (int dx = 0; dx < JxlFrameDimensions.BlockDimensions; dx += Vector256.Count) - { - Vector256 input = Vector256.Create(block[ - ((((iy * JxlFrameDimensions.BlockDimensions) + dy) * (acs.CoveredBlocksX * JxlFrameDimensions.BlockDimensions)) + (ix * JxlFrameDimensions.BlockDimensions) + dx)..]); - - if (x + (ix * 8) + dx + Vector256.Count <= config.Mask1x1Width) - { - Vector256 masku = - Vector256.LoadUnsafe(config.MaskingReference1x1( - x + (ix * 8) + dx, - y + (iy * 8) + dy)) * maskUOffset; - input = masku * input; - input *= input; - input *= input; - input *= input; - lossC += input; - } - } - } - } - } - - Span channelMultipliers = [ - MathF.Pow(8.2f, 8.0f), - MathF.Pow(1.0f, 8.0f), - MathF.Pow(1.03f, 8.0f) - ]; - - lossC *= Vector256.Create(channelMultipliers[c]); - loss += lossC; - } - - entropy += config.CostDelta * Vector256.Sum(entropyV); - int numNZeroes = (int)Vector256.Sum(numZerosV); - int nbits = JxlMath.CeilLog2Nonzero(numNZeroes + 1) + 1; - entropy += config.ZerosMultiplier * (JxlMath.CeilLog2Nonzero(nbits + 17) + nbits); - - if (c == 0 && numBlocks >= 2) - { - // It is X channel (red-green) and we often see ringing - // in the large blocks. Let's punish that more here. - float w = 1.0f + MathF.Max(3.0f, numBlocks / 8.0f); - entropy *= w; - loss *= Vector256.Create(w); - } - } - - float lossScalar = MathF.Pow(Vector256.Sum(loss) / (numBlocks * JxlFrameDimensions.BlockDimensions), 1.0f / 8.0f) * (numBlocks * JxlFrameDimensions.DctBlockSize) / quantNorm16; - - entropy *= entropyMultiplier; - entropy += config.InfoLossMultiplier * lossScalar; - - return true; - } - public static JxlAcStrategyType AcsSquare(int blocks) { if (blocks == 2) @@ -972,10 +792,7 @@ public static bool ProcessAcStrategyRectangle( new(JxlAcStrategyType.DCT16X32, 4, 4, 4, entropy_mul16X32), new(JxlAcStrategyType.DCT32X16, 4, 4, 4, entropy_mul16X32), new(JxlAcStrategyType.DCT64X32, 6, 1, 3, entropy_mul64X32), - new(JxlAcStrategyType.DCT32X64, 6, 1, 3, entropy_mul64X32), - default, - default, - default + new(JxlAcStrategyType.DCT32X64, 6, 1, 3, entropy_mul64X32) ]; Span priority = stackalloc byte[64]; @@ -1205,31 +1022,84 @@ public static bool ProcessAcStrategyRectangle( } /// - /// For , represents a small - /// model which the encoder uses to try all available 8x8 - /// transforms and see which one encodes best. + /// Describes one eight-by-eight transform candidate and its fixed search weights. + /// + private readonly struct TransformAttempt8x8 + { + /// + /// Initializes a candidate with the existing native search settings. + /// + /// The candidate transform. + /// The largest encoding speed tier that considers this candidate. + /// The native entropy weight before normalization against the DCT candidate. + public TransformAttempt8x8(JxlAcStrategyType type, int encodingSpeedTierMaxLimit, double entropyMultiplier) + { + this.Type = type; + this.EncodingSpeedTierMaxLimit = encodingSpeedTierMaxLimit; + this.EntropyMultiplier = entropyMultiplier; + } + + /// + /// Gets the candidate transform. + /// + public JxlAcStrategyType Type { get; } + + /// + /// Gets the largest encoding speed tier that considers this candidate. + /// + public int EncodingSpeedTierMaxLimit { get; } + + /// + /// Gets the native entropy weight before normalization against the DCT candidate. + /// + public double EntropyMultiplier { get; } + } + + /// + /// Describes one multiblock merge candidate in the existing sequential search. /// - /// - /// The transform type for this model. - /// - /// - /// If the encoding speed tier is higher (indicating - /// higher speeds but worse compression) than this - /// value, the attempt shall be ignored as it'll be - /// too slow. - /// - /// - /// Multiplication for the entropy. - /// - private readonly record struct TransformAttempt8x8( - JxlAcStrategyType Type, - int EncodingSpeedTierMaxLimit, - double EntropyMultiplier); - - private readonly record struct MergeAttempt( - JxlAcStrategyType Type, - byte Priority, - byte DecodingSpeedTierMaxLimit, - byte EncodingSpeedTierMaxLimit, - float EntropyMultiplier); + private readonly struct MergeAttempt + { + /// + /// Initializes a candidate with the existing native search settings. + /// + /// The candidate transform. + /// The priority used to prevent competing merges from overlapping. + /// The largest decoding speed tier that considers this candidate. + /// The encoding speed limit stored in the native candidate table. + /// The multiplier applied to the estimated entropy. + public MergeAttempt(JxlAcStrategyType type, byte priority, byte decodingSpeedTierMaxLimit, byte encodingSpeedTierMaxLimit, float entropyMultiplier) + { + this.Type = type; + this.Priority = priority; + this.DecodingSpeedTierMaxLimit = decodingSpeedTierMaxLimit; + this.EncodingSpeedTierMaxLimit = encodingSpeedTierMaxLimit; + this.EntropyMultiplier = entropyMultiplier; + } + + /// + /// Gets the candidate transform. + /// + public JxlAcStrategyType Type { get; } + + /// + /// Gets the priority used to prevent competing merges from overlapping. + /// + public byte Priority { get; } + + /// + /// Gets the largest decoding speed tier that considers this candidate. + /// + public byte DecodingSpeedTierMaxLimit { get; } + + /// + /// Gets the encoding speed limit stored in the native candidate table. + /// + public byte EncodingSpeedTierMaxLimit { get; } + + /// + /// Gets the multiplier applied to the estimated entropy. + /// + public float EntropyMultiplier { get; } + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlForwardCoefficientOrder.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlForwardCoefficientOrder.cs index 26a210e6fd..5a7b3a9f4f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlForwardCoefficientOrder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlForwardCoefficientOrder.cs @@ -5,20 +5,43 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +/// +/// Defines the canonical coefficient orientation shared by transform storage and natural ordering. +/// internal static class JxlForwardCoefficientOrder { + /// + /// The number of coefficient-order families used by the transform strategies. + /// public const byte OrderCount = 13; + /// + /// Returns the shorter axis for the canonical coefficient rows. + /// + /// The original row extent, in the caller's coordinate units. + /// The original column extent, in the same units. + /// The shorter extent. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int CoefficientRows(int rows, int columns) => rows < columns ? rows : columns; + /// + /// Returns the longer axis for the canonical coefficient columns. + /// + /// The original row extent, in the caller's coordinate units. + /// The original column extent, in the same units. + /// The longer extent. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int CoefficientColumns(int rows, int columns) => rows < columns ? columns : rows; + /// + /// Sorts both original extents into canonical coefficient orientation without losing the longer axis. + /// + /// The original row extent, replaced with the shorter extent. + /// The original column extent, replaced with the longer extent. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void CoefficientLayout(ref int rows, ref int columns) { - rows = CoefficientRows(rows, columns); - columns = CoefficientColumns(rows, columns); + // Both outputs depend on the original dimensions. Sorting them in one assignment retains the longer axis. + (rows, columns) = (CoefficientRows(rows, columns), CoefficientColumns(rows, columns)); } } diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/CoefficientOrderTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/CoefficientOrderTests.cs index 46ba83994e..64ea0d08fb 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/CoefficientOrderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/CoefficientOrderTests.cs @@ -4,6 +4,7 @@ using System.Buffers; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Jxl.Processing; +using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; @@ -15,6 +16,110 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; /// public class CoefficientOrderTests { + /// + /// Checks that canonical orientation retains both original axes, including when the first one is longer. + /// + /// The original row extent. + /// The original column extent. + [Theory] + [InlineData(1, 1)] + [InlineData(1, 2)] + [InlineData(2, 1)] + [InlineData(2, 4)] + [InlineData(4, 2)] + [InlineData(16, 32)] + [InlineData(32, 16)] + [InlineData(32, 32)] + public void CoefficientLayout_PreservesBothOriginalAxes(int rows, int columns) + { + int expectedRows = Math.Min(rows, columns); + int expectedColumns = Math.Max(rows, columns); + JxlForwardCoefficientOrder.CoefficientLayout(ref rows, ref columns); + Assert.Equal(expectedRows, rows); + Assert.Equal(expectedColumns, columns); + } + + /// + /// Checks stored natural-order vectors from libjxl and jxl-rs, including both spatial orientations of a rectangular transform. + /// + /// The transform whose canonical coefficient order is checked. + [Theory] + [InlineData(JxlAcStrategyType.DCT)] + [InlineData(JxlAcStrategyType.DCT8X16)] + [InlineData(JxlAcStrategyType.DCT16X8)] + public void NaturalOrder_MatchesStoredCanonicalOrders(JxlAcStrategyType type) + { + // These fixed numeric vectors are also stored in jxl-rs frame/coeff_order.rs. Its natural-order algorithm agrees with libjxl ac_strategy.cc. + // The first square diagonal visits column one before row one; rectangular orders retain the complete longer coefficient axis. + ReadOnlySpan expected = type == JxlAcStrategyType.DCT + ? + [ + 0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, + 12, 19, 26, 33, 40, 48, 41, 34, 27, 20, 13, 6, 7, 14, 21, 28, + 35, 42, 49, 56, 57, 50, 43, 36, 29, 22, 15, 23, 30, 37, 44, 51, + 58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63 + ] + : + [ + 0, 1, 16, 2, 3, 17, 32, 18, 4, 5, 19, 33, 48, 34, 20, 6, + 7, 21, 35, 49, 64, 50, 36, 22, 8, 9, 23, 37, 51, 65, 80, 66, + 52, 38, 24, 10, 11, 25, 39, 53, 67, 81, 96, 82, 68, 54, 40, 26, + 12, 13, 27, 41, 55, 69, 83, 97, 112, 98, 84, 70, 56, 42, 28, 14, + 15, 29, 43, 57, 71, 85, 99, 113, 114, 100, 86, 72, 58, 44, 30, 31, + 45, 59, 73, 87, 101, 115, 116, 102, 88, 74, 60, 46, 47, 61, 75, 89, + 103, 117, 118, 104, 90, 76, 62, 63, 77, 91, 105, 119, 120, 106, 92, 78, + 79, 93, 107, 121, 122, 108, 94, 95, 109, 123, 124, 110, 111, 125, 126, 127 + ]; + + JxlAcStrategy strategy = new(type); + int[] actual = new int[expected.Length]; + strategy.ComputeNaturalCoefficientOrder(actual); + for (int i = 0; i < expected.Length; i++) + { + Assert.Equal(expected[i], actual[i]); + } + } + + /// + /// Checks every valid strategy's complete order, low-frequency rectangle, inverse lookup and output guards. + /// + [Fact] + public void NaturalOrderAndLookup_CoverEveryCoefficient() + { + for (int raw = 0; raw < JxlAcStrategy.NumberOfValidStrategies; raw++) + { + JxlAcStrategy strategy = new(raw); + int blocksX = Math.Max(strategy.CoveredBlocksX, strategy.CoveredBlocksY); + int blocksY = Math.Min(strategy.CoveredBlocksX, strategy.CoveredBlocksY); + int area = blocksX * blocksY * 64; + int[] order = new int[area + 2]; + int[] lookup = new int[area + 2]; + order.AsSpan().Fill(-1); + lookup.AsSpan().Fill(-1); + strategy.ComputeNaturalCoefficientOrder(order.AsSpan(1, area)); + strategy.ComputeNaturalCoefficientOrderLookup(lookup.AsSpan(1, area)); + + for (int i = 0; i < area; i++) + { + int coefficient = order[1 + i]; + Assert.InRange(coefficient, 0, area - 1); + Assert.Equal(i, lookup[1 + coefficient]); + } + + // DC coefficients of the covered eight-by-eight blocks occupy the top-left frequency rectangle in row-major order. + for (int i = 0; i < blocksX * blocksY; i++) + { + int expected = ((i / blocksX) * blocksX * 8) + (i % blocksX); + Assert.Equal(expected, order[1 + i]); + } + + Assert.Equal(-1, order[0]); + Assert.Equal(-1, order[^1]); + Assert.Equal(-1, lookup[0]); + Assert.Equal(-1, lookup[^1]); + } + } + /// /// Number of swaps. See /// and . From fd5b336a5be5985731b0438a8debb8a6a3188448 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Oct 2026 03:02:25 +1000 Subject: [PATCH 300/328] Build JPEG XL quantization weights directly in final storage --- .../Quantization/JxlQuantWeights.cs | 97 +++++++++++++------ .../Jxl/Processing/QuantizationTests.cs | 83 ++++++++++++++++ 2 files changed, 152 insertions(+), 28 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantWeights.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantWeights.cs index f2cf945535..9b574d90e4 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantWeights.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantWeights.cs @@ -27,7 +27,9 @@ internal static class JxlQuantWeights public const int Log2NumQuantModes = 3; + // The format rejects quantization weights outside this reciprocal-safe interval. private const float AlmostZero = 1e-8f; + private const float MaximumWeight = 1F / AlmostZero; /// /// DCT quantizer encoding. (6 distance bands) @@ -824,12 +826,11 @@ public static bool GetQuantWeights(int rowCount, int columnCount, float[][] dist } /// - /// Builds forward and inverse quantization tables for one transform. + /// Builds dequantization weights and their reciprocals for one transform in the caller's final storage. /// /// The quantization weight parameters. - /// The destination forward weights. - /// The destination inverse weights. - /// The table index. This implementation does not yet use it. + /// The destination dequantization weights. Each entry is the reciprocal of the generated weight. + /// The destination quantization weights. Low-frequency entries are cleared after preparation. /// The transform table kind. /// The starting destination offset, advanced past the written weights. /// Whether the encoding supports the selected transform. @@ -837,7 +838,6 @@ public static bool ComputeQuantTable( JxlQuantizerEncoding encoding, Span quantizationTable, Span inverseQuantizationTable, - int tableIndex, JxlQuantTable kind, ref int writeOffset) { @@ -848,7 +848,9 @@ public static bool ComputeQuantTable( int wcols = 8 * JxlDequantMatrices.RequiredSizeY[quantSizeTable]; int num = wrows * wcols; - Span weights = stackalloc float[3 * num]; + // Generate all three channel planes directly in their final allocation. The largest transform has 196,608 entries. + // Keeping this view avoids a proportional stack allocation and subsequent copies of the complete table. + Span weights = inverseQuantizationTable.Slice(writeOffset, 3 * num); switch (encoding.Mode) { @@ -1074,32 +1076,16 @@ public static bool ComputeQuantTable( break; } + + default: + return false; } int prevPos = writeOffset; - // Don't zero-init - Span invVal = stackalloc float[64]; - Span val = stackalloc float[64]; - - for (int i = 0; i < num * 3; i += 64) - { - weights.Slice(i, 64).CopyTo(invVal); - - // TODO: there's an unlikely check right here in - // reference: - // if (JXL_UNLIKELY(!AllFalse(d, Ge(inv_val, Set(d, 1.0f / kAlmostZero))) || - // !AllFalse(d, Lt(inv_val, Set(d, kAlmostZero))))) - // { - // throw new InvalidOperationException("Invalid quantization table"); - // } - // should we trade performance for an unlikely check? - val.Fill(1.0f); - TensorPrimitives.Divide(val, invVal, val); - - val.CopyTo(quantizationTable.Slice(writeOffset + i, 64)); - invVal.CopyTo(inverseQuantizationTable.Slice(writeOffset + i, 64)); - } + // libjxl quant_weights.cc validates this interval before division. Validate and invert together; + // no temporary table, fill with ones, or extra validation pass is required. Each lane owns one coefficient. + TensorPrimitives_.Invoke(weights, quantizationTable.Slice(writeOffset, weights.Length), default(ReciprocalWeightOperator)); writeOffset += 3 * num; @@ -1122,6 +1108,61 @@ public static bool ComputeQuantTable( return true; } + /// + /// Validates generated weights and returns their reciprocals. Preparation rejects invalid weights instead of clipping them. + /// + private readonly struct ReciprocalWeightOperator : TensorPrimitives_.IUnaryOperator + { + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float Invoke(float value) + { + // Ordered comparisons also reject NaN. Positive finite weights in [1e-8,1e8) have finite positive reciprocals. + if (!(value >= AlmostZero && value < MaximumWeight)) + { + throw new InvalidImageContentException("The quantization weight is outside the supported range."); + } + + return 1F / value; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector128 Invoke(Vector128 value) + { + if (!(Vector128.GreaterThanOrEqualAll(value, Vector128.Create(AlmostZero)) && Vector128.LessThanAll(value, Vector128.Create(MaximumWeight)))) + { + throw new InvalidImageContentException("The quantization weight is outside the supported range."); + } + + return Vector128.One / value; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector256 Invoke(Vector256 value) + { + if (!(Vector256.GreaterThanOrEqualAll(value, Vector256.Create(AlmostZero)) && Vector256.LessThanAll(value, Vector256.Create(MaximumWeight)))) + { + throw new InvalidImageContentException("The quantization weight is outside the supported range."); + } + + return Vector256.One / value; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector512 Invoke(Vector512 value) + { + if (!(Vector512.GreaterThanOrEqualAll(value, Vector512.Create(AlmostZero)) && Vector512.LessThanAll(value, Vector512.Create(MaximumWeight)))) + { + throw new InvalidImageContentException("The quantization weight is outside the supported range."); + } + + return Vector512.One / value; + } + } + public static bool DecodeDctParameters(JxlBitReader reader, JxlDctQuantWeightParameters parameters) { parameters.NumDistanceBands = (int)reader.ReadBits32(JxlQuantizerConstants.Log2MaxDistanceBands) + 1; diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/QuantizationTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/QuantizationTests.cs index b1c59c7279..bfc00feef2 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/QuantizationTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/QuantizationTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; using SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; using SixLabors.ImageSharp.Tests.TestUtilities; @@ -8,6 +9,88 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; public class QuantizationTests { + /// + /// Checks final table layout, reciprocal weights, low-frequency exclusion and output guards at every SIMD width. + /// + [Fact] + public void QuantizationTables_WriteReciprocalsAndExcludeLowFrequencies() + { + static void RunTest() + { + JxlQuantizerEncoding encoding = JxlQuantizerEncoding.Dct(new JxlDctQuantWeightParameters([[2F], [4F], [8F]], 1)); + ReadOnlySpan kinds = [JxlQuantTable.DCT, JxlQuantTable.DCT8X16, JxlQuantTable.DCT16X16, JxlQuantTable.DCT256X256]; + + foreach (JxlQuantTable kind in kinds) + { + int blocksX = JxlDequantMatrices.RequiredSizeX[(int)kind]; + int blocksY = JxlDequantMatrices.RequiredSizeY[(int)kind]; + int channelLength = blocksX * blocksY * 64; + int length = 3 * channelLength; + float[] table = new float[length + 2]; + float[] inverse = new float[length + 2]; + table.AsSpan().Fill(-77F); + inverse.AsSpan().Fill(-77F); + int offset = 1; + Assert.True(JxlQuantWeights.ComputeQuantTable(encoding, table, inverse, kind, ref offset)); + Assert.Equal(length + 1, offset); + + // One distance band gives constant weights [2,4,8]. Their exact reciprocals are [.5,.25,.125]. + // The top-left covered-block rectangle is excluded only from AC quantization, as specified by the native table layout. + int columns = 8 * Math.Max(blocksX, blocksY); + int lowRows = Math.Min(blocksX, blocksY); + int lowColumns = Math.Max(blocksX, blocksY); + for (int c = 0; c < 3; c++) + { + float weight = 2 << c; + int start = 1 + (c * channelLength); + Assert.Equal(-1, table.AsSpan(start, channelLength).IndexOfAnyExcept(1F / weight)); + for (int row = 0; row < channelLength / columns; row++) + { + Span values = inverse.AsSpan(start + (row * columns), columns); + int excluded = row < lowRows ? lowColumns : 0; + Assert.Equal(-1, values[..excluded].IndexOfAnyExcept(0F)); + Assert.Equal(-1, values[excluded..].IndexOfAnyExcept(weight)); + } + } + + Assert.Equal(-77F, table[0]); + Assert.Equal(-77F, table[^1]); + Assert.Equal(-77F, inverse[0]); + Assert.Equal(-77F, inverse[^1]); + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } + + /// + /// Checks that invalid generated weights cannot publish a completed table at any SIMD width. + /// + [Fact] + public void QuantizationTables_RejectNonfiniteAndOutOfRangeWeights() + { + static void RunTest() + { + ReadOnlySpan invalidWeights = [1E8F, float.PositiveInfinity, float.NaN]; + foreach (float invalid in invalidWeights) + { + // A one-band table reaches the reciprocal validation directly. The third channel must still be checked. + JxlQuantizerEncoding encoding = JxlQuantizerEncoding.Dct(new JxlDctQuantWeightParameters([[2F], [4F], [invalid]], 1)); + float[] table = new float[3 * 64]; + float[] inverse = new float[3 * 64]; + int offset = 0; + Assert.Throws(() => JxlQuantWeights.ComputeQuantTable(encoding, table, inverse, JxlQuantTable.DCT, ref offset)); + Assert.Equal(0, offset); + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } + /// /// Checks the native default band count and the rectangular transform's predefined table selection. /// From 9b1a753cfd5d529233b133f9d4ec8fdc8f8d7a05 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Oct 2026 03:03:12 +1000 Subject: [PATCH 301/328] Correct JPEG XL perceptual comparison planes and frequency weights --- .../Jxl/Processing/Butteraugli/Butteraugli.cs | 132 ++++++++++----- .../Butteraugli/ButteraugliComparator.cs | 5 +- .../Jxl/Processing/ButteraugliTests.cs | 150 ++++++++++++++++++ 3 files changed, 248 insertions(+), 39 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs index 7825a176cd..e009869a45 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs @@ -1293,6 +1293,18 @@ private static float PaddedMaltaUnit(ReadOnlySpan diffs, int width, int h return isLf ? MaltaUnitLF(borderImage, center, paddedStride)[0] : MaltaUnit(borderImage, center, paddedStride)[0]; } + /// + /// Accumulates a Malta difference with the native scaling for its frequency range. + /// + /// Whether to use the low-frequency Malta neighborhood and scaling. + /// The borrowed reference band. + /// The borrowed distorted band. + /// The penalty for the first error direction. + /// The penalty for the opposite error direction. + /// The band-specific normalization. + /// The preallocated difference workspace. + /// The existing AC error accumulation plane. + /// Whether the input and workspace dimensions match. public static bool MaltaDiffMap( bool isLf, JxlPlane lum0, @@ -1302,22 +1314,10 @@ public static bool MaltaDiffMap( float norm1, JxlPlane diffs, JxlPlane blockDiffAc) - { - if (isLf) - { - const float len = 3.75f; - const float mulli = 0.39905817637f; - - return MaltaDiffMap(true, lum0, lum1, w0Gt1, w0Lt1, norm1, len, mulli, diffs, blockDiffAc); - } - else - { - const float len = 3.75f; - const float mulli = 0.611612573796f; - - return MaltaDiffMap(false, lum0, lum1, w0Gt1, w0Lt1, norm1, len, mulli, diffs, blockDiffAc); - } - } + // Keep neighborhood selection and its normalization together in the existing frequency-specific entry points. + => isLf + ? MaltaDiffMapLf(lum0, lum1, w0Gt1, w0Lt1, norm1, diffs, blockDiffAc) + : MaltaDiffMap(lum0, lum1, w0Gt1, w0Lt1, norm1, diffs, blockDiffAc); public static bool MaltaDiffMap( bool isLf, @@ -2090,6 +2090,59 @@ public static void L2DiffAsymmetric(JxlPlane i0, JxlPlane i1, floa } } + /// + /// Applies the native low- and high-frequency masks to independently accumulated squared errors. + /// + private readonly struct MaskedDifferenceOperator : TensorPrimitives_.ITernaryOperator + { + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float Invoke(float first, float second, float third) + => MathF.Sqrt((second * MaskDcY(first)) + (third * MaskY(first))); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector128 Invoke(Vector128 first, Vector128 second, Vector128 third) + { + // The mask plane controls sensitivity; second and third retain separate DC and AC squared-error totals. + Vector128 dcMask = Vector128.Create(MaskDcYMultiplier) / ((Vector128.Create(MaskDcYScaler) * first) + Vector128.Create(MaskDcYOffset)); + Vector128 acMask = Vector128.Create(MaskYMultiplier) / ((Vector128.Create(MaskYScaler) * first) + Vector128.Create(MaskYOffset)); + dcMask = Vector128.Create(GlobalScale) * (Vector128.One + dcMask); + acMask = Vector128.Create(GlobalScale) * (Vector128.One + acMask); + dcMask *= dcMask; + acMask *= acMask; + return Vector128.Sqrt((second * dcMask) + (third * acMask)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector256 Invoke(Vector256 first, Vector256 second, Vector256 third) + { + // The mask plane controls sensitivity; second and third retain separate DC and AC squared-error totals. + Vector256 dcMask = Vector256.Create(MaskDcYMultiplier) / ((Vector256.Create(MaskDcYScaler) * first) + Vector256.Create(MaskDcYOffset)); + Vector256 acMask = Vector256.Create(MaskYMultiplier) / ((Vector256.Create(MaskYScaler) * first) + Vector256.Create(MaskYOffset)); + dcMask = Vector256.Create(GlobalScale) * (Vector256.One + dcMask); + acMask = Vector256.Create(GlobalScale) * (Vector256.One + acMask); + dcMask *= dcMask; + acMask *= acMask; + return Vector256.Sqrt((second * dcMask) + (third * acMask)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector512 Invoke(Vector512 first, Vector512 second, Vector512 third) + { + // The mask plane controls sensitivity; second and third retain separate DC and AC squared-error totals. + Vector512 dcMask = Vector512.Create(MaskDcYMultiplier) / ((Vector512.Create(MaskDcYScaler) * first) + Vector512.Create(MaskDcYOffset)); + Vector512 acMask = Vector512.Create(MaskYMultiplier) / ((Vector512.Create(MaskYScaler) * first) + Vector512.Create(MaskYOffset)); + dcMask = Vector512.Create(GlobalScale) * (Vector512.One + dcMask); + acMask = Vector512.Create(GlobalScale) * (Vector512.One + acMask); + dcMask *= dcMask; + acMask *= acMask; + return Vector512.Sqrt((second * dcMask) + (third * acMask)); + } + } + /// /// Accumulates a weighted squared difference without changing the subtraction or multiplication order. /// @@ -2507,6 +2560,15 @@ public static bool OpsinDynamicsImage( return true; } + /// + /// Compares two linear RGB images while replacing their samples with intermediate frequency bands. + /// + /// The configuration used for temporary comparison storage. + /// The reference samples, overwritten during comparison. + /// The distorted samples, overwritten during comparison. + /// The perceptual comparison parameters. + /// The preallocated output plane, with the same dimensions as the inputs. + /// Whether all comparison stages succeeded. public static bool ButteraugliDiffmapInPlace( Configuration configuration, JxlImage3 image0, @@ -2717,34 +2779,30 @@ public static bool ButteraugliDiffmapInPlace( DeallocateHFAndUHF(hf0, uhf0); DeallocateHFAndUHF(hf1, uhf1); - using JxlPlane newBlockDiffAc = new(configuration, xSize, ySize); - if (!Mask(configuration, mask0, mask1, blurTemp, mask, newBlockDiffAc, image0.Plane(0), image0.Plane(1), image0.Plane(2), image1.Plane(0))) + // Native Mask adds its correction to the accumulated AC errors and writes the mask separately. + // DC errors remain independent: the final equation applies a different mask to each frequency range. + if (!Mask(configuration, mask0, mask1, blurTemp, blockDiffAc, mask, image0.Plane(0), image0.Plane(1), image0.Plane(2), image1.Plane(0))) { return false; } - { - ReadOnlySpan combinedSamples = newBlockDiffAc.GetRow(0); - Span outputSamples = diffmap.GetRow(0); - ReadOnlySpan maskSamples = mask.GetRow(0); - - for (int y = 0; y < ySize; y++) - { - ReadOnlySpan dc = combinedSamples[(y * newBlockDiffAc.PixelsPerRow)..]; - ReadOnlySpan ac = dc; - Span output = outputSamples[(y * diffmap.PixelsPerRow)..]; - ReadOnlySpan maskRow = maskSamples[(y * mask.PixelsPerRow)..]; - - for (int x = 0; x < xSize; x++) - { - float m = maskRow[x]; + ReadOnlySpan maskSamples = mask.GetRow(0); + ReadOnlySpan dcSamples = blockDiffDc.GetRow(0); + ReadOnlySpan acSamples = blockDiffAc.GetRow(0); + Span outputSamples = diffmap.GetRow(0); - output[x] = MathF.Sqrt((dc[x] * (float)MaskDcY(m)) + (ac[x] * (float)MaskY(m))); - } - } + for (int y = 0; y < ySize; y++) + { + ReadOnlySpan maskRow = maskSamples.Slice(y * mask.PixelsPerRow, xSize); + ReadOnlySpan dcRow = dcSamples.Slice(y * blockDiffDc.PixelsPerRow, xSize); + ReadOnlySpan acRow = acSamples.Slice(y * blockDiffAc.PixelsPerRow, xSize); + Span outputRow = outputSamples.Slice(y * diffmap.PixelsPerRow, xSize); - return true; + // Each lane is one pixel. The shared traversal selects 512-, 256-, 128-bit or scalar arithmetic and handles row tails. + TensorPrimitives_.Invoke(maskRow, dcRow, acRow, outputRow, default(MaskedDifferenceOperator)); } + + return true; } /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs index 6e72110ec3..0b3a39bc20 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs @@ -283,10 +283,11 @@ public bool DiffmapPsychoImage(Configuration configuration, ButteraugliPsychoIma return false; } + // Native MF comparison uses its MF weight in both directions. HF asymmetry applies only to the HF and UHF bands. if (!Butteraugli.MaltaDiffMapLf( this.pi0.Mf.Plane(1), pi1.Mf.Plane(1), - Butteraugli.WHfMalta, + Butteraugli.WMfMalta, Butteraugli.WMfMalta, Butteraugli.Norm1Mf, diffs, @@ -299,7 +300,7 @@ public bool DiffmapPsychoImage(Configuration configuration, ButteraugliPsychoIma if (!Butteraugli.MaltaDiffMapLf( this.pi0.Mf.Plane(0), pi1.Mf.Plane(0), - Butteraugli.WHfMaltaX, + Butteraugli.WMfMaltaX, Butteraugli.WMfMaltaX, Butteraugli.Norm1MfX, diffs, diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs index 1b7643918a..1bc023ccee 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs @@ -6,6 +6,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using SixLabors.ImageSharp.Tests.Memory; using SixLabors.ImageSharp.Tests.TestUtilities; namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; @@ -175,6 +176,155 @@ static void RunTest(string serialized) HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); } + /// + /// Checks that standalone comparison reads initialized DC and AC errors and leaves output padding unchanged. + /// + /// The row width, including SIMD tails. + [Theory] + [InlineData(17)] + [InlineData(33)] + public void InPlaceComparison_IdenticalSamplesHaveZeroDifference(int width) + { + static void RunTest(string serialized) + { + int sampleWidth = FeatureTestRunner.Deserialize(serialized); + const int height = 8; + const float padding = -123F; + Configuration configuration = TestEnvironment.Configuration.Clone(); + configuration.MemoryAllocator = new TestMemoryAllocator(); + using JxlImage3 reference = new(configuration, sampleWidth, height); + using JxlImage3 distorted = new(configuration, sampleWidth, height); + using JxlPlane difference = new(configuration, sampleWidth, height); + JxlImageOperations.ZeroFillImage(reference); + JxlImageOperations.ZeroFillImage(distorted); + Span result = difference.GetRow(0); + result.Fill(padding); + + // Equal samples have zero error in every frequency band. Dirty allocations expose an uninitialized AC result. + Assert.True(Butteraugli.ButteraugliDiffmapInPlace(configuration, reference, distorted, new ButteraugliParameters(), difference)); + + for (int y = 0; y < height; y++) + { + ReadOnlySpan row = result.Slice(y * difference.PixelsPerRow, difference.PixelsPerRow); + for (int x = 0; x < sampleWidth; x++) + { + Assert.Equal(0F, row[x]); + } + + for (int x = sampleWidth; x < row.Length; x++) + { + Assert.Equal(padding, row[x]); + } + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + width, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } + + /// + /// Checks the retained comparison's middle-frequency orchestration against the native band weights and scaling. + /// + /// The X or Y middle-frequency component to change. + [Theory] + [InlineData(0)] + [InlineData(1)] + public void PsychoComparison_MiddleFrequencyUsesNativeWeights(int component) + { + static void RunTest(string serialized) + { + int changedComponent = FeatureTestRunner.Deserialize(serialized); + const int width = 33; + const int height = 8; + Configuration configuration = TestEnvironment.Configuration; + ButteraugliParameters parameters = new(); + using JxlImage3 reference = new(configuration, width, height); + JxlImageOperations.ZeroFillImage(reference); + using ButteraugliComparator comparator = ButteraugliComparator.Make(configuration, reference, parameters); + using ButteraugliBlurTemp blur = new(configuration, width, height); + using JxlImage3 blurred = new(configuration, width, height); + using JxlImage3 transformed = new(configuration, width, height); + using ButteraugliPsychoImage referenceBands = new(); + using ButteraugliPsychoImage distortedBands = new(); + referenceBands.Allocate(configuration, width, height); + distortedBands.Allocate(configuration, width, height); + Assert.True(Butteraugli.OpsinDynamicsImage(configuration, reference, parameters, blurred, blur, transformed)); + Assert.True(Butteraugli.SeparateFrequencies(configuration, blur, transformed, referenceBands)); + JxlImageOperations.CopyImage(referenceBands.Lf, distortedBands.Lf); + JxlImageOperations.CopyImage(referenceBands.Mf, distortedBands.Mf); + for (int c = 0; c < 2; c++) + { + JxlImageOperations.CopyImage(referenceBands.Hf[c], distortedBands.Hf[c]); + JxlImageOperations.CopyImage(referenceBands.Uhf[c], distortedBands.Uhf[c]); + } + + // Alter only one MF band. LF, HF and UHF remain identical, so their errors and mask correction are zero. + Span middleSamples = distortedBands.Mf.Plane(changedComponent).GetRow(0); + int middleStride = distortedBands.Mf.Plane(changedComponent).PixelsPerRow; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + middleSamples[(y * middleStride) + x] += ((x % 3) - 1) * 0.125F; + } + } + + using JxlImage3 dc = new(configuration, width, height); + using JxlImage3 ac = new(configuration, width, height); + using JxlPlane scratch = new(configuration, width, height); + using JxlPlane mask = new(configuration, width, height); + using JxlPlane expected = new(configuration, width, height); + using JxlPlane actual = new(configuration, width, height); + JxlImageOperations.ZeroFillImage(dc); + JxlImageOperations.ZeroFillImage(ac); + + // Native butteraugli.cc assigns these MF penalties to both error directions, independently of HF weights. + // The named single-plane LF kernel owns native scaling 0.611612573796; the retained API must select that same kernel. + float maltaWeight = changedComponent == 0 ? 8246.75321353F : 37.0819870399F; + float normalization = changedComponent == 0 ? 1009002.70582F : 130262059.556F; + float squaredErrorWeight = changedComponent == 0 ? 2150F : 10.6195433239F; + Assert.True(Butteraugli.MaltaDiffMapLf( + referenceBands.Mf.Plane(changedComponent), + distortedBands.Mf.Plane(changedComponent), + maltaWeight, + maltaWeight, + normalization, + scratch, + ac.Plane(changedComponent))); + + Butteraugli.L2Diff( + referenceBands.Mf.Plane(changedComponent), + distortedBands.Mf.Plane(changedComponent), + squaredErrorWeight, + ac.Plane(changedComponent)); + + Assert.True(comparator.Mask(configuration, mask)); + Assert.True(Butteraugli.CombineChannelsToDiffmap(mask, dc, ac, 1F, expected)); + Assert.True(comparator.DiffmapPsychoImage(configuration, distortedBands, actual)); + ReadOnlySpan expectedSamples = expected.GetRow(0); + ReadOnlySpan actualSamples = actual.GetRow(0); + bool hasError = false; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + float expectedValue = expectedSamples[(y * expected.PixelsPerRow) + x]; + Assert.Equal(expectedValue, actualSamples[(y * actual.PixelsPerRow) + x]); + hasError |= expectedValue > 0; + } + } + + Assert.True(hasError); + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + component, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } + private static JxlImage3 SinglePixelImage(float r, float g, float b) { JxlImage3 img = new(TestEnvironment.Configuration, 1, 1); From daaa14148f97c26bea294788a39eb1876a756119 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Oct 2026 03:14:33 +1000 Subject: [PATCH 302/328] Own JPEG XL matrix storage and pass prepared spans through transform loops --- .../Decoder/Group/JxlGroupDecoder.cs | 6 +- .../JxlAcStrategyEncoder.Entropy.cs | 6 +- .../AcStrategy/JxlAcStrategyEncoder.cs | 49 +++ .../JxlChromaFromLumaEncoder.cs | 5 +- .../Heuristics/JxlAcStrategyHeuristics.cs | 6 +- .../Jxl/Processing/Encoder/JxlGroupEncoder.cs | 46 ++- .../Encoder/JxlQuantizerWeightsEncoder.cs | 27 +- .../Jxl/Processing/JxlPassesCommonContext.cs | 5 +- .../Quantization/JxlDequantMatrices.cs | 285 ++++++++++-------- .../Quantization/JxlQuantWeights.cs | 12 +- .../Processing/Quantization/JxlQuantizer.cs | 10 +- .../Jxl/Processing/QuantizationTests.cs | 127 ++++++++ 12 files changed, 425 insertions(+), 159 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs index 700f327b2c..6de2b3bd93 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs @@ -46,6 +46,7 @@ public static void Transpose8x8InPlace(Span block) /// The transform strategy. /// The coefficient count per channel. /// The prepared frame quantizer and matrices. + /// The complete coefficient tables borrowed before the group loops. /// The number of 8-by-8 blocks covered by the transform. /// The DC image's horizontal offset for each channel. /// The current DC samples. @@ -67,6 +68,7 @@ private static void DequantBlock( JxlAcStrategyType kind, int size, JxlQuantizer quantizer, + ReadOnlySpan matrixStorage, int coveredBlocks, Span sbx, Span dcRow, @@ -81,7 +83,7 @@ private static void DequantBlock( float inverseQuantization = invGlobalScale / quant; float xScale = inverseQuantization * xDmMultiplier; float bScale = inverseQuantization * bDmMultiplier; - ReadOnlySpan matrices = quantizer.DequantMatrix(kind, 0); + ReadOnlySpan matrices = quantizer.DequantMatrix(matrixStorage, kind, 0); int coefficientCount = coveredBlocks * JxlFrameDimensions.DctBlockSize; // The matrix table stores X, Y and B consecutively for this transform. Only logical coefficients participate. @@ -184,7 +186,7 @@ private static bool DecodeGroupCore( JxlFrameHeader.JpegOrder(frameHeader.ColorTransform, jpegIsGray, out jpegCMap); - Span qe = decState.Shared.Matrices.GetEncodings(); + ReadOnlySpan qe = decState.Shared.Matrices.GetEncodings(); if (qe.Length == 0 || qe[0].Mode != JxlQuantMode.Raw || diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyEncoder.Entropy.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyEncoder.Entropy.cs index 35323cfc01..f2a7b73438 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyEncoder.Entropy.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyEncoder.Entropy.cs @@ -31,6 +31,7 @@ internal static partial class JxlAcStrategyEncoder /// The source pixel column of the candidate's top-left corner. /// The source pixel row of the candidate's top-left corner. /// The borrowed source, quantization, mask and matrix storage, with their physical strides. + /// The prepared coefficient tables borrowed before the search loops. /// The X, Y and B color-correlation factors. The Y factor is zero. /// Three packed coefficient regions, each large enough for this strategy's sample count. /// One maximum-area error region followed by the existing transform workspace. @@ -42,6 +43,7 @@ public static bool EstimateEntropy( int x, int y, JxlAcStrategyConfiguration config, + ReadOnlySpan matrixStorage, Span cmapFactors, Span block, Span fullScratchSpace, @@ -105,8 +107,8 @@ public static bool EstimateEntropy( for (int c = 0; c < 3; c++) { ReadOnlySpan coefficients = block.Slice(c * size, size); - ReadOnlySpan inverseMatrix = config.Dequant.GetInverseMatrix(acs.Strategy, c)[..size]; - ReadOnlySpan matrix = config.Dequant.GetMatrix(acs.Strategy, c)[..size]; + ReadOnlySpan inverseMatrix = config.Dequant.GetInverseMatrix(matrixStorage, acs.Strategy, c)[..size]; + ReadOnlySpan matrix = config.Dequant.GetMatrix(matrixStorage, acs.Strategy, c)[..size]; EntropyOperator operation = new(quantNorm, cmapFactors[c]); float coefficientCost = ComputeErrors(coefficients, luminance, inverseMatrix, matrix, error, operation, out int nonzero); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyEncoder.cs index 97c45394af..61d393931d 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AcStrategy/JxlAcStrategyEncoder.cs @@ -333,12 +333,25 @@ public static void SetEntropyForTransform(int cx, int cy, JxlAcStrategyType acsR entropyEstimate[(cy * 8) + cx] = entropy; } + /// Selects the lowest-cost transform for one eight-pixel square. + /// The source pixel column. + /// The source pixel row. + /// The encoder search speed tier. + /// The requested perceptual distance. + /// The borrowed source views and search weights. + /// The coefficient tables borrowed before the rectangle search. + /// The X, Y and B color-correlation factors. + /// The reusable three-channel coefficient workspace. + /// The reusable error and transform workspace. + /// The selected transform's cost. + /// The selected transform strategy. public static void FindBest8x8Transform( int x, int y, int encodingSpeedTier, float butteraugliTarget, JxlAcStrategyConfiguration config, + ReadOnlySpan matrixStorage, Span contextMapFactors, Span block, Span scratchSpace, @@ -386,6 +399,7 @@ public static void FindBest8x8Transform( x, y, config, + matrixStorage, contextMapFactors, block, scratchSpace, @@ -404,6 +418,23 @@ public static void FindBest8x8Transform( entropyOut = (float)best; } + /// Replaces an allowed block region when the candidate transform lowers its estimated cost. + /// The candidate strategy. + /// The rectangle's frame block column. + /// The rectangle's frame block row. + /// The candidate's relative block column. + /// The candidate's relative block row. + /// The borrowed source views and search weights. + /// The coefficient tables borrowed before the rectangle search. + /// The X, Y and B color-correlation factors. + /// The destination strategy map. + /// The candidate's fixed cost multiplier. + /// The priority used to prevent overlapping accepted transforms. + /// The rectangle's existing block priorities. + /// The cost stored for each rectangle block. + /// The reusable three-channel coefficient workspace. + /// The reusable error and transform workspace. + /// Whether candidate estimation completed. public static bool TryMergeAcStrategy( JxlAcStrategyType acsRaw, int bx, @@ -411,6 +442,7 @@ public static bool TryMergeAcStrategy( int cx, int cy, JxlAcStrategyConfiguration config, + ReadOnlySpan matrixStorage, Span cmapFactors, JxlAcStrategyImage acStrategy, float entropyMul, @@ -445,6 +477,7 @@ public static bool TryMergeAcStrategy( (bx + cx) * 8, (by + cy) * 8, config, + matrixStorage, cmapFactors, block, scratchSpace, @@ -486,6 +519,7 @@ public static bool TryMergeAcStrategy( /// The square's block column within the search rectangle. /// The square's block row within the search rectangle. /// The borrowed source views, matrices and search weights. + /// The complete coefficient tables borrowed before the rectangle search. /// The three channel correlation factors. /// The selected strategies to update. /// The existing entropy multiplier for divided candidates. @@ -502,6 +536,7 @@ public static bool FindBestFirstLevelDivisionForSquare( int cx, int cy, JxlAcStrategyConfiguration config, + ReadOnlySpan matrixStorage, Span cmapFactors, JxlAcStrategyImage acStrategy, float entropyMulJXK, @@ -557,6 +592,7 @@ public static bool FindBestFirstLevelDivisionForSquare( (bx + cx + 0) * 8, (by + cy + 0) * 8, config, + matrixStorage, cmapFactors, block, scratchSpace, @@ -574,6 +610,7 @@ public static bool FindBestFirstLevelDivisionForSquare( (bx + cx + blocksHalf) * 8, (by + cy + 0) * 8, config, + matrixStorage, cmapFactors, block, scratchSpace, @@ -594,6 +631,7 @@ public static bool FindBestFirstLevelDivisionForSquare( (bx + cx + 0) * 8, (by + cy + 0) * 8, config, + matrixStorage, cmapFactors, block, scratchSpace, @@ -611,6 +649,7 @@ public static bool FindBestFirstLevelDivisionForSquare( (bx + cx + 0) * 8, (by + cy + blocksHalf) * 8, config, + matrixStorage, cmapFactors, block, scratchSpace, @@ -629,6 +668,7 @@ public static bool FindBestFirstLevelDivisionForSquare( (bx + cx + 0) * 8, (by + cy + 0) * 8, config, + matrixStorage, cmapFactors, block, scratchSpace, @@ -706,6 +746,7 @@ public static bool FindBestFirstLevelDivisionForSquare( /// /// The fixed compression settings. /// The borrowed source views, matrices and search weights. + /// The complete coefficient tables borrowed before the rectangle search. /// The rectangle in block coordinates, at most eight by eight blocks. /// The frame's color-correlation maps. /// The borrowed float allocation: three coefficient regions followed by error and transform scratch. @@ -714,6 +755,7 @@ public static bool FindBestFirstLevelDivisionForSquare( public static bool ProcessAcStrategyRectangle( JxlCompressionParameters cparams, JxlAcStrategyConfiguration config, + ReadOnlySpan matrixStorage, Rectangle rect, JxlColorCorrelationMap cmap, Span block, @@ -763,6 +805,7 @@ public static bool ProcessAcStrategyRectangle( (int)cparams.SpeedTier, butteraugliTarget, config, + matrixStorage, cmapFactors, block, scratchSpace, @@ -826,6 +869,7 @@ public static bool ProcessAcStrategyRectangle( cx, cy, config, + matrixStorage, cmapFactors, acStrategy, mt.EntropyMultiplier, @@ -865,6 +909,7 @@ public static bool ProcessAcStrategyRectangle( cx, cy, config, + matrixStorage, cmapFactors, acStrategy, mt.EntropyMultiplier, @@ -905,6 +950,7 @@ public static bool ProcessAcStrategyRectangle( cx, cy, config, + matrixStorage, cmapFactors, acStrategy, mt.EntropyMultiplier, @@ -938,6 +984,7 @@ public static bool ProcessAcStrategyRectangle( cx, cy, config, + matrixStorage, cmapFactors, acStrategy, mt.EntropyMultiplier, @@ -972,6 +1019,7 @@ public static bool ProcessAcStrategyRectangle( cx, cy, config, + matrixStorage, cmapFactors, acStrategy, entropy_mul16X8, @@ -1005,6 +1053,7 @@ public static bool ProcessAcStrategyRectangle( cx, cy, config, + matrixStorage, cmapFactors, acStrategy, entropy_mul16X32, diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlChromaFromLumaEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlChromaFromLumaEncoder.cs index 14bebb77e8..d4588906b3 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlChromaFromLumaEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/ChromaFromLuma/JxlChromaFromLumaEncoder.cs @@ -133,6 +133,7 @@ public static void ComputeTile( JxlPlane dcValues, Span mem) { + ReadOnlySpan matrixStorage = dequant.GetPreparedStorage(); int widthBlocks = opsinRect.Width / JxlFrameDimensions.BlockDimensions; const float distanceMultiplierAC = 1e-9f; int dctScratchSize = JxlDct.MaximumWorkingSetLength; @@ -219,8 +220,8 @@ public static void ComputeTile( JxlTransformsEncoder.TransformFromPixels(acs.Strategy, rowB[(x * JxlFrameDimensions.BlockDimensions)..], stride, blockB, scratchSpace); JxlTransformsEncoder.DcFromLowestFrequencies(acs.Strategy, blockB, dcB, xs, scratchSpace); - Span qmX = dequant.GetInverseMatrix(acs.Strategy, 0); - Span qmB = dequant.GetInverseMatrix(acs.Strategy, 2); + ReadOnlySpan qmX = dequant.GetInverseMatrix(matrixStorage, acs.Strategy, 0); + ReadOnlySpan qmB = dequant.GetInverseMatrix(matrixStorage, acs.Strategy, 2); float qDcX = useDct8 ? 1 : 1.0f / quantizer.GetInverseDcStep(0); float qDcB = useDct8 ? 1 : 1.0f / quantizer.GetInverseDcStep(2); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlAcStrategyHeuristics.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlAcStrategyHeuristics.cs index 2fd7152783..57ca7f4a39 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlAcStrategyHeuristics.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlAcStrategyHeuristics.cs @@ -109,14 +109,14 @@ public bool Initialize( acsMask |= (uint)(1 << i); } - if (!matrices.EnsureComputed(this.Configuration, (int)acsMask)) + if (!matrices.EnsureComputed(this.Configuration, acsMask)) { return false; } } else { - if (!matrices.EnsureComputed(this.Configuration, 1)) + if (!matrices.EnsureComputed(this.Configuration, 1U)) { return false; } @@ -185,10 +185,12 @@ public bool ProcessRectangle(Rectangle rect, JxlColorCorrelationMap cmap, JxlAcS // Borrow the span once for this rectangle. The kernel reuses its regions for every candidate; none escape this call. Span scratch = this.scratch.Span; + ReadOnlySpan matrixStorage = this.Config.Dequant.GetPreparedStorage(); return JxlAcStrategyEncoder.ProcessAcStrategyRectangle( this.CompressionParameters, this.Config, + matrixStorage, rect, cmap, scratch, diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs index fc101c3ea2..35a7c155ea 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGroupEncoder.cs @@ -75,6 +75,7 @@ internal static class JxlGroupEncoder /// Quantizes one transform's AC plane using its four frequency-quadrant thresholds. /// /// The prepared frame quantizer and inverse matrices. + /// The matrix storage borrowed before the group block loops. /// The XYB channel index. /// The channel's inverse matrix multiplier. /// The transform strategy. @@ -86,6 +87,7 @@ internal static class JxlGroupEncoder /// The caller-owned integer coefficient plane. public static void QuantizeBlockAc( JxlQuantizer quantizer, + ReadOnlySpan matrixStorage, int channelIndex, float matrixMultiplier, JxlAcStrategyType strategy, @@ -96,7 +98,7 @@ public static void QuantizeBlockAc( int quant, Span destination) { - ReadOnlySpan inverseMatrix = quantizer.InverseDequantMatrix(strategy, channelIndex); + ReadOnlySpan inverseMatrix = quantizer.InverseDequantMatrix(matrixStorage, strategy, channelIndex); float scale = (quantizer.Scale * quant) * matrixMultiplier; int blockCount = blocksWide * blocksHigh; @@ -128,8 +130,20 @@ public static void QuantizeBlockAc( } } + /// Refines the integer block quantizer against frequency-weighted coefficient error. + /// The prepared frame quantizer. + /// The complete coefficient tables borrowed before the group loops. + /// The XYB channel index. + /// The channel's inverse matrix multiplier. + /// The transform strategy. + /// The canonical coefficient width, in blocks. + /// The canonical coefficient height, in blocks. + /// The four frequency-quadrant thresholds, adjusted in place. + /// The source coefficient plane. + /// The positive integer quantizer, updated with the selected value. public static void AdjustQuantBlockAC( JxlQuantizer quantizer, + ReadOnlySpan matrixStorage, int c, float qmMultiplier, JxlAcStrategyType quantKind, @@ -155,7 +169,7 @@ public static void AdjustQuantBlockAC( return; } - ReadOnlySpan qm = quantizer.InverseDequantMatrix(quantKind, c); + ReadOnlySpan qm = quantizer.InverseDequantMatrix(matrixStorage, quantKind, c); float qac = quantizer.Scale * quant; if (width > 1 || height > 1) @@ -358,10 +372,24 @@ public static void AdjustQuantBlockAC( } } + /// Quantizes and reconstructs Y before the encoder removes Y correlation from X and B. + /// The borrowed encoder controls and channel multipliers. + /// The coefficient count per channel. + /// The prepared frame quantizer. + /// The complete coefficient tables borrowed before the group loops. + /// The requested error-diffusion setting. + /// The transform strategy. + /// The canonical coefficient width, in blocks. + /// The canonical coefficient height, in blocks. + /// The prepared channel biases and shared bias numerator. + /// The positive integer quantizer, updated with the selected value. + /// Three coefficient planes. The Y plane receives reconstructed coefficients. + /// The integer destination for the same three coefficient planes. public static void QuantizeRoundtripYBlockAc( JxlPassesEncoderState encoderState, int size, JxlQuantizer quantizer, + ReadOnlySpan matrixStorage, bool errorDiffusion, JxlAcStrategyType quantKind, int width, @@ -392,6 +420,7 @@ public static void QuantizeRoundtripYBlockAc( AdjustQuantBlockAC( quantizer, + matrixStorage, channel, val[channel], quantKind, @@ -421,6 +450,7 @@ public static void QuantizeRoundtripYBlockAc( QuantizeBlockAc( quantizer, + matrixStorage, 1, 1.0f, quantKind, @@ -431,7 +461,7 @@ public static void QuantizeRoundtripYBlockAc( quant, quantized[size..]); - ReadOnlySpan dequantMatrix = quantizer.DequantMatrix(quantKind, 1); + ReadOnlySpan dequantMatrix = quantizer.DequantMatrix(matrixStorage, quantKind, 1); // Reconstruct Y before quantizing its correlated X/B residuals. Bias, matrix and scale stay in registers; // the quantizer writes directly into the existing float coefficient plane and leaves integer output unchanged. @@ -439,6 +469,13 @@ public static void QuantizeRoundtripYBlockAc( JxlQuantizer.Dequantize(quantized.Slice(size, coefficientCount), dequantMatrix, quantizer.InverseQuantAc(quant), 1, biases, inOut[size..]); } + /// Transforms and quantizes one group while borrowing its prepared matrices once. + /// The configuration that owns coefficient scratch. + /// The frame group index. + /// The borrowed frame controls and coefficient destinations. + /// The source XYB planes. + /// The source region. + /// The destination DC planes. public static void ComputeCoefficients( Configuration configuration, int groupIndex, @@ -447,6 +484,7 @@ public static void ComputeCoefficients( Rectangle rectangle, JxlImage3 dc) { + ReadOnlySpan matrixStorage = encoderState.Shared.Matrices.GetPreparedStorage(); Rectangle blockGroupRect = encoderState.Shared.FrameDimensions.BlockGroupRect(groupIndex); Rectangle cmapRect = new( blockGroupRect.X0() / JxlChromaFromLuma.ColorTileDimensionInBlocks, @@ -577,6 +615,7 @@ public static void ComputeCoefficients( encoderState, size, encoderState.Shared.Quantizer, + matrixStorage, errorDiffusion, acs.Strategy, xBlocks, @@ -603,6 +642,7 @@ public static void ComputeCoefficients( QuantizeBlockAc( encoderState.Shared.Quantizer, + matrixStorage, c, c == 0 ? encoderState.XQmMultiplier : encoderState.BQmMultiplier, acs.Strategy, diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlQuantizerWeightsEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlQuantizerWeightsEncoder.cs index 1228ca0735..28a655947a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlQuantizerWeightsEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlQuantizerWeightsEncoder.cs @@ -173,11 +173,7 @@ public static bool DequantMatricesEncode(Configuration configuration, JxlDequant { bool allDefault = true; - // We need a Memory as well as Span. - // Memory - so we can use it in lambda expression - // Span - micro-optimization, offers slightly better performance - Memory encodingsMemory = matrices.GetEncodingsMemory(); - Span encodings = encodingsMemory.Span; + ReadOnlySpan encodings = matrices.GetEncodings(); for (int i = 0; i < encodings.Length; i++) { @@ -193,11 +189,12 @@ public static bool DequantMatricesEncode(Configuration configuration, JxlDequant { writer.Write(1, allDefault ? 1 : 0); - if (allDefault) + if (!allDefault) { - for (int i = 0; i < encodingsMemory.Length; i++) + ReadOnlySpan tables = matrices.GetEncodings(); + for (int i = 0; i < tables.Length; i++) { - if (!EncodeQuant(configuration, encodingsMemory.Span[i], i, JxlDequantMatrices.RequiredSizeX[i], JxlDequantMatrices.RequiredSizeY[i], writer, encoder!)) + if (!EncodeQuant(configuration, tables[i], i, JxlDequantMatrices.RequiredSizeX[i], JxlDequantMatrices.RequiredSizeY[i], writer, encoder!)) { return false; } @@ -211,20 +208,20 @@ public static bool DequantMatricesEncode(Configuration configuration, JxlDequant public static bool DequantMatricesEncodeDc(JxlDequantMatrices matrices, JxlBitWriter writer) { Span dcQuant = matrices.GetDcQuants(); - bool allDefault = dcQuant[0] != JxlQuantizerConstants.DcQuant[0] - || dcQuant[1] != JxlQuantizerConstants.DcQuant[1] - || dcQuant[2] != JxlQuantizerConstants.DcQuant[2]; + bool allDefault = dcQuant[0] == JxlQuantizerConstants.DcQuant[0] + && dcQuant[1] == JxlQuantizerConstants.DcQuant[1] + && dcQuant[2] == JxlQuantizerConstants.DcQuant[2]; return writer.WithMaxBits(1 + (sizeof(float) * JxlMath.BitsPerByte * 3), () => { - Span dcQuant = matrices.GetDcQuants(); + ReadOnlySpan values = matrices.GetDcQuants(); writer.Write(1, allDefault ? 1 : 0); if (!allDefault) { - if (!JxlF16Coder.Write(dcQuant[0] * 128.0f, writer) || - !JxlF16Coder.Write(dcQuant[1] * 128.0f, writer) || - !JxlF16Coder.Write(dcQuant[2] * 128.0f, writer)) + if (!JxlF16Coder.Write(values[0] * 128.0f, writer) || + !JxlF16Coder.Write(values[1] * 128.0f, writer) || + !JxlF16Coder.Write(values[2] * 128.0f, writer)) { return false; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlPassesCommonContext.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlPassesCommonContext.cs index a1ba01ce66..f04da9523b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlPassesCommonContext.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlPassesCommonContext.cs @@ -30,7 +30,10 @@ internal class JxlPassesCommonContext public JxlAcStrategyImage AcStrategy { get; set; } - public JxlDequantMatrices Matrices { get; set; } = new(); + /// + /// Gets the common context's matrix owner. Its quantizer and encoder views borrow this same instance. + /// + public JxlDequantMatrices Matrices { get; } = new(); public JxlQuantizer Quantizer { get; set; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlDequantMatrices.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlDequantMatrices.cs index 14751840b5..dea3854d4b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlDequantMatrices.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlDequantMatrices.cs @@ -1,112 +1,52 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; using System.Diagnostics; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; -using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; /// -/// Contains matrices used to inverse quantize coefficients. +/// Owns one frame's quantization parameters and the prepared coefficient matrices used by its transform strategies. /// -internal sealed class JxlDequantMatrices +/// +/// Prepare the complete search or decode strategy mask before processing blocks. Borrow the prepared storage once at that entry point. +/// Preparation can replace the allocation, so borrowed spans must not survive a later preparation or parameter change. +/// +internal sealed class JxlDequantMatrices : IDisposable { - /// - /// Sum(DotProduct(RequiredSizeX, RequiredSizeY)). - /// - private const int SumRequiredXY = 2056; - - private const int TotalTableSize = SumRequiredXY * JxlFrameDimensions.DctBlockSize * 3; - - /// - /// Contains weights & multipliers for transforms used by the codec (e.g. DCT, identity, AFV). - /// + /// The shared default parameters. Preparation reads these descriptors without modifying them. public static readonly JxlQuantizerEncoding[] Library = GetLibrary(); + private IMemoryOwner? tableOwner; + private Memory allocation; + private Memory preparedStorage; private uint computedMask; + private uint preparedKinds; + private int halfLength; - /// - /// Storage for quantization. - /// - private readonly Memory tableStorage; - - /// - /// Contains matrices for forward quantization. - /// - private readonly Memory table; - - /// - /// Contains matrices for inverse quantization. - /// - private readonly Memory inverseTable; - - /// - /// Quantization table for DC - /// + // Strategies that differ only by orientation share a table kind. Seventeen base offsets avoid duplicating those tables. + private InlineArray17 tableOffsets; private InlineArray3 dcQuant; - - /// - /// Inverse quantization table for DC - /// private InlineArray3 inverseDcQuant; + private JxlQuantizerEncoding[] encodings; - /// - /// Table offsets. - /// - private readonly int[] tableOffsets = new int[JxlAcStrategy.NumberOfValidStrategies * 3]; - - /// - /// Quantizer encodings. Multiple may be used depending on the kind of transform. - /// - private JxlQuantizerEncoding[] encodings = []; - - /// - /// Initializes a new instance of the class. - /// + /// Creates default quantization parameters without allocating coefficient matrices. public JxlDequantMatrices() { - // float dc_quant_[3] = {kDCQuant[0], kDCQuant[1], kDCQuant[2]}; - // float inv_dc_quant_[3] = {kInvDCQuant[0], kInvDCQuant[1], kInvDCQuant[2]}; - this.dcQuant[0] = JxlQuantizerConstants.DcQuant[0]; - this.dcQuant[1] = JxlQuantizerConstants.DcQuant[1]; - this.dcQuant[2] = JxlQuantizerConstants.DcQuant[2]; - - this.inverseDcQuant[0] = JxlQuantizerConstants.InverseDcQuant[0]; - this.inverseDcQuant[1] = JxlQuantizerConstants.InverseDcQuant[1]; - this.inverseDcQuant[2] = JxlQuantizerConstants.InverseDcQuant[2]; + for (int c = 0; c < 3; c++) + { + this.dcQuant[c] = JxlQuantizerConstants.DcQuant[c]; + this.inverseDcQuant[c] = JxlQuantizerConstants.InverseDcQuant[c]; + } this.encodings = new JxlQuantizerEncoding[JxlQuantizerConstants.NumberOfQuantizerTables]; for (int i = 0; i < this.encodings.Length; i++) { this.encodings[i] = JxlQuantizerEncoding.Library(0); } - - int pos = 0; - Span offsets = stackalloc int[JxlQuantizerConstants.NumberOfQuantizerTables * 3]; - - for (int i = 0; i < JxlQuantizerConstants.NumberOfQuantizerTables; i++) - { - int numBlocks = RequiredSizeX[i] * RequiredSizeY[i]; - int num = numBlocks * JxlFrameDimensions.DctBlockSize; - int i3 = 3 * i; - - for (int channelIndex = 0; channelIndex < 3; channelIndex++) - { - offsets[i3 + channelIndex] = pos + (channelIndex * num); - } - - pos += 3 * num; - } - - for (int i = 0; i < JxlAcStrategy.NumberOfValidStrategies; i++) - { - for (int channelIndex = 0; channelIndex < 3; channelIndex++) - { - this.tableOffsets[(i * 3) + channelIndex] = offsets[((int)JxlQuantizerConstants.AcStrategyToQuantTableMap[i] * 3) + channelIndex]; - } - } } /// @@ -186,28 +126,134 @@ static void Verify(int expected, JxlQuantTable actual) } } - /// - /// Returns the dequantization matrix for a transform and color channel. - /// - /// The AC transform type. - /// The XYB channel index. - /// Matrix - public Span GetMatrix(JxlAcStrategyType transformType, int channelIndex) + /// Prepares exactly the table kinds required by a complete strategy mask. + /// The configuration that owns coefficient storage. + /// The complete set of strategies used by the next search or decode operation. + /// Whether every requested table was generated successfully. + public bool EnsureComputed(Configuration configuration, uint strategyMask) + { + uint kinds = 0; + for (int i = 0; i < JxlAcStrategy.NumberOfValidStrategies; i++) + { + if ((strategyMask & (1U << i)) != 0) + { + kinds |= 1U << (int)JxlQuantizerConstants.AcStrategyToQuantTableMap[i]; + } + } + + // A new orientation can reuse an already prepared kind. No allocation or table calculation is needed. + if ((kinds & ~this.preparedKinds) == 0) + { + this.computedMask |= strategyMask; + return true; + } + + InlineArray17 offsets = default; + int length = 0; + for (int i = 0; i < JxlQuantizerConstants.NumberOfQuantizerTables; i++) + { + if ((kinds & (1U << i)) != 0) + { + offsets[i] = length; + length += RequiredSizeX[i] * RequiredSizeY[i] * JxlFrameDimensions.DctBlockSize * 3; + } + } + + int storageLength = 2 * length; + IMemoryOwner? replacement = null; + Memory nextAllocation = this.allocation; + try + { + if (storageLength > nextAllocation.Length) + { + replacement = configuration.MemoryAllocator.Allocate(storageLength); + nextAllocation = replacement.Memory; + } + else + { + // Reuse occurs only at the preparation boundary, after all previous borrowers have finished. + // A failed regeneration must not expose partially overwritten tables as prepared matrices. + this.computedMask = 0; + this.preparedKinds = 0; + this.preparedStorage = default; + } + + Span storage = nextAllocation.Span[..storageLength]; + Span dequantization = storage[..length]; + Span quantization = storage[length..]; + for (int i = 0; i < JxlQuantizerConstants.NumberOfQuantizerTables; i++) + { + if ((kinds & (1U << i)) == 0) + { + continue; + } + + JxlQuantizerEncoding encoding = this.encodings[i]; + if (encoding.Mode == JxlQuantMode.Library) + { + encoding = Library[i]; + } + + int offset = offsets[i]; + if (!JxlQuantWeights.ComputeQuantTable(encoding, dequantization, quantization, (JxlQuantTable)i, ref offset)) + { + return false; + } + + DebugGuard.IsTrue( + offset == offsets[i] + (RequiredSizeX[i] * RequiredSizeY[i] * JxlFrameDimensions.DctBlockSize * 3), + "The quantizer table did not fill its prepared region."); + } + + if (replacement is not null) + { + this.tableOwner?.Dispose(); + this.tableOwner = replacement; + replacement = null; + this.allocation = nextAllocation; + } + + this.tableOffsets = offsets; + this.halfLength = length; + this.preparedStorage = nextAllocation[..storageLength]; + this.preparedKinds = kinds; + this.computedMask = strategyMask; + return true; + } + finally + { + replacement?.Dispose(); + } + } + + /// Borrows all prepared coefficient storage once for a group or strategy search. + /// The dequantization table region followed by the quantization table region. + public ReadOnlySpan GetPreparedStorage() => this.preparedStorage.Span; + + /// Selects the requested channel and remaining channels within one prepared dequantization table kind. + /// The complete storage borrowed at the group or search entry point. + /// The prepared transform strategy. + /// The first XYB channel to return. + /// Exactly three, two or one channel planes. Channel zero includes the complete X, Y and B table. + public ReadOnlySpan GetMatrix(ReadOnlySpan storage, JxlAcStrategyType transformType, int channelIndex) { - DebugGuard.MustBeGreaterThan((1 << (int)transformType) & this.computedMask, 0, nameof(transformType)); - return this.table.Span[this.tableOffsets[((int)transformType * 3) + channelIndex]..]; + DebugGuard.IsTrue((this.computedMask & (1U << (int)transformType)) != 0, "The strategy matrices were not prepared."); + int kind = (int)JxlQuantizerConstants.AcStrategyToQuantTableMap[(int)transformType]; + int count = RequiredSizeX[kind] * RequiredSizeY[kind] * JxlFrameDimensions.DctBlockSize; + return storage.Slice(this.tableOffsets[kind] + (channelIndex * count), (3 - channelIndex) * count); } - /// - /// Returns the inverse dequantization matrix for a transform and color channel. - /// - /// The AC transform type. - /// The XYB channel index. - /// Inverse matrix - public Span GetInverseMatrix(JxlAcStrategyType transformType, int channelIndex) + /// Selects the requested channel and remaining channels within one prepared quantization table kind. + /// The complete storage borrowed at the group or search entry point. + /// The prepared transform strategy. + /// The first XYB channel to return. + /// Exactly three, two or one channel planes, without entering another table kind. + public ReadOnlySpan GetInverseMatrix(ReadOnlySpan storage, JxlAcStrategyType transformType, int channelIndex) { - DebugGuard.MustBeGreaterThan((1 << (int)transformType) & this.computedMask, 0, nameof(transformType)); - return this.inverseTable.Span[this.tableOffsets[((int)transformType * 3) + channelIndex]..]; + DebugGuard.IsTrue((this.computedMask & (1U << (int)transformType)) != 0, "The strategy matrices were not prepared."); + int kind = (int)JxlQuantizerConstants.AcStrategyToQuantTableMap[(int)transformType]; + int count = RequiredSizeX[kind] * RequiredSizeY[kind] * JxlFrameDimensions.DctBlockSize; + return storage.Slice(this.halfLength + this.tableOffsets[kind] + (channelIndex * count), (3 - channelIndex) * count); } /// @@ -245,33 +291,28 @@ public void SetDcQuant(InlineArray3 dcQuantizationValues) } } - /// - /// Sets custom quantizer encodings for transform functions. - /// - /// The encodings to identify required transform functions. + /// Transfers quantization descriptors to this owner and invalidates prepared matrices. + /// The complete descriptor array. The caller must not modify it after transfer. public void SetEncodings(JxlQuantizerEncoding[] encodings) { this.encodings = encodings; this.computedMask = 0; + this.preparedKinds = 0; + this.preparedStorage = default; } - /// - /// Returns quantizer encodings for this dequant matrices instance. - /// - /// - /// Encodings set by the method. - /// By default (when the aforementioned method wasn't invoked), the result - /// is simply an empty span. - /// - public Span GetEncodings() => this.encodings; + /// Borrows the frame's quantization descriptors for serialization or JPEG reconstruction. + /// The fixed set of table descriptors, which callers must not modify. + public ReadOnlySpan GetEncodings() => this.encodings; - /// - /// Returns quantizer encodings for this dequant matrices instance. - /// - /// - /// Encodings set by the method. - /// By default (when the aforementioned method wasn't invoked), the result - /// is simply an empty span. - /// - public Memory GetEncodingsMemory() => this.encodings; + /// Releases the prepared coefficient allocation. + public void Dispose() + { + this.tableOwner?.Dispose(); + this.tableOwner = null; + this.allocation = default; + this.preparedStorage = default; + this.computedMask = 0; + this.preparedKinds = 0; + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantWeights.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantWeights.cs index 9b574d90e4..bae16ea7ea 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantWeights.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantWeights.cs @@ -829,15 +829,15 @@ public static bool GetQuantWeights(int rowCount, int columnCount, float[][] dist /// Builds dequantization weights and their reciprocals for one transform in the caller's final storage. /// /// The quantization weight parameters. - /// The destination dequantization weights. Each entry is the reciprocal of the generated weight. - /// The destination quantization weights. Low-frequency entries are cleared after preparation. + /// The destination dequantization weights. Each entry is the reciprocal of the generated weight. + /// The destination quantization weights. Low-frequency entries are cleared after preparation. /// The transform table kind. /// The starting destination offset, advanced past the written weights. /// Whether the encoding supports the selected transform. public static bool ComputeQuantTable( JxlQuantizerEncoding encoding, + Span dequantizationTable, Span quantizationTable, - Span inverseQuantizationTable, JxlQuantTable kind, ref int writeOffset) { @@ -850,7 +850,7 @@ public static bool ComputeQuantTable( // Generate all three channel planes directly in their final allocation. The largest transform has 196,608 entries. // Keeping this view avoids a proportional stack allocation and subsequent copies of the complete table. - Span weights = inverseQuantizationTable.Slice(writeOffset, 3 * num); + Span weights = quantizationTable.Slice(writeOffset, 3 * num); switch (encoding.Mode) { @@ -1085,7 +1085,7 @@ public static bool ComputeQuantTable( // libjxl quant_weights.cc validates this interval before division. Validate and invert together; // no temporary table, fill with ones, or extra validation pass is required. Each lane owns one coefficient. - TensorPrimitives_.Invoke(weights, quantizationTable.Slice(writeOffset, weights.Length), default(ReciprocalWeightOperator)); + TensorPrimitives_.Invoke(weights, dequantizationTable.Slice(writeOffset, weights.Length), default(ReciprocalWeightOperator)); writeOffset += 3 * num; @@ -1100,7 +1100,7 @@ public static bool ComputeQuantTable( { for (int x = 0; x < xs; x++) { - inverseQuantizationTable[prevPos + (c * ys * xs * JxlFrameDimensions.DctBlockSize) + (y * JxlFrameDimensions.BlockDimensions * xs) + x] = 0; + quantizationTable[prevPos + (c * ys * xs * JxlFrameDimensions.DctBlockSize) + (y * JxlFrameDimensions.BlockDimensions * xs) + x] = 0; } } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs index a7f9d1c2fe..70a543c7b2 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs @@ -196,20 +196,22 @@ public void RecomputeFromGlobalScale() /// /// Returns the dequantization matrix. /// + /// The prepared matrix storage borrowed at the group or search entry. /// The quant kind /// The XYB channel index. /// The dequantization matrix. - public ReadOnlySpan DequantMatrix(JxlAcStrategyType strategy, int channelIndex) - => this.dequantizationMatrices.GetMatrix(strategy, channelIndex); + public ReadOnlySpan DequantMatrix(ReadOnlySpan storage, JxlAcStrategyType strategy, int channelIndex) + => this.dequantizationMatrices.GetMatrix(storage, strategy, channelIndex); /// /// Returns the inverse dequantization matrix. /// + /// The prepared matrix storage borrowed at the group or search entry. /// The quant kind /// The XYB channel index. /// The inverse dequantization matrix. - public ReadOnlySpan InverseDequantMatrix(JxlAcStrategyType strategy, int channelIndex) - => this.dequantizationMatrices.GetInverseMatrix(strategy, channelIndex); + public ReadOnlySpan InverseDequantMatrix(ReadOnlySpan storage, JxlAcStrategyType strategy, int channelIndex) + => this.dequantizationMatrices.GetInverseMatrix(storage, strategy, channelIndex); /// /// Returns the DC quantization step. diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/QuantizationTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/QuantizationTests.cs index bfc00feef2..be48419e91 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/QuantizationTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/QuantizationTests.cs @@ -1,8 +1,11 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; +using SixLabors.ImageSharp.Tests.Memory; using SixLabors.ImageSharp.Tests.TestUtilities; namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; @@ -294,4 +297,128 @@ static void RunTest() RunTest, HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); } + + /// + /// Checks native default flags and the half-precision fields written for custom DC quantization. + /// + [Fact] + public void QuantizationHeaders_WriteDefaultFlagsAndCustomDcValues() + { + using JxlDequantMatrices matrices = new(); + using MemoryStream stream = new(); + JxlBitWriter writer = new(stream); + Assert.True(JxlQuantizerWeightsEncoder.DequantMatricesEncode(TestEnvironment.Configuration, matrices, writer, null)); + Assert.True(JxlQuantizerWeightsEncoder.DequantMatricesEncodeDc(matrices, writer)); + + // Native headers use one bit equal to one for each default set, with no table fields after either flag. + Assert.Equal(2L, writer.BitsWritten); + writer.ZeroPadToByte(); + Assert.Equal(new byte[] { 3 }, stream.ToArray()); + + stream.SetLength(0); + stream.Position = 0; + writer = new(stream); + InlineArray3 inverseDc = default; + inverseDc[0] = inverseDc[1] = inverseDc[2] = 1F; + matrices.SetDcQuant(inverseDc); + Assert.True(JxlQuantizerWeightsEncoder.DequantMatricesEncodeDc(matrices, writer)); + + // Custom values use flag zero, then three half-precision fields equal to DCQuant*128. + // Each value here is 128 (half bits 0x5800), shifted one bit by the flag in this least-significant-bit-first stream. + Assert.Equal(49L, writer.BitsWritten); + writer.ZeroPadToByte(); + Assert.Equal(new byte[] { 0, 0xB0, 0, 0xB0, 0, 0xB0, 0 }, stream.ToArray()); + } + + /// + /// Checks compact table allocation, shared orientation storage, exact channel boundaries, and reuse at preparation. + /// + [Fact] + public void DequantMatrices_PrepareOnlyRequestedKindsAndReuseOrientations() + { + TestMemoryAllocator allocator = new(); + allocator.EnableNonThreadSafeLogging(); + Configuration configuration = TestEnvironment.Configuration.Clone(); + configuration.MemoryAllocator = allocator; + + using (JxlDequantMatrices matrices = new()) + { + JxlQuantizerEncoding[] encodings = new JxlQuantizerEncoding[JxlQuantizerConstants.NumberOfQuantizerTables]; + for (int i = 0; i < encodings.Length; i++) + { + encodings[i] = JxlQuantizerEncoding.Library(0); + } + + encodings[(int)JxlQuantTable.DCT8X16] = JxlQuantizerEncoding.Dct(new JxlDctQuantWeightParameters([[2F], [4F], [8F]], 1)); + matrices.SetEncodings(encodings); + uint mask = (1U << (int)JxlAcStrategyType.DCT) | (1U << (int)JxlAcStrategyType.DCT8X16); + Assert.True(matrices.EnsureComputed(configuration, mask)); + + // DCT8 uses 64 coefficients per channel; the rectangle uses 128. Three channels and two directions need 1152 floats. + // The other 15 table kinds require no coefficient storage. + ReadOnlySpan storage = matrices.GetPreparedStorage(); + Assert.Equal(1152, storage.Length); + Assert.Equal(4608L, allocator.LiveBufferBytes); + Assert.Equal(192, matrices.GetMatrix(storage, JxlAcStrategyType.DCT, 0).Length); + Assert.Equal(128, matrices.GetMatrix(storage, JxlAcStrategyType.DCT, 1).Length); + Assert.Equal(64, matrices.GetMatrix(storage, JxlAcStrategyType.DCT, 2).Length); + Assert.Equal(384, matrices.GetMatrix(storage, JxlAcStrategyType.DCT8X16, 0).Length); + + for (int c = 0; c < 3; c++) + { + float weight = c == 0 ? 2F : c == 1 ? 4F : 8F; + ReadOnlySpan forward = matrices.GetMatrix(storage, JxlAcStrategyType.DCT8X16, c); + ReadOnlySpan inverse = matrices.GetInverseMatrix(storage, JxlAcStrategyType.DCT8X16, c); + Assert.Equal(1F / weight, forward[127]); + Assert.Equal(weight, inverse[127]); + Assert.Equal(0F, inverse[0]); + } + + // These two orientations use the same quantization kind. Adding the second strategy must not rent or recompute. + mask |= 1U << (int)JxlAcStrategyType.DCT16X8; + Assert.True(matrices.EnsureComputed(configuration, mask)); + storage = matrices.GetPreparedStorage(); + ReadOnlySpan horizontal = matrices.GetMatrix(storage, JxlAcStrategyType.DCT16X8, 0); + ReadOnlySpan vertical = matrices.GetMatrix(storage, JxlAcStrategyType.DCT8X16, 0); + Assert.True(horizontal.Overlaps(vertical, out int offset)); + Assert.Equal(0, offset); + Assert.Single(allocator.AllocationLog); + } + + Assert.Equal(0, allocator.LiveBufferBytes); + Assert.Single(allocator.ReturnLog); + } + + /// + /// Checks that failed generation releases its allocation and permits a later complete preparation. + /// + [Fact] + public void DequantMatrices_FailedPreparationReleasesReplacement() + { + TestMemoryAllocator allocator = new(); + allocator.EnableNonThreadSafeLogging(); + Configuration configuration = TestEnvironment.Configuration.Clone(); + configuration.MemoryAllocator = allocator; + + using (JxlDequantMatrices matrices = new()) + { + JxlQuantizerEncoding[] encodings = JxlDequantMatrices.GetLibrary(); + + // Identity parameters are valid only for an 8 by 8 table. The 16 by 16 request must fail through real table generation. + encodings[(int)JxlQuantTable.DCT16X16] = JxlQuantizerEncoding.Identity([[1F, 1F, 1F], [1F, 1F, 1F], [1F, 1F, 1F]]); + matrices.SetEncodings(encodings); + Assert.False(matrices.EnsureComputed(configuration, 1U << (int)JxlAcStrategyType.DCT16X16)); + Assert.True(matrices.GetPreparedStorage().IsEmpty); + Assert.Equal(0, allocator.LiveBufferBytes); + Assert.Single(allocator.ReturnLog); + + matrices.SetEncodings(JxlDequantMatrices.GetLibrary()); + Assert.True(matrices.EnsureComputed(configuration, 1U << (int)JxlAcStrategyType.DCT)); + Assert.Equal(384, matrices.GetPreparedStorage().Length); + Assert.Equal(1536L, allocator.LiveBufferBytes); + } + + Assert.Equal(0, allocator.LiveBufferBytes); + Assert.Equal(2, allocator.ReturnLog.Count); + } } From da1fbf768d9813e9b654a93e2bd783d7180d2efd Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Oct 2026 03:18:32 +1000 Subject: [PATCH 303/328] Share bounded SIMD Gaborish row convolution and preserve input neighbors --- .../Formats/Jxl/Processing/JxlConvolve.cs | 142 ++++++++++++++++++ .../RenderPipeline/GaborishStage.cs | 130 ++++++++-------- .../Formats/Jxl/Processing/GaborishTests.cs | 108 +++++++++++++ 3 files changed, 310 insertions(+), 70 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs index 5bc0fb04d3..d6af278941 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs @@ -5,6 +5,8 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; @@ -228,6 +230,88 @@ public static bool Symmetric5( return true; } + /// + /// Applies a symmetric three-by-three stencil to three borrowed rows with an explicit one-sample horizontal halo. + /// + /// The row above the output, including one sample before and after its requested range. + /// The center row, with the same halo and logical origin as top. + /// The row below the output, with the same halo and logical origin as top. + /// The center sample weight. + /// The weight for each horizontal and vertical neighbor. + /// The weight for each diagonal neighbor. + /// The exact output range, distinct from all three inputs. + /// + /// Each input contains output.Length plus two samples. The caller supplies image-edge extension in the halo. + /// The stencil uses the pairwise sums and nested multiply-add operations from native stage_gaborish.cc. + /// + public static void Symmetric3Row( + ReadOnlySpan top, + ReadOnlySpan middle, + ReadOnlySpan bottom, + float centerWeight, + float axialWeight, + float diagonalWeight, + Span output) + { + ref float topReference = ref MemoryMarshal.GetReference(top); + ref float middleReference = ref MemoryMarshal.GetReference(middle); + ref float bottomReference = ref MemoryMarshal.GetReference(bottom); + ref float outputReference = ref MemoryMarshal.GetReference(output); + Symmetric3Operator operation = new(centerWeight, axialWeight, diagonalWeight); + int x = 0; + + // Input index x+1 is output pixel x. Neighbor loads start at x and x+2, so each full vector consumes only its declared halo. + // Loads can be unaligned. No output register crosses the requested range, and tails use the identical scalar equation. + if (Vector512.IsHardwareAccelerated) + { + for (; x <= output.Length - Vector512.Count; x += Vector512.Count) + { + Vector512 center = Vector512.LoadUnsafe(ref middleReference, (uint)(x + 1)); + Vector512 horizontal = Vector512.LoadUnsafe(ref middleReference, (uint)x) + Vector512.LoadUnsafe(ref middleReference, (uint)(x + 2)); + Vector512 vertical = Vector512.LoadUnsafe(ref topReference, (uint)(x + 1)) + Vector512.LoadUnsafe(ref bottomReference, (uint)(x + 1)); + Vector512 topDiagonal = Vector512.LoadUnsafe(ref topReference, (uint)x) + Vector512.LoadUnsafe(ref topReference, (uint)(x + 2)); + Vector512 bottomDiagonal = Vector512.LoadUnsafe(ref bottomReference, (uint)x) + Vector512.LoadUnsafe(ref bottomReference, (uint)(x + 2)); + Vector512 result = operation.Invoke(center, horizontal + vertical, topDiagonal + bottomDiagonal); + result.StoreUnsafe(ref outputReference, (uint)x); + } + } + + if (Vector256.IsHardwareAccelerated) + { + for (; x <= output.Length - Vector256.Count; x += Vector256.Count) + { + Vector256 center = Vector256.LoadUnsafe(ref middleReference, (uint)(x + 1)); + Vector256 horizontal = Vector256.LoadUnsafe(ref middleReference, (uint)x) + Vector256.LoadUnsafe(ref middleReference, (uint)(x + 2)); + Vector256 vertical = Vector256.LoadUnsafe(ref topReference, (uint)(x + 1)) + Vector256.LoadUnsafe(ref bottomReference, (uint)(x + 1)); + Vector256 topDiagonal = Vector256.LoadUnsafe(ref topReference, (uint)x) + Vector256.LoadUnsafe(ref topReference, (uint)(x + 2)); + Vector256 bottomDiagonal = Vector256.LoadUnsafe(ref bottomReference, (uint)x) + Vector256.LoadUnsafe(ref bottomReference, (uint)(x + 2)); + Vector256 result = operation.Invoke(center, horizontal + vertical, topDiagonal + bottomDiagonal); + result.StoreUnsafe(ref outputReference, (uint)x); + } + } + + if (Vector128.IsHardwareAccelerated) + { + for (; x <= output.Length - Vector128.Count; x += Vector128.Count) + { + Vector128 center = Vector128.LoadUnsafe(ref middleReference, (uint)(x + 1)); + Vector128 horizontal = Vector128.LoadUnsafe(ref middleReference, (uint)x) + Vector128.LoadUnsafe(ref middleReference, (uint)(x + 2)); + Vector128 vertical = Vector128.LoadUnsafe(ref topReference, (uint)(x + 1)) + Vector128.LoadUnsafe(ref bottomReference, (uint)(x + 1)); + Vector128 topDiagonal = Vector128.LoadUnsafe(ref topReference, (uint)x) + Vector128.LoadUnsafe(ref topReference, (uint)(x + 2)); + Vector128 bottomDiagonal = Vector128.LoadUnsafe(ref bottomReference, (uint)x) + Vector128.LoadUnsafe(ref bottomReference, (uint)(x + 2)); + Vector128 result = operation.Invoke(center, horizontal + vertical, topDiagonal + bottomDiagonal); + result.StoreUnsafe(ref outputReference, (uint)x); + } + } + + for (; x < output.Length; x++) + { + float sum1 = (middle[x] + middle[x + 2]) + (top[x + 1] + bottom[x + 1]); + float sum2 = (top[x] + top[x + 2]) + (bottom[x] + bottom[x + 2]); + output[x] = operation.Invoke(middle[x + 1], sum1, sum2); + } + } + public static float SlowSymmetric3Pixel( JxlPlane image, int x, @@ -432,6 +516,64 @@ public static void FirstL2(ReadOnlySpan c, Span dst) } } + /// + /// Combines a symmetric stencil's center, axial sum and diagonal sum with native Gaborish multiply-add order. + /// + private readonly struct Symmetric3Operator : TensorPrimitives_.ITernaryOperator + { + private readonly float centerWeight; + private readonly float axialWeight; + private readonly float diagonalWeight; + + /// + /// Retains the normalized weights shared by every sample in one output row. + /// + /// The center sample weight. + /// The weight for each sample in the axial sum. + /// The weight for each sample in the diagonal sum. + public Symmetric3Operator(float centerWeight, float axialWeight, float diagonalWeight) + { + this.centerWeight = centerWeight; + this.axialWeight = axialWeight; + this.diagonalWeight = diagonalWeight; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float Invoke(float first, float second, float third) + { + float result = float.MultiplyAddEstimate(second, this.axialWeight, first * this.centerWeight); + return float.MultiplyAddEstimate(third, this.diagonalWeight, result); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector128 Invoke(Vector128 first, Vector128 second, Vector128 third) + { + // Each lane keeps one output sample. Weight the center first, then add axial and diagonal contributions in native order. + Vector128 result = Vector128.MultiplyAddEstimate(second, Vector128.Create(this.axialWeight), first * Vector128.Create(this.centerWeight)); + return Vector128.MultiplyAddEstimate(third, Vector128.Create(this.diagonalWeight), result); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector256 Invoke(Vector256 first, Vector256 second, Vector256 third) + { + // Each lane keeps one output sample. Weight the center first, then add axial and diagonal contributions in native order. + Vector256 result = Vector256.MultiplyAddEstimate(second, Vector256.Create(this.axialWeight), first * Vector256.Create(this.centerWeight)); + return Vector256.MultiplyAddEstimate(third, Vector256.Create(this.diagonalWeight), result); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector512 Invoke(Vector512 first, Vector512 second, Vector512 third) + { + // Each lane keeps one output sample. Weight the center first, then add axial and diagonal contributions in native order. + Vector512 result = Vector512.MultiplyAddEstimate(second, Vector512.Create(this.axialWeight), first * Vector512.Create(this.centerWeight)); + return Vector512.MultiplyAddEstimate(third, Vector512.Create(this.diagonalWeight), result); + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int RoundUpTo(int value, int multiple) => ((value + multiple - 1) / multiple) * multiple; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/GaborishStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/GaborishStage.cs index fbc83ab214..fceed29663 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/GaborishStage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/GaborishStage.cs @@ -1,100 +1,90 @@ // 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.Memory; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; +/// +/// Applies the normalized Gaborish three-by-three filter to the three color channels. +/// internal sealed class GaborishStage : RenderPipelineStageBase { - private InlineArray9 weights; - - public GaborishStage(Configuration configuration, JxlLoopFilter lf) + // Each channel stores [center, axial neighbor, diagonal neighbor]. The inline storage contains no sample allocations. + private InlineArray3> weights; + + /// + /// Prepares the channel weights and declares the one-sample input border. + /// + /// The configuration for this stage. + /// The decoded per-channel Gaborish weights. + public GaborishStage(Configuration configuration, JxlLoopFilter loopFilter) : base(configuration) { this.Settings = RenderPipelineStageConfiguration.CreateSymmetricBorderOnly(1); + this.weights[0][0] = this.weights[1][0] = this.weights[2][0] = 1F; + this.weights[0][1] = loopFilter.GaborishXWeight1; + this.weights[0][2] = loopFilter.GaborishXWeight2; + this.weights[1][1] = loopFilter.GaborishYWeight1; + this.weights[1][2] = loopFilter.GaborishYWeight2; + this.weights[2][1] = loopFilter.GaborishBWeight1; + this.weights[2][2] = loopFilter.GaborishBWeight2; - this.weights[0] = 1; - this.weights[1] = lf.GaborishXWeight1; - this.weights[2] = lf.GaborishXWeight2; - this.weights[3] = 1; - this.weights[4] = lf.GaborishYWeight1; - this.weights[5] = lf.GaborishYWeight2; - this.weights[6] = 1; - this.weights[7] = lf.GaborishBWeight1; - this.weights[8] = lf.GaborishBWeight2; - - // Normalization for (int c = 0; c < 3; c++) { - int c3 = c * 3; // prevent repeated multiplication - - float div = this.weights[c3] + (4 * (this.weights[c3 + 1] + this.weights[c3 + 2])); - float mul = 1.0f / div; - - this.weights[c3] *= mul; - this.weights[c3 + 1] *= mul; - this.weights[c3 + 2] *= mul; + // Four axial and four diagonal samples surround the center. Normalize their total weight to one. + Span channelWeights = this.weights[c]; + float multiplier = 1F / (channelWeights[0] + (4F * (channelWeights[1] + channelWeights[2]))); + channelWeights[0] *= multiplier; + channelWeights[1] *= multiplier; + channelWeights[2] *= multiplier; } } /// public override string Name => "Gab"; - public override void ProcessRow(Buffer2D> inputRows, Buffer2D> outputRows, int xExtraLeft, int xExtraRight, int width, int xPos, int yPos) + /// + /// Filters exactly the requested center range and extra samples, using the pipeline's prepared neighboring rows and halo. + /// + /// The borrowed channel row table, with rows above, at and below the current row. + /// The distinct output row table. + /// The requested output samples before the center range. + /// The requested output samples after the center range. + /// The center range width. + /// The center range's image column. Prepared borders already account for this position. + /// The center range's image row. Prepared borders already account for this position. + /// + /// The row table places center sample zero at RenderPipelineXOffset. Inputs extend one sample beyond each requested output edge. + /// The pipeline reserves the left prefix for xExtraLeft plus this input halo and supplies the image-edge extension. + /// + public override void ProcessRow( + Buffer2D> inputRows, + Buffer2D> outputRows, + int xExtraLeft, + int xExtraRight, + int width, + int xPos, + int yPos) { - int xStart = -JxlMath.RoundUpTo(xExtraLeft, Vector.Count); - int xEnd = width + xExtraRight; + int outputLength = xExtraLeft + width + xExtraRight; + int inputLength = outputLength + 2; for (int c = 0; c < 3; c++) { - int c3 = c * 3; // prevent repeated multiplication - - Span rowT = this.GetInputRow(inputRows, c, -1); - Span rowM = this.GetInputRow(inputRows, c, 0); - Span rowB = this.GetInputRow(inputRows, c, 1); - Span rowOut = GetOutputRow(outputRows, c, 0); - - Vector w0 = Vector.Create(this.weights[c3]); - Vector w1 = Vector.Create(this.weights[c3 + 1]); - Vector w2 = Vector.Create(this.weights[c3 + 2]); - - // Ref for performance - ref float refRowT = ref MemoryMarshal.GetReference(rowT); - ref float refRowM = ref MemoryMarshal.GetReference(rowM); - ref float refRowB = ref MemoryMarshal.GetReference(rowB); - ref float refRowOut = ref MemoryMarshal.GetReference(rowOut); - - for (int x = xStart; x < xEnd; x += Vector.Count) - { - Vector t = Vector.LoadUnsafe(ref Unsafe.Add(ref refRowT, x)); - Vector tl = Vector.LoadUnsafe(ref Unsafe.Add(ref refRowT, x - 1)); - Vector tr = Vector.LoadUnsafe(ref Unsafe.Add(ref refRowT, x + 1)); - - Vector m = Vector.LoadUnsafe(ref Unsafe.Add(ref refRowM, x)); - Vector l = Vector.LoadUnsafe(ref Unsafe.Add(ref refRowM, x - 1)); - Vector r = Vector.LoadUnsafe(ref Unsafe.Add(ref refRowM, x + 1)); - - Vector b = Vector.LoadUnsafe(ref Unsafe.Add(ref refRowB, x)); - Vector bl = Vector.LoadUnsafe(ref Unsafe.Add(ref refRowB, x - 1)); - Vector br = Vector.LoadUnsafe(ref Unsafe.Add(ref refRowB, x + 1)); - - Vector sum0 = m; - Vector sum1 = (l + r) + (t + b); - Vector sum2 = (tl + tr) + (bl + br); - - Vector pixels = (sum2 * w2) + ((sum1 * w1) + (sum0 * w0)); - pixels.StoreUnsafe(ref Unsafe.Add(ref refRowOut, x)); - } + // Borrow the full requested range plus one input neighbor at each end. The kernel uses only nonnegative span indices. + // Keep the exact extra widths: vector alignment must not expand the pipeline's logical request or overwrite adjacent outputs. + ReadOnlySpan top = this.GetInputRow(inputRows, c, -1, xExtraLeft + 1)[..inputLength]; + ReadOnlySpan middle = this.GetInputRow(inputRows, c, 0, xExtraLeft + 1)[..inputLength]; + ReadOnlySpan bottom = this.GetInputRow(inputRows, c, 1, xExtraLeft + 1)[..inputLength]; + Span output = outputRows[c, 0].Span.Slice(RenderPipelineXOffset - xExtraLeft, outputLength); + ReadOnlySpan channelWeights = this.weights[c]; + JxlConvolve.Symmetric3Row(top, middle, bottom, channelWeights[0], channelWeights[1], channelWeights[2], output); } } /// - public override RenderPipelineChannelMode GetChannelMode(int channel) => - channel < 3 - ? RenderPipelineChannelMode.InPlace - : RenderPipelineChannelMode.Ignored; + public override RenderPipelineChannelMode GetChannelMode(int channel) + // Horizontal neighbors must retain their input values until the complete output row is written. + => channel < 3 ? RenderPipelineChannelMode.InOut : RenderPipelineChannelMode.Ignored; } diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/GaborishTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/GaborishTests.cs index 62f93eafb3..ade37cc524 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/GaborishTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/GaborishTests.cs @@ -8,11 +8,119 @@ using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Tests.TestUtilities; namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; public class GaborishTests { + /// + /// Checks stage row origins, declared halos, channel weights and exact output bounds at every SIMD capability. + /// + /// The center range width. + /// The requested samples before the center range. + /// The requested samples after the center range. + [Theory] + [InlineData(1, 0, 0)] + [InlineData(3, 1, 2)] + [InlineData(17, 7, 3)] + [InlineData(33, 2, 1)] + [InlineData(33, 31, 0)] + public void Stage_UsesExplicitHaloAndPreservesUnrequestedSamples(int width, int left, int right) + { + static void RunTest(string serialized) + { + (int sampleWidth, int extraLeft, int extraRight) = FeatureTestRunner.Deserialize<(int, int, int)>(serialized); + const int origin = RenderPipelineStageBase.RenderPipelineXOffset; + const float untouched = -123F; + int outputLength = extraLeft + sampleWidth + extraRight; + int storageWidth = origin + sampleWidth + extraRight + 2; + Configuration configuration = TestEnvironment.Configuration; + using JxlPlane inputStorage = new(configuration, storageWidth, 9); + using JxlPlane outputStorage = new(configuration, storageWidth, 3); + Span inputSamples = inputStorage.GetRow(0); + Span outputSamples = outputStorage.GetRow(0); + inputSamples.Fill(untouched); + outputSamples.Fill(untouched); + Memory[] inputTable = new Memory[9]; + Memory[] outputTable = new Memory[3]; + + // The table is managed metadata containing borrowed memories. Numeric samples remain in the allocator-owned planes. + // Three table columns select channels; its three rows select the upper, current and lower source row. + for (int row = 0; row < 3; row++) + { + for (int c = 0; c < 3; c++) + { + int storageRow = (row * 3) + c; + inputTable[storageRow] = inputStorage.GetRowMemory(storageRow)[..storageWidth]; + Span samples = inputTable[storageRow].Span; + for (int x = -extraLeft - 1; x <= sampleWidth + extraRight; x++) + { + // Nonlinear integer values distinguish axial and diagonal weights without rounding ambiguity. + samples[origin + x] = ((c + 1) * 32) + (row * row * 8) + (x * x) + ((row + 1) * (x % 3)); + } + } + } + + for (int c = 0; c < 3; c++) + { + outputTable[c] = outputStorage.GetRowMemory(c)[..storageWidth]; + } + + using Buffer2D> inputRows = new(MemoryGroup>.Wrap(inputTable.AsMemory()), 3, 3); + using Buffer2D> outputRows = new(MemoryGroup>.Wrap(outputTable.AsMemory()), 3, 1); + JxlLoopFilter filter = new() + { + GaborishXWeight1 = 0.25F, + GaborishXWeight2 = 0.5F, + GaborishYWeight1 = 0.5F, + GaborishYWeight2 = 0.25F, + GaborishBWeight1 = 0F, + GaborishBWeight2 = 0.75F + }; + + using GaborishStage stage = new(configuration, filter); + stage.ProcessRow(inputRows, outputRows, extraLeft, extraRight, sampleWidth, 0, 0); + + // All three kernels have raw sum4, so normalization gives exact binary fractions in center/axial/diagonal order. + ReadOnlySpan expectedWeights = [0.25F, 0.0625F, 0.125F, 0.25F, 0.125F, 0.0625F, 0.25F, 0F, 0.1875F]; + for (int c = 0; c < 3; c++) + { + Assert.Equal(RenderPipelineChannelMode.InOut, stage.GetChannelMode(c)); + ReadOnlySpan top = inputTable[c].Span; + ReadOnlySpan middle = inputTable[3 + c].Span; + ReadOnlySpan bottom = inputTable[6 + c].Span; + ReadOnlySpan actual = outputSamples.Slice(c * outputStorage.PixelsPerRow, outputStorage.PixelsPerRow); + int begin = origin - extraLeft; + int end = begin + outputLength; + for (int x = 0; x < actual.Length; x++) + { + if (x < begin || x >= end) + { + Assert.Equal(untouched, actual[x]); + continue; + } + + float axial = (middle[x - 1] + middle[x + 1]) + (top[x] + bottom[x]); + float diagonal = (top[x - 1] + top[x + 1]) + (bottom[x - 1] + bottom[x + 1]); + float expected = (middle[x] * expectedWeights[c * 3]) + + (axial * expectedWeights[(c * 3) + 1]) + + (diagonal * expectedWeights[(c * 3) + 2]); + Assert.Equal(expected, actual[x]); + } + } + + Assert.Equal(RenderPipelineChannelMode.Ignored, stage.GetChannelMode(3)); + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + (width, left, right), + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } + private static JxlWeightsSymmetric3 GaborishKernel(float weight1, float weight2) { const float weight0 = 1.0f; From 4ae89d84a51a379eb1c7df24980dc18652578001 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Oct 2026 03:31:44 +1000 Subject: [PATCH 304/328] Correct JPEG XL reconstruction coefficient orientation and fixed-point arithmetic --- .../Decoder/Group/JxlGroupDecoder.cs | 95 ++++++++----------- 1 file changed, 39 insertions(+), 56 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs index 6de2b3bd93..fb8a599586 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs @@ -3,7 +3,6 @@ using System.Numerics; using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; using SixLabors.ImageSharp.Formats.Jxl.Memory; @@ -22,17 +21,6 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Group; internal static class JxlGroupDecoder { - public static void Transpose8x8InPlace(Span block) - { - for (int x = 0; x < 8; x++) - { - for (int y = 0; y < 8; y++) - { - RuntimeUtility.Swap(ref block[(y * 8) + x], ref block[(x * 8) + y]); - } - } - } - /// /// Restores one transform's AC planes, color correlation and separately coded lowest frequencies. /// @@ -439,8 +427,7 @@ private static bool DecodeGroupCore( ? jpegRow1[(sbx[c] * JxlFrameDimensions.DctBlockSize)..] : jpegRow2[(sbx[c] * JxlFrameDimensions.DctBlockSize)..]; - // JPEG XL is transposed, JPEG is not. - JxlDctAcPointer transposedDct = c switch + JxlDctAcPointer quantizedDct = c switch { 0 => qblock0, 1 => qblock1, @@ -448,55 +435,51 @@ private static bool DecodeGroupCore( _ => throw new InvalidOperationException() }; - Transpose8x8InPlace(transposedDct.Pointer32); - - // No CfL, so there is no need to store the Y block converted to integers. - if (!cs.Is444 || (rowCMap0[absTx] == 0 && rowCMap2[absTx] == 0)) - { - for (int i = 0; i < JxlFrameDimensions.DctBlockSize; i += Vector.Count) - { - Vector ini = Vector.Create(transposedDct.Pointer32[i..]); - ini.CopyTo(jpegPos[i..]); - } - } - else if (c == 1) + // JPEG XL stores the two coefficient axes in the opposite order to JPEG. + // Source coefficients belong to the frame or group cache. Write their transpose directly into the distinct JPEG block. + // Keeping source storage unchanged also preserves accumulated coefficients for later progressive passes. + SimdUtils.Transpose( + quantizedDct.Pointer32[..JxlFrameDimensions.DctBlockSize], + JxlFrameDimensions.BlockDimensions, + jpegPos[..JxlFrameDimensions.DctBlockSize], + JxlFrameDimensions.BlockDimensions, + JxlFrameDimensions.BlockDimensions, + JxlFrameDimensions.BlockDimensions); + + if (cs.Is444 && (rowCMap0[absTx] != 0 || rowCMap2[absTx] != 0)) { - // Y channel: save for restoring X/B, but nothing else to do. - for (int i = 0; i < JxlFrameDimensions.DctBlockSize; i += Vector.Count) + if (c == 1) { - Vector ini = Vector.Create(transposedDct.Pointer32[i..]); - ini.CopyTo(transposedDctY[i..]); - ini.CopyTo(jpegPos[i..]); + // Y is processed before X and B. Retain its transposed coefficients before the separately coded DC replaces entry zero. + jpegPos[..JxlFrameDimensions.DctBlockSize].CopyTo(transposedDctY); } - } - else - { - // transposedDctY contains the Y channel block, transposed. - Vector scale = new(JxlColorCorrelation.RatioJpeg(c == 0 ? rowCMap0[absTx] : rowCMap2[absTx])); - Vector round = new(1 << (JxlChromaFromLuma.ColorTileDimensionInBlocks - 1)); - - for (int i = 0; i < JxlFrameDimensions.DctBlockSize; i += Vector.Count) + else { - Vector input = Vector.Create(transposedDct.Pointer32[i..]); - Vector inputY = Vector.Create(transposedDctY[i..]); - Vector qt = Vector.Create(scaledQTable[((c * size) + i)..]); - - Vector coeffScale = ((qt * scale) + round) >> JxlChromaFromLuma.ColorTileDimensionInBlocks; - Vector cflFactor = ((inputY * coeffScale) + round) >> JxlChromaFromLuma.ColorTileDimensionInBlocks; - Vector result = input + cflFactor; - - result.CopyTo(jpegPos[i..]); + // Correlation updates the completed JPEG block in place. The retained Y block keeps its pre-DC coefficient values. + // The quantization-table ratio and color ratio both carry 11 fractional bits. + // Each multiplication drops one fixed-point scale; add half a unit before the native arithmetic right shift. + Vector scale = new(JxlColorCorrelation.RatioJpeg(c == 0 ? rowCMap0[absTx] : rowCMap2[absTx])); + Vector round = new(1 << (JxlChromaFromLuma.CflFixedPointPrecision - 1)); + + for (int i = 0; i < JxlFrameDimensions.DctBlockSize; i += Vector.Count) + { + Vector input = Vector.Create(jpegPos[i..]); + Vector inputY = Vector.Create(transposedDctY[i..]); + Vector qt = Vector.Create(scaledQTable[((c * size) + i)..]); + + Vector coeffScale = ((qt * scale) + round) >> JxlChromaFromLuma.CflFixedPointPrecision; + Vector cflFactor = ((inputY * coeffScale) + round) >> JxlChromaFromLuma.CflFixedPointPrecision; + Vector result = input + cflFactor; + + result.CopyTo(jpegPos[i..]); + } } } - jpegPos[0] = (int)Math.Clamp( - c == 0 - ? dcRow0[sbx[c] - dcOff[c]] - : c == 1 - ? dcRow1[sbx[c] - dcOff[c]] - : dcRow2[sbx[c] - dcOff[c]], - -2047.0f, - 2047.0f); + // The DC offset is a coefficient value, not a column offset. Subtract it after reading this block's DC sample. + // JPEG stores this separately coded coefficient in the signed range [-2047,2047]. + float dc = c == 0 ? dcRow0[sbx[c]] : c == 1 ? dcRow1[sbx[c]] : dcRow2[sbx[c]]; + jpegPos[0] = (int)Math.Clamp(dc - dcOff[c], -2047F, 2047F); Vector overflow = Vector.Zero; Vector underflow = Vector.Zero; From a17a52a5b6d3f61edd934f96956aa41e74144e2b Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Oct 2026 03:49:53 +1000 Subject: [PATCH 305/328] Own JPEG XL fixed quantization parameters and raw table payloads --- src/ImageSharp/Common/InlineArray.cs | 9 + src/ImageSharp/Common/InlineArray.tt | 2 +- .../Dct/JxlDctQuantWeightParameters.cs | 45 +- .../Decoder/Group/JxlGroupDecoder.cs | 4 +- .../Decoder/Modular/JxlModularFrameDecoder.cs | 135 +-- .../Encoder/Heuristics/JxlHeuristics.cs | 10 +- .../Encoder/JxlQuantizerWeightsEncoder.cs | 34 +- .../Modular/Encoding/JxlModularEncoding.cs | 54 +- .../Quantization/JxlDequantMatrices.cs | 32 +- .../Quantization/JxlQuantWeights.cs | 775 ++++++++---------- .../Quantization/JxlQuantizerEncoding.cs | 327 ++++---- .../Jxl/Processing/QuantizationTests.cs | 136 ++- 12 files changed, 814 insertions(+), 749 deletions(-) diff --git a/src/ImageSharp/Common/InlineArray.cs b/src/ImageSharp/Common/InlineArray.cs index 9c3f6fcbbf..bc91f3b9a7 100644 --- a/src/ImageSharp/Common/InlineArray.cs +++ b/src/ImageSharp/Common/InlineArray.cs @@ -71,6 +71,15 @@ internal struct InlineArray8 private T t; } +/// +/// Represents a safe, fixed sized buffer of 9 elements. +/// +[InlineArray(9)] +internal struct InlineArray9 +{ + private T t; +} + /// /// Represents a safe, fixed sized buffer of 10 elements. /// diff --git a/src/ImageSharp/Common/InlineArray.tt b/src/ImageSharp/Common/InlineArray.tt index 25753a464d..6341097c4c 100644 --- a/src/ImageSharp/Common/InlineArray.tt +++ b/src/ImageSharp/Common/InlineArray.tt @@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp; <#GenerateInlineArrays();#> <#+ -private static int[] Lengths = [2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 16, 17, 18, 19, 24, 25, 26, 32, 33, 36, 64, 65, 128, 256]; +private static int[] Lengths = [2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 14, 16, 17, 18, 19, 24, 25, 26, 32, 33, 36, 64, 65, 128, 256]; void GenerateInlineArrays() { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctQuantWeightParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctQuantWeightParameters.cs index b2d9ae183e..e2349b29c6 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctQuantWeightParameters.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctQuantWeightParameters.cs @@ -1,32 +1,43 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Diagnostics.CodeAnalysis; + namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; -internal sealed class JxlDctQuantWeightParameters +/// +/// Stores one DCT weight seed and its distance-band deltas for each XYB channel. +/// +/// +/// The fixed rows contain 17 entries. Only NumDistanceBands entries participate in table generation or serialization. +/// Values are copied into descriptors during preparation. Parsers update this value by reference. +/// +internal struct JxlDctQuantWeightParameters { - /// - /// Log2 of the maximum number of distance bands. - /// + /// The bit count used to store the distance-band count minus one. public const int Log2MaxDistanceBands = 4; - private const int MaxDistanceBands = 1 + (1 << Log2MaxDistanceBands); - public JxlDctQuantWeightParameters() - { - this.DistanceBands = new float[3][]; - for (int i = 0; i < 3; i++) - { - this.DistanceBands[i] = new float[MaxDistanceBands]; - } - } + /// The three fixed channel rows. Entries after NumDistanceBands do not participate in interpolation. + [SuppressMessage( + "StyleCop.CSharp.MaintainabilityRules", + "SA1401:Fields should be private", + Justification = "Fixed parameter storage must support reference parsing without value copies.")] + public InlineArray3> DistanceBands; - public JxlDctQuantWeightParameters(float[][] distanceBands, int numDistanceBands) + /// Copies three packed channel rows into their fixed parameter storage. + /// Three consecutive rows, each containing numDistanceBands values. + /// The number of values in each channel row. + public JxlDctQuantWeightParameters(ReadOnlySpan distanceBands, int numDistanceBands) { + this = default; this.NumDistanceBands = numDistanceBands; - this.DistanceBands = distanceBands; + for (int c = 0; c < 3; c++) + { + Span destination = this.DistanceBands[c]; + distanceBands.Slice(c * numDistanceBands, numDistanceBands).CopyTo(destination); + } } + /// Gets or sets the number of active seed and delta values in each channel row. public int NumDistanceBands { get; set; } - - public float[][] DistanceBands { get; } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs index fb8a599586..d8e0fecfd4 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Group/JxlGroupDecoder.cs @@ -183,12 +183,12 @@ private static bool DecodeGroupCore( return false; } - if (qe[0].QuantizationTable!.Length != 3 * 8 * 8) + if (qe[0].QuantizationTable.Length != 3 * 8 * 8) { return false; } - ReadOnlySpan qTable = qe[0].QuantizationTable.AsSpan(); + ReadOnlySpan qTable = qe[0].QuantizationTable.Span; for (int c = 0; c < 3; c++) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Modular/JxlModularFrameDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Modular/JxlModularFrameDecoder.cs index 3c656fd95c..9c126faee3 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Modular/JxlModularFrameDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Modular/JxlModularFrameDecoder.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using System.Numerics.Tensors; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; @@ -189,86 +190,92 @@ private static void IntToFloat(ReadOnlySpan rowIn, Span rowOut, in } } - public void DecodeQuantizerTable(int requiredSizeX, int requiredSizeY, JxlBitReader reader, JxlQuantizerEncoding encoding, int index, JxlModularFrameDecoder? decoder) + /// Reads one positive raw quantization table through the existing modular decoder. + /// The configuration that owns decoded channels and retained raw values. + /// The table width in coefficients. + /// The table height in coefficients. + /// The reader positioned at the F16 table denominator. + /// The descriptor that owns the completed raw table. + /// The format's quantization-table index. + /// The frame's global modular context, or null for a standalone parameter roundtrip. + /// Whether the denominator and complete modular table were decoded successfully. + public static bool DecodeQuantizerTable( + Configuration configuration, + int requiredSizeX, + int requiredSizeY, + JxlBitReader reader, + JxlQuantizerEncoding encoding, + int index, + JxlModularFrameDecoder? decoder) { - float qtableDen = 0; - - // When we decode a quantizer table, there's an F16 - // indicating quantizer table denominator. - if (!JxlF16Coder.Read(reader, ref qtableDen)) + bool complete = false; + try { - throw new InvalidOperationException("Quantizer table denominator is too small"); - } - - encoding.QuantizationTableDenominator = qtableDen; - - using JxlModularImage img = JxlModularImage.Create(this.configuration, requiredSizeX, requiredSizeY, 8, 3); + float denominator = 0; + if (!JxlF16Coder.Read(reader, ref denominator)) + { + return false; + } - // Using default modular options. - JxlModularOptions options = new(); + // Native raw weights require both a positive denominator and positive integer entries. + // F16 read rejects nonfinite fields; this additional bound excludes zero and negative denominators. + if (denominator < AlmostZero) + { + throw new InvalidImageContentException("The raw quantization denominator is too small."); + } - if (decoder is not null) - { - JxlModularStreamId quantizerTable = JxlModularStreamId.CreateQuantizerTable(index); - JxlModularEncoding.ModularGenericDecompress( - this.configuration, + encoding.QuantizationTableDenominator = denominator; + using JxlModularImage image = JxlModularImage.Create(configuration, requiredSizeX, requiredSizeY, 8, 3); + JxlGroupHeader header = new(); + JxlModularOptions options = new(); + int groupId = decoder is null ? 0 : JxlModularStreamId.CreateQuantizerTable(index).GetId(decoder.frameDimensions); + if (!JxlModularEncoding.ModularGenericDecompress( + configuration, reader, - img, - null, - quantizerTable.GetId(decoder.frameDimensions), + image, + header, + groupId, options, true, - decoder.tree, - decoder.code, - decoder.contextMap); - } - else - { - JxlModularEncoding.ModularGenericDecompress( - this.configuration, - reader, - img, - null, - 0, - options, - true); - } - - if (encoding.QuantizationTable is null) - { - encoding.QuantizationTable = new int[requiredSizeX * requiredSizeY * 3]; - } - else - { - if (encoding.QuantizationTable.Length != requiredSizeX * requiredSizeY * 3) + decoder?.tree, + decoder?.code, + decoder?.contextMap, + false)) { - throw new InvalidOperationException("The length of the quantization table doesn't match"); + return false; } - } - - Span qtable = encoding.QuantizationTable.AsSpan(); - - for (int c = 0; c < 3; c++) - { - JxlModularChannel currentChannel = img.Channels[c]; - Span channelSamples = currentChannel.GetRow(0); - int channelStride = currentChannel.Plane.PixelsPerRow; - for (int y = 0; y < requiredSizeY; y++) + // Retain logical coefficients after the temporary modular image is disposed. Its physical row padding is not table data. + // Borrow each channel span once, then copy into the descriptor's single packed X, Y and B allocation. + Span destination = encoding.PrepareRawTable(configuration, requiredSizeX * requiredSizeY * 3); + for (int c = 0; c < 3; c++) { - Span row = channelSamples[(y * channelStride)..]; - - for (int x = 0; x < requiredSizeX; x++) + JxlModularChannel channel = image.Channels[c]; + ReadOnlySpan channelSamples = channel.GetRow(0); + int stride = channel.Plane.PixelsPerRow; + int channelOffset = c * requiredSizeX * requiredSizeY; + for (int y = 0; y < requiredSizeY; y++) { - qtable[(c * requiredSizeX * requiredSizeY) + (y * requiredSizeX) + x] = row[x]; - - // The raw quantization table should never have a negative or zero value. - if (row[x] <= 0) + ReadOnlySpan row = channelSamples.Slice(y * stride, requiredSizeX); + if (TensorPrimitives.Min(row) <= 0) { - throw new InvalidOperationException("Invalid raw quantization table"); + throw new InvalidImageContentException("The raw quantization table contains a nonpositive value."); } + + row.CopyTo(destination.Slice(channelOffset + (y * requiredSizeX), requiredSizeX)); } } + + complete = true; + return true; + } + finally + { + if (!complete) + { + // A failed replacement must not retain partially copied raw coefficients, including a reused allocation. + encoding.Dispose(); + } } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs index 044858cb33..2bb962eb24 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs @@ -651,10 +651,14 @@ public static void FindBestDequantMatrices( // otherwise these are max errors specified by user compression parameters. ReadOnlySpan wp = cparams.DisablePerceptualOptimizations ? mseWeights : cparams.MaxError; - float[][] weights = [[1.0f / wp[0]], [1.0f / wp[1]], [1.0f / wp[2]]]; + ReadOnlySpan weights = [1.0f / wp[0], 1.0f / wp[1], 1.0f / wp[2]]; - JxlDctQuantWeightParameters dctParameters = new(weights); - JxlQuantizerEncoding[] encodings = ArrayHelpers.CreateAndInitialize(JxlQuantizerConstants.NumberOfQuantizerTables, () => JxlQuantizerEncoding.Dct(dctParameters)); + JxlDctQuantWeightParameters dctParameters = new(weights, 1); + JxlQuantizerEncoding[] encodings = new JxlQuantizerEncoding[JxlQuantizerConstants.NumberOfQuantizerTables]; + for (int i = 0; i < encodings.Length; i++) + { + encodings[i] = JxlQuantizerEncoding.Dct(in dctParameters); + } dequantMatrices.SetCustom(encodings, modularFrameEncoder); Span dcWeights = [1.0f / wp[0], 1.0f / wp[1], 1.0f / wp[2]]; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlQuantizerWeightsEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlQuantizerWeightsEncoder.cs index 28a655947a..d596e93de9 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlQuantizerWeightsEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlQuantizerWeightsEncoder.cs @@ -16,7 +16,11 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; /// internal static class JxlQuantizerWeightsEncoder { - public static bool EncodeDctParameters(JxlDctQuantWeightParameters parameters, JxlBitWriter writer) + /// Writes the active fixed distance bands without copying the parameter value. + /// The borrowed channel bands and their active count. + /// The destination bit writer. + /// Whether the band count and all F16 values were written successfully. + public static bool EncodeDctParameters(in JxlDctQuantWeightParameters parameters, JxlBitWriter writer) { if (parameters.NumDistanceBands < 1) { @@ -28,7 +32,7 @@ public static bool EncodeDctParameters(JxlDctQuantWeightParameters parameters, J for (int c = 0; c < 3; c++) { - Span bands = parameters.DistanceBands[c]; + ReadOnlySpan bands = parameters.DistanceBands[c]; for (int i = 0; i < parameters.NumDistanceBands; i++) { @@ -59,7 +63,7 @@ public static bool EncodeQuant(Configuration configuration, JxlQuantizerEncoding case JxlQuantMode.Id: for (int c = 0; c < 3; c++) { - Span idWeight = encoding.IdWeights![c]; + ReadOnlySpan idWeight = encoding.ModeWeights[c]; for (int i = 0; i < 3; i++) { @@ -75,7 +79,7 @@ public static bool EncodeQuant(Configuration configuration, JxlQuantizerEncoding case JxlQuantMode.Dct2: for (int c = 0; c < 3; c++) { - Span dct2Weight = encoding.Dct2Weights![c]; + ReadOnlySpan dct2Weight = encoding.ModeWeights[c]; for (int i = 0; i < 6; i++) { @@ -89,16 +93,14 @@ public static bool EncodeQuant(Configuration configuration, JxlQuantizerEncoding break; case JxlQuantMode.Dct4x8: - Span multipliers = encoding.Dct4x8Multipliers!.AsSpan(); - - if (!JxlF16Coder.Write(multipliers[0], writer) || - !JxlF16Coder.Write(multipliers[1], writer) || - !JxlF16Coder.Write(multipliers[2], writer)) + if (!JxlF16Coder.Write(encoding.ModeWeights[0][0], writer) || + !JxlF16Coder.Write(encoding.ModeWeights[1][0], writer) || + !JxlF16Coder.Write(encoding.ModeWeights[2][0], writer)) { return false; } - if (!EncodeDctParameters(encoding.DctParameters!, writer)) + if (!EncodeDctParameters(in encoding.DctParameters, writer)) { return false; } @@ -108,7 +110,7 @@ public static bool EncodeQuant(Configuration configuration, JxlQuantizerEncoding case JxlQuantMode.Dct4: for (int c = 0; c < 3; c++) { - Span dct4Mul = encoding.Dct4Multipliers![c]; + ReadOnlySpan dct4Mul = encoding.ModeWeights[c]; if (!JxlF16Coder.Write(dct4Mul[0], writer) || !JxlF16Coder.Write(dct4Mul[1], writer)) { @@ -116,7 +118,7 @@ public static bool EncodeQuant(Configuration configuration, JxlQuantizerEncoding } } - if (!EncodeDctParameters(encoding.DctParameters!, writer)) + if (!EncodeDctParameters(in encoding.DctParameters, writer)) { return false; } @@ -124,7 +126,7 @@ public static bool EncodeQuant(Configuration configuration, JxlQuantizerEncoding break; case JxlQuantMode.Dct: - if (!EncodeDctParameters(encoding.DctParameters!, writer)) + if (!EncodeDctParameters(in encoding.DctParameters, writer)) { return false; } @@ -142,7 +144,7 @@ public static bool EncodeQuant(Configuration configuration, JxlQuantizerEncoding case JxlQuantMode.Afv: for (int c = 0; c < 3; c++) { - Span afvWeights = encoding.AfvWeights![c]; + ReadOnlySpan afvWeights = encoding.ModeWeights[c]; for (int i = 0; i < 9; i++) { @@ -153,12 +155,12 @@ public static bool EncodeQuant(Configuration configuration, JxlQuantizerEncoding } } - if (!EncodeDctParameters(encoding.DctParameters!, writer)) + if (!EncodeDctParameters(in encoding.DctParameters, writer)) { return false; } - if (!EncodeDctParameters(encoding.DctParametersAfv4x4!, writer)) + if (!EncodeDctParameters(in encoding.DctParametersAfv4x4, writer)) { return false; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncoding.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncoding.cs index 29e3346623..c86307cc3d 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncoding.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncoding.cs @@ -818,20 +818,18 @@ public static void ValidateChannelDimensions(JxlModularImage image, JxlModularOp } } - /// - /// Reads one modular group's channels using local or shared entropy tables and prediction trees. - /// - /// The configuration that owns entropy and prediction storage. - /// The encoded group fields. - /// The destination modular channels. - /// The group header. - /// The group identifier used by prediction. - /// The channel and transform limits. - /// The prediction tree available to groups that request it. - /// The entropy tables available to groups that request them. - /// The context mapping for the shared entropy tables. - /// Whether to keep partial channel data if decoding fails. - /// Whether the group completed its entropy checks. + /// Reads one modular group using either its local entropy tables or a complete borrowed global context. + /// The configuration used by decoded samples and prediction scratch. + /// The source reader positioned at the group header. + /// The owned destination channels. + /// The owned destination for this group's fields. + /// The format's modular stream identifier. + /// The modular decode limits. + /// The borrowed global tree, or null for a standalone stream. + /// The matching global entropy code, or null when no global context exists. + /// The matching global context map, or null when no global context exists. + /// Whether an incomplete group may retain decoded sample data. + /// Whether group decoding and terminal entropy checks succeeded. public static bool ModularDecode( Configuration configuration, JxlBitReader br, @@ -839,9 +837,9 @@ public static bool ModularDecode( JxlGroupHeader header, int groupId, JxlModularOptions options, - Tree globalTree, - JxlAnsCode globalCode, - List globalContextMap, + Tree? globalTree, + JxlAnsCode? globalCode, + List? globalContextMap, bool allowTruncatedGroup) { if (image.Channels.Count == 0) @@ -932,7 +930,8 @@ public static bool ModularDecode( } else { - if (globalTree.Count == 0) + // Standalone modular streams have no shared context. A wire request for a global tree requires all three parts. + if (globalTree is null || globalCode is null || globalContextMap is null || globalTree.Count == 0) { throw new InvalidOperationException("No global tree available but one was requested"); } @@ -1019,6 +1018,19 @@ public static bool ModularDecode( } } + /// Decodes a modular group and optionally reverses its transforms. + /// The configuration used by owned samples and scratch. + /// The source reader positioned at the group header. + /// The owned destination channels. + /// The owned group header, retained for inverse transforms. + /// The format's modular stream identifier. + /// The modular decode limits. + /// Whether to restore the requested channel layout after decoding. + /// The borrowed global tree, or null for a standalone stream. + /// The matching global entropy code, or null when no global context exists. + /// The matching global context map, or null when no global context exists. + /// Whether an incomplete group may retain decoded sample data. + /// Whether decoded samples and the final channel layout are valid. public static bool ModularGenericDecompress( Configuration configuration, JxlBitReader br, @@ -1027,9 +1039,9 @@ public static bool ModularGenericDecompress( int groupId, JxlModularOptions options, bool undoTransforms, - Tree tree, - JxlAnsCode code, - List contextMap, + Tree? tree, + JxlAnsCode? code, + List? contextMap, bool allowTruncatedGroup) { List reqSizes = new(capacity: image.Channels.Count); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlDequantMatrices.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlDequantMatrices.cs index dea3854d4b..b645146006 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlDequantMatrices.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlDequantMatrices.cs @@ -95,7 +95,7 @@ public static JxlQuantizerEncoding[] GetLibrary() Verify(15, JxlQuantTable.DCT256X256); Verify(16, JxlQuantTable.DCT128X256); - return + JxlQuantizerEncoding[] definitions = [ JxlQuantWeights.Dct, JxlQuantWeights.Identity, @@ -116,6 +116,23 @@ public static JxlQuantizerEncoding[] GetLibrary() JxlQuantWeights.Dct128x256 ]; + // These library definitions contain fixed parameters only. Value copies make each returned descriptor independent; + // frame disposal never reaches the shared defaults used by table preparation. + for (int i = 0; i < definitions.Length; i++) + { + JxlQuantizerEncoding source = definitions[i]; + definitions[i] = new JxlQuantizerEncoding + { + Mode = source.Mode, + Predefined = source.Predefined, + DctParameters = source.DctParameters, + DctParametersAfv4x4 = source.DctParametersAfv4x4, + ModeWeights = source.ModeWeights + }; + } + + return definitions; + [Conditional("DEBUG")] static void Verify(int expected, JxlQuantTable actual) { @@ -295,6 +312,11 @@ public void SetDcQuant(InlineArray3 dcQuantizationValues) /// The complete descriptor array. The caller must not modify it after transfer. public void SetEncodings(JxlQuantizerEncoding[] encodings) { + foreach (JxlQuantizerEncoding previous in this.encodings) + { + previous.Dispose(); + } + this.encodings = encodings; this.computedMask = 0; this.preparedKinds = 0; @@ -305,9 +327,15 @@ public void SetEncodings(JxlQuantizerEncoding[] encodings) /// The fixed set of table descriptors, which callers must not modify. public ReadOnlySpan GetEncodings() => this.encodings; - /// Releases the prepared coefficient allocation. + /// Releases the coefficient allocation and every transferred descriptor's raw storage. public void Dispose() { + foreach (JxlQuantizerEncoding encoding in this.encodings) + { + encoding.Dispose(); + } + + this.encodings = []; this.tableOwner?.Dispose(); this.tableOwner = null; this.allocation = default; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantWeights.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantWeights.cs index bae16ea7ea..79af65851c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantWeights.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantWeights.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Buffers; -using System.Numerics; using System.Numerics.Tensors; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -13,7 +11,6 @@ using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Modular; -using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; @@ -37,9 +34,9 @@ internal static class JxlQuantWeights public static readonly JxlQuantizerEncoding Dct = JxlQuantizerEncoding.Dct( new JxlDctQuantWeightParameters( [ - [3150f, 0f, -0.4f, -0.4f, -0.4f, -2f], - [560f, 0f, -0.3f, -0.3f, -0.3f, -0.3f], - [512f, -2f, -1f, 0f, -1f, -2f] + 3150f, 0f, -0.4f, -0.4f, -0.4f, -2f, + 560f, 0f, -0.3f, -0.3f, -0.3f, -0.3f, + 512f, -2f, -1f, 0f, -1f, -2f ], 6)); @@ -48,9 +45,9 @@ internal static class JxlQuantWeights /// public static readonly JxlQuantizerEncoding Identity = JxlQuantizerEncoding.Identity( [ - [280f, 3160f, 3160f], - [60f, 864f, 864f], - [18f, 200f, 200f], + 280f, 3160f, 3160f, + 60f, 864f, 864f, + 18f, 200f, 200f ]); /// @@ -58,9 +55,9 @@ internal static class JxlQuantWeights /// public static readonly JxlQuantizerEncoding Dct2x2 = JxlQuantizerEncoding.Dct2( [ - [3840f, 2560f, 1280f, 640f, 480f, 300f], - [960f, 640f, 320f, 180f, 140f, 120f], - [640f, 320f, 128f, 64f, 32f, 16f], + 3840f, 2560f, 1280f, 640f, 480f, 300f, + 960f, 640f, 320f, 180f, 140f, 120f, + 640f, 320f, 128f, 64f, 32f, 16f ]); /// @@ -69,15 +66,15 @@ internal static class JxlQuantWeights public static readonly JxlQuantizerEncoding Dct4x4 = JxlQuantizerEncoding.Dct4( new JxlDctQuantWeightParameters( [ - [2200, 0, 0, 0], - [392, 0, 0, 0], - [112, -0.25f, -0.25f, -0.5f] + 2200, 0, 0, 0, + 392, 0, 0, 0, + 112, -0.25f, -0.25f, -0.5f ], 4), [ - [1, 1], - [1, 1], - [1, 1] + 1, 1, + 1, 1, + 1, 1 ]); /// @@ -86,33 +83,27 @@ internal static class JxlQuantWeights public static readonly JxlQuantizerEncoding Dct16x16 = JxlQuantizerEncoding.Dct( new JxlDctQuantWeightParameters( [ - [ - 8996.8725711814115328f, - -1.3000777393353804f, - -0.49424529824571225f, - -0.439093774457103443f, - -0.6350101832695744f, - -0.90177264050827612f, - -1.6162099239887414f, - ], - [ - 3191.48366296844234752f, - -0.67424582104194355f, - -0.80745813428471001f, - -0.44925837484843441f, - -0.35865440981033403f, - -0.31322389111877305f, - -0.37615025315725483f, - ], - [ - 1157.50408145487200256f, - -2.0531423165804414f, - -1.4f, - -0.50687130033378396f, - -0.42708730624733904f, - -1.4856834539296244f, - -4.9209142884401604f, - ] + 8996.8725711814115328f, + -1.3000777393353804f, + -0.49424529824571225f, + -0.439093774457103443f, + -0.6350101832695744f, + -0.90177264050827612f, + -1.6162099239887414f, + 3191.48366296844234752f, + -0.67424582104194355f, + -0.80745813428471001f, + -0.44925837484843441f, + -0.35865440981033403f, + -0.31322389111877305f, + -0.37615025315725483f, + 1157.50408145487200256f, + -2.0531423165804414f, + -1.4f, + -0.50687130033378396f, + -0.42708730624733904f, + -1.4856834539296244f, + -4.9209142884401604f ], 7)); @@ -122,36 +113,30 @@ internal static class JxlQuantWeights public static readonly JxlQuantizerEncoding Dct32x32 = JxlQuantizerEncoding.Dct( new JxlDctQuantWeightParameters( [ - [ - 15718.40830982518931456f, - -1.025f, - -0.98f, - -0.9012f, - -0.4f, - -0.48819395464f, - -0.421064f, - -0.27f, - ], - [ - 7305.7636810695983104f, - -0.8041958212306401f, - -0.7633036457487539f, - -0.55660379990111464f, - -0.49785304658857626f, - -0.43699592683512467f, - -0.40180866526242109f, - -0.27321683125358037f, - ], - [ - 3803.53173721215041536f, - -3.060733579805728f, - -2.0413270132490346f, - -2.0235650159727417f, - -0.5495389509954993f, - -0.4f, - -0.4f, - -0.3f, - ] + 15718.40830982518931456f, + -1.025f, + -0.98f, + -0.9012f, + -0.4f, + -0.48819395464f, + -0.421064f, + -0.27f, + 7305.7636810695983104f, + -0.8041958212306401f, + -0.7633036457487539f, + -0.55660379990111464f, + -0.49785304658857626f, + -0.43699592683512467f, + -0.40180866526242109f, + -0.27321683125358037f, + 3803.53173721215041536f, + -3.060733579805728f, + -2.0413270132490346f, + -2.0235650159727417f, + -0.5495389509954993f, + -0.4f, + -0.4f, + -0.3f ], 8)); @@ -161,33 +146,27 @@ internal static class JxlQuantWeights public static readonly JxlQuantizerEncoding Dct8x16 = JxlQuantizerEncoding.Dct( new JxlDctQuantWeightParameters( [ - [ - 7240.7734393502f, - -0.7f, - -0.7f, - -0.2f, - -0.2f, - -0.2f, - -0.5f, - ], - [ - 1448.15468787004f, - -0.5f, - -0.5f, - -0.5f, - -0.2f, - -0.2f, - -0.2f, - ], - [ - 506.854140754517f, - -1.4f, - -0.2f, - -0.5f, - -0.5f, - -1.5f, - -3.6f, - ] + 7240.7734393502f, + -0.7f, + -0.7f, + -0.2f, + -0.2f, + -0.2f, + -0.5f, + 1448.15468787004f, + -0.5f, + -0.5f, + -0.5f, + -0.2f, + -0.2f, + -0.2f, + 506.854140754517f, + -1.4f, + -0.2f, + -0.5f, + -0.5f, + -1.5f, + -3.6f ], 7)); @@ -197,36 +176,30 @@ internal static class JxlQuantWeights public static readonly JxlQuantizerEncoding Dct8x32 = JxlQuantizerEncoding.Dct( new JxlDctQuantWeightParameters( [ - [ - 16283.2494710648897f, - -1.7812845336559429f, - -1.6309059012653515f, - -1.0382179034313539f, - -0.85f, - -0.7f, - -0.9f, - -1.2360638576849587f, - ], - [ - 5089.15750884921511936f, - -0.320049391452786891f, - -0.35362849922161446f, - -0.30340000000000003f, - -0.61f, - -0.5f, - -0.5f, - -0.6f, - ], - [ - 3397.77603275308720128f, - -0.321327362693153371f, - -0.34507619223117997f, - -0.70340000000000003f, - -0.9f, - -1.0f, - -1.0f, - -1.1754605576265209f, - ] + 16283.2494710648897f, + -1.7812845336559429f, + -1.6309059012653515f, + -1.0382179034313539f, + -0.85f, + -0.7f, + -0.9f, + -1.2360638576849587f, + 5089.15750884921511936f, + -0.320049391452786891f, + -0.35362849922161446f, + -0.30340000000000003f, + -0.61f, + -0.5f, + -0.5f, + -0.6f, + 3397.77603275308720128f, + -0.321327362693153371f, + -0.34507619223117997f, + -0.70340000000000003f, + -0.9f, + -1.0f, + -1.0f, + -1.1754605576265209f ], 8)); @@ -236,36 +209,30 @@ internal static class JxlQuantWeights public static readonly JxlQuantizerEncoding Dct16x32 = JxlQuantizerEncoding.Dct( new JxlDctQuantWeightParameters( [ - [ - 13844.97076442300573f, - -0.97113799999999995f, - -0.658f, - -0.42026f, - -0.22712f, - -0.2206f, - -0.226f, - -0.6f, - ], - [ - 4798.964084220744293f, - -0.61125308982767057f, - -0.83770786552491361f, - -0.79014862079498627f, - -0.2692727459704829f, - -0.38272769465388551f, - -0.22924222653091453f, - -0.20719098826199578f, - ], - [ - 1807.236946760964614f, - -1.2f, - -1.2f, - -0.7f, - -0.7f, - -0.7f, - -0.4f, - -0.5f, - ] + 13844.97076442300573f, + -0.97113799999999995f, + -0.658f, + -0.42026f, + -0.22712f, + -0.2206f, + -0.226f, + -0.6f, + 4798.964084220744293f, + -0.61125308982767057f, + -0.83770786552491361f, + -0.79014862079498627f, + -0.2692727459704829f, + -0.38272769465388551f, + -0.22924222653091453f, + -0.20719098826199578f, + 1807.236946760964614f, + -1.2f, + -1.2f, + -0.7f, + -0.7f, + -0.7f, + -0.4f, + -0.5f ], 8)); @@ -275,24 +242,18 @@ internal static class JxlQuantWeights public static readonly JxlQuantizerEncoding Dct4x8 = JxlQuantizerEncoding.Dct4x8( new JxlDctQuantWeightParameters( [ - [ - 2198.050556016380522f, - -0.96269623020744692f, - -0.76194253026666783f, - -0.6551140670773547f - ], - [ - 764.3655248643528689f, - -0.92630200888366945f, - -0.9675229603596517f, - -0.27845290869168118f - ], - [ - 527.107573587542228f, - -1.4594385811273854f, - -1.450082094097871593f, - -1.5843722511996204f - ] + 2198.050556016380522f, + -0.96269623020744692f, + -0.76194253026666783f, + -0.6551140670773547f, + 764.3655248643528689f, + -0.92630200888366945f, + -0.9675229603596517f, + -0.27845290869168118f, + 527.107573587542228f, + -1.4594385811273854f, + -1.450082094097871593f, + -1.5843722511996204f ], 4), [1, 1, 1]); @@ -301,12 +262,12 @@ internal static class JxlQuantWeights /// AFV quantizer encoding. /// public static readonly JxlQuantizerEncoding Afv = JxlQuantizerEncoding.Afv( - Dct4x8.DctParameters!, - Dct4x4.DctParameters!, + Dct4x8.DctParameters, + Dct4x4.DctParameters, [ - [3072, 3072, 256, 256, 256, 414, 0, 0, 0], - [1024, 1024, 50, 50, 50, 58, 0, 0, 0], - [384, 384, 12, 12, 12, 22, -0.25f, -0.25f, -0.25f] + 3072, 3072, 256, 256, 256, 414, 0, 0, 0, + 1024, 1024, 50, 50, 50, 58, 0, 0, 0, + 384, 384, 12, 12, 12, 22, -0.25f, -0.25f, -0.25f ]); /// @@ -315,36 +276,30 @@ internal static class JxlQuantWeights public static readonly JxlQuantizerEncoding Dct64x64 = JxlQuantizerEncoding.Dct( new JxlDctQuantWeightParameters( [ - [ - 0.9f * 26629.073922049845f, - -1.025f, - -0.78f, - -0.65012f, - -0.19041574084286472f, - -0.20819395464f, - -0.421064f, - -0.32733845535848671f, - ], - [ - 0.9f * 9311.3238710010046f, - -0.3041958212306401f, - -0.3633036457487539f, - -0.35660379990111464f, - -0.3443074455424403f, - -0.33699592683512467f, - -0.30180866526242109f, - -0.27321683125358037f, - ], - [ - 0.9f * 4992.2486445538634f, - -1.2f, - -1.2f, - -0.8f, - -0.7f, - -0.7f, - -0.4f, - -0.5f, - ] + 0.9f * 26629.073922049845f, + -1.025f, + -0.78f, + -0.65012f, + -0.19041574084286472f, + -0.20819395464f, + -0.421064f, + -0.32733845535848671f, + 0.9f * 9311.3238710010046f, + -0.3041958212306401f, + -0.3633036457487539f, + -0.35660379990111464f, + -0.3443074455424403f, + -0.33699592683512467f, + -0.30180866526242109f, + -0.27321683125358037f, + 0.9f * 4992.2486445538634f, + -1.2f, + -1.2f, + -0.8f, + -0.7f, + -0.7f, + -0.4f, + -0.5f ], 8)); @@ -354,36 +309,30 @@ internal static class JxlQuantWeights public static readonly JxlQuantizerEncoding Dct32x64 = JxlQuantizerEncoding.Dct( new JxlDctQuantWeightParameters( [ - [ - 0.65f * 23629.073922049845f, - -1.025f, - -0.78f, - -0.65012f, - -0.19041574084286472f, - -0.20819395464f, - -0.421064f, - -0.32733845535848671f, - ], - [ - 0.65f * 8611.3238710010046f, - -0.3041958212306401f, - -0.3633036457487539f, - -0.35660379990111464f, - -0.3443074455424403f, - -0.33699592683512467f, - -0.30180866526242109f, - -0.27321683125358037f, - ], - [ - 0.65f * 4492.2486445538634f, - -1.2f, - -1.2f, - -0.8f, - -0.7f, - -0.7f, - -0.4f, - -0.5f, - ] + 0.65f * 23629.073922049845f, + -1.025f, + -0.78f, + -0.65012f, + -0.19041574084286472f, + -0.20819395464f, + -0.421064f, + -0.32733845535848671f, + 0.65f * 8611.3238710010046f, + -0.3041958212306401f, + -0.3633036457487539f, + -0.35660379990111464f, + -0.3443074455424403f, + -0.33699592683512467f, + -0.30180866526242109f, + -0.27321683125358037f, + 0.65f * 4492.2486445538634f, + -1.2f, + -1.2f, + -0.8f, + -0.7f, + -0.7f, + -0.4f, + -0.5f ], 8)); @@ -393,36 +342,30 @@ internal static class JxlQuantWeights public static readonly JxlQuantizerEncoding Dct128x128 = JxlQuantizerEncoding.Dct( new JxlDctQuantWeightParameters( [ - [ - 1.8f * 26629.073922049845f, - -1.025f, - -0.78f, - -0.65012f, - -0.19041574084286472f, - -0.20819395464f, - -0.421064f, - -0.32733845535848671f, - ], - [ - 1.8f * 9311.3238710010046f, - -0.3041958212306401f, - -0.3633036457487539f, - -0.35660379990111464f, - -0.3443074455424403f, - -0.33699592683512467f, - -0.30180866526242109f, - -0.27321683125358037f, - ], - [ - 1.8f * 4992.2486445538634f, - -1.2f, - -1.2f, - -0.8f, - -0.7f, - -0.7f, - -0.4f, - -0.5f, - ] + 1.8f * 26629.073922049845f, + -1.025f, + -0.78f, + -0.65012f, + -0.19041574084286472f, + -0.20819395464f, + -0.421064f, + -0.32733845535848671f, + 1.8f * 9311.3238710010046f, + -0.3041958212306401f, + -0.3633036457487539f, + -0.35660379990111464f, + -0.3443074455424403f, + -0.33699592683512467f, + -0.30180866526242109f, + -0.27321683125358037f, + 1.8f * 4992.2486445538634f, + -1.2f, + -1.2f, + -0.8f, + -0.7f, + -0.7f, + -0.4f, + -0.5f ], 8)); @@ -432,36 +375,30 @@ internal static class JxlQuantWeights public static readonly JxlQuantizerEncoding Dct64x128 = JxlQuantizerEncoding.Dct( new JxlDctQuantWeightParameters( [ - [ - 1.3f * 23629.073922049845f, - -1.025f, - -0.78f, - -0.65012f, - -0.19041574084286472f, - -0.20819395464f, - -0.421064f, - -0.32733845535848671f, - ], - [ - 1.3f * 8611.3238710010046f, - -0.3041958212306401f, - -0.3633036457487539f, - -0.35660379990111464f, - -0.3443074455424403f, - -0.33699592683512467f, - -0.30180866526242109f, - -0.27321683125358037f, - ], - [ - 1.3f * 4492.2486445538634f, - -1.2f, - -1.2f, - -0.8f, - -0.7f, - -0.7f, - -0.4f, - -0.5f, - ] + 1.3f * 23629.073922049845f, + -1.025f, + -0.78f, + -0.65012f, + -0.19041574084286472f, + -0.20819395464f, + -0.421064f, + -0.32733845535848671f, + 1.3f * 8611.3238710010046f, + -0.3041958212306401f, + -0.3633036457487539f, + -0.35660379990111464f, + -0.3443074455424403f, + -0.33699592683512467f, + -0.30180866526242109f, + -0.27321683125358037f, + 1.3f * 4492.2486445538634f, + -1.2f, + -1.2f, + -0.8f, + -0.7f, + -0.7f, + -0.4f, + -0.5f ], 8)); @@ -471,36 +408,30 @@ internal static class JxlQuantWeights public static readonly JxlQuantizerEncoding Dct256x256 = JxlQuantizerEncoding.Dct( new JxlDctQuantWeightParameters( [ - [ - 3.6f * 26629.073922049845f, - -1.025f, - -0.78f, - -0.65012f, - -0.19041574084286472f, - -0.20819395464f, - -0.421064f, - -0.32733845535848671f, - ], - [ - 3.6f * 9311.3238710010046f, - -0.3041958212306401f, - -0.3633036457487539f, - -0.35660379990111464f, - -0.3443074455424403f, - -0.33699592683512467f, - -0.30180866526242109f, - -0.27321683125358037f, - ], - [ - 3.6f * 4992.2486445538634f, - -1.2f, - -1.2f, - -0.8f, - -0.7f, - -0.7f, - -0.4f, - -0.5f, - ] + 3.6f * 26629.073922049845f, + -1.025f, + -0.78f, + -0.65012f, + -0.19041574084286472f, + -0.20819395464f, + -0.421064f, + -0.32733845535848671f, + 3.6f * 9311.3238710010046f, + -0.3041958212306401f, + -0.3633036457487539f, + -0.35660379990111464f, + -0.3443074455424403f, + -0.33699592683512467f, + -0.30180866526242109f, + -0.27321683125358037f, + 3.6f * 4992.2486445538634f, + -1.2f, + -1.2f, + -0.8f, + -0.7f, + -0.7f, + -0.4f, + -0.5f ], 8)); @@ -510,36 +441,30 @@ internal static class JxlQuantWeights public static readonly JxlQuantizerEncoding Dct128x256 = JxlQuantizerEncoding.Dct( new JxlDctQuantWeightParameters( [ - [ - 2.6f * 23629.073922049845f, - -1.025f, - -0.78f, - -0.65012f, - -0.19041574084286472f, - -0.20819395464f, - -0.421064f, - -0.32733845535848671f, - ], - [ - 2.6f * 8611.3238710010046f, - -0.3041958212306401f, - -0.3633036457487539f, - -0.35660379990111464f, - -0.3443074455424403f, - -0.33699592683512467f, - -0.30180866526242109f, - -0.27321683125358037f, - ], - [ - 2.6f * 4492.2486445538634f, - -1.2f, - -1.2f, - -0.8f, - -0.7f, - -0.7f, - -0.4f, - -0.5f, - ] + 2.6f * 23629.073922049845f, + -1.025f, + -0.78f, + -0.65012f, + -0.19041574084286472f, + -0.20819395464f, + -0.421064f, + -0.32733845535848671f, + 2.6f * 8611.3238710010046f, + -0.3041958212306401f, + -0.3633036457487539f, + -0.35660379990111464f, + -0.3443074455424403f, + -0.33699592683512467f, + -0.30180866526242109f, + -0.27321683125358037f, + 2.6f * 4492.2486445538634f, + -1.2f, + -1.2f, + -0.8f, + -0.7f, + -0.7f, + -0.4f, + -0.5f ], 8)); @@ -563,12 +488,17 @@ internal static class JxlQuantWeights 12.97166202570235f, ]; - public static void GetQuantWeightsDCT2(float[][] dct2Weights, Span weights) + + /// Expands the six DCT2 weights per channel into coefficient order. + /// Three fixed mode rows. Only the first six entries of each row are active. + /// The destination for three 8 by 8 coefficient planes. + public static void GetQuantWeightsDCT2(in InlineArray3> dct2Weights, Span weights) { for (int c = 0; c < 3; c++) { int start = c * 64; + // DC uses separate quantization. This unused slot follows the native table and is cleared after the dequantization weights are calculated. weights[start] = 0xBAD; weights[start + 1] = weights[start + 8] = dct2Weights[c][0]; weights[start + 9] = dct2Weights[c][1]; @@ -609,16 +539,16 @@ public static void GetQuantWeightsDCT2(float[][] dct2Weights, Span weight } } - public static void GetQuantWeightsIdentity(float[][] idWeights, Span weights) + /// Expands the three identity weights per channel into coefficient order. + /// Three fixed mode rows. Only the first three entries of each row are active. + /// The destination for three 8 by 8 coefficient planes. + public static void GetQuantWeightsIdentity(in InlineArray3> idWeights, Span weights) { for (int c = 0; c < 3; c++) { int c64 = 64 * c; - for (int i = 0; i < 64; i++) - { - weights[c64 + i] = idWeights[c][0]; - } + weights.Slice(c64, 64).Fill(idWeights[c][0]); weights[c64 + 1] = idWeights[c][1]; weights[c64 + 8] = idWeights[c][1]; @@ -728,12 +658,12 @@ private static void PrepareLogRatios(ReadOnlySpan bands, Span logR /// /// The number of coefficient rows. /// The number of coefficients per row. - /// The distance-band parameters for each channel. - /// The number of bands per channel. + /// The fixed channel rows and their active distance-band count. /// The destination weights, in channel, row, and column order. /// Whether the cumulative bands satisfy the native minimum weight. - public static bool GetQuantWeights(int rowCount, int columnCount, float[][] distanceBands, int bandCount, Span output) + public static bool GetQuantWeights(int rowCount, int columnCount, in JxlDctQuantWeightParameters parameters, Span output) { + int bandCount = parameters.NumDistanceBands; Span bands = stackalloc float[JxlQuantizerConstants.MaxDistanceBands]; Span logStorage = stackalloc float[JxlQuantizerConstants.MaxDistanceBands - 1]; Span logRatios = logStorage[..(bandCount - 1)]; @@ -744,7 +674,7 @@ public static bool GetQuantWeights(int rowCount, int columnCount, float[][] dist for (int c = 0; c < 3; c++) { - bands[0] = distanceBands[c][0]; + bands[0] = parameters.DistanceBands[c][0]; if (bands[0] < AlmostZero) { @@ -753,7 +683,7 @@ public static bool GetQuantWeights(int rowCount, int columnCount, float[][] dist for (int i = 1; i < bandCount; i++) { - bands[i] = bands[i - 1] * Mult(distanceBands[c][i]); + bands[i] = bands[i - 1] * Mult(parameters.DistanceBands[c][i]); if (bands[i] < AlmostZero) { @@ -868,7 +798,7 @@ public static bool ComputeQuantTable( return false; } - GetQuantWeightsIdentity(encoding.IdWeights!, weights); + GetQuantWeightsIdentity(in encoding.ModeWeights, weights); break; } @@ -879,7 +809,7 @@ public static bool ComputeQuantTable( return false; } - GetQuantWeightsDCT2(encoding.Dct2Weights!, weights); + GetQuantWeightsDCT2(in encoding.ModeWeights, weights); break; } @@ -893,7 +823,7 @@ public static bool ComputeQuantTable( Span weights4x4 = stackalloc float[3 * 4 * 4]; // Always use 4x4 GetQuantWeights for DCT4 quantization tables. - if (!GetQuantWeights(4, 4, encoding.DctParameters!.DistanceBands, encoding.DctParameters.NumDistanceBands, weights4x4)) + if (!GetQuantWeights(4, 4, in encoding.DctParameters, weights4x4)) { return false; } @@ -908,9 +838,9 @@ public static bool ComputeQuantTable( } } - weights[(c * num) + 1] /= encoding.Dct4Multipliers![c][0]; - weights[(c * num) + n] /= encoding.Dct4Multipliers[c][0]; - weights[(c * num) + n + 1] /= encoding.Dct4Multipliers[c][1]; + weights[(c * num) + 1] /= encoding.ModeWeights[c][0]; + weights[(c * num) + n] /= encoding.ModeWeights[c][0]; + weights[(c * num) + n + 1] /= encoding.ModeWeights[c][1]; } break; @@ -925,7 +855,7 @@ public static bool ComputeQuantTable( Span weights4x8 = stackalloc float[3 * 4 * 8]; - if (!GetQuantWeights(4, 8, encoding.DctParameters!.DistanceBands, encoding.DctParameters.NumDistanceBands, weights4x8)) + if (!GetQuantWeights(4, 8, in encoding.DctParameters, weights4x8)) { return false; } @@ -940,7 +870,7 @@ public static bool ComputeQuantTable( } } - weights[(c * num) + n] /= encoding.Dct4x8Multipliers![c]; + weights[(c * num) + n] /= encoding.ModeWeights[c][0]; } break; @@ -948,7 +878,7 @@ public static bool ComputeQuantTable( case JxlQuantMode.Dct: { - if (!GetQuantWeights(wrows, wcols, encoding.DctParameters!.DistanceBands, encoding.DctParameters.NumDistanceBands, weights)) + if (!GetQuantWeights(wrows, wcols, in encoding.DctParameters, weights)) { return false; } @@ -958,12 +888,12 @@ public static bool ComputeQuantTable( case JxlQuantMode.Raw: { - if (encoding.QuantizationTable is null || encoding.QuantizationTable.Length != 3 * num) + if (encoding.QuantizationTable.Length != 3 * num) { throw new InvalidOperationException("Invalid raw quantizer table encoding"); } - Span qtable = encoding.QuantizationTable.AsSpan(); + ReadOnlySpan qtable = encoding.QuantizationTable.Span; for (int i = 0; i < 3 * num; i++) { @@ -977,14 +907,14 @@ public static bool ComputeQuantTable( { Span weights4x8 = stackalloc float[3 * 4 * 8]; - if (!GetQuantWeights(4, 8, encoding.DctParameters!.DistanceBands, encoding.DctParameters.NumDistanceBands, weights4x8)) + if (!GetQuantWeights(4, 8, in encoding.DctParameters, weights4x8)) { return false; } Span weights4x4 = stackalloc float[3 * 4 * 4]; - if (!GetQuantWeights(4, 4, encoding.DctParametersAfv4x4!.DistanceBands, encoding.DctParametersAfv4x4.NumDistanceBands, weights4x4)) + if (!GetQuantWeights(4, 4, in encoding.DctParametersAfv4x4, weights4x4)) { return false; } @@ -997,7 +927,7 @@ public static bool ComputeQuantTable( for (int c = 0; c < 3; c++) { - bands[0] = encoding.AfvWeights![c][5]; + bands[0] = encoding.ModeWeights[c][5]; if (bands[0] < AlmostZero) { @@ -1006,7 +936,7 @@ public static bool ComputeQuantTable( for (int i = 1; i < 4; i++) { - bands[i] = bands[i - 1] * Mult(encoding.AfvWeights[c][i + 5]); + bands[i] = bands[i - 1] * Mult(encoding.ModeWeights[c][i + 5]); if (bands[i] < AlmostZero) { @@ -1022,11 +952,11 @@ public static bool ComputeQuantTable( weights[start] = 1; - SetWeight(0, 1, encoding.AfvWeights[c][0], weights); - SetWeight(1, 0, encoding.AfvWeights[c][1], weights); - SetWeight(0, 2, encoding.AfvWeights[c][2], weights); - SetWeight(2, 0, encoding.AfvWeights[c][3], weights); - SetWeight(2, 2, encoding.AfvWeights[c][4], weights); + SetWeight(0, 1, encoding.ModeWeights[c][0], weights); + SetWeight(1, 0, encoding.ModeWeights[c][1], weights); + SetWeight(0, 2, encoding.ModeWeights[c][2], weights); + SetWeight(2, 0, encoding.ModeWeights[c][3], weights); + SetWeight(2, 2, encoding.ModeWeights[c][4], weights); // All other AFV weights. for (int y = 0; y < 4; y++) @@ -1163,7 +1093,11 @@ public Vector512 Invoke(Vector512 value) } } - public static bool DecodeDctParameters(JxlBitReader reader, JxlDctQuantWeightParameters parameters) + /// Reads the complete active distance-band rows into the existing value storage. + /// The source bit reader. + /// The destination value, updated without copying its fixed rows. + /// Whether all band values were read successfully. + public static bool DecodeDctParameters(JxlBitReader reader, ref JxlDctQuantWeightParameters parameters) { parameters.NumDistanceBands = (int)reader.ReadBits32(JxlQuantizerConstants.Log2MaxDistanceBands) + 1; @@ -1193,11 +1127,11 @@ public static bool DecodeDctParameters(JxlBitReader reader, JxlDctQuantWeightPar /// /// The configuration used by the modular decoder. /// The source bit reader. - /// The destination quantization parameters. + /// The owned destination. Reading a new header releases its earlier raw payload. /// The required horizontal extent in blocks. /// The required vertical extent in blocks. /// The index that identifies the table in the modular stream. - /// The decoder for raw quantization tables. + /// The frame modular context, or null for a standalone raw-table stream. /// Whether the parameters were read successfully. public static bool Decode( Configuration configuration, @@ -1206,13 +1140,15 @@ public static bool Decode( int requiredSizeX, int requiredSizeY, int quantizationTableIndex, - JxlModularFrameDecoder modularFrameDecoder) + JxlModularFrameDecoder? modularFrameDecoder) { int requiredSize = requiredSizeX * requiredSizeY; requiredSizeX *= JxlFrameDimensions.BlockDimensions; requiredSizeY *= JxlFrameDimensions.BlockDimensions; + // This is a new descriptor header. Release an earlier raw payload before the wire mode selects different storage. + encoding.Dispose(); int mode = (int)reader.ReadBits32(JxlQuantizerConstants.Log2NumQuantModes); switch ((JxlQuantMode)mode) @@ -1240,17 +1176,17 @@ public static bool Decode( { for (int i = 0; i < 3; i++) { - if (!JxlF16Coder.Read(reader, ref encoding.IdWeights![c][i])) + if (!JxlF16Coder.Read(reader, ref encoding.ModeWeights[c][i])) { return false; } - if (Math.Abs(encoding.IdWeights[c][i]) < AlmostZero) + if (Math.Abs(encoding.ModeWeights[c][i]) < AlmostZero) { throw new InvalidOperationException("ID Quantizer is too small"); } - encoding.IdWeights[c][i] *= 64; + encoding.ModeWeights[c][i] *= 64; } } @@ -1268,17 +1204,17 @@ public static bool Decode( { for (int i = 0; i < 6; i++) { - if (!JxlF16Coder.Read(reader, ref encoding.Dct2Weights![c][i])) + if (!JxlF16Coder.Read(reader, ref encoding.ModeWeights[c][i])) { return false; } - if (Math.Abs(encoding.Dct2Weights[c][i]) < AlmostZero) + if (Math.Abs(encoding.ModeWeights[c][i]) < AlmostZero) { throw new InvalidOperationException("Quantizer is too small"); } - encoding.Dct2Weights[c][i] *= 64; + encoding.ModeWeights[c][i] *= 64; } } @@ -1294,18 +1230,18 @@ public static bool Decode( for (int c = 0; c < 3; c++) { - if (!JxlF16Coder.Read(reader, ref encoding.Dct4x8Multipliers![c])) + if (!JxlF16Coder.Read(reader, ref encoding.ModeWeights[c][0])) { return false; } - if (Math.Abs(encoding.Dct4x8Multipliers[c]) < AlmostZero) + if (Math.Abs(encoding.ModeWeights[c][0]) < AlmostZero) { throw new InvalidOperationException("DCT4X8 multiplier is too small"); } } - if (!DecodeDctParameters(reader, encoding.DctParameters!)) + if (!DecodeDctParameters(reader, ref encoding.DctParameters)) { return false; } @@ -1324,19 +1260,19 @@ public static bool Decode( { for (int i = 0; i < 2; i++) { - if (!JxlF16Coder.Read(reader, ref encoding.Dct4Multipliers![c][i])) + if (!JxlF16Coder.Read(reader, ref encoding.ModeWeights[c][i])) { return false; } - if (Math.Abs(encoding.Dct4Multipliers[c][i]) < AlmostZero) + if (Math.Abs(encoding.ModeWeights[c][i]) < AlmostZero) { throw new InvalidOperationException("DCT4 multiplier is too small"); } } } - if (!DecodeDctParameters(reader, encoding.DctParameters!)) + if (!DecodeDctParameters(reader, ref encoding.DctParameters)) { return false; } @@ -1355,7 +1291,7 @@ public static bool Decode( { for (int i = 0; i < 9; i++) { - if (!JxlF16Coder.Read(reader, ref encoding.AfvWeights![c][i])) + if (!JxlF16Coder.Read(reader, ref encoding.ModeWeights[c][i])) { return false; } @@ -1363,16 +1299,16 @@ public static bool Decode( for (int i = 0; i < 6; i++) { - encoding.AfvWeights![c][i] *= 64; + encoding.ModeWeights[c][i] *= 64; } } - if (!DecodeDctParameters(reader, encoding.DctParameters!)) + if (!DecodeDctParameters(reader, ref encoding.DctParameters)) { return false; } - if (!DecodeDctParameters(reader, encoding.DctParametersAfv4x4!)) + if (!DecodeDctParameters(reader, ref encoding.DctParametersAfv4x4)) { return false; } @@ -1382,7 +1318,7 @@ public static bool Decode( case JxlQuantMode.Dct: { - if (!DecodeDctParameters(reader, encoding.DctParameters!)) + if (!DecodeDctParameters(reader, ref encoding.DctParameters)) { return false; } @@ -1392,10 +1328,7 @@ public static bool Decode( case JxlQuantMode.Raw: { - // Set mode early, to avoid mem-leak. - encoding.Mode = JxlQuantMode.Raw; - - if (!JxlModularFrameDecoder.DecodeQuantTable( + if (!JxlModularFrameDecoder.DecodeQuantizerTable( configuration, requiredSizeX, requiredSizeY, diff --git a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerEncoding.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerEncoding.cs index 283892392a..681ca6ddd4 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerEncoding.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerEncoding.cs @@ -1,227 +1,174 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; +using System.Diagnostics.CodeAnalysis; using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; /// -/// Specifies weights and quantizer modes. +/// Owns one quantization descriptor's fixed parameters and optional raw integer table. /// -internal sealed class JxlQuantizerEncoding +/// +/// Fixed mode weights use three rows of nine floats. Identity, DCT2, DCT4, DCT4x8 and AFV use the first 3, 6, 2, 1 and 9 values per row. +/// DCT distance bands use separate fixed value fields. Only raw mode needs allocator-owned sample storage. +/// +internal sealed class JxlQuantizerEncoding : IDisposable { - public JxlQuantizerEncoding() - { - } + private IMemoryOwner? rawOwner; + private Memory rawAllocation; + private Memory rawTable; + + /// The primary DCT distance bands. Parsing updates this field by reference. + [SuppressMessage( + "StyleCop.CSharp.MaintainabilityRules", + "SA1401:Fields should be private", + Justification = "Fixed parameter storage must support reference parsing without value copies.")] + public JxlDctQuantWeightParameters DctParameters; + + /// The AFV 4 by 4 sub-block's distance bands, independent of its primary 4 by 8 bands. + [SuppressMessage( + "StyleCop.CSharp.MaintainabilityRules", + "SA1401:Fields should be private", + Justification = "Fixed parameter storage must support reference parsing without value copies.")] + public JxlDctQuantWeightParameters DctParametersAfv4x4; + + /// The three fixed weight rows. The selected mode determines the active prefix of each row. + [SuppressMessage( + "StyleCop.CSharp.MaintainabilityRules", + "SA1401:Fields should be private", + Justification = "Fixed parameter storage must support reference parsing without value copies.")] + public InlineArray3> ModeWeights; + + /// Gets or sets the wire-defined quantization mode. + public JxlQuantMode Mode { get; set; } + + /// Gets the logical raw table, stored as consecutive X, Y and B coefficient planes. + /// The borrowed memory remains valid until raw preparation or disposal changes its allocation. + public ReadOnlyMemory QuantizationTable => this.rawTable; + + /// Gets or sets the raw table's positive F16 denominator. + public float QuantizationTableDenominator { get; set; } = 1F / (8 * 255); + + /// Gets or sets the library index used only by Library mode. + public byte Predefined { get; set; } - public JxlQuantizerEncoding(JxlQuantizerEncoding other) + /// Prepares raw sample storage at the table-read boundary and returns its logical writable span. + /// The configuration that owns raw sample storage. + /// The complete X, Y and B table length established by the transform kind. + /// The destination for the decoded positive integer values. + public Span PrepareRawTable(Configuration configuration, int length) { - // Simple shallow copy - this.AfvWeights = other.AfvWeights; - this.Dct2Weights = other.Dct2Weights; - this.Dct4Multipliers = other.Dct4Multipliers; - this.Dct4x8Multipliers = other.Dct4x8Multipliers; - this.DctParameters = other.DctParameters; - this.DctParametersAfv4x4 = other.DctParametersAfv4x4; - this.IdWeights = other.IdWeights; - this.Mode = other.Mode; - this.Predefined = other.Predefined; - this.QuantizationTable = other.QuantizationTable; - this.QuantizationTableDenominator = other.QuantizationTableDenominator; - - if (other.QuantizationTable is not null) + if (length > this.rawAllocation.Length) { - // Do a deep clone for the quantization table. - // Using AsSpan() should be way faster than a normal array copy... - this.QuantizationTable = GC.AllocateUninitializedArray(other.QuantizationTable.Length); - other.QuantizationTable.AsSpan().CopyTo(this.QuantizationTable); + IMemoryOwner replacement = configuration.MemoryAllocator.Allocate(length); + Memory memory = replacement.Memory; + this.rawOwner?.Dispose(); + this.rawOwner = replacement; + this.rawAllocation = memory; } - } - - /// - /// Gets or sets the kind of transform used for this quantizer encoding. - /// - public JxlQuantMode Mode { get; set; } - /// - /// Gets or sets the weights for DCT4+ tables. - /// - public JxlDctQuantWeightParameters? DctParameters { get; set; } - - /// - /// Gets or sets the weights for the 4x4 sub-block in AFV. - /// - public JxlDctQuantWeightParameters? DctParametersAfv4x4 { get; set; } - - /// - /// Gets or sets the weights for the identity transform. - /// - public float[][]? IdWeights { get; set; } - - /// - /// Gets or sets the weights for the DCT2 transform. - /// - public float[][]? Dct2Weights { get; set; } - - /// - /// Gets or sets the multipliers for the DCT4 transform. - /// - public float[][]? Dct4Multipliers { get; set; } - - /// - /// Gets or sets the weights for the AFV transform. - /// - public float[][]? AfvWeights { get; set; } - - /// - /// Gets or sets the multipliers for the 4x8 DCT block-based transform. - /// - public float[]? Dct4x8Multipliers { get; set; } - - /// - /// Gets or sets the explicit quantization table (like in JPEG). - /// - /// - /// Only used when == . - /// - public int[]? QuantizationTable { get; set; } - - /// - /// Gets or sets the denominator for each item in the explicit quantization table. - /// - /// - /// Only used when == . - /// - public float QuantizationTableDenominator { get; set; } = 1f / (8 * 255); - - /// - /// Gets or sets a value indicating which predefined table to use. The value is - /// only used when == . - /// - public byte Predefined { get; set; } + this.rawTable = this.rawAllocation[..length]; + return this.rawTable.Span; + } - /// - /// Creates a new quantizer encoding with the Library quantizer mode - /// and the specified library index. - /// - /// The library index (aka predefined table). - /// A new Library quantizer encoding. + /// Creates a library descriptor without allocating coefficient or raw table storage. + /// The predefined library index. + /// The new owned descriptor. public static JxlQuantizerEncoding Library(int libraryIndex) { DebugGuard.MustBeLessThan(libraryIndex, JxlQuantWeights.NumPredefinedTables, nameof(libraryIndex)); - - return new() - { - Mode = JxlQuantMode.Library, - Predefined = (byte)libraryIndex - }; + return new() { Mode = JxlQuantMode.Library, Predefined = (byte)libraryIndex }; } - /// - /// Creates a new quantizer encoding with the Identity quantizer mode - /// and the specified XYB/identity weights. - /// - /// Weights for the identity transform. - /// A new Identity quantizer encoding. - public static JxlQuantizerEncoding Identity(float[][] xybWeights) - => new() - { - Mode = JxlQuantMode.Id, - IdWeights = xybWeights - }; - - /// - /// Creates a new quantizer encoding with the DCT2x2 quantizer mode - /// and the specified XYB/DCT2x2 weights. - /// - /// Weights for the DCT2x2 transform. - /// A new DCT2x2 quantizer encoding. - public static JxlQuantizerEncoding Dct2(float[][] xybWeights) - => new() - { - Mode = JxlQuantMode.Dct2, - Dct2Weights = xybWeights - }; + /// Copies identity weights into a new descriptor. + /// Three consecutive channel rows of three values. + /// The new owned descriptor. + public static JxlQuantizerEncoding Identity(ReadOnlySpan xybWeights) + { + JxlQuantizerEncoding encoding = new() { Mode = JxlQuantMode.Id }; + encoding.CopyWeights(xybWeights, 3); + return encoding; + } - /// - /// Creates a new quantizer encoding with the DCT4x4 quantizer mode, - /// the specified XYB/DCT4x4 multipliers, and quantizer weight parameters. - /// - /// Quantizer weights for the DCT4x4 transform. - /// XYB multipliers for the DCT4x4 transform. - /// A new DCT4x4 quantizer encoding. - public static JxlQuantizerEncoding Dct4(JxlDctQuantWeightParameters parameters, float[][] xybMul) - => new() - { - Mode = JxlQuantMode.Dct4, - DctParameters = parameters, - Dct4Multipliers = xybMul - }; + /// Copies DCT2 weights into a new descriptor. + /// Three consecutive channel rows of six values. + /// The new owned descriptor. + public static JxlQuantizerEncoding Dct2(ReadOnlySpan xybWeights) + { + JxlQuantizerEncoding encoding = new() { Mode = JxlQuantMode.Dct2 }; + encoding.CopyWeights(xybWeights, 6); + return encoding; + } - /// - /// Creates a new quantizer encoding with the DCT4x8 quantizer mode, - /// the specified XYB/DCT4x8 multipliers, and quantizer weight parameters. - /// - /// Quantizer weights for the DCT4x8 transform. - /// XYB multipliers for the DCT4x8 transform. - /// A new DCT4x8 quantizer encoding. - public static JxlQuantizerEncoding Dct4x8(JxlDctQuantWeightParameters parameters, float[] xybMul) - => new() - { - Mode = JxlQuantMode.Dct4x8, - DctParameters = parameters, - Dct4x8Multipliers = xybMul - }; + /// Copies the DCT4 distance bands and sub-block multipliers into a new descriptor. + /// The distance-band values, copied once. + /// Three consecutive channel rows of two multipliers. + /// The new owned descriptor. + public static JxlQuantizerEncoding Dct4(in JxlDctQuantWeightParameters parameters, ReadOnlySpan xybMul) + { + JxlQuantizerEncoding encoding = new() { Mode = JxlQuantMode.Dct4, DctParameters = parameters }; + encoding.CopyWeights(xybMul, 2); + return encoding; + } - /// - /// Creates a new quantizer encoding with the DCT quantizer mode - /// and quantizer weight parameters. - /// - /// Quantizer weights for the DCT transform. - /// A new DCT quantizer encoding. - public static JxlQuantizerEncoding Dct(JxlDctQuantWeightParameters parameters) - => new() - { - Mode = JxlQuantMode.Dct, - DctParameters = parameters, - }; + /// Copies the DCT4x8 distance bands and channel multipliers into a new descriptor. + /// The distance-band values, copied once. + /// One multiplier for each of the three channels. + /// The new owned descriptor. + public static JxlQuantizerEncoding Dct4x8(in JxlDctQuantWeightParameters parameters, ReadOnlySpan xybMul) + { + JxlQuantizerEncoding encoding = new() { Mode = JxlQuantMode.Dct4x8, DctParameters = parameters }; + encoding.CopyWeights(xybMul, 1); + return encoding; + } - /// - /// Creates a new quantizer encoding with the AFV quantizer mode, - /// quantizer weight parameters for 4x8/4x4 blocks, and weights. - /// - /// Quantizer weights for the 4x8 sub-block for the AFV transform. - /// Quantizer weights for the 4x4 sub-block for the AFV transform. - /// Quantizer weights. - /// A new DCT quantizer encoding. + /// Copies DCT distance bands into a new descriptor. + /// The three distance-band rows and their active count. + /// The new owned descriptor. + public static JxlQuantizerEncoding Dct(in JxlDctQuantWeightParameters parameters) + => new() { Mode = JxlQuantMode.Dct, DctParameters = parameters }; + + /// Copies independent AFV sub-block bands and fixed weights into a new descriptor. + /// The 4 by 8 distance-band values. + /// The 4 by 4 distance-band values. + /// Three consecutive channel rows of nine fixed weights. + /// The new owned descriptor. public static JxlQuantizerEncoding Afv( - JxlDctQuantWeightParameters params4x8, - JxlDctQuantWeightParameters params4x4, - float[][] weights) - => new() + in JxlDctQuantWeightParameters params4x8, + in JxlDctQuantWeightParameters params4x4, + ReadOnlySpan weights) + { + JxlQuantizerEncoding encoding = new() { Mode = JxlQuantMode.Afv, DctParameters = params4x8, - AfvWeights = weights, DctParametersAfv4x4 = params4x4 }; - /// - /// Creates a new raw quantizer encoding. - /// - /// The quantization table for raw quantization. - /// The shift value for the denominator. - /// A raw quantizer encoding. - public static JxlQuantizerEncoding Raw(Span quantizationTable, int shift = 0) + encoding.CopyWeights(weights, 9); + return encoding; + } + + /// Copies packed channel prefixes into the common fixed mode bank. + /// Three consecutive channel prefixes. + /// The mode's number of active values in each row. + private void CopyWeights(ReadOnlySpan weights, int valuesPerChannel) { - JxlQuantizerEncoding encoding = new() + for (int c = 0; c < 3; c++) { - Mode = JxlQuantMode.Raw, - QuantizationTableDenominator = (1 << shift) * (1f / (8 * 255)), - QuantizationTable = GC.AllocateUninitializedArray(quantizationTable.Length) - }; + Span row = this.ModeWeights[c]; + weights.Slice(c * valuesPerChannel, valuesPerChannel).CopyTo(row); + } + } - quantizationTable.CopyTo(encoding.QuantizationTable); - return encoding; + /// Releases raw storage. Fixed fields remain unchanged, including shared default parameters. + public void Dispose() + { + this.rawOwner?.Dispose(); + this.rawOwner = null; + this.rawAllocation = default; + this.rawTable = default; } } diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/QuantizationTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/QuantizationTests.cs index be48419e91..cb2ffa2441 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/QuantizationTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/QuantizationTests.cs @@ -3,6 +3,8 @@ using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Modular; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; using SixLabors.ImageSharp.Tests.Memory; @@ -20,7 +22,7 @@ public void QuantizationTables_WriteReciprocalsAndExcludeLowFrequencies() { static void RunTest() { - JxlQuantizerEncoding encoding = JxlQuantizerEncoding.Dct(new JxlDctQuantWeightParameters([[2F], [4F], [8F]], 1)); + using JxlQuantizerEncoding encoding = JxlQuantizerEncoding.Dct(new JxlDctQuantWeightParameters([2F, 4F, 8F], 1)); ReadOnlySpan kinds = [JxlQuantTable.DCT, JxlQuantTable.DCT8X16, JxlQuantTable.DCT16X16, JxlQuantTable.DCT256X256]; foreach (JxlQuantTable kind in kinds) @@ -80,7 +82,7 @@ static void RunTest() foreach (float invalid in invalidWeights) { // A one-band table reaches the reciprocal validation directly. The third channel must still be checked. - JxlQuantizerEncoding encoding = JxlQuantizerEncoding.Dct(new JxlDctQuantWeightParameters([[2F], [4F], [invalid]], 1)); + using JxlQuantizerEncoding encoding = JxlQuantizerEncoding.Dct(new JxlDctQuantWeightParameters([2F, 4F, invalid], 1)); float[] table = new float[3 * 64]; float[] inverse = new float[3 * 64]; int offset = 0; @@ -100,8 +102,18 @@ static void RunTest() [Fact] public void DefaultWeights_MatchNativeBandAndTableMetadata() { - Assert.Equal(8, JxlQuantWeights.Dct32x32.DctParameters!.NumDistanceBands); - Assert.Same(JxlQuantWeights.Dct32x64, JxlDequantMatrices.Library[(int)JxlQuantTable.DCT32X64]); + Assert.Equal(8, JxlQuantWeights.Dct32x32.DctParameters.NumDistanceBands); + JxlQuantizerEncoding expected = JxlQuantWeights.Dct32x64; + JxlQuantizerEncoding actual = JxlDequantMatrices.Library[(int)JxlQuantTable.DCT32X64]; + Assert.NotSame(expected, actual); + Assert.Equal(expected.Mode, actual.Mode); + Assert.Equal(expected.DctParameters.NumDistanceBands, actual.DctParameters.NumDistanceBands); + for (int c = 0; c < 3; c++) + { + ReadOnlySpan expectedBands = expected.DctParameters.DistanceBands[c]; + ReadOnlySpan actualBands = actual.DctParameters.DistanceBands[c]; + Assert.True(expectedBands.SequenceEqual(actualBands)); + } } /// @@ -113,8 +125,8 @@ public void DistanceWeights_ApplyMultipliersAndPreserveExactIntervalValues() static void RunTest() { // Mult(0)=1 keeps every interval constant. This exercises the real interpolation path rather than the single-band fill. - float[][] constantBands = [[2F, 0F, 0F], [4F, 0F, 0F], [8F, 0F, 0F]]; - float[][] varyingBands = [[2F, 1F, -1F], [4F, 1F, -1F], [8F, 1F, -1F]]; + JxlDctQuantWeightParameters constantBands = new([2F, 0F, 0F, 4F, 0F, 0F, 8F, 0F, 0F], 3); + JxlDctQuantWeightParameters varyingBands = new([2F, 1F, -1F, 4F, 1F, -1F, 8F, 1F, -1F], 3); ReadOnlySpan columns = [3, 4, 7, 8, 15, 16, 17, 31, 32, 33]; foreach (int columnCount in columns) @@ -124,13 +136,13 @@ static void RunTest() float[] storage = new float[(3 * channelSize) + 2]; storage.AsSpan().Fill(-77F); Span weights = storage.AsSpan(1, 3 * channelSize); - Assert.True(JxlQuantWeights.GetQuantWeights(rowCount, columnCount, constantBands, 3, weights)); + Assert.True(JxlQuantWeights.GetQuantWeights(rowCount, columnCount, in constantBands, weights)); for (int c = 0; c < 3; c++) { for (int i = 0; i < channelSize; i++) { - Assert.Equal(constantBands[c][0], weights[(c * channelSize) + i]); + Assert.Equal(constantBands.DistanceBands[c][0], weights[(c * channelSize) + i]); } } @@ -138,10 +150,10 @@ static void RunTest() Assert.Equal(-77F, storage[^1]); // Mult(1)=2 and Mult(-1)=1/2 produce positive cumulative bands [a,2a,a]. Raw multiplication would reject them. - Assert.True(JxlQuantWeights.GetQuantWeights(rowCount, columnCount, varyingBands, 3, weights)); + Assert.True(JxlQuantWeights.GetQuantWeights(rowCount, columnCount, in varyingBands, weights)); for (int c = 0; c < 3; c++) { - Assert.Equal(varyingBands[c][0], weights[c * channelSize]); + Assert.Equal(varyingBands.DistanceBands[c][0], weights[c * channelSize]); } Assert.Equal(-77F, storage[0]); @@ -349,7 +361,7 @@ public void DequantMatrices_PrepareOnlyRequestedKindsAndReuseOrientations() encodings[i] = JxlQuantizerEncoding.Library(0); } - encodings[(int)JxlQuantTable.DCT8X16] = JxlQuantizerEncoding.Dct(new JxlDctQuantWeightParameters([[2F], [4F], [8F]], 1)); + encodings[(int)JxlQuantTable.DCT8X16] = JxlQuantizerEncoding.Dct(new JxlDctQuantWeightParameters([2F, 4F, 8F], 1)); matrices.SetEncodings(encodings); uint mask = (1U << (int)JxlAcStrategyType.DCT) | (1U << (int)JxlAcStrategyType.DCT8X16); Assert.True(matrices.EnsureComputed(configuration, mask)); @@ -405,7 +417,7 @@ public void DequantMatrices_FailedPreparationReleasesReplacement() JxlQuantizerEncoding[] encodings = JxlDequantMatrices.GetLibrary(); // Identity parameters are valid only for an 8 by 8 table. The 16 by 16 request must fail through real table generation. - encodings[(int)JxlQuantTable.DCT16X16] = JxlQuantizerEncoding.Identity([[1F, 1F, 1F], [1F, 1F, 1F], [1F, 1F, 1F]]); + encodings[(int)JxlQuantTable.DCT16X16] = JxlQuantizerEncoding.Identity([1F, 1F, 1F, 1F, 1F, 1F, 1F, 1F, 1F]); matrices.SetEncodings(encodings); Assert.False(matrices.EnsureComputed(configuration, 1U << (int)JxlAcStrategyType.DCT16X16)); Assert.True(matrices.GetPreparedStorage().IsEmpty); @@ -421,4 +433,104 @@ public void DequantMatrices_FailedPreparationReleasesReplacement() Assert.Equal(0, allocator.LiveBufferBytes); Assert.Equal(2, allocator.ReturnLog.Count); } + + /// Checks fixed mode and DCT parameter storage through independently specified wire fields. + [Fact] + public void QuantizerDescriptors_ParseFixedModeAndDistanceBands() + { + // ID mode is 1 in three low bits. Nine half values of 1 (0x3C00) follow; the decoder multiplies each by 64. + using (MemoryStream input = new([0x01, 0xE0, 0x01, 0xE0, 0x01, 0xE0, 0x01, 0xE0, 0x01, 0xE0, 0x01, 0xE0, 0x01, 0xE0, 0x01, 0xE0, 0x01, 0xE0, 0x01])) + using (JxlQuantizerEncoding encoding = new()) + { + JxlBitReader reader = new(input); + Assert.True(JxlQuantWeights.Decode(TestEnvironment.Configuration, reader, encoding, 1, 1, 0, null)); + Assert.Equal(JxlQuantMode.Id, encoding.Mode); + Assert.Equal(147, reader.TotalBitsConsumed); + for (int c = 0; c < 3; c++) + { + for (int i = 0; i < 3; i++) + { + Assert.Equal(64F, encoding.ModeWeights[c][i]); + } + } + } + + // DCT mode 6 uses three bits, followed by four zero count bits (one band). + // Half seeds 0x2800,0x2C00,0x3000 represent 1/32,1/16,1/8. Multiplication by 64 yields 2,4,8. + using (MemoryStream input = new([0x06, 0x00, 0x14, 0x00, 0x16, 0x00, 0x18])) + using (JxlQuantizerEncoding encoding = new()) + { + JxlBitReader reader = new(input); + Assert.True(JxlQuantWeights.Decode(TestEnvironment.Configuration, reader, encoding, 1, 1, 0, null)); + Assert.Equal(JxlQuantMode.Dct, encoding.Mode); + Assert.Equal(1, encoding.DctParameters.NumDistanceBands); + Assert.Equal(2F, encoding.DctParameters.DistanceBands[0][0]); + Assert.Equal(4F, encoding.DctParameters.DistanceBands[1][0]); + Assert.Equal(8F, encoding.DctParameters.DistanceBands[2][0]); + Assert.Equal(55, reader.TotalBitsConsumed); + } + } + + /// Checks fixed value copies and raw ownership transfer, replacement, reuse and disposal. + [Fact] + public void QuantizerDescriptors_CopyFixedValuesAndTransferRawOwnership() + { + Span packed = [2F, 4F, 8F]; + JxlDctQuantWeightParameters parameters = new(packed, 1); + using JxlQuantizerEncoding fixedDescriptor = JxlQuantizerEncoding.Dct(in parameters); + packed.Fill(-1F); + parameters.DistanceBands[0][0] = -1F; + Assert.Equal(2F, fixedDescriptor.DctParameters.DistanceBands[0][0]); + + TestMemoryAllocator allocator = new(); + allocator.EnableNonThreadSafeLogging(); + Configuration configuration = TestEnvironment.Configuration.Clone(); + configuration.MemoryAllocator = allocator; + using (JxlDequantMatrices matrices = new()) + { + JxlQuantizerEncoding[] descriptors = JxlDequantMatrices.GetLibrary(); + JxlQuantizerEncoding raw = descriptors[(int)JxlQuantTable.DCT]; + raw.Mode = JxlQuantMode.Raw; + raw.PrepareRawTable(configuration, 3 * 64).Fill(7); + raw.PrepareRawTable(configuration, 3 * 64); + Assert.Single(allocator.AllocationLog); + + // A larger real transform replaces its raw allocation once. Returning to DCT8 reuses that capacity without copying. + raw.PrepareRawTable(configuration, 3 * 256).Fill(11); + raw.PrepareRawTable(configuration, 3 * 64); + Assert.Equal(2, allocator.AllocationLog.Count); + Assert.Single(allocator.ReturnLog); + Assert.Equal(192, raw.QuantizationTable.Length); + matrices.SetEncodings(descriptors); + matrices.SetEncodings(JxlDequantMatrices.GetLibrary()); + Assert.True(raw.QuantizationTable.IsEmpty); + Assert.Equal(0, allocator.LiveBufferBytes); + } + + Assert.Equal(2, allocator.ReturnLog.Count); + } + + /// Checks the native raw-denominator boundary and cleanup of a failed descriptor replacement. + /// The low byte of the fixed half field. + /// The high byte: zero or the sign and exponent bits for minus one. + [Theory] + [InlineData((byte)0, (byte)0)] + [InlineData((byte)0, (byte)0xBC)] + public void RawQuantizer_InvalidDenominatorReleasesOwnedSamples(byte low, byte high) + { + TestMemoryAllocator allocator = new(); + allocator.EnableNonThreadSafeLogging(); + Configuration configuration = TestEnvironment.Configuration.Clone(); + configuration.MemoryAllocator = allocator; + using JxlQuantizerEncoding encoding = new() { Mode = JxlQuantMode.Raw }; + encoding.PrepareRawTable(configuration, 3 * 64).Fill(1); + using MemoryStream input = new([low, high]); + JxlBitReader reader = new(input); + Assert.Throws( + () => JxlModularFrameDecoder.DecodeQuantizerTable(configuration, 8, 8, reader, encoding, 0, null)); + + Assert.True(encoding.QuantizationTable.IsEmpty); + Assert.Equal(0, allocator.LiveBufferBytes); + Assert.Single(allocator.ReturnLog); + } } From 6702ef41be32da5de703c8b709d3e796818ebece Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Oct 2026 03:49:54 +1000 Subject: [PATCH 306/328] Preserve high-precision JPEG XL palette scoring and bound implicit cube digits --- .../Modular/Transforms/JxlPalette.cs | 23 +++-- .../Formats/Jxl/Processing/PaletteTests.cs | 97 ++++++++++++++++++- 2 files changed, 110 insertions(+), 10 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs index ae5861e030..c4ffa7ad91 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs @@ -683,7 +683,8 @@ private static float ColorDistance(ReadOnlySpan a, ReadOnlySpan b) } distance += diff * diff * weight * weight; - int sumWeight = c == 0 ? 3 : c == 1 ? 5 : 1; + // Weight both sums in float. Explicit entries can use the full integer precision; multiplying them as Int32 can overflow before conversion. + float sumWeight = c == 0 ? 3F : c == 1 ? 5F : 1F; sumA += a[c] * sumWeight; sumB += b[c] * sumWeight; } @@ -694,26 +695,30 @@ private static float ColorDistance(ReadOnlySpan a, ReadOnlySpan b) } /// - /// Selects the nearest implicit cube entry using the native integer quantization formula. + /// Selects a canonical implicit RGB cube entry in the decoder's capped sample range. /// - /// The bounded source components. + /// The rounded, nonnegative source components. Their values remain unchanged. /// The number of explicit entries preceding the two implicit cubes. - /// The source precision used to quantize cube positions. + /// The decoder's implicit-color precision, at most 24 bits. /// Whether to select the larger five-position cube instead of the offset four-position cube. /// The implicit index after the explicit palette entries. - private static int QuantizeColorToImplicitPaletteIndex(Span color, int paletteSize, int bitDepth, bool highQuality) + private static int QuantizeColorToImplicitPaletteIndex(ReadOnlySpan color, int paletteSize, int bitDepth, bool highQuality) { int index = 0; int quant = (1 << bitDepth) - 1; int half = bitDepth > 1 ? (1 << (bitDepth - 1)) : 0; + int components = Math.Min(color.Length, RgbChannels); + // Native palette.cc and Rust palette.rs cap implicit colors to 24 bits and define only RGB cube digits; later components are zero. + // Upstream rounding already makes color nonnegative. Bound only each local cube input, preserving full-precision scoring data. + // At 24 bits, 4*quant+half is at most 75,497,468, so the existing Int32 quantization arithmetic cannot overflow. if (highQuality) { int multiplier = 1; - for (int i = 0; i < color.Length; i++) + for (int i = 0; i < components; i++) { - int value = color[i]; + int value = Math.Min(color[i], quant); int quantized = (((LargeCube - 1) * value) + half) / quant; index += quantized * multiplier; multiplier *= LargeCube; @@ -726,9 +731,9 @@ private static int QuantizeColorToImplicitPaletteIndex(Span color, int pale int multiplier = 1; int bdMinus3 = 1 << Math.Max(0, bitDepth - 3); - for (int i = 0; i < color.Length; i++) + for (int i = 0; i < components; i++) { - int value = color[i]; + int value = Math.Min(color[i], quant); value -= bdMinus3; value = Math.Max(0, value); diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/PaletteTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/PaletteTests.cs index 467df133c0..b186dbfe10 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/PaletteTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/PaletteTests.cs @@ -10,10 +10,105 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; /// -/// Tests inverse palette sample reconstruction and its channel ownership. +/// Tests palette index selection, sample reconstruction and channel ownership. /// public class PaletteTests { + /// + /// Checks that scoring preserves an exact high-precision explicit match instead of overflowing its weighted luminance. + /// + [Fact] + public void ForwardPalette_HighPrecisionExplicitMatchesRemainExact() + { + Configuration configuration = TestEnvironment.Configuration; + using JxlModularImage image = JxlModularImage.Create(configuration, 2, 1, 30, 1); + Span source = image.Channels[0].GetRow(0); + source[0] = 1000000000; + source[1] = 600000000; + int colors = 2; + int deltas = 0; + JxlPredictor predictor = JxlPredictor.Zero; + JxlModularHeader header = new(); + + // One component skips RGB preprocessing. Two allowed entries retain these exact float-representable source values before scoring. + // At the first pixel, an Int32 luminance product turns the exact match's zero error into 2^64. + // The 600-million competitor then scores lower. Float multiplication keeps the exact match's error zero, so diffusion also remains zero. + JxlPalette.ForwardPalette(configuration, image, 0, 0, ref colors, ref deltas, false, true, ref predictor, header); + + Assert.Equal(2, colors); + Assert.Equal(0, deltas); + JxlPalette.InversePalette(configuration, image, 0, colors, deltas, predictor, header); + ReadOnlySpan restored = image.Channels[0].GetRow(0); + Assert.Equal(1000000000, restored[0]); + Assert.Equal(600000000, restored[1]); + } + + /// + /// Checks that 31-bit source maxima select bounded implicit digits in the decoder's 24-bit range. + /// + [Fact] + public void ForwardPalette_HighPrecisionImplicitDigitsUseDecoderRange() + { + Configuration configuration = TestEnvironment.Configuration; + using JxlModularImage image = JxlModularImage.Create(configuration, 1, 1, 31, 2); + image.Channels[0].GetRow(0)[0] = int.MaxValue; + image.Channels[1].GetRow(0)[0] = int.MaxValue; + int colors = 0; + int deltas = 0; + JxlPredictor predictor = JxlPredictor.Zero; + JxlModularHeader header = new(); + + // Two components skip RGB delta preprocessing. Zero explicit colors and no earlier pixels leave only the fixed implicit candidates. + // The large cube is nearest in both components. Its luminance-only advantage over the small cube exceeds four billion score units. + // The largest index-penalty difference is 380, so penalties cannot change this first-pixel selection. + JxlPalette.ForwardPalette(configuration, image, 0, 1, ref colors, ref deltas, false, true, ref predictor, header); + + Assert.Equal(0, colors); + Assert.Equal(0, deltas); + Assert.Equal(0, image.Channels[0].Width); + + // Native palette.cc and Rust palette.rs cap implicit precision to 24. Digits [R,G,B]=[4,4,0] encode as 64+4+5*4=88. + Assert.Equal(88, image.Channels[1].GetRow(0)[0]); + JxlPalette.InversePalette(configuration, image, 0, colors, deltas, predictor, header); + Assert.Equal(16777215, image.Channels[0].GetRow(0)[0]); + Assert.Equal(16777215, image.Channels[1].GetRow(0)[0]); + } + + /// + /// Checks that extra components do not add digits beyond the three-component implicit cube. + /// + [Fact] + public void ForwardPalette_ImplicitCubeUsesOnlyRgbDigits() + { + Configuration configuration = TestEnvironment.Configuration; + using JxlModularImage image = JxlModularImage.Create(configuration, 1, 1, 24, 6); + for (int c = 0; c < image.Channels.Count; c++) + { + image.Channels[c].GetRow(0)[0] = 16777215; + } + + int colors = 0; + int deltas = 0; + JxlPredictor predictor = JxlPredictor.Zero; + JxlModularHeader header = new(); + + // A one-pixel source cannot create frequent absolute colors. The preprocessing residual rounds down to 16711680 in its delta bucket. + // The large cube represents all three RGB maxima exactly; extra components are zero for every implicit candidate and cannot add digits. + // Its luminance-only advantage over the learned delta exceeds 444000 score units, so index penalties cannot select that delta. + JxlPalette.ForwardPalette(configuration, image, 0, 5, ref colors, ref deltas, false, true, ref predictor, header); + + Assert.Equal(0, colors); + int paletteSize = image.Channels[0].Width; + + // Large-cube digits [4,4,4] encode as 64+4+5*4+25*4=188, regardless of any retained explicit delta prefix. + Assert.Equal(188, image.Channels[1].GetRow(0)[0] - paletteSize); + JxlPalette.InversePalette(configuration, image, 0, colors, deltas, predictor, header); + for (int c = 0; c < image.Channels.Count; c++) + { + Assert.Equal(c < 3 ? 16777215 : 0, image.Channels[c].GetRow(0)[0]); + } + } + /// /// Checks component selection, retained index strides, channel placement and allocation ownership at every SIMD width. /// From b19ed50b81b03cbe5800bd7bff2fcb7641cd7813 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Oct 2026 03:54:10 +1000 Subject: [PATCH 307/328] Preserve decimal precision when preparing default JPEG XL weight seeds --- .../Quantization/JxlQuantWeights.cs | 37 ++++++++++--------- .../Jxl/Processing/QuantizationTests.cs | 27 ++++++++++++++ 2 files changed, 46 insertions(+), 18 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantWeights.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantWeights.cs index 79af65851c..c40a2bbf21 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantWeights.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantWeights.cs @@ -270,13 +270,14 @@ internal static class JxlQuantWeights 384, 384, 12, 12, 12, 22, -0.25f, -0.25f, -0.25f ]); + // Round these compile-time seed products to float once. Rounding their operands first can change the resulting coefficient. /// /// DCT64x64 quantizer encoding. /// public static readonly JxlQuantizerEncoding Dct64x64 = JxlQuantizerEncoding.Dct( new JxlDctQuantWeightParameters( [ - 0.9f * 26629.073922049845f, + (float)(0.9 * 26629.073922049845), -1.025f, -0.78f, -0.65012f, @@ -284,7 +285,7 @@ internal static class JxlQuantWeights -0.20819395464f, -0.421064f, -0.32733845535848671f, - 0.9f * 9311.3238710010046f, + (float)(0.9 * 9311.3238710010046), -0.3041958212306401f, -0.3633036457487539f, -0.35660379990111464f, @@ -292,7 +293,7 @@ internal static class JxlQuantWeights -0.33699592683512467f, -0.30180866526242109f, -0.27321683125358037f, - 0.9f * 4992.2486445538634f, + (float)(0.9 * 4992.2486445538634), -1.2f, -1.2f, -0.8f, @@ -309,7 +310,7 @@ internal static class JxlQuantWeights public static readonly JxlQuantizerEncoding Dct32x64 = JxlQuantizerEncoding.Dct( new JxlDctQuantWeightParameters( [ - 0.65f * 23629.073922049845f, + (float)(0.65 * 23629.073922049845), -1.025f, -0.78f, -0.65012f, @@ -317,7 +318,7 @@ internal static class JxlQuantWeights -0.20819395464f, -0.421064f, -0.32733845535848671f, - 0.65f * 8611.3238710010046f, + (float)(0.65 * 8611.3238710010046), -0.3041958212306401f, -0.3633036457487539f, -0.35660379990111464f, @@ -325,7 +326,7 @@ internal static class JxlQuantWeights -0.33699592683512467f, -0.30180866526242109f, -0.27321683125358037f, - 0.65f * 4492.2486445538634f, + (float)(0.65 * 4492.2486445538634), -1.2f, -1.2f, -0.8f, @@ -342,7 +343,7 @@ internal static class JxlQuantWeights public static readonly JxlQuantizerEncoding Dct128x128 = JxlQuantizerEncoding.Dct( new JxlDctQuantWeightParameters( [ - 1.8f * 26629.073922049845f, + (float)(1.8 * 26629.073922049845), -1.025f, -0.78f, -0.65012f, @@ -350,7 +351,7 @@ internal static class JxlQuantWeights -0.20819395464f, -0.421064f, -0.32733845535848671f, - 1.8f * 9311.3238710010046f, + (float)(1.8 * 9311.3238710010046), -0.3041958212306401f, -0.3633036457487539f, -0.35660379990111464f, @@ -358,7 +359,7 @@ internal static class JxlQuantWeights -0.33699592683512467f, -0.30180866526242109f, -0.27321683125358037f, - 1.8f * 4992.2486445538634f, + (float)(1.8 * 4992.2486445538634), -1.2f, -1.2f, -0.8f, @@ -375,7 +376,7 @@ internal static class JxlQuantWeights public static readonly JxlQuantizerEncoding Dct64x128 = JxlQuantizerEncoding.Dct( new JxlDctQuantWeightParameters( [ - 1.3f * 23629.073922049845f, + (float)(1.3 * 23629.073922049845), -1.025f, -0.78f, -0.65012f, @@ -383,7 +384,7 @@ internal static class JxlQuantWeights -0.20819395464f, -0.421064f, -0.32733845535848671f, - 1.3f * 8611.3238710010046f, + (float)(1.3 * 8611.3238710010046), -0.3041958212306401f, -0.3633036457487539f, -0.35660379990111464f, @@ -391,7 +392,7 @@ internal static class JxlQuantWeights -0.33699592683512467f, -0.30180866526242109f, -0.27321683125358037f, - 1.3f * 4492.2486445538634f, + (float)(1.3 * 4492.2486445538634), -1.2f, -1.2f, -0.8f, @@ -408,7 +409,7 @@ internal static class JxlQuantWeights public static readonly JxlQuantizerEncoding Dct256x256 = JxlQuantizerEncoding.Dct( new JxlDctQuantWeightParameters( [ - 3.6f * 26629.073922049845f, + (float)(3.6 * 26629.073922049845), -1.025f, -0.78f, -0.65012f, @@ -416,7 +417,7 @@ internal static class JxlQuantWeights -0.20819395464f, -0.421064f, -0.32733845535848671f, - 3.6f * 9311.3238710010046f, + (float)(3.6 * 9311.3238710010046), -0.3041958212306401f, -0.3633036457487539f, -0.35660379990111464f, @@ -424,7 +425,7 @@ internal static class JxlQuantWeights -0.33699592683512467f, -0.30180866526242109f, -0.27321683125358037f, - 3.6f * 4992.2486445538634f, + (float)(3.6 * 4992.2486445538634), -1.2f, -1.2f, -0.8f, @@ -441,7 +442,7 @@ internal static class JxlQuantWeights public static readonly JxlQuantizerEncoding Dct128x256 = JxlQuantizerEncoding.Dct( new JxlDctQuantWeightParameters( [ - 2.6f * 23629.073922049845f, + (float)(2.6 * 23629.073922049845), -1.025f, -0.78f, -0.65012f, @@ -449,7 +450,7 @@ internal static class JxlQuantWeights -0.20819395464f, -0.421064f, -0.32733845535848671f, - 2.6f * 8611.3238710010046f, + (float)(2.6 * 8611.3238710010046), -0.3041958212306401f, -0.3633036457487539f, -0.35660379990111464f, @@ -457,7 +458,7 @@ internal static class JxlQuantWeights -0.33699592683512467f, -0.30180866526242109f, -0.27321683125358037f, - 2.6f * 4492.2486445538634f, + (float)(2.6 * 4492.2486445538634), -1.2f, -1.2f, -0.8f, diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/QuantizationTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/QuantizationTests.cs index cb2ffa2441..547c892ac5 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/QuantizationTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/QuantizationTests.cs @@ -114,6 +114,33 @@ public void DefaultWeights_MatchNativeBandAndTableMetadata() ReadOnlySpan actualBands = actual.DctParameters.DistanceBands[c]; Assert.True(expectedBands.SequenceEqual(actualBands)); } + + // These are the default decimal seed products rounded once to float, as native quant_weights.cc defines them. + // Checking the exact bits catches rounding operands before multiplication, without changing interpolation tolerances. + ReadOnlySpan largeTables = + [ + JxlQuantWeights.Dct64x64, JxlQuantWeights.Dct32x64, + JxlQuantWeights.Dct128x128, JxlQuantWeights.Dct64x128, + JxlQuantWeights.Dct256x256, JxlQuantWeights.Dct128x256 + ]; + + ReadOnlySpan seedBits = + [ + 0x46BB3C55, 0x4602F0C4, 0x458C6831, + 0x466FFB98, 0x45AEEAE2, 0x45367F63, + 0x473B3C55, 0x4682F0C4, 0x460C6831, + 0x46EFFB98, 0x462EEAE2, 0x45B67F63, + 0x47BB3C55, 0x4702F0C4, 0x468C6831, + 0x476FFB98, 0x46AEEAE2, 0x46367F63 + ]; + + for (int table = 0; table < largeTables.Length; table++) + { + for (int c = 0; c < 3; c++) + { + Assert.Equal(seedBits[(table * 3) + c], BitConverter.SingleToInt32Bits(largeTables[table].DctParameters.DistanceBands[c][0])); + } + } } /// From e30a858fc143d1eac5818862b3ea0634694c68a5 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Oct 2026 04:01:22 +1000 Subject: [PATCH 308/328] Complete JPEG XL quantization header parsing and shared wire constants --- .../Decoder/Modular/JxlModularFrameDecoder.cs | 4 +- .../Encoder/JxlQuantizerWeightsEncoder.cs | 16 +- .../Quantization/JxlDequantMatrices.cs | 101 ++++++++++++ .../Quantization/JxlQuantWeights.cs | 40 ++--- .../Quantization/JxlQuantizerConstants.cs | 9 + .../Jxl/Processing/QuantizationTests.cs | 156 ++++++++++++++++++ 6 files changed, 296 insertions(+), 30 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Modular/JxlModularFrameDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Modular/JxlModularFrameDecoder.cs index 9c126faee3..202bb47dce 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Modular/JxlModularFrameDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Modular/JxlModularFrameDecoder.cs @@ -24,8 +24,6 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Modular; /// The borrowed parsed frame dimensions. internal sealed class JxlModularFrameDecoder(Configuration configuration, JxlFrameDimensions frameDimensions) : IDisposable { - private const float AlmostZero = 1e-8f; - private readonly Configuration configuration = configuration; /// @@ -219,7 +217,7 @@ public static bool DecodeQuantizerTable( // Native raw weights require both a positive denominator and positive integer entries. // F16 read rejects nonfinite fields; this additional bound excludes zero and negative denominators. - if (denominator < AlmostZero) + if (denominator < JxlQuantizerConstants.MinimumWeight) { throw new InvalidImageContentException("The raw quantization denominator is too small."); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlQuantizerWeightsEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlQuantizerWeightsEncoder.cs index d596e93de9..4db2d99c8e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlQuantizerWeightsEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlQuantizerWeightsEncoder.cs @@ -37,7 +37,7 @@ public static bool EncodeDctParameters(in JxlDctQuantWeightParameters parameters for (int i = 0; i < parameters.NumDistanceBands; i++) { // each DCT distance band is a 16-bit half-precision floating-point number - if (!JxlF16Coder.Write(bands[i] * (i == 0 ? (1 / 64.0f) : 1.0f), writer)) + if (!JxlF16Coder.Write(bands[i] * (i == 0 ? (1F / JxlQuantizerConstants.AcWeightScale) : 1.0f), writer)) { return false; } @@ -67,7 +67,7 @@ public static bool EncodeQuant(Configuration configuration, JxlQuantizerEncoding for (int i = 0; i < 3; i++) { - if (!JxlF16Coder.Write(idWeight[i] * (1.0f / 64), writer)) + if (!JxlF16Coder.Write(idWeight[i] * (1F / JxlQuantizerConstants.AcWeightScale), writer)) { return false; } @@ -83,7 +83,7 @@ public static bool EncodeQuant(Configuration configuration, JxlQuantizerEncoding for (int i = 0; i < 6; i++) { - if (!JxlF16Coder.Write(dct2Weight[i] * (1.0f / 64), writer)) + if (!JxlF16Coder.Write(dct2Weight[i] * (1F / JxlQuantizerConstants.AcWeightScale), writer)) { return false; } @@ -148,7 +148,7 @@ public static bool EncodeQuant(Configuration configuration, JxlQuantizerEncoding for (int i = 0; i < 9; i++) { - if (!JxlF16Coder.Write(afvWeights[i] * (i < 6 ? 1.0f / 64 : 1.0f), writer)) + if (!JxlF16Coder.Write(afvWeights[i] * (i < 6 ? 1F / JxlQuantizerConstants.AcWeightScale : 1F), writer)) { return false; } @@ -221,9 +221,9 @@ public static bool DequantMatricesEncodeDc(JxlDequantMatrices matrices, JxlBitWr if (!allDefault) { - if (!JxlF16Coder.Write(values[0] * 128.0f, writer) || - !JxlF16Coder.Write(values[1] * 128.0f, writer) || - !JxlF16Coder.Write(values[2] * 128.0f, writer)) + if (!JxlF16Coder.Write(values[0] * JxlQuantizerConstants.DcWeightScale, writer) || + !JxlF16Coder.Write(values[1] * JxlQuantizerConstants.DcWeightScale, writer) || + !JxlF16Coder.Write(values[2] * JxlQuantizerConstants.DcWeightScale, writer)) { return false; } @@ -288,7 +288,7 @@ public static bool DequantMatricesRoundtrip(Configuration configuration, JxlDequ ms.Position = 0; JxlBitReader reader = new(ms); - if (!matrices.Decode(configuration, reader)) + if (!matrices.Decode(configuration, reader, null)) { return false; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlDequantMatrices.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlDequantMatrices.cs index b645146006..5b3db72e90 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlDequantMatrices.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlDequantMatrices.cs @@ -4,7 +4,10 @@ using System.Buffers; using System.Diagnostics; using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Modular; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; @@ -143,6 +146,104 @@ static void Verify(int expected, JxlQuantTable actual) } } + /// Reads a complete matrix header into this frame's existing descriptor slots. + /// The configuration that owns any raw table samples. + /// The reader positioned at the all-default flag. + /// The frame modular context, or null for a standalone table stream. + /// Whether all descriptors were read successfully. + /// + /// A new header invalidates prepared views and releases earlier raw payloads. Failed reads release partial raw payloads too. + /// The descriptor slots and coefficient allocation remain owned by this instance for later preparation. + /// + public bool Decode(Configuration configuration, JxlBitReader reader, JxlModularFrameDecoder? modularFrameDecoder) + { + this.computedMask = 0; + this.preparedKinds = 0; + this.preparedStorage = default; + + // Every header replaces all seventeen modes. Reuse their fixed storage; inactive parameter banks need no clearing. + foreach (JxlQuantizerEncoding encoding in this.encodings) + { + encoding.Dispose(); + encoding.Mode = JxlQuantMode.Library; + encoding.Predefined = 0; + } + + bool success = false; + try + { + if (!reader.ReadBoolean()) + { + for (int i = 0; i < this.encodings.Length; i++) + { + if (!JxlQuantWeights.Decode(configuration, reader, this.encodings[i], RequiredSizeX[i], RequiredSizeY[i], i, modularFrameDecoder)) + { + return false; + } + } + } + + success = true; + return true; + } + finally + { + if (!success) + { + // No caller can use a partial matrix header. Release raw tables read before the failing field or truncated input. + foreach (JxlQuantizerEncoding encoding in this.encodings) + { + encoding.Dispose(); + encoding.Mode = JxlQuantMode.Library; + encoding.Predefined = 0; + } + } + } + } + + /// Reads the three DC multipliers, restoring format defaults when the default flag is set. + /// The reader positioned at the DC all-default flag. + /// Whether all custom fields are finite and at least 1e-8 after scaling. + /// A failed read leaves all three previous multipliers unchanged. DC changes do not invalidate the separate AC matrices. + public bool DecodeDc(JxlBitReader reader) + { + InlineArray3 dc = default; + InlineArray3 inverseDc = default; + if (reader.ReadBoolean()) + { + // Native frame setup starts from these defaults. Reset explicitly when the same owner reads another default header. + for (int c = 0; c < 3; c++) + { + dc[c] = JxlQuantizerConstants.DcQuant[c]; + inverseDc[c] = JxlQuantizerConstants.InverseDcQuant[c]; + } + } + else + { + for (int c = 0; c < 3; c++) + { + if (!JxlF16Coder.Read(reader, ref dc[c])) + { + return false; + } + + // The wire stores each multiplier scaled by 128. Validate after undoing that scale, as required by the format. + dc[c] *= 1F / JxlQuantizerConstants.DcWeightScale; + if (dc[c] < JxlQuantizerConstants.MinimumWeight) + { + return false; + } + + inverseDc[c] = 1F / dc[c]; + } + } + + // Publish both directions only after every field succeeds. An invalid later channel cannot leave mixed old and new values. + this.dcQuant = dc; + this.inverseDcQuant = inverseDc; + return true; + } + /// Prepares exactly the table kinds required by a complete strategy mask. /// The configuration that owns coefficient storage. /// The complete set of strategies used by the next search or decode operation. diff --git a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantWeights.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantWeights.cs index c40a2bbf21..168ed9c024 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantWeights.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantWeights.cs @@ -25,8 +25,7 @@ internal static class JxlQuantWeights public const int Log2NumQuantModes = 3; // The format rejects quantization weights outside this reciprocal-safe interval. - private const float AlmostZero = 1e-8f; - private const float MaximumWeight = 1F / AlmostZero; + private const float MaximumWeight = 1F / JxlQuantizerConstants.MinimumWeight; /// /// DCT quantizer encoding. (6 distance bands) @@ -677,7 +676,7 @@ public static bool GetQuantWeights(int rowCount, int columnCount, in JxlDctQuant { bands[0] = parameters.DistanceBands[c][0]; - if (bands[0] < AlmostZero) + if (bands[0] < JxlQuantizerConstants.MinimumWeight) { return false; } @@ -686,7 +685,7 @@ public static bool GetQuantWeights(int rowCount, int columnCount, in JxlDctQuant { bands[i] = bands[i - 1] * Mult(parameters.DistanceBands[c][i]); - if (bands[i] < AlmostZero) + if (bands[i] < JxlQuantizerConstants.MinimumWeight) { return false; } @@ -930,7 +929,7 @@ public static bool ComputeQuantTable( { bands[0] = encoding.ModeWeights[c][5]; - if (bands[0] < AlmostZero) + if (bands[0] < JxlQuantizerConstants.MinimumWeight) { throw new InvalidOperationException("Invalid AFV bands"); } @@ -939,7 +938,7 @@ public static bool ComputeQuantTable( { bands[i] = bands[i - 1] * Mult(encoding.ModeWeights[c][i + 5]); - if (bands[i] < AlmostZero) + if (bands[i] < JxlQuantizerConstants.MinimumWeight) { throw new InvalidOperationException("Invalid AFV bands"); } @@ -1049,7 +1048,7 @@ public static bool ComputeQuantTable( public float Invoke(float value) { // Ordered comparisons also reject NaN. Positive finite weights in [1e-8,1e8) have finite positive reciprocals. - if (!(value >= AlmostZero && value < MaximumWeight)) + if (!(value >= JxlQuantizerConstants.MinimumWeight && value < MaximumWeight)) { throw new InvalidImageContentException("The quantization weight is outside the supported range."); } @@ -1061,7 +1060,8 @@ public float Invoke(float value) [MethodImpl(MethodImplOptions.AggressiveInlining)] public Vector128 Invoke(Vector128 value) { - if (!(Vector128.GreaterThanOrEqualAll(value, Vector128.Create(AlmostZero)) && Vector128.LessThanAll(value, Vector128.Create(MaximumWeight)))) + if (!(Vector128.GreaterThanOrEqualAll(value, Vector128.Create(JxlQuantizerConstants.MinimumWeight)) + && Vector128.LessThanAll(value, Vector128.Create(MaximumWeight)))) { throw new InvalidImageContentException("The quantization weight is outside the supported range."); } @@ -1073,7 +1073,8 @@ public Vector128 Invoke(Vector128 value) [MethodImpl(MethodImplOptions.AggressiveInlining)] public Vector256 Invoke(Vector256 value) { - if (!(Vector256.GreaterThanOrEqualAll(value, Vector256.Create(AlmostZero)) && Vector256.LessThanAll(value, Vector256.Create(MaximumWeight)))) + if (!(Vector256.GreaterThanOrEqualAll(value, Vector256.Create(JxlQuantizerConstants.MinimumWeight)) + && Vector256.LessThanAll(value, Vector256.Create(MaximumWeight)))) { throw new InvalidImageContentException("The quantization weight is outside the supported range."); } @@ -1085,7 +1086,8 @@ public Vector256 Invoke(Vector256 value) [MethodImpl(MethodImplOptions.AggressiveInlining)] public Vector512 Invoke(Vector512 value) { - if (!(Vector512.GreaterThanOrEqualAll(value, Vector512.Create(AlmostZero)) && Vector512.LessThanAll(value, Vector512.Create(MaximumWeight)))) + if (!(Vector512.GreaterThanOrEqualAll(value, Vector512.Create(JxlQuantizerConstants.MinimumWeight)) + && Vector512.LessThanAll(value, Vector512.Create(MaximumWeight)))) { throw new InvalidImageContentException("The quantization weight is outside the supported range."); } @@ -1112,12 +1114,12 @@ public static bool DecodeDctParameters(JxlBitReader reader, ref JxlDctQuantWeigh } } - if (parameters.DistanceBands[c][0] < AlmostZero) + if (parameters.DistanceBands[c][0] < JxlQuantizerConstants.MinimumWeight) { throw new InvalidOperationException("Distance band seed is too small"); } - parameters.DistanceBands[c][0] *= 64f; + parameters.DistanceBands[c][0] *= JxlQuantizerConstants.AcWeightScale; } return true; @@ -1182,12 +1184,12 @@ public static bool Decode( return false; } - if (Math.Abs(encoding.ModeWeights[c][i]) < AlmostZero) + if (Math.Abs(encoding.ModeWeights[c][i]) < JxlQuantizerConstants.MinimumWeight) { throw new InvalidOperationException("ID Quantizer is too small"); } - encoding.ModeWeights[c][i] *= 64; + encoding.ModeWeights[c][i] *= JxlQuantizerConstants.AcWeightScale; } } @@ -1210,12 +1212,12 @@ public static bool Decode( return false; } - if (Math.Abs(encoding.ModeWeights[c][i]) < AlmostZero) + if (Math.Abs(encoding.ModeWeights[c][i]) < JxlQuantizerConstants.MinimumWeight) { throw new InvalidOperationException("Quantizer is too small"); } - encoding.ModeWeights[c][i] *= 64; + encoding.ModeWeights[c][i] *= JxlQuantizerConstants.AcWeightScale; } } @@ -1236,7 +1238,7 @@ public static bool Decode( return false; } - if (Math.Abs(encoding.ModeWeights[c][0]) < AlmostZero) + if (Math.Abs(encoding.ModeWeights[c][0]) < JxlQuantizerConstants.MinimumWeight) { throw new InvalidOperationException("DCT4X8 multiplier is too small"); } @@ -1266,7 +1268,7 @@ public static bool Decode( return false; } - if (Math.Abs(encoding.ModeWeights[c][i]) < AlmostZero) + if (Math.Abs(encoding.ModeWeights[c][i]) < JxlQuantizerConstants.MinimumWeight) { throw new InvalidOperationException("DCT4 multiplier is too small"); } @@ -1300,7 +1302,7 @@ public static bool Decode( for (int i = 0; i < 6; i++) { - encoding.ModeWeights[c][i] *= 64; + encoding.ModeWeights[c][i] *= JxlQuantizerConstants.AcWeightScale; } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerConstants.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerConstants.cs index bc0042afaa..64d0a1829c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerConstants.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerConstants.cs @@ -8,6 +8,15 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; /// internal static class JxlQuantizerConstants { + /// The lower bound used to validate nonzero quantization weights and raw denominators. + public const float MinimumWeight = 1E-8F; + + /// The multiplier that converts a stored F16 AC seed or fixed weight to its coefficient units. + public const float AcWeightScale = 64F; + + /// The multiplier applied to a DC weight before its F16 field is stored. + public const float DcWeightScale = 128F; + public const int Log2MaxDistanceBands = 4; public const int MaxDistanceBands = 1 + (1 << Log2MaxDistanceBands); diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/QuantizationTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/QuantizationTests.cs index 547c892ac5..6f455b463c 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/QuantizationTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/QuantizationTests.cs @@ -560,4 +560,160 @@ public void RawQuantizer_InvalidDenominatorReleasesOwnedSamples(byte low, byte h Assert.Equal(0, allocator.LiveBufferBytes); Assert.Single(allocator.ReturnLog); } + + /// Checks both complete matrix-header forms, descriptor reuse, and retained coefficient allocation capacity. + /// Whether the wire uses its one-bit shortcut instead of seventeen Library modes. + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DequantMatrices_ReadHeadersReusesSlotsAndInvalidatesPreparedViews(bool allDefault) + { + TestMemoryAllocator allocator = new(); + allocator.EnableNonThreadSafeLogging(); + Configuration configuration = TestEnvironment.Configuration.Clone(); + configuration.MemoryAllocator = allocator; + using (JxlDequantMatrices matrices = new()) + { + JxlQuantizerEncoding first = matrices.GetEncodings()[0]; + JxlQuantizerEncoding last = matrices.GetEncodings()[^1]; + uint strategy = 1U << (int)JxlAcStrategyType.DCT; + Assert.True(matrices.EnsureComputed(configuration, strategy)); + long preparedBytes = allocator.LiveBufferBytes; + last.Mode = JxlQuantMode.Raw; + last.PrepareRawTable(configuration, 192).Fill(1); + + // Flag one has no payload. Flag zero requires seventeen three-bit Library modes (all zero); the library index has zero bits. + using MemoryStream input = new(allDefault ? [1] : [0, 0, 0, 0, 0, 0, 0]); + JxlBitReader reader = new(input); + Assert.True(matrices.Decode(configuration, reader, null)); + Assert.Equal(allDefault ? 1L : 52L, reader.TotalBitsConsumed); + Assert.Same(first, matrices.GetEncodings()[0]); + Assert.Same(last, matrices.GetEncodings()[^1]); + Assert.True(last.QuantizationTable.IsEmpty); + Assert.True(matrices.GetPreparedStorage().IsEmpty); + Assert.Equal(preparedBytes, allocator.LiveBufferBytes); + foreach (JxlQuantizerEncoding descriptor in matrices.GetEncodings()) + { + Assert.Equal(JxlQuantMode.Library, descriptor.Mode); + Assert.Equal(0, descriptor.Predefined); + } + + // Parsing invalidates coefficient contents, but preparation can regenerate into the same allocation after borrowers have finished. + Assert.True(matrices.EnsureComputed(configuration, strategy)); + Assert.Equal(2, allocator.AllocationLog.Count); + Assert.Equal(preparedBytes, allocator.LiveBufferBytes); + } + + Assert.Equal(0, allocator.LiveBufferBytes); + Assert.Equal(2, allocator.ReturnLog.Count); + } + + /// Checks cleanup after a later invalid descriptor and after truncated custom matrix fields. + /// Whether the first DCT descriptor ends before its second channel seed. + [Theory] + [InlineData(false)] + [InlineData(true)] + public void DequantMatrices_FailedHeaderReleasesRawPayloadsAndResetsSlots(bool truncated) + { + TestMemoryAllocator allocator = new(); + allocator.EnableNonThreadSafeLogging(); + Configuration configuration = TestEnvironment.Configuration.Clone(); + configuration.MemoryAllocator = allocator; + using JxlDequantMatrices matrices = new(); + JxlQuantizerEncoding last = matrices.GetEncodings()[^1]; + last.Mode = JxlQuantMode.Raw; + last.PrepareRawTable(configuration, 192).Fill(1); + + // Flag zero, DCT mode 6, one band, then half seeds 1/32,1/16,1/8 occupy exactly seven bytes. + // The next descriptor is ID mode 1 followed by a forbidden half NaN (0x7E00). No fields come from the library writer. + using MemoryStream input = new(truncated + ? [0x0C, 0x00, 0x28] + : [0x0C, 0x00, 0x28, 0x00, 0x2C, 0x00, 0x30, 0x01, 0xF0, 0x03]); + + JxlBitReader reader = new(input); + if (truncated) + { + Assert.Throws(() => matrices.Decode(configuration, reader, null)); + } + else + { + Assert.False(matrices.Decode(configuration, reader, null)); + } + + Assert.Same(last, matrices.GetEncodings()[^1]); + Assert.True(last.QuantizationTable.IsEmpty); + Assert.True(matrices.GetPreparedStorage().IsEmpty); + foreach (JxlQuantizerEncoding descriptor in matrices.GetEncodings()) + { + Assert.Equal(JxlQuantMode.Library, descriptor.Mode); + } + + Assert.Equal(0, allocator.LiveBufferBytes); + Assert.Single(allocator.ReturnLog); + } + + /// Checks DC wire scaling, reciprocal publication, and explicit defaults on a reused owner. + [Fact] + public void DequantMatrices_ReadDcScalesFieldsAndRestoresDefaults() + { + using JxlDequantMatrices matrices = new(); + + // Flag zero precedes half fields 128,256,512 (0x5800,0x5C00,0x6000). The decoded DC multipliers are 1,2,4. + using (MemoryStream input = new([0x00, 0xB0, 0x00, 0xB8, 0x00, 0xC0, 0x00])) + { + JxlBitReader reader = new(input); + Assert.True(matrices.DecodeDc(reader)); + Assert.Equal(49L, reader.TotalBitsConsumed); + for (int c = 0; c < 3; c++) + { + Assert.Equal((float)(1 << c), matrices.GetDcQuant(c)); + Assert.Equal(1F / (1 << c), matrices.GetInverseDcQuant(c)); + } + } + + using MemoryStream defaults = new([1]); + Assert.True(matrices.DecodeDc(new(defaults))); + for (int c = 0; c < 3; c++) + { + Assert.Equal(JxlQuantizerConstants.DcQuant[c], matrices.GetDcQuant(c)); + Assert.Equal(JxlQuantizerConstants.InverseDcQuant[c], matrices.GetInverseDcQuant(c)); + } + } + + /// Checks that invalid later DC fields cannot publish an earlier valid channel. + /// The byte that contains the second half field's low seven bits. + /// The byte that contains the middle eight bits of that shifted field. + /// The byte that contains the field's remaining sign bit. + [Theory] + [InlineData((byte)0, (byte)0, (byte)0)] + [InlineData((byte)0, (byte)0x78, (byte)1)] + [InlineData((byte)0, (byte)0xFC, (byte)0)] + [InlineData((byte)2, (byte)0, (byte)0)] + public void DequantMatrices_InvalidDcFieldsLeaveAllPreviousValues(byte byte2, byte byte3, byte byte4) + { + using JxlDequantMatrices matrices = new(); + + // First channel is a valid half 128. Second is zero, minus one, NaN, or the smallest subnormal; F16/128 is too small for the last case. + using MemoryStream input = new([0x00, 0xB0, byte2, byte3, byte4]); + Assert.False(matrices.DecodeDc(new(input))); + for (int c = 0; c < 3; c++) + { + Assert.Equal(JxlQuantizerConstants.DcQuant[c], matrices.GetDcQuant(c)); + Assert.Equal(JxlQuantizerConstants.InverseDcQuant[c], matrices.GetInverseDcQuant(c)); + } + } + + /// Checks the existing reader's EOF boundary after a valid first DC field. + [Fact] + public void DequantMatrices_TruncatedDcHeaderLeavesAllPreviousValues() + { + using JxlDequantMatrices matrices = new(); + using MemoryStream input = new([0x00, 0xB0, 0x00]); + Assert.Throws(() => matrices.DecodeDc(new(input))); + for (int c = 0; c < 3; c++) + { + Assert.Equal(JxlQuantizerConstants.DcQuant[c], matrices.GetDcQuant(c)); + Assert.Equal(JxlQuantizerConstants.InverseDcQuant[c], matrices.GetInverseDcQuant(c)); + } + } } From 290d285ebd5dedb4cd5e36de4751a7036fa9743f Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Oct 2026 04:03:16 +1000 Subject: [PATCH 309/328] Remove unused JPEG XL inverse-test shell without executable tests --- .../Jxl/Processing/OpsinInverseTests.cs | 76 ------------------- 1 file changed, 76 deletions(-) delete mode 100644 tests/ImageSharp.Tests/Formats/Jxl/Processing/OpsinInverseTests.cs diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/OpsinInverseTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/OpsinInverseTests.cs deleted file mode 100644 index ef2c71b78a..0000000000 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/OpsinInverseTests.cs +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Formats.Jxl.Memory; - -namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; - -public class OpsinInverseTests -{ - /// - /// Converts Y'Cb'Cr to RGB without any SIMD/parallel acceleration. This is - /// a reference, so it's meant to be stable. The conversion is performed - /// as specified in ITU-R BT.601. - /// - /// Input Y'Cb'Cr source. - /// Output RGB. - /// Region in which conversion will happen. - private static void SlowYCbCrToRgb(JxlImage3 yCbCr, JxlImage3 rgb, Rectangle rect) - { - int width = rect.Width; - int height = rect.Height; - - if (width == 0 || height == 0) - { - // When either dimension has 0 pixels, there's really - // nothing to process. So return immediately. - return; - } - - // Defining constants used by the conversion process - // as specified by ITU-R BT.601 - const float c128 = 128.0f / 255; - const float crcr = 1.402f; - const float cgcb = -0.114f * 1.772f / 0.587f; - const float cgcr = -0.299f * 1.402f / 0.587f; - const float cbcb = 1.772f; - - // Main conversion loop here - for (int y = 0; y < height; y++) - { - // First we retrieve the input Y'Cb'Cr rows - // for each channel. - // NOTE: The Y'Cb'Cr planes are loaded in the - // { 1, 0, 2 } order instead of the traditional - // { 0, 1, 2 } order. See - // https://github.com/libjxl/libjxl/blob/777e88f46a6b35948d1967afd9ac5c7fb5595d95/lib/jxl/opsin_inverse_test.cc#L69-L71. - Span yRow = yCbCr.Plane(1).GetRow(rect, y); - Span cbRow = yCbCr.Plane(0).GetRow(rect, y); - Span crRow = yCbCr.Plane(2).GetRow(rect, y); - - // Similarly, we retrieve the output rows. Conversion - // outputs go here. - Span rRow = rgb.Plane(0).GetRow(rect, y); - Span gRow = rgb.Plane(1).GetRow(rect, y); - Span bRow = rgb.Plane(2).GetRow(rect, y); - - for (int x = 0; x < width; x++) - { - // Input Y'Cb'Cr pixels - float yValue = yRow[x] + c128; - float cbValue = cbRow[x]; - float crValue = crRow[x]; - - // The pixels are converted here. - float rVec = (crcr * crValue) + yValue; - float gVec = (cgcr * crValue) + (cgcb * cbValue) + yValue; - float bVec = (cbcb * cbValue) + yValue; - - // Storing them back to the output rows - rRow[x] = rVec; - gRow[x] = gVec; - bRow[x] = bVec; - } - } - } -} From c330181e1a18b3b8e0bed0af5d0ba23807bb92de Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Oct 2026 04:16:51 +1000 Subject: [PATCH 310/328] Correct adaptive JPEG XL quantization search and fused erosion traversal --- .../Encoder/JxlAdaptiveQuantization.cs | 598 +++++++++++++++--- .../Jxl/Processing/QuantizationTests.cs | 94 +++ 2 files changed, 602 insertions(+), 90 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs index 9842fc4d71..449a264e91 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs @@ -4,6 +4,8 @@ using System.Buffers; using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Jxl.Cms; using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; @@ -350,6 +352,12 @@ private static Vector MaskingSqrt(Vector v) return Vector.Create(0.25f) * Vector.SquareRoot((v * Vector.SquareRoot(kMul * 1e8f)) + kLogOffset); } + /// Inserts one finite sample into four ordered minima. + /// The new sample. + /// The lowest retained sample. + /// The second-lowest retained sample. + /// The third-lowest retained sample. + /// The fourth-lowest retained sample. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void StoreMin4(float v, ref float min0, ref float min1, ref float min2, ref float min3) { @@ -380,125 +388,532 @@ private static void StoreMin4(float v, ref float min0, ref float min1, ref float } } - // Look for smooth areas near the area of degradation. - // If the areas are generally smooth, don't do masking. - // Output is downsampled by 2x. + /// + /// Filters the four lowest samples of each clamped three-by-three neighborhood and downsamples the result by two in each axis. + /// + /// The perceptual target used to select the four minimum weights. + /// The source region, twice the destination width and height. + /// The borrowed source plane. Neighbor positions clamp at its global image edges. + /// The destination region, independent of the source origin and physical stride. + /// The distinct destination plane. Samples outside toRect remain unchanged. + /// Whether the source and destination regions have the required two-to-one dimensions. public static bool FuzzyErosion(float butteraugliTarget, Rectangle fromRect, JxlPlane from, Rectangle toRect, JxlPlane to) { - int width = from.XSize; - int height = from.YSize; - const int step = 1; - - if (toRect.Width != fromRect.Width || toRect.Height != fromRect.Height) + // Each output combines four source neighborhoods. This checks the actual downsampling contract, not equal-sized image regions. + if ((long)toRect.Width * 2 != fromRect.Width || (long)toRect.Height * 2 != fromRect.Height) { return false; } - ReadOnlySpan mulBase = [0.125f, 0.1f, 0.09f, 0.06f]; - ReadOnlySpan mulAdd = [0.0f, -0.1f, -0.09f, -0.06f]; - float mul = 0f; + ReadOnlySpan mulBase = [0.125F, 0.1F, 0.09F, 0.06F]; + ReadOnlySpan mulAdd = [0F, -0.1F, -0.09F, -0.06F]; + float mul = butteraugliTarget < 2F ? (2F - butteraugliTarget) * 0.5F : 0F; + InlineArray4 weights = default; + float normSum = 0F; - if (butteraugliTarget < 2.0f) + for (int i = 0; i < 4; i++) { - mul = (2.0f - butteraugliTarget) * (1.0f / 2.0f); + weights[i] = mulBase[i] + (mul * mulAdd[i]); + normSum += weights[i]; } - InlineArray4 multipliers = default; - float normSum = 0; - - for (int ii = 0; ii < 4; ii++) + const float total = 0.29959705784054957F; + for (int i = 0; i < 4; i++) { - multipliers[ii] = mulBase[ii] + (mul * mulAdd[ii]); - normSum += multipliers[ii]; + weights[i] *= total / normSum; } - const float total = 0.29959705784054957f; + FuzzyErosionOperator operation = new(weights[0], weights[1], weights[2], weights[3]); + ReadOnlySpan source = from.GetRow(0); + Span destination = to.GetRow(0); + // Reserve a right neighbor after every vector's final source pair. This bound uses the full source width, not the tile edge. + int vectorEnd = Math.Min(toRect.Width, (from.XSize - fromRect.Left - 1) / 2); - for (int ii = 0; ii < 4; ii++) + for (int y = 0; y < toRect.Height; y++) { - multipliers[ii] *= total / normSum; - } - - for (int fy = 0; fy < fromRect.Height; fy++) - { - int y = fy + fromRect.Y0(); - int ym1 = y >= step ? y - step : y; - int yp1 = y + step < height ? y + step : y; - - Span rowt = from.GetRow(ym1); - Span row = from.GetRow(y); - Span rowb = from.GetRow(yp1); - Span rowOut = to.GetRow(toRect, fy / 2); + int sourceY = fromRect.Top + (2 * y); + + // A source row pair needs four rows of neighborhoods. Clamp at the plane edges, including repeated rows at height two. + ReadOnlySpan top = source.Slice(Math.Max(0, sourceY - 1) * from.PixelsPerRow, from.XSize); + ReadOnlySpan upper = source.Slice(sourceY * from.PixelsPerRow, from.XSize); + ReadOnlySpan lower = source.Slice((sourceY + 1) * from.PixelsPerRow, from.XSize); + ReadOnlySpan bottom = source.Slice(Math.Min(from.YSize - 1, sourceY + 2) * from.PixelsPerRow, from.XSize); + Span output = destination.Slice(((toRect.Top + y) * to.PixelsPerRow) + toRect.Left, toRect.Width); + int x = 0; + + // The first output pair can touch the global left edge. Complete it with clamped scalar reads before vector interiors. + if (fromRect.Left == 0 && x < output.Length) + { + output[x++] = Erode2x2(top, upper, lower, bottom, fromRect.Left, operation); + } - for (int fx = 0; fx < fromRect.Height; fx++) + if (Vector512.IsHardwareAccelerated) { - int x = fx + fromRect.X0(); - int xm1 = x >= step ? x - step : x; - int xp1 = x + step < width ? x + step : x; - - // float min[4] = { row[x], row[xm1], row[xp1], rowt[xm1] }; - InlineArray4 min = default; - min[0] = row[x]; - min[1] = row[xm1]; - min[2] = row[xp1]; - min[3] = rowt[xm1]; - - if (min[0] > min[1]) + for (; x <= vectorEnd - Vector512.Count; x += Vector512.Count) { - RuntimeUtility.Swap(ref min[0], ref min[1]); + int sourceX = fromRect.Left + (2 * x); + ErodePair(top, upper, lower, bottom, sourceX, operation, out Vector512 upper0, out Vector512 lower0); + ErodePair(top, upper, lower, bottom, sourceX + Vector512.Count, operation, out Vector512 upper1, out Vector512 lower1); + SimdUtils.Deinterleave2(upper0.AsInt32(), upper1.AsInt32(), out Vector512 upperEven, out Vector512 upperOdd); + SimdUtils.Deinterleave2(lower0.AsInt32(), lower1.AsInt32(), out Vector512 lowerEven, out Vector512 lowerOdd); + + // Deinterleaving only moves bits. Add the four neighborhoods in native row-major order, without regrouping the lower pair. + Vector512 result = upperEven.AsSingle() + upperOdd.AsSingle(); + result += lowerEven.AsSingle(); + result += lowerOdd.AsSingle(); + result.StoreUnsafe(ref MemoryMarshal.GetReference(output), (uint)x); } + } - if (min[0] > min[2]) + if (Vector256.IsHardwareAccelerated) + { + for (; x <= vectorEnd - Vector256.Count; x += Vector256.Count) { - RuntimeUtility.Swap(ref min[0], ref min[2]); + int sourceX = fromRect.Left + (2 * x); + ErodePair(top, upper, lower, bottom, sourceX, operation, out Vector256 upper0, out Vector256 lower0); + ErodePair(top, upper, lower, bottom, sourceX + Vector256.Count, operation, out Vector256 upper1, out Vector256 lower1); + SimdUtils.Deinterleave2(upper0.AsInt32(), upper1.AsInt32(), out Vector256 upperEven, out Vector256 upperOdd); + SimdUtils.Deinterleave2(lower0.AsInt32(), lower1.AsInt32(), out Vector256 lowerEven, out Vector256 lowerOdd); + + // Deinterleaving only moves bits. Add the four neighborhoods in native row-major order, without regrouping the lower pair. + Vector256 result = upperEven.AsSingle() + upperOdd.AsSingle(); + result += lowerEven.AsSingle(); + result += lowerOdd.AsSingle(); + result.StoreUnsafe(ref MemoryMarshal.GetReference(output), (uint)x); } + } - if (min[0] > min[3]) + if (Vector128.IsHardwareAccelerated) + { + for (; x <= vectorEnd - Vector128.Count; x += Vector128.Count) { - RuntimeUtility.Swap(ref min[0], ref min[3]); + int sourceX = fromRect.Left + (2 * x); + ErodePair(top, upper, lower, bottom, sourceX, operation, out Vector128 upper0, out Vector128 lower0); + ErodePair(top, upper, lower, bottom, sourceX + Vector128.Count, operation, out Vector128 upper1, out Vector128 lower1); + SimdUtils.Deinterleave2(upper0.AsInt32(), upper1.AsInt32(), out Vector128 upperEven, out Vector128 upperOdd); + SimdUtils.Deinterleave2(lower0.AsInt32(), lower1.AsInt32(), out Vector128 lowerEven, out Vector128 lowerOdd); + + // Deinterleaving only moves bits. Add the four neighborhoods in native row-major order, without regrouping the lower pair. + Vector128 result = upperEven.AsSingle() + upperOdd.AsSingle(); + result += lowerEven.AsSingle(); + result += lowerOdd.AsSingle(); + result.StoreUnsafe(ref MemoryMarshal.GetReference(output), (uint)x); } + } - if (min[1] > min[2]) - { - RuntimeUtility.Swap(ref min[1], ref min[2]); - } + for (; x < output.Length; x++) + { + output[x] = Erode2x2(top, upper, lower, bottom, fromRect.Left + (2 * x), operation); + } + } - if (min[1] > min[3]) - { - RuntimeUtility.Swap(ref min[1], ref min[3]); - } + return true; + } - if (min[2] > min[3]) - { - RuntimeUtility.Swap(ref min[2], ref min[3]); - } + /// + /// Combines one clamped source pair from each of two rows with the same order as the vector interior. + /// + /// The row above the upper source row. + /// The upper source row. + /// The lower source row. + /// The row below the lower source row. + /// The global source column of the left sample. + /// The prepared four-minimum weights. + /// The sum of the four filtered neighborhoods in upper-left, upper-right, lower-left, lower-right order. + private static float Erode2x2( + ReadOnlySpan top, + ReadOnlySpan upper, + ReadOnlySpan lower, + ReadOnlySpan bottom, + int x, + in FuzzyErosionOperator operation) + { + int left = Math.Max(0, x - 1); + int right = Math.Min(upper.Length - 1, x + 2); + float result = operation.Invoke(upper[x], upper[left], upper[x + 1], top[left], top[x], top[x + 1], lower[left], lower[x], lower[x + 1]); + result += operation.Invoke(upper[x + 1], upper[x], upper[right], top[x], top[x + 1], top[right], lower[x], lower[x + 1], lower[right]); + result += operation.Invoke(lower[x], lower[left], lower[x + 1], upper[left], upper[x], upper[x + 1], bottom[left], bottom[x], bottom[x + 1]); + result += operation.Invoke(lower[x + 1], lower[x], lower[right], upper[x], upper[x + 1], upper[right], bottom[x], bottom[x + 1], bottom[right]); + return result; + } - // remaining 5 values of a 3x3 neighborhood - ref float rmin0 = ref min[0]; - ref float rmin1 = ref min[1]; - ref float rmin2 = ref min[2]; - ref float rmin3 = ref min[3]; - StoreMin4(rowt[x], ref rmin0, ref rmin1, ref rmin2, ref rmin3); - StoreMin4(rowt[xp1], ref rmin0, ref rmin1, ref rmin2, ref rmin3); - StoreMin4(rowb[xm1], ref rmin0, ref rmin1, ref rmin2, ref rmin3); - StoreMin4(rowb[x], ref rmin0, ref rmin1, ref rmin2, ref rmin3); - StoreMin4(rowb[xp1], ref rmin0, ref rmin1, ref rmin2, ref rmin3); - - float v = (multipliers[0] * min[0]) + (multipliers[1] * min[1]) + - (multipliers[2] * min[2]) + (multipliers[3] * min[3]); - - if (fx % 2 == 0 && fy % 2 == 0) - { - rowOut[fx / 2] = v; - } - else - { - rowOut[fx / 2] += v; - } - } + /// + /// Filters corresponding source columns in two adjacent rows while reusing their common neighborhood loads. + /// + /// The row above the upper source row. + /// The upper source row. + /// The lower source row. + /// The row below the lower source row. + /// The first global source column. Its vector and one-sample halo stay inside every row. + /// The prepared four-minimum weights. + /// Filtered upper-row samples in original column order. + /// Filtered lower-row samples in original column order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ErodePair( + ReadOnlySpan top, + ReadOnlySpan upper, + ReadOnlySpan lower, + ReadOnlySpan bottom, + int x, + in FuzzyErosionOperator operation, + out Vector128 upperResult, + out Vector128 lowerResult) + { + Vector128 topLeft = Vector128.LoadUnsafe(ref MemoryMarshal.GetReference(top), (uint)(x - 1)); + Vector128 topCenter = Vector128.LoadUnsafe(ref MemoryMarshal.GetReference(top), (uint)(x)); + Vector128 topRight = Vector128.LoadUnsafe(ref MemoryMarshal.GetReference(top), (uint)(x + 1)); + Vector128 upperLeft = Vector128.LoadUnsafe(ref MemoryMarshal.GetReference(upper), (uint)(x - 1)); + Vector128 upperCenter = Vector128.LoadUnsafe(ref MemoryMarshal.GetReference(upper), (uint)(x)); + Vector128 upperRight = Vector128.LoadUnsafe(ref MemoryMarshal.GetReference(upper), (uint)(x + 1)); + Vector128 lowerLeft = Vector128.LoadUnsafe(ref MemoryMarshal.GetReference(lower), (uint)(x - 1)); + Vector128 lowerCenter = Vector128.LoadUnsafe(ref MemoryMarshal.GetReference(lower), (uint)(x)); + Vector128 lowerRight = Vector128.LoadUnsafe(ref MemoryMarshal.GetReference(lower), (uint)(x + 1)); + Vector128 bottomLeft = Vector128.LoadUnsafe(ref MemoryMarshal.GetReference(bottom), (uint)(x - 1)); + Vector128 bottomCenter = Vector128.LoadUnsafe(ref MemoryMarshal.GetReference(bottom), (uint)(x)); + Vector128 bottomRight = Vector128.LoadUnsafe(ref MemoryMarshal.GetReference(bottom), (uint)(x + 1)); + + upperResult = operation.Invoke(upperCenter, upperLeft, upperRight, topLeft, topCenter, topRight, lowerLeft, lowerCenter, lowerRight); + + lowerResult = operation.Invoke(lowerCenter, lowerLeft, lowerRight, upperLeft, upperCenter, upperRight, bottomLeft, bottomCenter, bottomRight); + } + + /// + /// Filters corresponding source columns in two adjacent rows while reusing their common neighborhood loads. + /// + /// The row above the upper source row. + /// The upper source row. + /// The lower source row. + /// The row below the lower source row. + /// The first global source column. Its vector and one-sample halo stay inside every row. + /// The prepared four-minimum weights. + /// Filtered upper-row samples in original column order. + /// Filtered lower-row samples in original column order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ErodePair( + ReadOnlySpan top, + ReadOnlySpan upper, + ReadOnlySpan lower, + ReadOnlySpan bottom, + int x, + in FuzzyErosionOperator operation, + out Vector256 upperResult, + out Vector256 lowerResult) + { + Vector256 topLeft = Vector256.LoadUnsafe(ref MemoryMarshal.GetReference(top), (uint)(x - 1)); + Vector256 topCenter = Vector256.LoadUnsafe(ref MemoryMarshal.GetReference(top), (uint)(x)); + Vector256 topRight = Vector256.LoadUnsafe(ref MemoryMarshal.GetReference(top), (uint)(x + 1)); + Vector256 upperLeft = Vector256.LoadUnsafe(ref MemoryMarshal.GetReference(upper), (uint)(x - 1)); + Vector256 upperCenter = Vector256.LoadUnsafe(ref MemoryMarshal.GetReference(upper), (uint)(x)); + Vector256 upperRight = Vector256.LoadUnsafe(ref MemoryMarshal.GetReference(upper), (uint)(x + 1)); + Vector256 lowerLeft = Vector256.LoadUnsafe(ref MemoryMarshal.GetReference(lower), (uint)(x - 1)); + Vector256 lowerCenter = Vector256.LoadUnsafe(ref MemoryMarshal.GetReference(lower), (uint)(x)); + Vector256 lowerRight = Vector256.LoadUnsafe(ref MemoryMarshal.GetReference(lower), (uint)(x + 1)); + Vector256 bottomLeft = Vector256.LoadUnsafe(ref MemoryMarshal.GetReference(bottom), (uint)(x - 1)); + Vector256 bottomCenter = Vector256.LoadUnsafe(ref MemoryMarshal.GetReference(bottom), (uint)(x)); + Vector256 bottomRight = Vector256.LoadUnsafe(ref MemoryMarshal.GetReference(bottom), (uint)(x + 1)); + + upperResult = operation.Invoke(upperCenter, upperLeft, upperRight, topLeft, topCenter, topRight, lowerLeft, lowerCenter, lowerRight); + + lowerResult = operation.Invoke(lowerCenter, lowerLeft, lowerRight, upperLeft, upperCenter, upperRight, bottomLeft, bottomCenter, bottomRight); + } + + /// + /// Filters corresponding source columns in two adjacent rows while reusing their common neighborhood loads. + /// + /// The row above the upper source row. + /// The upper source row. + /// The lower source row. + /// The row below the lower source row. + /// The first global source column. Its vector and one-sample halo stay inside every row. + /// The prepared four-minimum weights. + /// Filtered upper-row samples in original column order. + /// Filtered lower-row samples in original column order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ErodePair( + ReadOnlySpan top, + ReadOnlySpan upper, + ReadOnlySpan lower, + ReadOnlySpan bottom, + int x, + in FuzzyErosionOperator operation, + out Vector512 upperResult, + out Vector512 lowerResult) + { + Vector512 topLeft = Vector512.LoadUnsafe(ref MemoryMarshal.GetReference(top), (uint)(x - 1)); + Vector512 topCenter = Vector512.LoadUnsafe(ref MemoryMarshal.GetReference(top), (uint)(x)); + Vector512 topRight = Vector512.LoadUnsafe(ref MemoryMarshal.GetReference(top), (uint)(x + 1)); + Vector512 upperLeft = Vector512.LoadUnsafe(ref MemoryMarshal.GetReference(upper), (uint)(x - 1)); + Vector512 upperCenter = Vector512.LoadUnsafe(ref MemoryMarshal.GetReference(upper), (uint)(x)); + Vector512 upperRight = Vector512.LoadUnsafe(ref MemoryMarshal.GetReference(upper), (uint)(x + 1)); + Vector512 lowerLeft = Vector512.LoadUnsafe(ref MemoryMarshal.GetReference(lower), (uint)(x - 1)); + Vector512 lowerCenter = Vector512.LoadUnsafe(ref MemoryMarshal.GetReference(lower), (uint)(x)); + Vector512 lowerRight = Vector512.LoadUnsafe(ref MemoryMarshal.GetReference(lower), (uint)(x + 1)); + Vector512 bottomLeft = Vector512.LoadUnsafe(ref MemoryMarshal.GetReference(bottom), (uint)(x - 1)); + Vector512 bottomCenter = Vector512.LoadUnsafe(ref MemoryMarshal.GetReference(bottom), (uint)(x)); + Vector512 bottomRight = Vector512.LoadUnsafe(ref MemoryMarshal.GetReference(bottom), (uint)(x + 1)); + + upperResult = operation.Invoke(upperCenter, upperLeft, upperRight, topLeft, topCenter, topRight, lowerLeft, lowerCenter, lowerRight); + + lowerResult = operation.Invoke(lowerCenter, lowerLeft, lowerRight, upperLeft, upperCenter, upperRight, bottomLeft, bottomCenter, bottomRight); + } + + /// + /// Evaluates the adaptive quantizer's four-lowest-neighbor filter, distinct from Butteraugli's three-minimum masking filter. + /// + /// The lowest sample weight. + /// The second-lowest sample weight. + /// The third-lowest sample weight. + /// The fourth-lowest sample weight. + private readonly struct FuzzyErosionOperator(float weight0, float weight1, float weight2, float weight3) + { + /// + /// Returns the weighted four lowest finite values of one three-by-three neighborhood. + /// + /// The center sample. + /// The left sample. + /// The right sample. + /// The upper-left sample. + /// The upper sample. + /// The upper-right sample. + /// The lower-left sample. + /// The lower sample. + /// The lower-right sample. + /// The weighted four-minimum sum in native arithmetic order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float Invoke( + float first, + float second, + float third, + float fourth, + float fifth, + float sixth, + float seventh, + float eighth, + float ninth) + { + // Sort the initial four values, then retain only the four lowest while inserting the remaining neighbors. + // Every SIMD lane performs this same finite-value ordering independently; equal values retain the same four magnitudes. + float minimum = MathF.Min(first, second); + second = MathF.Max(first, second); + first = minimum; + minimum = MathF.Min(third, fourth); + fourth = MathF.Max(third, fourth); + third = minimum; + minimum = MathF.Min(first, third); + third = MathF.Max(first, third); + first = minimum; + minimum = MathF.Min(second, fourth); + fourth = MathF.Max(second, fourth); + second = minimum; + minimum = MathF.Min(second, third); + third = MathF.Max(second, third); + second = minimum; + + StoreMin4(fifth, ref first, ref second, ref third, ref fourth); + StoreMin4(sixth, ref first, ref second, ref third, ref fourth); + StoreMin4(seventh, ref first, ref second, ref third, ref fourth); + StoreMin4(eighth, ref first, ref second, ref third, ref fourth); + StoreMin4(ninth, ref first, ref second, ref third, ref fourth); + return (((weight0 * first) + (weight1 * second)) + (weight2 * third)) + (weight3 * fourth); } - return true; + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector128 Invoke( + Vector128 first, + Vector128 second, + Vector128 third, + Vector128 fourth, + Vector128 fifth, + Vector128 sixth, + Vector128 seventh, + Vector128 eighth, + Vector128 ninth) + { + // Sort the initial four values, then retain only the four lowest while inserting the remaining neighbors. + // Every SIMD lane performs this same finite-value ordering independently; equal values retain the same four magnitudes. + Vector128 minimum = Vector128.Min(first, second); + second = Vector128.Max(first, second); + first = minimum; + minimum = Vector128.Min(third, fourth); + fourth = Vector128.Max(third, fourth); + third = minimum; + minimum = Vector128.Min(first, third); + third = Vector128.Max(first, third); + first = minimum; + minimum = Vector128.Min(second, fourth); + fourth = Vector128.Max(second, fourth); + second = minimum; + minimum = Vector128.Min(second, third); + third = Vector128.Max(second, third); + second = minimum; + + StoreMin4(fifth, ref first, ref second, ref third, ref fourth); + StoreMin4(sixth, ref first, ref second, ref third, ref fourth); + StoreMin4(seventh, ref first, ref second, ref third, ref fourth); + StoreMin4(eighth, ref first, ref second, ref third, ref fourth); + StoreMin4(ninth, ref first, ref second, ref third, ref fourth); + Vector128 result = (Vector128.Create(weight0) * first) + (Vector128.Create(weight1) * second); + result += Vector128.Create(weight2) * third; + return result + (Vector128.Create(weight3) * fourth); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector256 Invoke( + Vector256 first, + Vector256 second, + Vector256 third, + Vector256 fourth, + Vector256 fifth, + Vector256 sixth, + Vector256 seventh, + Vector256 eighth, + Vector256 ninth) + { + // Sort the initial four values, then retain only the four lowest while inserting the remaining neighbors. + // Every SIMD lane performs this same finite-value ordering independently; equal values retain the same four magnitudes. + Vector256 minimum = Vector256.Min(first, second); + second = Vector256.Max(first, second); + first = minimum; + minimum = Vector256.Min(third, fourth); + fourth = Vector256.Max(third, fourth); + third = minimum; + minimum = Vector256.Min(first, third); + third = Vector256.Max(first, third); + first = minimum; + minimum = Vector256.Min(second, fourth); + fourth = Vector256.Max(second, fourth); + second = minimum; + minimum = Vector256.Min(second, third); + third = Vector256.Max(second, third); + second = minimum; + + StoreMin4(fifth, ref first, ref second, ref third, ref fourth); + StoreMin4(sixth, ref first, ref second, ref third, ref fourth); + StoreMin4(seventh, ref first, ref second, ref third, ref fourth); + StoreMin4(eighth, ref first, ref second, ref third, ref fourth); + StoreMin4(ninth, ref first, ref second, ref third, ref fourth); + Vector256 result = (Vector256.Create(weight0) * first) + (Vector256.Create(weight1) * second); + result += Vector256.Create(weight2) * third; + return result + (Vector256.Create(weight3) * fourth); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector512 Invoke( + Vector512 first, + Vector512 second, + Vector512 third, + Vector512 fourth, + Vector512 fifth, + Vector512 sixth, + Vector512 seventh, + Vector512 eighth, + Vector512 ninth) + { + // Sort the initial four values, then retain only the four lowest while inserting the remaining neighbors. + // Every SIMD lane performs this same finite-value ordering independently; equal values retain the same four magnitudes. + Vector512 minimum = Vector512.Min(first, second); + second = Vector512.Max(first, second); + first = minimum; + minimum = Vector512.Min(third, fourth); + fourth = Vector512.Max(third, fourth); + third = minimum; + minimum = Vector512.Min(first, third); + third = Vector512.Max(first, third); + first = minimum; + minimum = Vector512.Min(second, fourth); + fourth = Vector512.Max(second, fourth); + second = minimum; + minimum = Vector512.Min(second, third); + third = Vector512.Max(second, third); + second = minimum; + + StoreMin4(fifth, ref first, ref second, ref third, ref fourth); + StoreMin4(sixth, ref first, ref second, ref third, ref fourth); + StoreMin4(seventh, ref first, ref second, ref third, ref fourth); + StoreMin4(eighth, ref first, ref second, ref third, ref fourth); + StoreMin4(ninth, ref first, ref second, ref third, ref fourth); + Vector512 result = (Vector512.Create(weight0) * first) + (Vector512.Create(weight1) * second); + result += Vector512.Create(weight2) * third; + return result + (Vector512.Create(weight3) * fourth); + } + } + + /// + /// Inserts independent finite samples into four ordered minima without changing lane order. + /// + /// The new sample in each lane. + /// The lowest retained sample in each lane. + /// The second-lowest retained sample in each lane. + /// The third-lowest retained sample in each lane. + /// The fourth-lowest retained sample in each lane. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void StoreMin4( + Vector128 value, + ref Vector128 min0, + ref Vector128 min1, + ref Vector128 min2, + ref Vector128 min3) + { + Vector128 next = Vector128.Max(value, min0); + min0 = Vector128.Min(value, min0); + value = Vector128.Max(next, min1); + min1 = Vector128.Min(next, min1); + next = Vector128.Max(value, min2); + min2 = Vector128.Min(value, min2); + min3 = Vector128.Min(next, min3); + } + + /// + /// Inserts independent finite samples into four ordered minima without changing lane order. + /// + /// The new sample in each lane. + /// The lowest retained sample in each lane. + /// The second-lowest retained sample in each lane. + /// The third-lowest retained sample in each lane. + /// The fourth-lowest retained sample in each lane. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void StoreMin4( + Vector256 value, + ref Vector256 min0, + ref Vector256 min1, + ref Vector256 min2, + ref Vector256 min3) + { + Vector256 next = Vector256.Max(value, min0); + min0 = Vector256.Min(value, min0); + value = Vector256.Max(next, min1); + min1 = Vector256.Min(next, min1); + next = Vector256.Max(value, min2); + min2 = Vector256.Min(value, min2); + min3 = Vector256.Min(next, min3); + } + + /// + /// Inserts independent finite samples into four ordered minima without changing lane order. + /// + /// The new sample in each lane. + /// The lowest retained sample in each lane. + /// The second-lowest retained sample in each lane. + /// The third-lowest retained sample in each lane. + /// The fourth-lowest retained sample in each lane. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void StoreMin4( + Vector512 value, + ref Vector512 min0, + ref Vector512 min1, + ref Vector512 min2, + ref Vector512 min3) + { + Vector512 next = Vector512.Max(value, min0); + min0 = Vector512.Min(value, min0); + value = Vector512.Max(next, min1); + min1 = Vector512.Min(next, min1); + next = Vector512.Max(value, min2); + min2 = Vector512.Min(value, min2); + min3 = Vector512.Min(next, min3); } private static void Blur1x1Masking(Configuration configuration, JxlPlane mask1x1, Rectangle rect) @@ -961,7 +1376,8 @@ public static bool FindBestQuantization( float qfLower = initialQfMin / (asymmetry * qfMaxDeviationLow); float qfHigher = initialQfMax * (qfMaxDeviationLow / asymmetry); - if (qfHigher / qfLower < 253) + // Native requires a representable quantization range below 253 before the iterative search begins. + if (qfHigher / qfLower >= 253) { return false; } @@ -1070,6 +1486,8 @@ public static bool FindBestQuantization( if (diff > 1.0f) { float old = rowQ[x]; + // Increase the requested precision before checking whether integer quantization preserves that change. + rowQ[x] *= diff; int qfOld = (int)MathF.Round(old * quantizer.InverseGlobalScale, MidpointRounding.AwayFromZero); int qfNew = (int)MathF.Round(rowQ[x] * quantizer.InverseGlobalScale, MidpointRounding.AwayFromZero); @@ -1120,7 +1538,7 @@ public static bool FindBestQuantization( } } - if (rowQ[x] < qfHigher) + if (rowQ[x] > qfHigher) { rowQ[x] = qfHigher; } diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/QuantizationTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/QuantizationTests.cs index 6f455b463c..3bb82fcd01 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/QuantizationTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/QuantizationTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; @@ -14,6 +15,99 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; public class QuantizationTests { + /// + /// Checks the adaptive quantizer's four-minimum filter and two-to-one rectangle traversal against independently sorted neighborhoods. + /// + [Fact] + public void AdaptiveFuzzyErosion_DownsamplesRectanglesWithoutTouchingPadding() + { + static void RunTest() + { + // The last case places both regions inside larger planes. Neighbors use global source edges, not the selected rectangle. + ReadOnlySpan<(int Width, int Height, Rectangle Source, Rectangle Destination)> cases = + [ + (2, 2, new Rectangle(0, 0, 2, 2), new Rectangle(0, 0, 1, 1)), + (6, 4, new Rectangle(0, 0, 6, 4), new Rectangle(1, 1, 3, 2)), + (34, 6, new Rectangle(0, 0, 34, 6), new Rectangle(2, 1, 17, 3)), + (66, 4, new Rectangle(0, 0, 66, 4), new Rectangle(1, 2, 33, 2)), + (70, 8, new Rectangle(2, 2, 66, 4), new Rectangle(3, 1, 33, 2)) + ]; + + const float untouched = -123F; + // At target 2, native weights [.125,.1,.09,.06] sum to .375 before normalization. All four minima contribute. + float normalization = 0.29959705784054957F / 0.375F; + ReadOnlySpan weights = [0.125F * normalization, 0.1F * normalization, 0.09F * normalization, 0.06F * normalization]; + Span neighbors = stackalloc float[9]; + + foreach ((int width, int height, Rectangle sourceRect, Rectangle destinationRect) in cases) + { + using JxlPlane source = new(TestEnvironment.Configuration, width, height); + using JxlPlane destination = new(TestEnvironment.Configuration, destinationRect.Right + 2, destinationRect.Bottom + 2); + + Span input = source.GetRow(0); + Span output = destination.GetRow(0); + input.Fill(untouched); + output.Fill(untouched); + + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + input[(y * source.PixelsPerRow) + x] = ((x * 13 + y * 7) % 19) + 1; + } + } + + Assert.True(JxlAdaptiveQuantization.FuzzyErosion(2F, sourceRect, source, destinationRect, destination)); + + for (int y = 0; y < destination.YSize; y++) + { + ReadOnlySpan row = output.Slice(y * destination.PixelsPerRow, destination.PixelsPerRow); + for (int x = 0; x < row.Length; x++) + { + if (!destinationRect.Contains(x, y)) + { + Assert.Equal(untouched, row[x]); + continue; + } + + float expected = 0F; + for (int pairY = 0; pairY < 2; pairY++) + { + int sourceY = sourceRect.Top + (2 * (y - destinationRect.Top)) + pairY; + for (int pairX = 0; pairX < 2; pairX++) + { + int sourceX = sourceRect.Left + (2 * (x - destinationRect.Left)) + pairX; + int count = 0; + for (int dy = -1; dy <= 1; dy++) + { + int neighborY = Math.Clamp(sourceY + dy, 0, height - 1); + for (int dx = -1; dx <= 1; dx++) + { + int neighborX = Math.Clamp(sourceX + dx, 0, width - 1); + neighbors[count++] = input[(neighborY * source.PixelsPerRow) + neighborX]; + } + } + + // Full sorting is an independent oracle for the production four-minimum insertion network. + neighbors.Sort(); + float value = (((weights[0] * neighbors[0]) + (weights[1] * neighbors[1])) + + (weights[2] * neighbors[2])) + (weights[3] * neighbors[3]); + + expected += value; + } + } + + Assert.Equal(expected, row[x]); + } + } + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } + /// /// Checks final table layout, reciprocal weights, low-frequency exclusion and output guards at every SIMD width. /// From cd1f170a700c2e8c3ff5fced1d8f023cbd95d9a1 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Oct 2026 04:20:16 +1000 Subject: [PATCH 311/328] Bound JPEG XL perceptual row transforms and share SIMD suppression --- .../Jxl/Processing/Butteraugli/Butteraugli.cs | 211 ++++++++++++------ .../Jxl/Processing/ButteraugliTests.cs | 138 ++++++++++++ 2 files changed, 280 insertions(+), 69 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs index e009869a45..62d32cd6f1 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs @@ -454,62 +454,91 @@ public static Vector AmplifyRangeAroundZero(Vector x, float width) x + x)); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void XybLowFrequencyToValues( - Vector x, - Vector y, - Vector bArg, - out Vector valX, - out Vector valY, - out Vector valB) - { - Vector xMul = new(33.832837186260f); - Vector yMul = new(14.458268100570f); - Vector bMul = new(49.87984651440f); - Vector yToBMul = new(-0.362267051518f); - - Vector b = (yToBMul * y) + bArg; - - valB = b * bMul; - valX = x * xMul; - valY = y * yMul; - } - /// /// Converts low-frequency XYB planes to the weighted values used by the comparison equations. /// - /// The preallocated low-frequency image, updated in place. + /// The preallocated low-frequency image, updated in place without changing row padding. public static void XybLowFrequencyToValues(JxlImage3 xybLf) { - int lanes = Vector.Count; - - Span samples0 = xybLf.Plane(0).GetRow(0); - Span samples1 = xybLf.Plane(1).GetRow(0); - Span samples2 = xybLf.Plane(2).GetRow(0); + // These are the native Butteraugli low-frequency weights. B first removes its correlated Y contribution, + // then applies its own weight. X and Y receive independent weights for the later squared differences. + const float xMultiplier = 33.832837186260F; + const float yMultiplier = 14.458268100570F; + const float bMultiplier = 49.87984651440F; + const float yToBMultiplier = -0.362267051518F; + JxlPlane planeX = xybLf.Plane(0); + JxlPlane planeY = xybLf.Plane(1); + JxlPlane planeB = xybLf.Plane(2); + Span samplesX = planeX.GetRow(0); + Span samplesY = planeY.GetRow(0); + Span samplesB = planeB.GetRow(0); + int strideX = planeX.PixelsPerRow; + int strideY = planeY.PixelsPerRow; + int strideB = planeB.PixelsPerRow; + int width = xybLf.XSize; for (int y = 0; y < xybLf.YSize; y++) { - Span rowX = samples0[(y * xybLf.Plane(0).PixelsPerRow)..]; - Span rowY = samples1[(y * xybLf.Plane(1).PixelsPerRow)..]; - Span rowB = samples2[(y * xybLf.Plane(2).PixelsPerRow)..]; + ref float rowX = ref MemoryMarshal.GetReference(samplesX.Slice(y * strideX, width)); + ref float rowY = ref MemoryMarshal.GetReference(samplesY.Slice(y * strideY, width)); + ref float rowB = ref MemoryMarshal.GetReference(samplesB.Slice(y * strideB, width)); + int x = 0; - for (int x = 0; x < xybLf.XSize; x += lanes) + // Each lane contains one position from each plane. Load all three original components before writing; + // in particular, B must use the original Y rather than its weighted value. Only complete logical blocks + // enter SIMD, so no load or store depends on the allocator's physical row padding. + if (Vector512.IsHardwareAccelerated) { - Vector valX = new(rowX.Slice(x, lanes)); - Vector valY = new(rowY.Slice(x, lanes)); - Vector valB = new(rowB.Slice(x, lanes)); + for (; x <= width - Vector512.Count; x += Vector512.Count) + { + Vector512 inputX = Vector512.LoadUnsafe(ref rowX, (uint)x); + Vector512 inputY = Vector512.LoadUnsafe(ref rowY, (uint)x); + Vector512 inputB = Vector512.LoadUnsafe(ref rowB, (uint)x); + Vector512 outputB = Vector512.MultiplyAddEstimate(Vector512.Create(yToBMultiplier), inputY, inputB); + (inputX * Vector512.Create(xMultiplier)).StoreUnsafe(ref rowX, (uint)x); + (inputY * Vector512.Create(yMultiplier)).StoreUnsafe(ref rowY, (uint)x); + (outputB * Vector512.Create(bMultiplier)).StoreUnsafe(ref rowB, (uint)x); + } + } + + if (Vector256.IsHardwareAccelerated) + { + for (; x <= width - Vector256.Count; x += Vector256.Count) + { + Vector256 inputX = Vector256.LoadUnsafe(ref rowX, (uint)x); + Vector256 inputY = Vector256.LoadUnsafe(ref rowY, (uint)x); + Vector256 inputB = Vector256.LoadUnsafe(ref rowB, (uint)x); + Vector256 outputB = Vector256.MultiplyAddEstimate(Vector256.Create(yToBMultiplier), inputY, inputB); + (inputX * Vector256.Create(xMultiplier)).StoreUnsafe(ref rowX, (uint)x); + (inputY * Vector256.Create(yMultiplier)).StoreUnsafe(ref rowY, (uint)x); + (outputB * Vector256.Create(bMultiplier)).StoreUnsafe(ref rowB, (uint)x); + } + } - XybLowFrequencyToValues( - valX, - valY, - valB, - out valX, - out valY, - out valB); + if (Vector128.IsHardwareAccelerated) + { + for (; x <= width - Vector128.Count; x += Vector128.Count) + { + Vector128 inputX = Vector128.LoadUnsafe(ref rowX, (uint)x); + Vector128 inputY = Vector128.LoadUnsafe(ref rowY, (uint)x); + Vector128 inputB = Vector128.LoadUnsafe(ref rowB, (uint)x); + Vector128 outputB = Vector128.MultiplyAddEstimate(Vector128.Create(yToBMultiplier), inputY, inputB); + (inputX * Vector128.Create(xMultiplier)).StoreUnsafe(ref rowX, (uint)x); + (inputY * Vector128.Create(yMultiplier)).StoreUnsafe(ref rowY, (uint)x); + (outputB * Vector128.Create(bMultiplier)).StoreUnsafe(ref rowB, (uint)x); + } + } - valX.CopyTo(rowX.Slice(x, lanes)); - valY.CopyTo(rowY.Slice(x, lanes)); - valB.CopyTo(rowB.Slice(x, lanes)); + // The remainder uses the same multiply-add followed by multiplication as each SIMD lane and native MulAdd. + for (; x < width; x++) + { + float inputX = Unsafe.Add(ref rowX, x); + float inputY = Unsafe.Add(ref rowY, x); + float inputB = Unsafe.Add(ref rowB, x); + float outputB = float.MultiplyAddEstimate(yToBMultiplier, inputY, inputB); + Unsafe.Add(ref rowX, x) = inputX * xMultiplier; + Unsafe.Add(ref rowY, x) = inputY * yMultiplier; + Unsafe.Add(ref rowB, x) = outputB * bMultiplier; } } } @@ -527,35 +556,17 @@ public static bool SuppressXByY(JxlPlane inY, JxlPlane inOutX) return false; } - int xSize = inY.XSize; - int ySize = inY.YSize; - int lanes = Vector.Count; - - const float suppress = 46.0f; - const float s = 0.653020556257f; - - Vector sv = new(s); - Vector oneMinusS = new(1.0f - s); - Vector ywv = new(suppress); - - Span samplesY = inY.GetRow(0); + ReadOnlySpan samplesY = inY.GetRow(0); Span samplesX = inOutX.GetRow(0); - for (int y = 0; y < ySize; y++) + for (int y = 0; y < inY.YSize; y++) { - ReadOnlySpan rowY = samplesY[(y * inY.PixelsPerRow)..]; - Span rowX = samplesX[(y * inOutX.PixelsPerRow)..]; - - for (int x = 0; x < xSize; x += lanes) - { - Vector vx = new(rowX.Slice(x, lanes)); - Vector vy = new(rowY.Slice(x, lanes)); + ReadOnlySpan rowY = samplesY.Slice(y * inY.PixelsPerRow, inY.XSize); + Span rowX = samplesX.Slice(y * inOutX.PixelsPerRow, inOutX.XSize); - Vector scaler = - ((ywv / ((vy * vy) + ywv)) * oneMinusS) + sv; - - (scaler * vx).CopyTo(rowX.Slice(x, lanes)); - } + // Each lane combines X and Y at the same position. The shared loop handles complete registers and scalar tails, + // so the result can replace X without reading or changing physical row padding. + TensorPrimitives_.Invoke(rowX, rowY, rowX, default(SuppressXByYOperator)); } return true; @@ -2090,6 +2101,68 @@ public static void L2DiffAsymmetric(JxlPlane i0, JxlPlane i1, floa } } + /// + /// Reduces X sensitivity where the same sample has a large Y response. + /// + private readonly struct SuppressXByYOperator : TensorPrimitives_.IBinaryOperator + { + /// The squared Y magnitude that halves the variable part of the response. + private const float Suppression = 46F; + + /// The response retained when the Y magnitude grows without bound. + private const float MinimumResponse = 0.653020556257F; + + /// The variable response, rounded once from the native coefficient. + private const float VariableResponse = (float)(1D - 0.653020556257D); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float Invoke(float first, float second) + { + // Native Butteraugli uses Y squared, so positive and negative Y suppress X equally. + // The response is one at Y = 0 and tends to MinimumResponse as the Y magnitude increases. + float denominator = float.MultiplyAddEstimate(second, second, Suppression); + + return float.MultiplyAddEstimate(Suppression / denominator, VariableResponse, MinimumResponse) * first; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector128 Invoke(Vector128 first, Vector128 second) + { + Vector128 denominator = Vector128.MultiplyAddEstimate(second, second, Vector128.Create(Suppression)); + + return Vector128.MultiplyAddEstimate( + Vector128.Create(Suppression) / denominator, + Vector128.Create(VariableResponse), + Vector128.Create(MinimumResponse)) * first; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector256 Invoke(Vector256 first, Vector256 second) + { + Vector256 denominator = Vector256.MultiplyAddEstimate(second, second, Vector256.Create(Suppression)); + + return Vector256.MultiplyAddEstimate( + Vector256.Create(Suppression) / denominator, + Vector256.Create(VariableResponse), + Vector256.Create(MinimumResponse)) * first; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector512 Invoke(Vector512 first, Vector512 second) + { + Vector512 denominator = Vector512.MultiplyAddEstimate(second, second, Vector512.Create(Suppression)); + + return Vector512.MultiplyAddEstimate( + Vector512.Create(Suppression) / denominator, + Vector512.Create(VariableResponse), + Vector512.Create(MinimumResponse)) * first; + } + } + /// /// Applies the native low- and high-frequency masks to independently accumulated squared errors. /// diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs index 1bc023ccee..61b10a3e52 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs @@ -23,6 +23,64 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; /// public class ButteraugliTests { + /// + /// Checks the native suppression endpoints, signed Y symmetry and exact logical row bounds at each SIMD width. + /// + [Fact] + public void SuppressXByY_PreservesZeroYAndApproachesMinimumResponse() + { + static void RunTest() + { + const float padding = -123F; + // At |Y| = 2^20, the variable response is less than half a float ULP at the native minimum response. + // Both signs must therefore round to the same minimum, while zero Y must retain X unchanged. + const float largeY = 1048576F; + const float minimumResponse = 0.653020556257F; + ReadOnlySpan widths = [1, 3, 17, 33]; + + foreach (int width in widths) + { + using JxlPlane inputY = new(TestEnvironment.Configuration, width, 3); + using JxlPlane inputOutputX = new(TestEnvironment.Configuration, width, 3); + Span ySamples = inputY.GetRow(0); + Span xSamples = inputOutputX.GetRow(0); + ySamples.Fill(padding); + xSamples.Fill(padding); + + for (int y = 0; y < 3; y++) + { + for (int x = 0; x < width; x++) + { + ySamples[(y * inputY.PixelsPerRow) + x] = y == 0 ? 0F : y == 1 ? largeY : -largeY; + xSamples[(y * inputOutputX.PixelsPerRow) + x] = ((x % 5) - 2) * 0.25F; + } + } + + Assert.True(Butteraugli.SuppressXByY(inputY, inputOutputX)); + + for (int y = 0; y < 3; y++) + { + ReadOnlySpan row = xSamples.Slice(y * inputOutputX.PixelsPerRow, inputOutputX.PixelsPerRow); + + for (int x = 0; x < width; x++) + { + float original = ((x % 5) - 2) * 0.25F; + Assert.Equal(y == 0 ? original : original * minimumResponse, row[x]); + } + + for (int x = width; x < row.Length; x++) + { + Assert.Equal(padding, row[x]); + } + } + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } + /// /// Checks weighted squared differences, accumulation and reused output across all SIMD widths and scalar tails. /// @@ -441,4 +499,84 @@ public void TestLargeImage() Assert.True(new TolerantMath(5e-7).AreEqual(diffval, diffval2), $"Diffval != diffval2 (diffval={diffval}, diffval2={diffval2})"); Assert.True(new TolerantMath(1e-7).AreEqual(distp, distp2), $"Distp != distp2 (distp={distp}, distp2={distp2})"); } + + /// + /// Checks the native low-frequency basis weights, Y removal from B and exact row bounds at each SIMD width. + /// + [Fact] + public void LowFrequencyToValues_UsesOriginalYAndPreservesPadding() + { + static void RunTest() + { + const float padding = -123F; + const float xWeight = 33.832837186260F; + const float yWeight = 14.458268100570F; + const float bWeight = 49.87984651440F; + const float yToBWeight = -0.362267051518F; + ReadOnlySpan widths = [1, 3, 17, 33]; + + foreach (int width in widths) + { + using JxlImage3 image = new(TestEnvironment.Configuration, width, 3); + JxlPlane planeX = image.Plane(0); + JxlPlane planeY = image.Plane(1); + JxlPlane planeB = image.Plane(2); + Span samplesX = planeX.GetRow(0); + Span samplesY = planeY.GetRow(0); + Span samplesB = planeB.GetRow(0); + samplesX.Fill(padding); + samplesY.Fill(padding); + samplesB.Fill(padding); + + // The first two rows isolate B and Y. The third cancels the native Y-to-B term exactly. + // Dyadic samples keep that cancellation exact with fused or separate multiply-add instructions. + for (int y = 0; y < 3; y++) + { + for (int x = 0; x < width; x++) + { + float sample = ((x % 5) - 2) * 0.25F; + samplesX[(y * planeX.PixelsPerRow) + x] = sample; + samplesY[(y * planeY.PixelsPerRow) + x] = y == 0 ? 0F : sample; + samplesB[(y * planeB.PixelsPerRow) + x] = y == 0 ? sample : y == 1 ? 0F : -(sample * yToBWeight); + } + } + + Butteraugli.XybLowFrequencyToValues(image); + + for (int y = 0; y < 3; y++) + { + ReadOnlySpan rowX = samplesX.Slice(y * planeX.PixelsPerRow, planeX.PixelsPerRow); + ReadOnlySpan rowY = samplesY.Slice(y * planeY.PixelsPerRow, planeY.PixelsPerRow); + ReadOnlySpan rowB = samplesB.Slice(y * planeB.PixelsPerRow, planeB.PixelsPerRow); + + for (int x = 0; x < width; x++) + { + float sample = ((x % 5) - 2) * 0.25F; + Assert.Equal(sample * xWeight, rowX[x]); + Assert.Equal(y == 0 ? 0F : sample * yWeight, rowY[x]); + Assert.Equal(y == 0 ? sample * bWeight : y == 1 ? (sample * yToBWeight) * bWeight : 0F, rowB[x]); + } + + for (int x = width; x < rowX.Length; x++) + { + Assert.Equal(padding, rowX[x]); + } + + for (int x = width; x < rowY.Length; x++) + { + Assert.Equal(padding, rowY[x]); + } + + for (int x = width; x < rowB.Length; x++) + { + Assert.Equal(padding, rowB[x]); + } + } + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } } From abb88416cbf6c42f46602de58227f14846c95c3a Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Oct 2026 04:24:56 +1000 Subject: [PATCH 312/328] Retain separate JPEG XL comparison results and prepare owned output --- .../Jxl/Processing/Butteraugli/Butteraugli.cs | 21 +++++-- .../Jxl/Processing/ButteraugliTests.cs | 55 ++++++++++++++++++- 2 files changed, 70 insertions(+), 6 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs index 62d32cd6f1..498ce266f8 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs @@ -3087,7 +3087,7 @@ public static float ButteraugliFuzzyClass(float score) /// The reference pixels, which full-size comparison can modify. /// The distorted pixels, which full-size comparison can modify. /// The perceptual comparison parameters. - /// The caller-owned output plane. Its identity does not change. + /// The caller-owned output plane. Storage is reused when it fits; the plane's identity does not change. /// The comparison score, or zero when comparison fails. /// Whether every comparison stage succeeded. public static bool ButteraugliInterfaceInPlace( @@ -3134,25 +3134,36 @@ public static bool ButteraugliInterfaceInPlace( } } - JxlPlane subDiffmap = new(); + // The half-size result must survive the full-size comparison. Each result has its own dimensions and allocation. + // Smaller images need no second comparison; zero dimensions leave this scoped owner without sample storage. + bool compareSubsample = xsize >= 15 && ysize >= 15; + using JxlPlane subDiffmap = new(configuration, compareSubsample ? (xsize + 1) / 2 : 0, compareSubsample ? (ysize + 1) / 2 : 0); - if (xsize >= 15 && ysize >= 15) + if (compareSubsample) { using JxlImage3 rgb0Sub = SubSample2x(configuration, rgb0); using JxlImage3 rgb1Sub = SubSample2x(configuration, rgb1); - if (!ButteraugliDiffmapInPlace(configuration, rgb0Sub, rgb1Sub, parameters, diffmap)) + if (!ButteraugliDiffmapInPlace(configuration, rgb0Sub, rgb1Sub, parameters, subDiffmap)) { return false; } } + // The raw comparison writes preallocated output. This entry point owns preparation, including an initially empty output plane. + // Swap only after allocation succeeds, and release the displaced allocation through the scoped replacement. + if (!diffmap.ShrinkTo(xsize, ysize)) + { + using JxlPlane outputStorage = new(configuration, xsize, ysize); + diffmap.Swap(outputStorage); + } + if (!ButteraugliDiffmapInPlace(configuration, rgb0, rgb1, parameters, diffmap)) { return false; } - if (xsize >= 15 && ysize >= 15) + if (compareSubsample) { AddSupersampled2x(subDiffmap, 0.5f, diffmap); } diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs index 61b10a3e52..6593d0c10c 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs @@ -81,6 +81,59 @@ static void RunTest() HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); } + /// + /// Checks standalone output preparation, separate half-resolution results and allocator cleanup for identical samples. + /// + [Fact] + public void InPlaceInterface_PreparesOutputAndRetainsSubsampledResult() + { + static void RunTest() + { + Configuration configuration = TestEnvironment.Configuration.Clone(); + TestMemoryAllocator allocator = new(); + allocator.EnableNonThreadSafeLogging(); + configuration.MemoryAllocator = allocator; + ReadOnlySpan heights = [8, 17]; + + foreach (int height in heights) + { + // Height 8 selects only the full-size comparison. Height 17 also produces and merges a half-size map. + // Start with empty or undersized output to exercise this entry point's ownership, rather than its raw kernel. + using (JxlImage3 reference = new(configuration, 17, height)) + using (JxlImage3 distorted = new(configuration, 17, height)) + using (JxlPlane difference = new(configuration, height == 8 ? 0 : 1, height == 8 ? 0 : 1)) + { + JxlImageOperations.ZeroFillImage(reference); + JxlImageOperations.ZeroFillImage(distorted); + Assert.True(Butteraugli.ButteraugliInterfaceInPlace( + configuration, + reference, + distorted, + new ButteraugliParameters(), + difference, + out double score)); + + Assert.Equal(0D, score); + Assert.Equal(17, difference.XSize); + Assert.Equal(height, difference.YSize); + ReadOnlySpan samples = difference.GetRow(0); + + for (int y = 0; y < height; y++) + { + Assert.Equal(-1, samples.Slice(y * difference.PixelsPerRow, difference.XSize).IndexOfAnyExcept(0F)); + } + } + + // The old output allocation and both comparison levels must be returned when the outer owners are disposed. + Assert.Equal(0, allocator.LiveBufferBytes); + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } + /// /// Checks weighted squared differences, accumulation and reused output across all SIMD widths and scalar tails. /// @@ -476,7 +529,7 @@ public void TestLargeImage() ButteraugliParameters butteraugliParameters = new(); JxlPlane diffmap = new(); Assert.True( - Butteraugli.ButteraugliInterface(TestEnvironment.Configuration,, rgb0, rgb1, butteraugliParameters, diffmap, out double diffval), + Butteraugli.ButteraugliInterface(TestEnvironment.Configuration, rgb0, rgb1, butteraugliParameters, diffmap, out double diffval), "Couldn't initialize Butteraugli"); double distp = Butteraugli.ComputeDistanceP(diffmap, butteraugliParameters, 3.0); From 4927d0c96a5d0ec4f7135b70db6f65c487a68b89 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Oct 2026 04:26:38 +1000 Subject: [PATCH 313/328] Correct inverse JPEG XL opsin arithmetic and shared SIMD row traversal --- .../Formats/Jxl/Cms/JxlOpsinConstants.cs | 6 + .../Processing/Decoder/JxlOpsinParameters.cs | 48 ++- .../Decoder/JxlOutputEncodingInfo.cs | 13 +- .../Jxl/Processing/Decoder/JxlXybDecoder.cs | 376 ++++++++++++------ .../Jxl/Processing/RenderPipeline/XybStage.cs | 166 +++++--- .../Formats/Jxl/Processing/OpsinImageTests.cs | 173 +++++++- 6 files changed, 589 insertions(+), 193 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlOpsinConstants.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlOpsinConstants.cs index 246affd45f..ef887ceab9 100644 --- a/src/ImageSharp/Formats/Jxl/Cms/JxlOpsinConstants.cs +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlOpsinConstants.cs @@ -24,6 +24,12 @@ internal static class JxlOpsinConstants public static ReadOnlySpan OpsinAbsorbanceBias => [OpsinAbsorbanceBias0, OpsinAbsorbanceBias1, OpsinAbsorbanceBias2]; + /// Gets the output scales for X, Y and B minus Y in the native scaled-XYB representation. + public static ReadOnlySpan ScaledXybScale => [22.995788804F, 1.183000077F, 1.502141333F]; + + /// Gets the offsets added to X, Y and B minus Y before scaled-XYB multiplication. + public static ReadOnlySpan ScaledXybOffset => [0.015386134F, 0F, 0.27770459F]; + /// /// Gets the frozen default inverse opsin matrix in row-major order, as defined by the JPEG XL bitstream. /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOpsinParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOpsinParameters.cs index 6fdfa28f9f..35a5fe6cc1 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOpsinParameters.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOpsinParameters.cs @@ -1,17 +1,55 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -#pragma warning disable SA1401 // Fields should be private +using System.Numerics; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +/// Stores operation-wide coefficients for the inverse opsin transform. internal sealed class JxlOpsinParameters { - public InlineArray36 InverseOpsinMatrix; + /// Gets the intensity-scaled inverse matrix in the convention used by Vector3.Transform and SimdUtils.Transform3. + public Matrix4x4 InverseOpsinMatrix { get; private set; } - public InlineArray4 OpsinBiases; + /// Gets the three negative absorbance biases added after cubing. + public Vector3 OpsinBiases { get; private set; } - public InlineArray4 OpsinBiasesCbrt; + /// Gets the signed cube roots subtracted before cubing. + public Vector3 OpsinBiasesCbrt { get; private set; } - public InlineArray4 QuantBiases; + /// Gets or sets the three coefficient biases and shared numerator used by coefficient reconstruction. + public InlineArray4 QuantBiases { get; set; } + + /// Prepares the selected inverse matrix, negative biases and intensity scale before row processing. + /// The selected row-major inverse opsin matrix. + /// The three negative absorbance biases from the transform header. + /// The positive original intensity target, in nits, established by the image header. + public void Initialize(in JxlMatrix3x3F inverseMatrix, InlineArray3 biases, float intensityTarget) + { + // XYB uses absolute intensity. The matrix converts it to relative RGB, where one equals the original intensity target. + // Transpose the row-major 3 by 3 coefficients once. SIMD broadcasts each scalar; it needs no replicated coefficient bank. + ReadOnlySpan matrix = inverseMatrix.AsReadOnlySpan(); + float scale = 255F / intensityTarget; + this.InverseOpsinMatrix = new( + matrix[0] * scale, + matrix[3] * scale, + matrix[6] * scale, + 0F, + matrix[1] * scale, + matrix[4] * scale, + matrix[7] * scale, + 0F, + matrix[2] * scale, + matrix[5] * scale, + matrix[8] * scale, + 0F, + 0F, + 0F, + 0F, + 1F); + + this.OpsinBiases = new(biases[0], biases[1], biases[2]); + this.OpsinBiasesCbrt = new(MathF.Cbrt(biases[0]), MathF.Cbrt(biases[1]), MathF.Cbrt(biases[2])); + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOutputEncodingInfo.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOutputEncodingInfo.cs index 9571c84f9f..da7d523559 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOutputEncodingInfo.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOutputEncodingInfo.cs @@ -46,6 +46,8 @@ internal sealed class JxlOutputEncodingInfo public JxlCmsInterface Cms { get; set; } + /// Loads the original encoding metadata and prepares its inverse opsin coefficients. + /// The parsed image and transform headers. public void SetFromMetadata(JxlCodecMetadata metadata) { JxlImageMetadata imageMetadata = metadata.ImageMetadata; @@ -63,15 +65,8 @@ public void SetFromMetadata(JxlCodecMetadata metadata) JxlOpsinParameters parameters = this.OpsinParameters; - imageMetadata.OpsinBiases.CopyTo(parameters.OpsinBiases); - parameters.OpsinBiasesCbrt[0] = MathF.Cbrt(parameters.OpsinBiases[0]); - parameters.OpsinBiasesCbrt[1] = MathF.Cbrt(parameters.OpsinBiases[1]); - parameters.OpsinBiasesCbrt[2] = MathF.Cbrt(parameters.OpsinBiases[2]); - - parameters.OpsinBiasesCbrt[3] = 1; - parameters.OpsinBiases[3] = 1; - - inverseMatrix.QuantBiases.AsSpan().CopyTo(parameters.QuantBiases); + parameters.Initialize(this.OriginalInverseMatrix, inverseMatrix.OpsinBiases, this.OriginalIntensityTarget); + parameters.QuantBiases = inverseMatrix.QuantBiases; bool origOK = JxlXybDecoder.CanOutputToColorEncoding(imageMetadata.ColorEncoding); bool origGrey = this.OriginalColorEncoding.IsGray; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs index 3fb298621a..12160fe4d4 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs @@ -4,159 +4,305 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Jxl.Cms; using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; -/// -/// Decodes the XYB color format (which JPEG XL uses) into RGB. -/// +/// Reconstructs linear RGB from JPEG XL XYB samples. internal static class JxlXybDecoder { - /// - /// Converts XYB to RGB using SIMD, one vector at a time. - /// - /// X channel - /// Y channel - /// B channel - /// Opsin parameters & configuration - /// Output R - /// Output G - /// Output B + /// Converts three equally sized XYB rows to linear RGB without clamping reconstructed components. + /// The logical X samples. + /// The corresponding Y samples. + /// The corresponding B samples. + /// The equally sized red destination. + /// The equally sized green destination. + /// The equally sized blue destination. + /// The matrix and biases prepared before processing rows. + /// Corresponding source and destination rows can start at the same address. Offset overlap is not supported. public static void ConvertXybToRgb( - Vector opsinX, - Vector opsinY, - Vector opsinB, - JxlOpsinParameters opsinParameters, - ref Vector linearR, - ref Vector linearG, - ref Vector linearB) + ReadOnlySpan opsinX, + ReadOnlySpan opsinY, + ReadOnlySpan opsinB, + Span linearR, + Span linearG, + Span linearB, + JxlOpsinParameters parameters) { - Vector negBiasR = new(opsinParameters.OpsinBiases[0]); - Vector negBiasG = new(opsinParameters.OpsinBiases[1]); - Vector negBiasB = new(opsinParameters.OpsinBiases[2]); + Matrix4x4 matrix = parameters.InverseOpsinMatrix; + Vector3 biases = parameters.OpsinBiases; + Vector3 cubeRootBiases = parameters.OpsinBiasesCbrt; + ref float inputX = ref MemoryMarshal.GetReference(opsinX); + ref float inputY = ref MemoryMarshal.GetReference(opsinY); + ref float inputB = ref MemoryMarshal.GetReference(opsinB); + ref float outputR = ref MemoryMarshal.GetReference(linearR); + ref float outputG = ref MemoryMarshal.GetReference(linearG); + ref float outputB = ref MemoryMarshal.GetReference(linearB); + int x = 0; - Vector gammaR = opsinX + opsinY; - Vector gammaG = opsinY - opsinX; - Vector gammaB = opsinB; + // Each register contains one component from consecutive pixels. Load all three components before the first store. + // Only complete registers enter a SIMD loop. Narrower widths and the scalar tail start at the first unprocessed pixel. + if (Vector512.IsHardwareAccelerated) + { + for (; x <= opsinX.Length - Vector512.Count; x += Vector512.Count) + { + Vector512 input0 = Vector512.LoadUnsafe(ref inputX, (uint)x); + Vector512 input1 = Vector512.LoadUnsafe(ref inputY, (uint)x); + Vector512 input2 = Vector512.LoadUnsafe(ref inputB, (uint)x); + ConvertXybToRgb( + input0, + input1, + input2, + in matrix, + biases, + cubeRootBiases, + out Vector512 r, + out Vector512 g, + out Vector512 b); - Vector gammaR2 = gammaR * gammaR; - Vector gammaG2 = gammaG * gammaG; - Vector gammaB2 = gammaB * gammaB; + r.StoreUnsafe(ref outputR, (uint)x); + g.StoreUnsafe(ref outputG, (uint)x); + b.StoreUnsafe(ref outputB, (uint)x); + } + } - Vector mixedR = (gammaR2 * gammaR) + negBiasR; - Vector mixedG = (gammaG2 * gammaG) + negBiasG; - Vector mixedB = (gammaB2 * gammaB) + negBiasB; + if (Vector256.IsHardwareAccelerated) + { + for (; x <= opsinX.Length - Vector256.Count; x += Vector256.Count) + { + Vector256 input0 = Vector256.LoadUnsafe(ref inputX, (uint)x); + Vector256 input1 = Vector256.LoadUnsafe(ref inputY, (uint)x); + Vector256 input2 = Vector256.LoadUnsafe(ref inputB, (uint)x); + ConvertXybToRgb( + input0, + input1, + input2, + in matrix, + biases, + cubeRootBiases, + out Vector256 r, + out Vector256 g, + out Vector256 b); - Span inverseMatrix = opsinParameters.InverseOpsinMatrix; + r.StoreUnsafe(ref outputR, (uint)x); + g.StoreUnsafe(ref outputG, (uint)x); + b.StoreUnsafe(ref outputB, (uint)x); + } + } - linearR = LoadDuplicate128(ref inverseMatrix[0 * 4]) * mixedR; - linearG = LoadDuplicate128(ref inverseMatrix[3 * 4]) * mixedR; - linearB = LoadDuplicate128(ref inverseMatrix[6 * 4]) * mixedR; + if (Vector128.IsHardwareAccelerated) + { + for (; x <= opsinX.Length - Vector128.Count; x += Vector128.Count) + { + Vector128 input0 = Vector128.LoadUnsafe(ref inputX, (uint)x); + Vector128 input1 = Vector128.LoadUnsafe(ref inputY, (uint)x); + Vector128 input2 = Vector128.LoadUnsafe(ref inputB, (uint)x); + ConvertXybToRgb( + input0, + input1, + input2, + in matrix, + biases, + cubeRootBiases, + out Vector128 r, + out Vector128 g, + out Vector128 b); - linearR = (LoadDuplicate128(ref inverseMatrix[1 * 4]) * mixedG) + linearR; - linearG = (LoadDuplicate128(ref inverseMatrix[4 * 4]) * mixedG) + linearG; - linearB = (LoadDuplicate128(ref inverseMatrix[7 * 4]) * mixedG) + linearB; + r.StoreUnsafe(ref outputR, (uint)x); + g.StoreUnsafe(ref outputG, (uint)x); + b.StoreUnsafe(ref outputB, (uint)x); + } + } - linearR = (LoadDuplicate128(ref inverseMatrix[2 * 4]) * mixedB) + linearR; - linearG = (LoadDuplicate128(ref inverseMatrix[5 * 4]) * mixedB) + linearG; - linearB = (LoadDuplicate128(ref inverseMatrix[8 * 4]) * mixedB) + linearB; + for (; x < opsinX.Length; x++) + { + ConvertXybToRgb(opsinX[x], opsinY[x], opsinB[x], in matrix, biases, cubeRootBiases, out float r, out float g, out float b); + linearR[x] = r; + linearG[x] = g; + linearB[x] = b; + } } - public static bool OpsinToLinear(JxlImage3 opsin, Rectangle rect, JxlImage3 linear, JxlOpsinParameters opsinParameters) + /// Converts an XYB rectangle to an equally sized linear RGB image. + /// The source planes that contain the rectangle. + /// The logical source rectangle. + /// The destination, whose logical dimensions must equal the rectangle dimensions. + /// The prepared matrix and biases. + /// Whether the rectangle and destination have equal dimensions. + public static bool OpsinToLinear(JxlImage3 opsin, Rectangle rect, JxlImage3 linear, JxlOpsinParameters parameters) { if (!JxlImageOperations.SameSize(rect, linear)) { return false; } - if (Vector.Count < 4) - { - // TODO: support 64bit vectors or no SIMD? - throw new PlatformNotSupportedException("XYB to RGB conversion requires at least 128-bit SIMD"); - } - - // Reuse variables instead of creating them over - // and over again - Unsafe.SkipInit(out Vector linearR); - Unsafe.SkipInit(out Vector linearG); - Unsafe.SkipInit(out Vector linearB); + // Borrow all allocations once. Rectangle origins belong only to the input; the output starts at its own origin. + // Source and destination strides can differ. Exact row slices keep every vector and scalar store out of padding. + ReadOnlySpan source0 = opsin.PlaneRow(0, 0); + ReadOnlySpan source1 = opsin.PlaneRow(1, 0); + ReadOnlySpan source2 = opsin.PlaneRow(2, 0); + Span destination0 = linear.PlaneRow(0, 0); + Span destination1 = linear.PlaneRow(1, 0); + Span destination2 = linear.PlaneRow(2, 0); + int sourceStride = opsin.PixelsPerRow; + int destinationStride = linear.PixelsPerRow; for (int y = 0; y < rect.Height; y++) { - ReadOnlySpan rowOpsin0 = opsin.PlaneRow(rect, 0, y); - ReadOnlySpan rowOpsin1 = opsin.PlaneRow(rect, 1, y); - ReadOnlySpan rowOpsin2 = opsin.PlaneRow(rect, 2, y); - - ref float rowOpsin0Reference = ref MemoryMarshal.GetReference(rowOpsin0); - ref float rowOpsin1Reference = ref MemoryMarshal.GetReference(rowOpsin1); - ref float rowOpsin2Reference = ref MemoryMarshal.GetReference(rowOpsin2); - - Span rowLinear0 = linear.PlaneRow(0, y); - Span rowLinear1 = linear.PlaneRow(1, y); - Span rowLinear2 = linear.PlaneRow(2, y); - - ref float rowLinear0Reference = ref MemoryMarshal.GetReference(rowLinear0); - ref float rowLinear1Reference = ref MemoryMarshal.GetReference(rowLinear1); - ref float rowLinear2Reference = ref MemoryMarshal.GetReference(rowLinear2); + int sourceOffset = ((rect.Top + y) * sourceStride) + rect.Left; + int destinationOffset = y * destinationStride; + ConvertXybToRgb( + source0.Slice(sourceOffset, rect.Width), + source1.Slice(sourceOffset, rect.Width), + source2.Slice(sourceOffset, rect.Width), + destination0.Slice(destinationOffset, rect.Width), + destination1.Slice(destinationOffset, rect.Width), + destination2.Slice(destinationOffset, rect.Width), + parameters); + } - for (int x = 0; x < rect.Height; x += Vector.Count) - { - Vector inOpsinX = Vector.LoadUnsafe(ref Unsafe.Add(ref rowOpsin0Reference, x)); - Vector inOpsinY = Vector.LoadUnsafe(ref Unsafe.Add(ref rowOpsin1Reference, x)); - Vector inOpsinB = Vector.LoadUnsafe(ref Unsafe.Add(ref rowOpsin2Reference, x)); + return true; + } - ConvertXybToRgb(inOpsinX, inOpsinY, inOpsinB, opsinParameters, ref linearR, ref linearG, ref linearB); + /// Applies the inverse opsin equations to one pixel in the same order as each SIMD lane. + /// The X sample. + /// The Y sample. + /// The B sample. + /// The transposed, intensity-scaled inverse matrix. + /// The negative absorbance biases. + /// The signed cube roots of the negative biases. + /// The reconstructed red sample. + /// The reconstructed green sample. + /// The reconstructed blue sample. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ConvertXybToRgb( + float x, + float y, + float b, + in Matrix4x4 matrix, + Vector3 biases, + Vector3 cubeRootBiases, + out float r, + out float g, + out float blue) + { + // Undo the opponent mix and the forward transform's cube-root offset before cubing. + float gammaR = (y + x) - cubeRootBiases.X; + float gammaG = (y - x) - cubeRootBiases.Y; + float gammaB = b - cubeRootBiases.Z; + float mixedR = float.MultiplyAddEstimate(gammaR * gammaR, gammaR, biases.X); + float mixedG = float.MultiplyAddEstimate(gammaG * gammaG, gammaG, biases.Y); + float mixedB = float.MultiplyAddEstimate(gammaB * gammaB, gammaB, biases.Z); - linearR.StoreUnsafe(ref Unsafe.Add(ref rowLinear0Reference, x)); - linearG.StoreUnsafe(ref Unsafe.Add(ref rowLinear1Reference, x)); - linearB.StoreUnsafe(ref Unsafe.Add(ref rowLinear2Reference, x)); - } - } + // Match shared Transform3's X, then Y, then Z multiplication order. Translation is zero for this inverse matrix. + r = float.MultiplyAddEstimate(mixedG, matrix.M21, mixedR * matrix.M11); + r = float.MultiplyAddEstimate(mixedB, matrix.M31, r) + matrix.M41; + g = float.MultiplyAddEstimate(mixedG, matrix.M22, mixedR * matrix.M12); + g = float.MultiplyAddEstimate(mixedB, matrix.M32, g) + matrix.M42; + blue = float.MultiplyAddEstimate(mixedG, matrix.M23, mixedR * matrix.M13); + blue = float.MultiplyAddEstimate(mixedB, matrix.M33, blue) + matrix.M43; + } - return true; + /// Reconstructs 4 independent pixels; each register holds one component in pixel order. + /// The X samples. + /// The Y samples. + /// The B samples. + /// The transposed, intensity-scaled inverse matrix. + /// The negative absorbance biases, broadcast uniformly to all lanes. + /// The signed cube roots subtracted before cubing. + /// The reconstructed red samples. + /// The reconstructed green samples. + /// The reconstructed blue samples. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ConvertXybToRgb( + Vector128 x, + Vector128 y, + Vector128 b, + in Matrix4x4 matrix, + Vector3 biases, + Vector3 cubeRootBiases, + out Vector128 r, + out Vector128 g, + out Vector128 blue) + { + Vector128 gammaR = (y + x) - Vector128.Create(cubeRootBiases.X); + Vector128 gammaG = (y - x) - Vector128.Create(cubeRootBiases.Y); + Vector128 gammaB = b - Vector128.Create(cubeRootBiases.Z); + Vector128 mixedR = Vector128.MultiplyAddEstimate(gammaR * gammaR, gammaR, Vector128.Create(biases.X)); + Vector128 mixedG = Vector128.MultiplyAddEstimate(gammaG * gammaG, gammaG, Vector128.Create(biases.Y)); + Vector128 mixedB = Vector128.MultiplyAddEstimate(gammaB * gammaB, gammaB, Vector128.Create(biases.Z)); + SimdUtils.Transform3(in matrix, mixedR, mixedG, mixedB, out r, out g, out blue); } - /// - /// A SIMD utility method which reads next 128 bits - /// (which in this case happens to be next 4 floats), - /// and duplicates them to fit in the CPU vector size. - /// For example, - /// - /// 128 bit vectors: A B C D (as-is) - /// 256 bit vectors: A B C D A B C D (duplicate once) - /// 512 bit vectors: A B C D A B C D A B C D A B C D (duplicate three times) - /// - /// Vector<T> has support for arbitrarily large - /// vector sizes. For example, some ARM CPUs support 2048-bit - /// vectors through Vector<T>. In that specific case, this - /// method can be used for future-proofing. - /// - /// Note that this method, albeit future-proof, may be considered - /// slow for smaller vector sizes (think CPUs with 128bit or 256bit vectors). - /// - /// Reference to first element to load & duplicate. - /// Vector with first 128 bits duplicated across the vector width. - private static Vector LoadDuplicate128(ref float reference) + /// Reconstructs 8 independent pixels; each register holds one component in pixel order. + /// The X samples. + /// The Y samples. + /// The B samples. + /// The transposed, intensity-scaled inverse matrix. + /// The negative absorbance biases, broadcast uniformly to all lanes. + /// The signed cube roots subtracted before cubing. + /// The reconstructed red samples. + /// The reconstructed green samples. + /// The reconstructed blue samples. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ConvertXybToRgb( + Vector256 x, + Vector256 y, + Vector256 b, + in Matrix4x4 matrix, + Vector3 biases, + Vector3 cubeRootBiases, + out Vector256 r, + out Vector256 g, + out Vector256 blue) { - Span value = stackalloc float[Vector.Count]; - Span values128 = [ - reference, - Unsafe.Add(ref reference, 1), - Unsafe.Add(ref reference, 2), - Unsafe.Add(ref reference, 3) - ]; - - for (int i = 0; i < Vector.Count; i += 4) - { - values128[i..].CopyTo(value[i..]); - } + Vector256 gammaR = (y + x) - Vector256.Create(cubeRootBiases.X); + Vector256 gammaG = (y - x) - Vector256.Create(cubeRootBiases.Y); + Vector256 gammaB = b - Vector256.Create(cubeRootBiases.Z); + Vector256 mixedR = Vector256.MultiplyAddEstimate(gammaR * gammaR, gammaR, Vector256.Create(biases.X)); + Vector256 mixedG = Vector256.MultiplyAddEstimate(gammaG * gammaG, gammaG, Vector256.Create(biases.Y)); + Vector256 mixedB = Vector256.MultiplyAddEstimate(gammaB * gammaB, gammaB, Vector256.Create(biases.Z)); + SimdUtils.Transform3(in matrix, mixedR, mixedG, mixedB, out r, out g, out blue); + } - return new(value); + /// Reconstructs 16 independent pixels; each register holds one component in pixel order. + /// The X samples. + /// The Y samples. + /// The B samples. + /// The transposed, intensity-scaled inverse matrix. + /// The negative absorbance biases, broadcast uniformly to all lanes. + /// The signed cube roots subtracted before cubing. + /// The reconstructed red samples. + /// The reconstructed green samples. + /// The reconstructed blue samples. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ConvertXybToRgb( + Vector512 x, + Vector512 y, + Vector512 b, + in Matrix4x4 matrix, + Vector3 biases, + Vector3 cubeRootBiases, + out Vector512 r, + out Vector512 g, + out Vector512 blue) + { + Vector512 gammaR = (y + x) - Vector512.Create(cubeRootBiases.X); + Vector512 gammaG = (y - x) - Vector512.Create(cubeRootBiases.Y); + Vector512 gammaB = b - Vector512.Create(cubeRootBiases.Z); + Vector512 mixedR = Vector512.MultiplyAddEstimate(gammaR * gammaR, gammaR, Vector512.Create(biases.X)); + Vector512 mixedG = Vector512.MultiplyAddEstimate(gammaG * gammaG, gammaG, Vector512.Create(biases.Y)); + Vector512 mixedB = Vector512.MultiplyAddEstimate(gammaB * gammaB, gammaB, Vector512.Create(biases.Z)); + SimdUtils.Transform3(in matrix, mixedR, mixedG, mixedB, out r, out g, out blue); } + /// Checks whether native reconstruction supports the encoding's direct fields and transfer function. + /// The requested encoding. + /// Whether its direct color fields can describe supported reconstruction output. public static bool CanOutputToColorEncoding(JxlColorEncoding colorEncoding) { if (!colorEncoding.HaveFields) diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/XybStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/XybStage.cs index 96db48e48d..0dda4385e7 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/XybStage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/XybStage.cs @@ -1,32 +1,26 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Numerics; -using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; using SixLabors.ImageSharp.Formats.Jxl.Cms; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; -/// -/// Accepts XYB as input channels. If the output is XYB, this stage -/// scales them. Otherwise, converts them to RGB. -/// +/// Converts reconstructed XYB rows to linear RGB or the scaled-XYB output representation. internal class XybStage : RenderPipelineStageBase { - /// - /// Parameters for XYB to RGB conversion. - /// + /// The inverse transform prepared before processing rows. private readonly JxlOpsinParameters opsinParameters; - /// - /// Is the output channel XYB? If so, this stage will - /// scale the input XYB channels for output. Otherwise, - /// the input XYB channels are converted to RGB. - /// + /// Whether the requested output retains XYB components instead of reconstructing RGB. private readonly bool outputIsXYB; + /// Creates the in-place row stage with the selected output transform. + /// The pipeline configuration. + /// The selected output encoding and prepared inverse transform. public XybStage(Configuration configuration, JxlOutputEncodingInfo outputEncodingInfo) : base(configuration) { @@ -39,72 +33,118 @@ public XybStage(Configuration configuration, JxlOutputEncodingInfo outputEncodin /// public override RenderPipelineChannelMode GetChannelMode(int channel) - => channel < 3 ? RenderPipelineChannelMode.InPlace - : RenderPipelineChannelMode.Ignored; + => channel < 3 ? RenderPipelineChannelMode.InPlace : RenderPipelineChannelMode.Ignored; /// - public override void ProcessRow(Buffer2D> inputRows, Buffer2D> outputRows, int xExtraLeft, int xExtraRight, int width, int xPos, int yPos) + public override void ProcessRow( + Buffer2D> inputRows, + Buffer2D> outputRows, + int xExtraLeft, + int xExtraRight, + int width, + int xPos, + int yPos) { - if (xExtraLeft == 0 || xExtraRight == 0) + if (xExtraLeft != 0 || xExtraRight != 0) { - throw new InvalidOperationException("xExtraLeft/xExtraRight must both be > 0"); + throw new InvalidOperationException("The XYB stage does not consume horizontal border samples."); } - Span row0 = this.GetInputRow(inputRows, 0, 0); - Span row1 = this.GetInputRow(inputRows, 1, 0); - Span row2 = this.GetInputRow(inputRows, 2, 0); + // This point-wise transform needs no halo. Logical slices exclude the pipeline prefix and all right-border padding. + Span row0 = this.GetInputRow(inputRows, 0, 0)[..width]; + Span row1 = this.GetInputRow(inputRows, 1, 0)[..width]; + Span row2 = this.GetInputRow(inputRows, 2, 0)[..width]; if (this.outputIsXYB) { - // If the output is preferred to be XYB, then - // we simply scale the input XYB channels. - Vector scaleX = Vector.Create(JxlOpsinConstants.ScaledXybScale[0]); - Vector scaleY = Vector.Create(JxlOpsinConstants.ScaledXybScale[1]); - Vector scaleBMY = Vector.Create(JxlOpsinConstants.ScaledXybScale[2]); - Vector offsetX = Vector.Create(JxlOpsinConstants.ScaledXybOffset[0]); - Vector offsetY = Vector.Create(JxlOpsinConstants.ScaledXybOffset[1]); - Vector offsetBMY = Vector.Create(JxlOpsinConstants.ScaledXybOffset[2]); - - // Main scaling loop - for (int x = 0; x <= width - Vector.Count; x += Vector.Count) - { - // Input XYB. - Vector inX = Vector.Create(row0[x..]); - Vector inY = Vector.Create(row1[x..]); - Vector inB = Vector.Create(row2[x..]); + ScaleXyb(row0, row1, row2); + } + else + { + JxlXybDecoder.ConvertXybToRgb(row0, row1, row2, row0, row1, row2, this.opsinParameters); + } + } - // Scaled input XYB. - Vector outX = (inX + offsetX) * scaleX; - Vector outY = (inY + offsetY) * scaleY; - Vector outB = ((inB - inY) + offsetBMY) * scaleBMY; + /// Applies native output offsets and scales to the three logical XYB rows in place. + /// The X row. + /// The corresponding Y row. + /// The corresponding B row, replaced by scaled B minus Y. + private static void ScaleXyb(Span rowX, Span rowY, Span rowB) + { + ReadOnlySpan scales = JxlOpsinConstants.ScaledXybScale; + ReadOnlySpan offsets = JxlOpsinConstants.ScaledXybOffset; + ref float firstX = ref MemoryMarshal.GetReference(rowX); + ref float firstY = ref MemoryMarshal.GetReference(rowY); + ref float firstB = ref MemoryMarshal.GetReference(rowB); + int x = 0; - // Write scaled XYB in-place. - outX.CopyTo(row0[x..]); - outY.CopyTo(row1[x..]); - outB.CopyTo(row2[x..]); + // Each lane is one pixel. B minus Y uses the original Y, so load all components before any in-place store. + // Full registers never cross the logical row end; narrower vectors and scalar pixels complete the remainder. + if (Vector512.IsHardwareAccelerated) + { + Vector512 scaleX = Vector512.Create(scales[0]); + Vector512 scaleY = Vector512.Create(scales[1]); + Vector512 scaleB = Vector512.Create(scales[2]); + Vector512 offsetX = Vector512.Create(offsets[0]); + Vector512 offsetY = Vector512.Create(offsets[1]); + Vector512 offsetB = Vector512.Create(offsets[2]); + for (; x <= rowX.Length - Vector512.Count; x += Vector512.Count) + { + Vector512 inputX = Vector512.LoadUnsafe(ref firstX, (uint)x); + Vector512 inputY = Vector512.LoadUnsafe(ref firstY, (uint)x); + Vector512 inputB = Vector512.LoadUnsafe(ref firstB, (uint)x); + ((inputX + offsetX) * scaleX).StoreUnsafe(ref firstX, (uint)x); + ((inputY + offsetY) * scaleY).StoreUnsafe(ref firstY, (uint)x); + (((inputB - inputY) + offsetB) * scaleB).StoreUnsafe(ref firstB, (uint)x); } } - else + + if (Vector256.IsHardwareAccelerated) { - // In this case, we convert XYB to RGB in-place. - for (int x = 0; x <= width - Vector.Count; x += Vector.Count) + Vector256 scaleX = Vector256.Create(scales[0]); + Vector256 scaleY = Vector256.Create(scales[1]); + Vector256 scaleB = Vector256.Create(scales[2]); + Vector256 offsetX = Vector256.Create(offsets[0]); + Vector256 offsetY = Vector256.Create(offsets[1]); + Vector256 offsetB = Vector256.Create(offsets[2]); + for (; x <= rowX.Length - Vector256.Count; x += Vector256.Count) { - // Input XYB. - Vector inOpsinX = Vector.Create(row0[x..]); - Vector inOpsinY = Vector.Create(row1[x..]); - Vector inOpsinB = Vector.Create(row2[x..]); - - // Convert XYB->RGB - Unsafe.SkipInit(out Vector r); - Unsafe.SkipInit(out Vector g); - Unsafe.SkipInit(out Vector b); - JxlXybDecoder.ConvertXybToRgb(inOpsinX, inOpsinY, inOpsinB, this.opsinParameters, ref r, ref g, ref b); + Vector256 inputX = Vector256.LoadUnsafe(ref firstX, (uint)x); + Vector256 inputY = Vector256.LoadUnsafe(ref firstY, (uint)x); + Vector256 inputB = Vector256.LoadUnsafe(ref firstB, (uint)x); + ((inputX + offsetX) * scaleX).StoreUnsafe(ref firstX, (uint)x); + ((inputY + offsetY) * scaleY).StoreUnsafe(ref firstY, (uint)x); + (((inputB - inputY) + offsetB) * scaleB).StoreUnsafe(ref firstB, (uint)x); + } + } - // Write RGB in-place. - r.CopyTo(row0[x..]); - g.CopyTo(row1[x..]); - b.CopyTo(row2[x..]); + if (Vector128.IsHardwareAccelerated) + { + Vector128 scaleX = Vector128.Create(scales[0]); + Vector128 scaleY = Vector128.Create(scales[1]); + Vector128 scaleB = Vector128.Create(scales[2]); + Vector128 offsetX = Vector128.Create(offsets[0]); + Vector128 offsetY = Vector128.Create(offsets[1]); + Vector128 offsetB = Vector128.Create(offsets[2]); + for (; x <= rowX.Length - Vector128.Count; x += Vector128.Count) + { + Vector128 inputX = Vector128.LoadUnsafe(ref firstX, (uint)x); + Vector128 inputY = Vector128.LoadUnsafe(ref firstY, (uint)x); + Vector128 inputB = Vector128.LoadUnsafe(ref firstB, (uint)x); + ((inputX + offsetX) * scaleX).StoreUnsafe(ref firstX, (uint)x); + ((inputY + offsetY) * scaleY).StoreUnsafe(ref firstY, (uint)x); + (((inputB - inputY) + offsetB) * scaleB).StoreUnsafe(ref firstB, (uint)x); } } + + for (; x < rowX.Length; x++) + { + float inputX = rowX[x]; + float inputY = rowY[x]; + float inputB = rowB[x]; + rowX[x] = (inputX + offsets[0]) * scales[0]; + rowY[x] = (inputY + offsets[1]) * scales[1]; + rowB[x] = ((inputB - inputY) + offsets[2]) * scales[2]; + } } } diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/OpsinImageTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/OpsinImageTests.cs index 935f3b825f..3dbf4b9efd 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/OpsinImageTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/OpsinImageTests.cs @@ -2,11 +2,15 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Jxl.Cms; +using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Tests.TestUtilities; namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; @@ -63,7 +67,8 @@ private static void OpsinToLinearSRgb(float xybX, float xybY, float xybB, out fl using JxlImage3 linear = new(TestEnvironment.Configuration, 1, 1); JxlOpsinParameters opsinParameters = new(); - opsinParameters.Initialize(intensityTarget: 255.0f); + JxlOpsinInverseMatrix inverse = new(); + opsinParameters.Initialize(inverse.InverseMatrix, inverse.OpsinBiases, 255F); Assert.True(JxlXybDecoder.OpsinToLinear( opsin, @@ -157,4 +162,170 @@ public void VerifyGray() Assert.Equal(JxlOpsinConstants.YToBRatio, b / y, 3e-5f); } } + + /// Checks bias subtraction, intensity scaling, rectangular origins and exact row guards at every SIMD capability. + [Fact] + public void InverseOpsin_ReconstructsDyadicSamplesWithinRectangle() + { + static void RunTest() + { + ReadOnlySpan widths = [1, 3, 17, 33]; + ReadOnlySpan intensities = [255F, 510F]; + ReadOnlySpan xyb = [0F, 0F, 0F, 0.5F, 0.5F, 1F, -0.5F, 0.5F, -1F, 0.25F, 0.25F, -0.5F]; + + // Bias -1 has cube root -1. Cubing (Y+X+1,Y-X+1,B+1), then subtracting one, gives exact dyadic LMS values. + // The selected matrix gives these RGB triples independently: black; (21,-7,35)/4; (15,55,-18)/4; (83,-45,-9)/32. + ReadOnlySpan expected = [0F, 0F, 0F, 5.25F, -1.75F, 8.75F, 3.75F, 13.75F, -4.5F, 2.59375F, -1.40625F, -0.28125F]; + const float untouched = -777F; + foreach (float intensity in intensities) + { + JxlOpsinParameters parameters = CreateDyadicOpsinParameters(intensity); + foreach (int width in widths) + { + using JxlImage3 source = new(TestEnvironment.Configuration, width + 19, 6); + using JxlImage3 destination = new(TestEnvironment.Configuration, width + 5, 5); + Assert.True(destination.ShrinkTo(width, 3)); + Rectangle rectangle = new(2, 1, width, 3); + for (int c = 0; c < 3; c++) + { + Span samples = source.PlaneRow(c, 0); + samples.Fill(untouched); + destination.PlaneRow(c, 0).Fill(untouched); + for (int y = 0; y < rectangle.Height; y++) + { + for (int x = 0; x < width; x++) + { + samples[((y + 1) * source.PixelsPerRow) + x + 2] = xyb[(((x + (2 * y)) % 4) * 3) + c]; + } + } + } + + Assert.True(JxlXybDecoder.OpsinToLinear(source, rectangle, destination, parameters)); + for (int c = 0; c < 3; c++) + { + ReadOnlySpan samples = destination.PlaneRow(c, 0); + for (int i = 0; i < samples.Length; i++) + { + int y = i / destination.PixelsPerRow; + int x = i % destination.PixelsPerRow; + float value = y < 3 && x < width + ? expected[(((x + (2 * y)) % 4) * 3) + c] * (255F / intensity) + : untouched; + + Assert.Equal(value, samples[i]); + } + + // The out-of-place conversion must leave every source sample, its row prefix and its padding unchanged. + samples = source.PlaneRow(c, 0); + for (int i = 0; i < samples.Length; i++) + { + int y = (i / source.PixelsPerRow) - 1; + int x = (i % source.PixelsPerRow) - 2; + float value = y >= 0 && y < 3 && x >= 0 && x < width + ? xyb[(((x + (2 * y)) % 4) * 3) + c] + : untouched; + + Assert.Equal(value, samples[i]); + } + } + } + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } + + /// Checks in-place RGB and scaled-XYB stage output with zero halos, short rows and unchanged extra-channel storage. + [Fact] + public void XybStage_ConvertsLogicalRowsAndPreservesPadding() + { + static void RunTest() + { + ReadOnlySpan widths = [1, 3, 17, 33]; + ReadOnlySpan xyb = [0F, 0F, 0F, 0.5F, 0.5F, 1F, -0.5F, 0.5F, -1F, 0.25F, 0.25F, -0.5F]; + ReadOnlySpan rgb = [0F, 0F, 0F, 5.25F, -1.75F, 8.75F, 3.75F, 13.75F, -4.5F, 2.59375F, -1.40625F, -0.28125F]; + const int origin = RenderPipelineStageBase.RenderPipelineXOffset; + const float untouched = -777F; + foreach (int width in widths) + { + for (int scaled = 0; scaled < 2; scaled++) + { + int storageWidth = origin + width + 3; + using JxlPlane storage = new(TestEnvironment.Configuration, storageWidth, 4); + Span samples = storage.GetRow(0); + samples.Fill(untouched); + Memory[] table = new Memory[4]; + for (int c = 0; c < table.Length; c++) + { + table[c] = storage.GetRowMemory(c)[..storageWidth]; + if (c < 3) + { + for (int x = 0; x < width; x++) + { + samples[(c * storage.PixelsPerRow) + origin + x] = xyb[((x % 4) * 3) + c]; + } + } + } + + // The row table borrows numeric storage. The stage reads no vertical or horizontal border samples and writes in place. + using Buffer2D> rows = new(MemoryGroup>.Wrap(table.AsMemory()), 4, 1); + JxlOutputEncodingInfo info = new() + { + OpsinParameters = CreateDyadicOpsinParameters(510F), + ColorEncoding = new() { ColorSpace = scaled == 0 ? JxlColorSpace.Rgb : JxlColorSpace.Xyb } + }; + + using XybStage stage = new(TestEnvironment.Configuration, info); + stage.ProcessRow(rows, rows, 0, 0, width, 0, 0); + for (int c = 0; c < 4; c++) + { + ReadOnlySpan actual = samples.Slice(c * storage.PixelsPerRow, storage.PixelsPerRow); + for (int i = 0; i < actual.Length; i++) + { + int x = i - origin; + float expected = untouched; + if (c < 3 && x >= 0 && x < width) + { + int sample = (x % 4) * 3; + if (scaled == 0) + { + expected = rgb[sample + c] * 0.5F; + } + else + { + // Native scaled output uses (X+offset)*scale, Y*scale, and (B-Y+offset)*scale in this order. + expected = c == 0 ? (xyb[sample] + 0.015386134F) * 22.995788804F + : c == 1 ? xyb[sample + 1] * 1.183000077F + : ((xyb[sample + 2] - xyb[sample + 1]) + 0.27770459F) * 1.502141333F; + } + } + + Assert.Equal(expected, actual[i]); + } + } + } + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } + + /// Prepares a nonsymmetric dyadic inverse matrix and exact cube-root biases for independent numerical checks. + /// The relative-output intensity target: 255 or 510 nits. + /// The prepared inverse transform. + private static JxlOpsinParameters CreateDyadicOpsinParameters(float intensityTarget) + { + JxlMatrix3x3F matrix = default; + ReadOnlySpan coefficients = [1F, 0.5F, -0.25F, -0.5F, 2F, 0.25F, 0.25F, -0.5F, 1F]; + coefficients.CopyTo(matrix.AsSpan()); + InlineArray3 biases = default; + biases[0] = biases[1] = biases[2] = -1F; + JxlOpsinParameters parameters = new(); + parameters.Initialize(in matrix, biases, intensityTarget); + return parameters; + } } From 7debf4c39c61470eb7c7962992904a6fd80737af Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Oct 2026 04:32:57 +1000 Subject: [PATCH 314/328] Fuse JPEG XL subsampling with shared SIMD deinterleaving --- .../Jxl/Processing/Butteraugli/Butteraugli.cs | 169 ++++++++++++++++-- .../Jxl/Processing/ButteraugliTests.cs | 122 +++++++++++++ 2 files changed, 273 insertions(+), 18 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs index 498ce266f8..23bc2cac5d 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs @@ -2906,42 +2906,175 @@ public static JxlImage3 SubSample2x(Configuration configuration, JxlImage /// The distinct preallocated image with each dimension rounded up after division by two. public static void SubSample2x(JxlImage3 input, JxlImage3 output) { + const float contribution = 0.25F; + int pairedWidth = input.XSize / 2; + int pairedHeight = input.YSize / 2; + bool hasFinalColumn = (input.XSize & 1) != 0; + bool hasFinalRow = (input.YSize & 1) != 0; + for (int c = 0; c < 3; c++) { JxlPlane inputPlane = input.Plane(c); JxlPlane outputPlane = output.Plane(c); ReadOnlySpan sourceSamples = inputPlane.GetRow(0); Span outputSamples = outputPlane.GetRow(0); + int sourceStride = inputPlane.PixelsPerRow; + int outputStride = outputPlane.PixelsPerRow; - for (int y = 0; y < output.YSize; y++) + for (int y = 0; y < pairedHeight; y++) { - outputSamples.Slice(y * outputPlane.PixelsPerRow, output.XSize).Clear(); - } + ref float upper = ref MemoryMarshal.GetReference(sourceSamples.Slice(2 * y * sourceStride, input.XSize)); + ref float lower = ref MemoryMarshal.GetReference(sourceSamples.Slice(((2 * y) + 1) * sourceStride, input.XSize)); + ref float destination = ref MemoryMarshal.GetReference(outputSamples.Slice(y * outputStride, output.XSize)); + int x = 0; - for (int y = 0; y < input.YSize; y++) - { - ReadOnlySpan sourceRow = sourceSamples[(y * inputPlane.PixelsPerRow)..]; - Span outputRow = outputSamples[((y / 2) * outputPlane.PixelsPerRow)..]; + // Two source registers contain consecutive even/odd pairs. Deinterleave2 places one source pair in each + // output lane, without changing float bits. The four quarter contributions retain native row-major order: + // upper even, upper odd, lower even, lower odd. The initial zero addition also retains native signed-zero behavior. + // Only complete pairs enter SIMD; source padding and an unmatched final column are never loaded here. + if (Vector512.IsHardwareAccelerated) + { + for (; x <= pairedWidth - Vector512.Count; x += Vector512.Count) + { + uint sourceX = (uint)(2 * x); + Vector512 upperFirst = Vector512.LoadUnsafe(ref upper, sourceX).AsInt32(); + Vector512 upperSecond = Vector512.LoadUnsafe(ref upper, sourceX + (uint)Vector512.Count).AsInt32(); + SimdUtils.Deinterleave2(upperFirst, upperSecond, out Vector512 upperEven, out Vector512 upperOdd); + Vector512 lowerFirst = Vector512.LoadUnsafe(ref lower, sourceX).AsInt32(); + Vector512 lowerSecond = Vector512.LoadUnsafe(ref lower, sourceX + (uint)Vector512.Count).AsInt32(); + SimdUtils.Deinterleave2(lowerFirst, lowerSecond, out Vector512 lowerEven, out Vector512 lowerOdd); + Vector512 result = Vector512.Zero + (contribution * upperEven.AsSingle()); + result += contribution * upperOdd.AsSingle(); + result += contribution * lowerEven.AsSingle(); + result += contribution * lowerOdd.AsSingle(); + result.StoreUnsafe(ref destination, (uint)x); + } + } - for (int x = 0; x < input.XSize; x++) + if (Vector256.IsHardwareAccelerated) { - outputRow[x / 2] += 0.25F * sourceRow[x]; + for (; x <= pairedWidth - Vector256.Count; x += Vector256.Count) + { + uint sourceX = (uint)(2 * x); + Vector256 upperFirst = Vector256.LoadUnsafe(ref upper, sourceX).AsInt32(); + Vector256 upperSecond = Vector256.LoadUnsafe(ref upper, sourceX + (uint)Vector256.Count).AsInt32(); + SimdUtils.Deinterleave2(upperFirst, upperSecond, out Vector256 upperEven, out Vector256 upperOdd); + Vector256 lowerFirst = Vector256.LoadUnsafe(ref lower, sourceX).AsInt32(); + Vector256 lowerSecond = Vector256.LoadUnsafe(ref lower, sourceX + (uint)Vector256.Count).AsInt32(); + SimdUtils.Deinterleave2(lowerFirst, lowerSecond, out Vector256 lowerEven, out Vector256 lowerOdd); + Vector256 result = Vector256.Zero + (contribution * upperEven.AsSingle()); + result += contribution * upperOdd.AsSingle(); + result += contribution * lowerEven.AsSingle(); + result += contribution * lowerOdd.AsSingle(); + result.StoreUnsafe(ref destination, (uint)x); + } } - } - // A final unmatched row or column supplies two samples rather than four. Preserve the native edge normalization. - if ((input.XSize & 1) != 0) - { - for (int y = 0; y < output.YSize; y++) + if (Vector128.IsHardwareAccelerated) + { + for (; x <= pairedWidth - Vector128.Count; x += Vector128.Count) + { + uint sourceX = (uint)(2 * x); + Vector128 upperFirst = Vector128.LoadUnsafe(ref upper, sourceX).AsInt32(); + Vector128 upperSecond = Vector128.LoadUnsafe(ref upper, sourceX + (uint)Vector128.Count).AsInt32(); + SimdUtils.Deinterleave2(upperFirst, upperSecond, out Vector128 upperEven, out Vector128 upperOdd); + Vector128 lowerFirst = Vector128.LoadUnsafe(ref lower, sourceX).AsInt32(); + Vector128 lowerSecond = Vector128.LoadUnsafe(ref lower, sourceX + (uint)Vector128.Count).AsInt32(); + SimdUtils.Deinterleave2(lowerFirst, lowerSecond, out Vector128 lowerEven, out Vector128 lowerOdd); + Vector128 result = Vector128.Zero + (contribution * upperEven.AsSingle()); + result += contribution * upperOdd.AsSingle(); + result += contribution * lowerEven.AsSingle(); + result += contribution * lowerOdd.AsSingle(); + result.StoreUnsafe(ref destination, (uint)x); + } + } + + for (; x < pairedWidth; x++) { - outputSamples[(y * outputPlane.PixelsPerRow) + output.XSize - 1] *= 2F; + int sourceX = 2 * x; + float result = 0F + (contribution * Unsafe.Add(ref upper, sourceX)); + result += contribution * Unsafe.Add(ref upper, sourceX + 1); + result += contribution * Unsafe.Add(ref lower, sourceX); + result += contribution * Unsafe.Add(ref lower, sourceX + 1); + Unsafe.Add(ref destination, x) = result; + } + + if (hasFinalColumn) + { + // An unmatched column contributes two samples instead of four. Native doubles after both additions. + float result = 0F + (contribution * Unsafe.Add(ref upper, input.XSize - 1)); + result += contribution * Unsafe.Add(ref lower, input.XSize - 1); + Unsafe.Add(ref destination, pairedWidth) = result * 2F; } } - if ((input.YSize & 1) != 0) + if (hasFinalRow) { - Span lastRow = outputSamples.Slice((output.YSize - 1) * outputPlane.PixelsPerRow, output.XSize); - TensorPrimitives.Multiply(lastRow, 2F, lastRow); + ref float upper = ref MemoryMarshal.GetReference(sourceSamples.Slice((input.YSize - 1) * sourceStride, input.XSize)); + ref float destination = ref MemoryMarshal.GetReference(outputSamples.Slice(pairedHeight * outputStride, output.XSize)); + int x = 0; + + // The unmatched row has only the two upper contributions. Keep it outside the full-row loops so their + // register loads need no missing-row branch. Native doubles this completed row after its column correction. + if (Vector512.IsHardwareAccelerated) + { + for (; x <= pairedWidth - Vector512.Count; x += Vector512.Count) + { + uint sourceX = (uint)(2 * x); + Vector512 upperFirst = Vector512.LoadUnsafe(ref upper, sourceX).AsInt32(); + Vector512 upperSecond = Vector512.LoadUnsafe(ref upper, sourceX + (uint)Vector512.Count).AsInt32(); + SimdUtils.Deinterleave2(upperFirst, upperSecond, out Vector512 upperEven, out Vector512 upperOdd); + Vector512 result = Vector512.Zero + (contribution * upperEven.AsSingle()); + result += contribution * upperOdd.AsSingle(); + result *= 2F; + result.StoreUnsafe(ref destination, (uint)x); + } + } + + if (Vector256.IsHardwareAccelerated) + { + for (; x <= pairedWidth - Vector256.Count; x += Vector256.Count) + { + uint sourceX = (uint)(2 * x); + Vector256 upperFirst = Vector256.LoadUnsafe(ref upper, sourceX).AsInt32(); + Vector256 upperSecond = Vector256.LoadUnsafe(ref upper, sourceX + (uint)Vector256.Count).AsInt32(); + SimdUtils.Deinterleave2(upperFirst, upperSecond, out Vector256 upperEven, out Vector256 upperOdd); + Vector256 result = Vector256.Zero + (contribution * upperEven.AsSingle()); + result += contribution * upperOdd.AsSingle(); + result *= 2F; + result.StoreUnsafe(ref destination, (uint)x); + } + } + + if (Vector128.IsHardwareAccelerated) + { + for (; x <= pairedWidth - Vector128.Count; x += Vector128.Count) + { + uint sourceX = (uint)(2 * x); + Vector128 upperFirst = Vector128.LoadUnsafe(ref upper, sourceX).AsInt32(); + Vector128 upperSecond = Vector128.LoadUnsafe(ref upper, sourceX + (uint)Vector128.Count).AsInt32(); + SimdUtils.Deinterleave2(upperFirst, upperSecond, out Vector128 upperEven, out Vector128 upperOdd); + Vector128 result = Vector128.Zero + (contribution * upperEven.AsSingle()); + result += contribution * upperOdd.AsSingle(); + result *= 2F; + result.StoreUnsafe(ref destination, (uint)x); + } + } + + for (; x < pairedWidth; x++) + { + int sourceX = 2 * x; + float result = 0F + (contribution * Unsafe.Add(ref upper, sourceX)); + result += contribution * Unsafe.Add(ref upper, sourceX + 1); + Unsafe.Add(ref destination, x) = result * 2F; + } + + if (hasFinalColumn) + { + float result = 0F + (contribution * Unsafe.Add(ref upper, input.XSize - 1)); + result *= 2F; + Unsafe.Add(ref destination, pairedWidth) = result * 2F; + } } } } diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs index 6593d0c10c..9681876669 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs @@ -632,4 +632,126 @@ static void RunTest() RunTest, HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); } + + /// + /// Checks native accumulation order, odd edges, input preservation and logical output bounds at every SIMD width. + /// + [Fact] + public void SubSample2x_PreservesContributionOrderAndNormalizesOddEdges() + { + static void RunTest() + { + const float padding = -123F; + ReadOnlySpan sizes = [new(1, 1), new(3, 1), new(1, 3), new(6, 4), new(34, 6), new(35, 7), new(66, 4)]; + + foreach (Size size in sizes) + { + using JxlImage3 input = new(TestEnvironment.Configuration, size.Width, size.Height); + using JxlImage3 output = new(TestEnvironment.Configuration, (size.Width + 1) / 2, (size.Height + 1) / 2); + + for (int c = 0; c < 3; c++) + { + JxlPlane inputPlane = input.Plane(c); + JxlPlane outputPlane = output.Plane(c); + Span source = inputPlane.GetRow(0); + source.Fill(padding); + outputPlane.GetRow(0).Fill(padding); + + for (int y = 0; y < size.Height; y++) + { + for (int x = 0; x < size.Width; x++) + { + // The first plane distinguishes chronological addition from a regrouped average: complete + // groups produce 3/4096, rather than the exact-arithmetic mean 1/1024. All samples stay within the HDR range. + // The third plane uses negative zero to check the initial positive-zero addition. + source[(y * inputPlane.PixelsPerRow) + x] = c == 0 + ? (((y & 1) * 2) + (x & 1)) switch + { + 0 => 32768F, + 1 => 1F / 1024F, + 2 => -32768F, + _ => 3F / 1024F + } + : c == 1 ? ((x * 3) - (y * 5) + 1) * 0.25F : BitConverter.Int32BitsToSingle(int.MinValue); + + } + } + } + + Butteraugli.SubSample2x(input, output); + + for (int c = 0; c < 3; c++) + { + JxlPlane inputPlane = input.Plane(c); + JxlPlane outputPlane = output.Plane(c); + ReadOnlySpan source = inputPlane.GetRow(0); + ReadOnlySpan destination = outputPlane.GetRow(0); + + for (int y = 0; y < output.YSize; y++) + { + for (int x = 0; x < output.XSize; x++) + { + // Use the native source-pixel visitation order as an independent scalar oracle. Each available + // source contributes one quarter before the unmatched column and row are doubled, in that order. + float expected = 0F; + for (int dy = 0; dy < 2 && ((2 * y) + dy) < size.Height; dy++) + { + for (int dx = 0; dx < 2 && ((2 * x) + dx) < size.Width; dx++) + { + expected += 0.25F * source[(((2 * y) + dy) * inputPlane.PixelsPerRow) + (2 * x) + dx]; + } + } + + if ((size.Width & 1) != 0 && x == output.XSize - 1) + { + expected *= 2F; + } + + if ((size.Height & 1) != 0 && y == output.YSize - 1) + { + expected *= 2F; + } + + Assert.Equal( + BitConverter.SingleToInt32Bits(expected), + BitConverter.SingleToInt32Bits(destination[(y * outputPlane.PixelsPerRow) + x])); + + } + + for (int x = output.XSize; x < outputPlane.PixelsPerRow; x++) + { + Assert.Equal(padding, destination[(y * outputPlane.PixelsPerRow) + x]); + } + } + + for (int y = 0; y < size.Height; y++) + { + for (int x = 0; x < size.Width; x++) + { + float expected = c == 0 + ? (((y & 1) * 2) + (x & 1)) switch + { + 0 => 32768F, + 1 => 1F / 1024F, + 2 => -32768F, + _ => 3F / 1024F + } + : c == 1 ? ((x * 3) - (y * 5) + 1) * 0.25F : BitConverter.Int32BitsToSingle(int.MinValue); + + Assert.Equal(BitConverter.SingleToInt32Bits(expected), BitConverter.SingleToInt32Bits(source[(y * inputPlane.PixelsPerRow) + x])); + } + + for (int x = size.Width; x < inputPlane.PixelsPerRow; x++) + { + Assert.Equal(padding, source[(y * inputPlane.PixelsPerRow) + x]); + } + } + } + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } } From 057aad05bc72dcaab16c0e59a7c6c72037fb8474 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Oct 2026 04:46:02 +1000 Subject: [PATCH 315/328] Share JPEG XL frequency operators and remove pre-blur image copies --- .../Jxl/Processing/Butteraugli/Butteraugli.cs | 517 ++++++++++++------ .../Jxl/Processing/Decoder/JxlXybDecoder.cs | 12 +- .../Jxl/Processing/ButteraugliTests.cs | 107 ++++ 3 files changed, 454 insertions(+), 182 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs index 23bc2cac5d..a44fcab3d4 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs @@ -413,47 +413,6 @@ private static bool Blur(JxlPlane input, ReadOnlySpan kernel, Butt return ConvolutionWithTranspose(input, kernel, transposed) && ConvolutionWithTranspose(transposed, kernel, output); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector MaximumClamp(Vector value, float maximum) - { - Vector multiplier = new(0.724216145665f); - Vector maximumValue = new(maximum); - Vector ifPositive = ((value - maximumValue) * multiplier) + maximumValue; - Vector ifNegative = ((value + maximumValue) * multiplier) - maximumValue; - Vector positiveOrValue = Vector.ConditionalSelect(Vector.GreaterThan(value, maximumValue), ifPositive, value); - Vector result = Vector.ConditionalSelect(Vector.LessThan(value, Vector.Negate(maximumValue)), ifNegative, positiveOrValue); - - return result; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector RemoveRangeAroundZero(Vector x, float width) - { - Vector w = new(width); - - return Vector.ConditionalSelect( - Vector.GreaterThan(x, w), - x - w, - Vector.ConditionalSelect( - Vector.LessThan(x, -w), - x + w, - Vector.Zero)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector AmplifyRangeAroundZero(Vector x, float width) - { - Vector w = new(width); - - return Vector.ConditionalSelect( - Vector.GreaterThan(x, w), - x + w, - Vector.ConditionalSelect( - Vector.LessThan(x, -w), - x - w, - x + x)); - } - /// /// Converts low-frequency XYB planes to the weighted values used by the comparison equations. /// @@ -637,87 +596,43 @@ public static bool SeparateLFAndMF( /// Whether all blur and frequency-separation stages succeeded. public static bool SeparateMfAndHf(Configuration configuration, JxlImage3 mf, Span> hf, ButteraugliBlurTemp blurTemp) { - int xSize = mf.XSize; - int ySize = mf.YSize; - - int lanes = Vector.Count; - - for (int i = 0; i < 3; i++) + for (int c = 0; c < 2; c++) { - if (i == 2) - { - if (!Blur(mf.Plane(i), HighFrequencyKernel, blurTemp, mf.Plane(i))) - { - return false; - } - - break; - } - - Span firstSamples = mf.Plane(i).GetRow(0); - Span secondSamples = hf[i].GetRow(0); - - for (int y = 0; y < ySize; y++) - { - Span rowMf = firstSamples[(y * mf.Plane(i).PixelsPerRow)..]; - Span rowHf = secondSamples[(y * hf[i].PixelsPerRow)..]; - - for (int x = 0; x < xSize; x += lanes) - { - new Vector(rowMf.Slice(x, lanes)) - .CopyTo(rowHf.Slice(x, lanes)); - } - } - - if (!Blur(mf.Plane(i), HighFrequencyKernel, blurTemp, mf.Plane(i))) + // Keep the original middle-frequency samples in place. This blur writes its result into the prepared HF plane + // through the existing transposed workspace; separation can then consume both inputs without a copy pass. + if (!Blur(mf.Plane(c), HighFrequencyKernel, blurTemp, hf[c])) { return false; } - if (i == 0) + if (c == 0) { - const float removeMfRange = 0.29f; - - for (int y = 0; y < ySize; y++) - { - Span rowMf = firstSamples[(y * mf.Plane(0).PixelsPerRow)..]; - Span rowHf = secondSamples[(y * hf[0].PixelsPerRow)..]; - - for (int x = 0; x < xSize; x += lanes) - { - Vector mfv = new(rowMf.Slice(x, lanes)); - Vector hfv = new Vector(rowHf.Slice(x, lanes)) - mfv; + SeparateFrequencyBands( + mf.Plane(c), + hf[c], + default(TensorPrimitives_.IdentityOperator), + new RemoveRangeAroundZeroOperator(0.29F), + default(TensorPrimitives_.IdentityOperator)); - mfv = RemoveRangeAroundZero(mfv, removeMfRange); - - mfv.CopyTo(rowMf.Slice(x, lanes)); - hfv.CopyTo(rowHf.Slice(x, lanes)); - } - } } else { - const float addMfRange = 0.1f; - - for (int y = 0; y < ySize; y++) - { - Span rowMf = firstSamples[(y * mf.Plane(1).PixelsPerRow)..]; - Span rowHf = secondSamples[(y * hf[1].PixelsPerRow)..]; - - for (int x = 0; x < xSize; x += lanes) - { - Vector mfv = new(rowMf.Slice(x, lanes)); - Vector hfv = new Vector(rowHf.Slice(x, lanes)) - mfv; + SeparateFrequencyBands( + mf.Plane(c), + hf[c], + default(TensorPrimitives_.IdentityOperator), + new AmplifyRangeAroundZeroOperator(0.1F), + default(TensorPrimitives_.IdentityOperator)); - mfv = AmplifyRangeAroundZero(mfv, addMfRange); - - mfv.CopyTo(rowMf.Slice(x, lanes)); - hfv.CopyTo(rowHf.Slice(x, lanes)); - } - } } } + // B has no separate HF plane. Its complete result remains in the middle-frequency image. + if (!Blur(mf.Plane(2), HighFrequencyKernel, blurTemp, mf.Plane(2))) + { + return false; + } + return SuppressXByY(hf[1], hf[0]); } @@ -731,93 +646,121 @@ public static bool SeparateMfAndHf(Configuration configuration, JxlImage3 /// Whether both frequency separations succeeded. public static bool SeparateHFAndUHF(Configuration configuration, Span> hf, Span> uhf, ButteraugliBlurTemp blurTemp) { - int xSize = hf[0].XSize; - int ySize = hf[0].YSize; - - int lanes = Vector.Count; - - for (int i = 0; i < 2; i++) + for (int c = 0; c < 2; c++) { - Span firstSamples = hf[i].GetRow(0); - Span secondSamples = uhf[i].GetRow(0); - - for (int y = 0; y < ySize; y++) - { - Span rowUhf = secondSamples[(y * uhf[i].PixelsPerRow)..]; - Span rowHf = firstSamples[(y * hf[i].PixelsPerRow)..]; - - for (int x = 0; x < xSize; x += lanes) - { - new Vector(rowHf.Slice(x, lanes)).CopyTo(rowUhf.Slice(x, lanes)); - } - } - - if (!Blur(hf[i], UltraHighFrequencyKernel, blurTemp, hf[i])) + if (!Blur(hf[c], UltraHighFrequencyKernel, blurTemp, uhf[c])) { return false; } - if (i == 0) + if (c == 0) { - const float removeHfRange = 1.5f; - const float removeUhfRange = 0.04f; - - for (int y = 0; y < ySize; y++) - { - Span rowUhf = secondSamples[(y * uhf[0].PixelsPerRow)..]; - Span rowHf = firstSamples[(y * hf[0].PixelsPerRow)..]; - - for (int x = 0; x < xSize; x += lanes) - { - Vector hfv = new(rowHf.Slice(x, lanes)); - Vector uhfv = new Vector(rowUhf.Slice(x, lanes)) - hfv; + SeparateFrequencyBands( + hf[c], + uhf[c], + default(TensorPrimitives_.IdentityOperator), + new RemoveRangeAroundZeroOperator(1.5F), + new RemoveRangeAroundZeroOperator(0.04F)); - hfv = RemoveRangeAroundZero(hfv, removeHfRange); - uhfv = RemoveRangeAroundZero(uhfv, removeUhfRange); - - hfv.CopyTo(rowHf.Slice(x, lanes)); - uhfv.CopyTo(rowUhf.Slice(x, lanes)); - } - } } else { - const float addHfRange = 0.132f; - const float maxClampHf = 28.4691806922f; - const float maxClampUhf = 5.19175294647f; - const float mulYHf = 2.155f; - const float mulYUhf = 2.69313763794f; - - Vector mulHf = new(mulYHf); - Vector mulUhf = new(mulYUhf); + // Native Y separation clips the blurred HF before subtracting it from the original HF. + // The retained HF is then scaled and amplified; UHF is clipped and scaled independently. + SeparateFrequencyBands( + hf[c], + uhf[c], + new MaximumClampOperator(28.4691806922F), + default(HighFrequencyYOperator), + default(UltraHighFrequencyYOperator)); - for (int y = 0; y < ySize; y++) - { - Span rowUhf = secondSamples[(y * uhf[1].PixelsPerRow)..]; - Span rowHf = firstSamples[(y * hf[1].PixelsPerRow)..]; + } + } - for (int x = 0; x < xSize; x += lanes) - { - Vector hfv = new(rowHf.Slice(x, lanes)); - hfv = MaximumClamp(hfv, maxClampHf); + return true; + } - Vector uhfv = new Vector(rowUhf.Slice(x, lanes)) - hfv; + /// + /// Separates original and blurred samples into two bands using the selected component equations. + /// + /// The operation applied to blurred samples before residual subtraction. + /// The operation applied to the subtraction base for the retained lower-frequency band. + /// The operation applied to residual samples for the higher-frequency band. + /// The original samples, replaced with the lower-frequency result. + /// The distinct blurred samples, replaced with the higher-frequency result. + /// The selected operation for the subtraction base. + /// The selected lower-frequency equation. + /// The selected higher-frequency equation. + private static void SeparateFrequencyBands( + JxlPlane original, + JxlPlane blurred, + TBeforeSubtraction beforeSubtraction, + TLowFrequency lowFrequency, + THighFrequency highFrequency) + where TBeforeSubtraction : struct, TensorPrimitives_.IUnaryOperator + where TLowFrequency : struct, TensorPrimitives_.IUnaryOperator + where THighFrequency : struct, TensorPrimitives_.IUnaryOperator + { + Span originalSamples = original.GetRow(0); + Span blurredSamples = blurred.GetRow(0); + int originalStride = original.PixelsPerRow; + int blurredStride = blurred.PixelsPerRow; + int width = original.XSize; - uhfv = MaximumClamp(uhfv, maxClampUhf); - uhfv *= mulUhf; + for (int y = 0; y < original.YSize; y++) + { + ref float originalRow = ref MemoryMarshal.GetReference(originalSamples.Slice(y * originalStride, width)); + ref float blurredRow = ref MemoryMarshal.GetReference(blurredSamples.Slice(y * blurredStride, width)); + int x = 0; - uhfv.CopyTo(rowUhf.Slice(x, lanes)); + // Corresponding lanes contain one original sample and its blurred base. Load both before either store. + // Closed operator types select each channel's equations outside traversal; no channel or stage flag enters + // these loops. Complete registers and scalar tails touch only logical samples, regardless of physical stride. + if (Vector512.IsHardwareAccelerated) + { + for (; x <= width - Vector512.Count; x += Vector512.Count) + { + Vector512 source = Vector512.LoadUnsafe(ref originalRow, (uint)x); + Vector512 baseFrequency = beforeSubtraction.Invoke(Vector512.LoadUnsafe(ref blurredRow, (uint)x)); + Vector512 residual = source - baseFrequency; + lowFrequency.Invoke(baseFrequency).StoreUnsafe(ref originalRow, (uint)x); + highFrequency.Invoke(residual).StoreUnsafe(ref blurredRow, (uint)x); + } + } - hfv *= mulHf; - hfv = AmplifyRangeAroundZero(hfv, addHfRange); + if (Vector256.IsHardwareAccelerated) + { + for (; x <= width - Vector256.Count; x += Vector256.Count) + { + Vector256 source = Vector256.LoadUnsafe(ref originalRow, (uint)x); + Vector256 baseFrequency = beforeSubtraction.Invoke(Vector256.LoadUnsafe(ref blurredRow, (uint)x)); + Vector256 residual = source - baseFrequency; + lowFrequency.Invoke(baseFrequency).StoreUnsafe(ref originalRow, (uint)x); + highFrequency.Invoke(residual).StoreUnsafe(ref blurredRow, (uint)x); + } + } - hfv.CopyTo(rowHf.Slice(x, lanes)); - } + if (Vector128.IsHardwareAccelerated) + { + for (; x <= width - Vector128.Count; x += Vector128.Count) + { + Vector128 source = Vector128.LoadUnsafe(ref originalRow, (uint)x); + Vector128 baseFrequency = beforeSubtraction.Invoke(Vector128.LoadUnsafe(ref blurredRow, (uint)x)); + Vector128 residual = source - baseFrequency; + lowFrequency.Invoke(baseFrequency).StoreUnsafe(ref originalRow, (uint)x); + highFrequency.Invoke(residual).StoreUnsafe(ref blurredRow, (uint)x); } } - } - return true; + for (; x < width; x++) + { + float source = Unsafe.Add(ref originalRow, x); + float baseFrequency = beforeSubtraction.Invoke(Unsafe.Add(ref blurredRow, x)); + float residual = source - baseFrequency; + Unsafe.Add(ref originalRow, x) = lowFrequency.Invoke(baseFrequency); + Unsafe.Add(ref blurredRow, x) = highFrequency.Invoke(residual); + } + } } /// @@ -2101,6 +2044,230 @@ public static void L2DiffAsymmetric(JxlPlane i0, JxlPlane i1, floa } } + /// + /// Removes the interval around zero from independent frequency samples. + /// + /// The nonnegative half-width of the central interval. + private readonly struct RemoveRangeAroundZeroOperator(float width) : TensorPrimitives_.IUnaryOperator + { + /// The half-width used by the native frequency equation. + private readonly float width = width; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float Invoke(float value) + => value > this.width ? value - this.width : value < -this.width ? value + this.width : 0F; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector128 Invoke(Vector128 value) + { + Vector128 width = Vector128.Create(this.width); + + return Vector128.ConditionalSelect( + Vector128.GreaterThan(value, width), + value - width, + Vector128.ConditionalSelect(Vector128.LessThan(value, -width), value + width, Vector128.Zero)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector256 Invoke(Vector256 value) + { + Vector256 width = Vector256.Create(this.width); + + return Vector256.ConditionalSelect( + Vector256.GreaterThan(value, width), + value - width, + Vector256.ConditionalSelect(Vector256.LessThan(value, -width), value + width, Vector256.Zero)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector512 Invoke(Vector512 value) + { + Vector512 width = Vector512.Create(this.width); + + return Vector512.ConditionalSelect( + Vector512.GreaterThan(value, width), + value - width, + Vector512.ConditionalSelect(Vector512.LessThan(value, -width), value + width, Vector512.Zero)); + } + } + + /// + /// Doubles samples inside the central interval and offsets samples outside it. + /// + /// The nonnegative half-width of the central interval. + private readonly struct AmplifyRangeAroundZeroOperator(float width) : TensorPrimitives_.IUnaryOperator + { + /// The half-width used by the native frequency equation. + private readonly float width = width; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float Invoke(float value) + => value > this.width ? value + this.width : value < -this.width ? value - this.width : value + value; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector128 Invoke(Vector128 value) + { + Vector128 width = Vector128.Create(this.width); + + return Vector128.ConditionalSelect( + Vector128.GreaterThan(value, width), + value + width, + Vector128.ConditionalSelect(Vector128.LessThan(value, -width), value - width, value + value)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector256 Invoke(Vector256 value) + { + Vector256 width = Vector256.Create(this.width); + + return Vector256.ConditionalSelect( + Vector256.GreaterThan(value, width), + value + width, + Vector256.ConditionalSelect(Vector256.LessThan(value, -width), value - width, value + value)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector512 Invoke(Vector512 value) + { + Vector512 width = Vector512.Create(this.width); + + return Vector512.ConditionalSelect( + Vector512.GreaterThan(value, width), + value + width, + Vector512.ConditionalSelect(Vector512.LessThan(value, -width), value - width, value + value)); + } + } + + /// + /// Reduces the slope beyond positive and negative frequency thresholds using the native multiply-add order. + /// + /// The positive magnitude at which the slope changes. + private readonly struct MaximumClampOperator(float maximum) : TensorPrimitives_.IUnaryOperator + { + /// The slope retained outside the central interval. + private const float Multiplier = 0.724216145665F; + + /// The magnitude at which the native response changes slope. + private readonly float maximum = maximum; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float Invoke(float value) + { + if (value >= this.maximum) + { + return float.MultiplyAddEstimate(value - this.maximum, Multiplier, this.maximum); + } + + if (value < -this.maximum) + { + return float.MultiplyAddEstimate(value + this.maximum, Multiplier, -this.maximum); + } + + return value; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector128 Invoke(Vector128 value) + { + Vector128 maximum = Vector128.Create(this.maximum); + Vector128 positive = Vector128.MultiplyAddEstimate(value - maximum, Vector128.Create(Multiplier), maximum); + Vector128 negative = Vector128.MultiplyAddEstimate(value + maximum, Vector128.Create(Multiplier), -maximum); + Vector128 positiveOrValue = Vector128.ConditionalSelect(Vector128.GreaterThanOrEqual(value, maximum), positive, value); + + return Vector128.ConditionalSelect(Vector128.LessThan(value, -maximum), negative, positiveOrValue); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector256 Invoke(Vector256 value) + { + Vector256 maximum = Vector256.Create(this.maximum); + Vector256 positive = Vector256.MultiplyAddEstimate(value - maximum, Vector256.Create(Multiplier), maximum); + Vector256 negative = Vector256.MultiplyAddEstimate(value + maximum, Vector256.Create(Multiplier), -maximum); + Vector256 positiveOrValue = Vector256.ConditionalSelect(Vector256.GreaterThanOrEqual(value, maximum), positive, value); + + return Vector256.ConditionalSelect(Vector256.LessThan(value, -maximum), negative, positiveOrValue); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector512 Invoke(Vector512 value) + { + Vector512 maximum = Vector512.Create(this.maximum); + Vector512 positive = Vector512.MultiplyAddEstimate(value - maximum, Vector512.Create(Multiplier), maximum); + Vector512 negative = Vector512.MultiplyAddEstimate(value + maximum, Vector512.Create(Multiplier), -maximum); + Vector512 positiveOrValue = Vector512.ConditionalSelect(Vector512.GreaterThanOrEqual(value, maximum), positive, value); + + return Vector512.ConditionalSelect(Vector512.LessThan(value, -maximum), negative, positiveOrValue); + } + } + + /// + /// Scales retained Y high frequencies before amplifying their central interval. + /// + private readonly struct HighFrequencyYOperator : TensorPrimitives_.IUnaryOperator + { + /// The native scale applied before central amplification. + private const float Multiplier = 2.155F; + + /// The central half-width after scaling. + private const float Width = 0.132F; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float Invoke(float value) => new AmplifyRangeAroundZeroOperator(Width).Invoke(value * Multiplier); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector128 Invoke(Vector128 value) => new AmplifyRangeAroundZeroOperator(Width).Invoke(value * Multiplier); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector256 Invoke(Vector256 value) => new AmplifyRangeAroundZeroOperator(Width).Invoke(value * Multiplier); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector512 Invoke(Vector512 value) => new AmplifyRangeAroundZeroOperator(Width).Invoke(value * Multiplier); + } + + /// + /// Reduces large Y ultra-high-frequency responses before applying their native scale. + /// + private readonly struct UltraHighFrequencyYOperator : TensorPrimitives_.IUnaryOperator + { + /// The magnitude where the unscaled response changes slope. + private const float Maximum = 5.19175294647F; + + /// The native scale applied after the response changes slope. + private const float Multiplier = 2.69313763794F; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float Invoke(float value) => new MaximumClampOperator(Maximum).Invoke(value) * Multiplier; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector128 Invoke(Vector128 value) => new MaximumClampOperator(Maximum).Invoke(value) * Multiplier; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector256 Invoke(Vector256 value) => new MaximumClampOperator(Maximum).Invoke(value) * Multiplier; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector512 Invoke(Vector512 value) => new MaximumClampOperator(Maximum).Invoke(value) * Multiplier; + } + /// /// Reduces X sensitivity where the same sample has a large Y response. /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs index 12160fe4d4..70d025cab7 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs @@ -198,13 +198,11 @@ private static void ConvertXybToRgb( float mixedG = float.MultiplyAddEstimate(gammaG * gammaG, gammaG, biases.Y); float mixedB = float.MultiplyAddEstimate(gammaB * gammaB, gammaB, biases.Z); - // Match shared Transform3's X, then Y, then Z multiplication order. Translation is zero for this inverse matrix. - r = float.MultiplyAddEstimate(mixedG, matrix.M21, mixedR * matrix.M11); - r = float.MultiplyAddEstimate(mixedB, matrix.M31, r) + matrix.M41; - g = float.MultiplyAddEstimate(mixedG, matrix.M22, mixedR * matrix.M12); - g = float.MultiplyAddEstimate(mixedB, matrix.M32, g) + matrix.M42; - blue = float.MultiplyAddEstimate(mixedG, matrix.M23, mixedR * matrix.M13); - blue = float.MultiplyAddEstimate(mixedB, matrix.M33, blue) + matrix.M43; + // The runtime uses the same X, then Y, then Z multiply-add order as shared Transform3. + Vector3 linear = Vector3.Transform(new Vector3(mixedR, mixedG, mixedB), matrix); + r = linear.X; + g = linear.Y; + blue = linear.Z; } /// Reconstructs 4 independent pixels; each register holds one component in pixel order. diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs index 9681876669..a5e0612821 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs @@ -754,4 +754,111 @@ static void RunTest() RunTest, HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); } + + /// + /// Checks native single-sample frequency equations and logical row bounds at every SIMD width. + /// + [Fact] + public void FrequencySeparation_PreservesSingleSampleEquationsAndPadding() + { + static void RunTest() + { + const float padding = -123F; + ReadOnlySpan widths = [1, 3, 17, 33]; + ReadOnlySpan<(float X, float Y)> samples = [(-2F, -80F), (0.125F, 0.01F), (2F, 80F)]; + + foreach (int width in widths) + { + using JxlImage3 mf = new(TestEnvironment.Configuration, width, 1); + using JxlPlane hfX = new(TestEnvironment.Configuration, width, 1); + using JxlPlane hfY = new(TestEnvironment.Configuration, width, 1); + using JxlPlane uhfX = new(TestEnvironment.Configuration, width, 1); + using JxlPlane uhfY = new(TestEnvironment.Configuration, width, 1); + using ButteraugliBlurTemp blurTemp = new(TestEnvironment.Configuration, width, 1); + InlineArray2> hf = default; + InlineArray2> uhf = default; + hf[0] = hfX; + hf[1] = hfY; + uhf[0] = uhfX; + uhf[1] = uhfY; + + for (int sampleIndex = 0; sampleIndex < (width == 1 ? samples.Length : 1); sampleIndex++) + { + // A single sample has only the Gaussian center weight, exactly one, in both passes. Its blur is + // therefore known independently of kernel construction. Wider zero samples exercise SIMD bounds only. + float inputX = width == 1 ? samples[sampleIndex].X : 0F; + float inputY = width == 1 ? samples[sampleIndex].Y : 0F; + for (int c = 0; c < 3; c++) + { + Span values = mf.Plane(c).GetRow(0); + values.Fill(padding); + values[..width].Fill(c == 0 ? inputX : c == 1 ? inputY : 0F); + } + + hfX.GetRow(0).Fill(padding); + hfY.GetRow(0).Fill(padding); + uhfX.GetRow(0).Fill(padding); + uhfY.GetRow(0).Fill(padding); + Assert.True(Butteraugli.SeparateMfAndHf(TestEnvironment.Configuration, mf, hf, blurTemp)); + float mfX = MathF.CopySign(MathF.Max(MathF.Abs(inputX) - 0.29F, 0F), inputX); + float mfY = MathF.Abs(inputY) > 0.1F ? MathF.CopySign(MathF.Abs(inputY) + 0.1F, inputY) : inputY + inputY; + CheckPlane(mf.Plane(0), mfX); + CheckPlane(mf.Plane(1), mfY); + CheckPlane(mf.Plane(2), 0F); + CheckPlane(hfX, 0F); + CheckPlane(hfY, 0F); + + hfX.GetRow(0)[..width].Fill(inputX); + hfY.GetRow(0)[..width].Fill(inputY); + Assert.True(Butteraugli.SeparateHFAndUHF(TestEnvironment.Configuration, hf, uhf, blurTemp)); + + // Native Y clips the blurred base BEFORE residual subtraction. Using the unclipped original would + // produce zero UHF here; values outside the base threshold must instead produce a nonzero residual. + const float slope = 0.724216145665F; + const float hfMaximum = 28.4691806922F; + const float uhfMaximum = 5.19175294647F; + float clippedY = inputY; + if (MathF.Abs(clippedY) > hfMaximum) + { + clippedY = MathF.CopySign(float.MultiplyAddEstimate(MathF.Abs(clippedY) - hfMaximum, slope, hfMaximum), clippedY); + } + + float residualY = inputY - clippedY; + if (MathF.Abs(residualY) > uhfMaximum) + { + residualY = MathF.CopySign(float.MultiplyAddEstimate(MathF.Abs(residualY) - uhfMaximum, slope, uhfMaximum), residualY); + } + + float scaledY = clippedY * 2.155F; + float hfResultY = MathF.Abs(scaledY) > 0.132F + ? MathF.CopySign(MathF.Abs(scaledY) + 0.132F, scaledY) + : scaledY + scaledY; + + CheckPlane(hfX, MathF.CopySign(MathF.Max(MathF.Abs(inputX) - 1.5F, 0F), inputX)); + CheckPlane(hfY, hfResultY); + CheckPlane(uhfX, 0F); + CheckPlane(uhfY, residualY * 2.69313763794F); + } + } + + // Check each logical result and the padded suffix separately. Padding belongs to the plane, not the frequency calculation. + static void CheckPlane(JxlPlane plane, float expected) + { + ReadOnlySpan values = plane.GetRow(0); + for (int x = 0; x < plane.XSize; x++) + { + Assert.Equal(expected, values[x]); + } + + for (int x = plane.XSize; x < plane.PixelsPerRow; x++) + { + Assert.Equal(-123F, values[x]); + } + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } } From bb188fb62c6bc724a2480e5aa30ad4431656d9b7 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Oct 2026 04:52:45 +1000 Subject: [PATCH 316/328] Prepare immutable JPEG XL Gaussian coefficients at native precision Verify all 73 float values against native float-sigma/double-exponential construction and independent 110-digit calculations. Preserve float normalization order. No codec tests ran because production dependencies remain incomplete. --- .../Jxl/Processing/Butteraugli/Butteraugli.cs | 71 ++++++++++++------- 1 file changed, 44 insertions(+), 27 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs index a44fcab3d4..38696d6a62 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs @@ -101,12 +101,50 @@ internal static class Butteraugli /// private const float MaskDcYMultiplier = 0.505054525019F; - // These five Gaussian kernels are fixed by the comparison equations. Build them once, outside repeated comparisons. - private static readonly float[] OpsinDynamicsKernel = ComputeKernel(1.2F); - private static readonly float[] LowFrequencyKernel = ComputeKernel(7.15593339443F); - private static readonly float[] HighFrequencyKernel = ComputeKernel(3.22489901262F); - private static readonly float[] UltraHighFrequencyKernel = ComputeKernel(1.56416327805F); - private static readonly float[] MaskingKernel = ComputeKernel(2.7F); + // Native ComputeKernel first converts sigma to float, then evaluates its scaler and exponential in double. + // These fixed coefficients retain the final rounded float bits. Border normalization and Opsin normalization + // remain separate float operations, in native order. + /// + /// Gets the five-tap Opsin Gaussian with float sigma 1.2 and a radius of two samples. + /// + private static ReadOnlySpan OpsinDynamicsKernel => [0.249352232F, 0.70664829F, 1F, 0.70664829F, 0.249352232F]; + + /// + /// Gets the 33-tap low-frequency Gaussian with float sigma 7.15593339443 and a radius of 16 samples. + /// + private static ReadOnlySpan LowFrequencyKernel => + [ + 0.0821145922F, 0.111141749F, 0.147520736F, 0.19202058F, 0.245110229F, 0.306827277F, 0.376656473F, 0.453435808F, 0.535309672F, 0.619745314F, + 0.703623533F, 0.783405066F, 0.855364621F, 0.915872633F, 0.961696029F, 0.99028331F, + 1F, + 0.99028331F, 0.961696029F, 0.915872633F, 0.855364621F, 0.783405066F, 0.703623533F, 0.619745314F, 0.535309672F, 0.453435808F, 0.376656473F, + 0.306827277F, 0.245110229F, 0.19202058F, 0.147520736F, 0.111141749F, 0.0821145922F + ]; + + /// + /// Gets the 15-tap high-frequency Gaussian with float sigma 3.22489901262 and a radius of seven samples. + /// + private static ReadOnlySpan HighFrequencyKernel => + [ + 0.0948199779F, 0.177147314F, 0.300614655F, 0.463368475F, 0.648759365F, 0.825052559F, 0.953060329F, + 1F, + 0.953060329F, 0.825052559F, 0.648759365F, 0.463368475F, 0.300614655F, 0.177147314F, 0.0948199779F + ]; + + /// + /// Gets the seven-tap ultra-high-frequency Gaussian with float sigma 1.56416327805 and a radius of three samples. + /// + private static ReadOnlySpan UltraHighFrequencyKernel => [0.158931509F, 0.441552371F, 0.815165043F, 1F, 0.815165043F, 0.441552371F, 0.158931509F]; + + /// + /// Gets the 13-tap masking Gaussian with float sigma 2.7 and a radius of six samples. + /// + private static ReadOnlySpan MaskingKernel => + [ + 0.0846579969F, 0.180021748F, 0.333739787F, 0.539407492F, 0.760067225F, 0.933712125F, + 1F, + 0.933712125F, 0.760067225F, 0.539407492F, 0.333739787F, 0.180021748F, 0.0846579969F + ]; /// /// The normalized five-tap weights used by the mirrored single-scan Opsin blur. @@ -141,27 +179,6 @@ static Butteraugli() OpsinBlurWeights = weights; } - /// - /// Builds one fixed Gaussian kernel without normalizing its border weights. - /// - /// The standard deviation fixed by the comparison stage. - /// The symmetric samples from minus to plus two-and-one-quarter standard deviations. - private static float[] ComputeKernel(float sigma) - { - const float m = 2.25f; // Accuracy increases when m is increased - float scaler = -1.0f / (2.0f * sigma * sigma); - int diff = Math.Max(1, (int)(m * MathF.Abs(sigma))); - - float[] kernel = new float[(2 * diff) + 1]; - - for (int i = -diff; i <= diff; i++) - { - kernel[i + diff] = MathF.Exp(scaler * i * i); - } - - return kernel; - } - /// /// Convolves one border column, normalizing only the Gaussian weights whose source samples exist. /// From 2d9ec26e4acba7bfeb1c3b6e4d7a2d9d434b5a21 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Oct 2026 05:00:21 +1000 Subject: [PATCH 317/328] Share JPEG XL forward opsin rows and normalize SIMD cube-root inputs Use scalar coefficient preparation and existing bounded sRGB operators. Preserve native arithmetic and reuse optional linear output without image copies. Source review and independent float arithmetic modeling completed; C# and SIMD execution remain pending the real codec dependencies. --- .../Companding/CompandingUtilities.cs | 3 +- .../Formats/Jxl/Cms/JxlOpsinConstants.cs | 8 + .../Jxl/Processing/Encoder/JxlXybEncoder.cs | 904 +++++++++++------- .../Formats/Jxl/Processing/OpsinImageTests.cs | 177 ++++ 4 files changed, 750 insertions(+), 342 deletions(-) diff --git a/src/ImageSharp/ColorProfiles/Companding/CompandingUtilities.cs b/src/ImageSharp/ColorProfiles/Companding/CompandingUtilities.cs index 9fd5ce0852..528a45e3c7 100644 --- a/src/ImageSharp/ColorProfiles/Companding/CompandingUtilities.cs +++ b/src/ImageSharp/ColorProfiles/Companding/CompandingUtilities.cs @@ -264,7 +264,8 @@ public static Vector4 Compand(Vector4 vector, float[] table) /// Evaluates a transfer curve and enforces the result contract for every companding entry point. /// /// The curve arithmetic, independent of color range and pixel layout. - private readonly struct BoundedOperator : TensorPrimitives_.IUnaryOperator + /// Configure this value once before a fused component traversal. It owns the same result limits as the companding entry points. + internal readonly struct BoundedOperator : TensorPrimitives_.IUnaryOperator where TOperator : struct, TensorPrimitives_.IUnaryOperator { private readonly TOperator operation; diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlOpsinConstants.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlOpsinConstants.cs index ef887ceab9..981c0a9456 100644 --- a/src/ImageSharp/Formats/Jxl/Cms/JxlOpsinConstants.cs +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlOpsinConstants.cs @@ -24,6 +24,14 @@ internal static class JxlOpsinConstants public static ReadOnlySpan OpsinAbsorbanceBias => [OpsinAbsorbanceBias0, OpsinAbsorbanceBias1, OpsinAbsorbanceBias2]; + /// Gets the frozen forward absorbance matrix in row-major order. + public static ReadOnlySpan OpsinAbsorbanceMatrix => + [ + 0.30F, 1F - 0.078F - 0.30F, 0.078F, + 0.23F, 1F - 0.078F - 0.23F, 0.078F, + 0.24342268924547819F, 0.20476744424496821F, 1F - 0.24342268924547819F - 0.20476744424496821F + ]; + /// Gets the output scales for X, Y and B minus Y in the native scaled-XYB representation. public static ReadOnlySpan ScaledXybScale => [22.995788804F, 1.183000077F, 1.502141333F]; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlXybEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlXybEncoder.cs index aa97f9d682..7ad16d13a7 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlXybEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlXybEncoder.cs @@ -2,382 +2,613 @@ // Licensed under the Six Labors Split License. using System.Numerics; -using System.Numerics.Tensors; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.ColorProfiles.Companding; +using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Jxl.Cms; -using SixLabors.ImageSharp.Formats.Jxl.Cms.TransferFunctions; using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; -/// -/// When the color transform is set to XYB, this class lets the encoder -/// convert input RGB color spaces to output XYB. -/// +/// Converts linear or encoded sRGB planes to the encoder's XYB samples. internal static class JxlXybEncoder { - /// - /// Computes the Opsin absorbance for the input R, G, and B colors. - /// - /// The input Red color - /// The input Green color - /// The input Blue color - /// The premultiplied absorbance. - /// The output X absorbance - /// The output Y absorbance - /// The output B absorbance - private static void OpsinAbsorbance( - Vector r, - Vector g, - Vector b, - ReadOnlySpan premulAbsorb, - out Vector mixed0, - out Vector mixed1, - out Vector mixed2) - { - ReadOnlySpan bias = JxlOpsinConstants.OpsinAbsorbanceBias; - - mixed0 = Vector.FusedMultiplyAdd( - Vector.Create(premulAbsorb), - r, - Vector.FusedMultiplyAdd( - Vector.Create(premulAbsorb[8..]), - g, - Vector.FusedMultiplyAdd( - Vector.Create(premulAbsorb[16..]), - b, - Vector.Create(bias[0])))); - - mixed1 = Vector.FusedMultiplyAdd( - Vector.Create(premulAbsorb[24..]), - r, - Vector.FusedMultiplyAdd( - Vector.Create(premulAbsorb[32..]), - g, - Vector.FusedMultiplyAdd( - Vector.Create(premulAbsorb[40..]), - b, - Vector.Create(bias[1])))); - - mixed2 = Vector.FusedMultiplyAdd( - Vector.Create(premulAbsorb[48..]), - r, - Vector.FusedMultiplyAdd( - Vector.Create(premulAbsorb[56..]), - g, - Vector.FusedMultiplyAdd( - Vector.Create(premulAbsorb[64..]), - b, - Vector.Create(bias[2])))); - } + /// The IEEE single-precision mask that removes the sign bit before root normalization. + private const uint MagnitudeMask = 0x7FFFFFFFU; - /// - /// Stores the input RGB color spaces into XYB spans. - /// - /// Input Red components - /// Input Green components - /// Input Blue components - /// Output X components - /// Output Y components - /// Output B components - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void StoreXYB( - Vector r, - Vector g, - Vector b, - Span valX, - Span valY, - Span valZ) - { - Vector half = Vector.Create(0.5f); + /// The IEEE single-precision fraction bits retained when rebuilding the normalized mantissa. + private const uint MantissaMask = 0x007FFFFFU; - (half * (r - g)).CopyTo(valX); - (half * (r + g)).CopyTo(valY); - b.CopyTo(valZ); - } + /// The IEEE bit pattern of the smallest positive normal float; smaller magnitudes need exponent normalization. + private const uint MinimumNormalBits = 0x00800000U; - /// - /// Converts RGB to XYB. The conversion happens one SIMD vector at a time. - /// - /// Input Red values - /// Input Green values - /// Input Blue values - /// Premultiplied Opsin absorbance. - /// Converted R->X values are stored here. - /// Converted G->Y values are stored here. - /// Converted B->B values are stored here. - private static void LinearRgbToXyb( - Vector r, - Vector g, - Vector b, - ReadOnlySpan premulAbsorb, - Span valX, - Span valY, - Span valZ) - { - OpsinAbsorbance( - r, - g, - b, - premulAbsorb, - out Vector mixed0, - out Vector mixed1, - out Vector mixed2); - - mixed0 = ZeroIfNegative(mixed0); - mixed1 = ZeroIfNegative(mixed1); - mixed2 = ZeroIfNegative(mixed2); - - int n = Vector.Count; - - mixed0 = CubeRootAndAdd( - mixed0, - Vector.Create(premulAbsorb[(9 * n)..])); - - mixed1 = CubeRootAndAdd( - mixed1, - Vector.Create(premulAbsorb[(10 * n)..])); - - mixed2 = CubeRootAndAdd( - mixed2, - Vector.Create(premulAbsorb[(11 * n)..])); - - StoreXYB(mixed0, mixed1, mixed2, valX, valY, valZ); - } + /// The number of fraction bits below the IEEE single-precision exponent field. + private const int MantissaBits = 23; + + /// The bias added to an IEEE single-precision normal exponent. + private const int ExponentBias = 127; + + /// The exact power-of-two shift that makes every nonzero subnormal float normal. + private const int SubnormalExponentShift = 24; + + /// The exact multiplier used before extracting a subnormal input's exponent. + private const float SubnormalScale = 1 << SubnormalExponentShift; /// - /// Performs an in-place conversion from RGB to XYB. This function - /// is meant to be invoked on each row. Parameters , - /// and initially contain - /// input RGB values. When this function returns, every , - /// and value will - /// contain the converted X, Y, and B, respectively. + /// The exponent offset divisible by three that makes all finite input exponents positive. + /// The smallest subnormal exponent is -149, so the adjusted exponent range is [1,277]. /// - /// - /// This contains input values for the R (Red) component (RGB). - /// When this method returns, each value in this span will have - /// the converted X component (XYB). - /// - /// - /// This contains input values for the G (Green) component (RGB). - /// When this method returns, each value in this span will have - /// the converted Y component (XYB). - /// - /// - /// This contains input values for the B (Blue) component (RGB). - /// When this method returns, each value in this span will have - /// the converted B component (XYB). - /// - /// - /// Contains the Opsin absorbance. - /// - /// - /// Represents the number of items within each row. - /// - public static void LinearRgbRowToXyb( - Span row0, - Span row1, - Span row2, - ReadOnlySpan premulAbsorb, - int xSize) + private const int ExponentOffset = 150; + + /// The cube-root exponent offset removed after dividing the adjusted exponent by three. + private const int ExponentQuotientOffset = ExponentOffset / 3; + + /// The fixed-point reciprocal shift used for exact division by three over the adjusted exponent range. + private const int ThirdShift = 16; + + /// Ceil(2^16/3), which gives floor(v/3) after shifting for every integer v in [1,277]. + private const int ThirdMultiplier = 21846; + + /// The native bit-pattern bias used to seed the reciprocal cube root of a normalized mantissa. + private const int ReciprocalRootSeedBias = 0x54800000; + + /// The native exponent multiplier used by the reciprocal-root seed. + private const int ReciprocalRootExponentMultiplier = 0x002AAAAA; + + /// The rounded single-precision one-third coefficient used by native root refinement. + private const float OneThird = 1F / 3F; + + /// The rounded single-precision four-thirds coefficient used by native root refinement. + private const float FourThirds = 4F / 3F; + + /// The number of native reciprocal-root iterations before its final correction. + private const int ReciprocalRootIterations = 3; + + /// Prepares nine intensity-scaled matrix entries and three negative cube-root biases. + /// The positive image intensity target, in nits. + /// The twelve scalar coefficients reused by every row and SIMD width. + private static void ComputeOpsinAbsorbanceCoefficients(float intensityTarget, Span coefficients) { - int n = Vector.Count; + ReadOnlySpan matrix = JxlOpsinConstants.OpsinAbsorbanceMatrix; + ReadOnlySpan biases = JxlOpsinConstants.OpsinAbsorbanceBias; + float scale = intensityTarget / 255F; + for (int i = 0; i < matrix.Length; i++) + { + coefficients[i] = matrix[i] * scale; + } - for (int x = 0; x < xSize; x += n) + for (int c = 0; c < 3; c++) { - Vector r = Vector.Create(row0[x..]); - Vector g = Vector.Create(row1[x..]); - Vector b = Vector.Create(row2[x..]); - - LinearRgbToXyb( - r, - g, - b, - premulAbsorb, - row0[x..], - row1[x..], - row2[x..]); + coefficients[9 + c] = -MathF.Cbrt(biases[c]); } } - private static Vector CubeRootAndAdd(Vector v, Vector add) + /// Converts source planes to XYB and optionally retains the expanded linear samples. + /// The shared identity or sRGB expansion operator selected before traversal. + /// The twelve prepared scalar coefficients. + /// The source planes, which can also be the XYB destination. + /// The equally sized XYB destination. + /// The optional equally sized destination for expanded linear RGB. + /// The input component operation. + private static void RgbToXyb( + ReadOnlySpan coefficients, + JxlImage3 source, + JxlImage3 destination, + JxlImage3? linear, + TOperator operation) + where TOperator : struct, TensorPrimitives_.IUnaryOperator { - // HACK: Vector doesn't support cuberoot - Span span = stackalloc float[Vector.Count]; - v.StoreUnsafe(ref MemoryMarshal.GetReference(span)); - TensorPrimitives.Cbrt(span, span); - return Vector.LoadUnsafe(ref MemoryMarshal.GetReference(span)) + add; + // Borrow every allocation before row processing. Independent source, XYB and retained-linear owners can have different strides. + ReadOnlySpan input0 = source.PlaneRow(0, 0); + ReadOnlySpan input1 = source.PlaneRow(1, 0); + ReadOnlySpan input2 = source.PlaneRow(2, 0); + Span output0 = destination.PlaneRow(0, 0); + Span output1 = destination.PlaneRow(1, 0); + Span output2 = destination.PlaneRow(2, 0); + Span linear0 = linear is null ? [] : linear.PlaneRow(0, 0); + Span linear1 = linear is null ? [] : linear.PlaneRow(1, 0); + Span linear2 = linear is null ? [] : linear.PlaneRow(2, 0); + int sourceStride = source.PixelsPerRow; + int destinationStride = destination.PixelsPerRow; + int linearStride = linear is null ? 0 : linear.PixelsPerRow; + int width = source.XSize; + + for (int y = 0; y < source.YSize; y++) + { + int inputOffset = y * sourceStride; + int outputOffset = y * destinationStride; + int linearOffset = y * linearStride; + RgbRowToXyb( + input0.Slice(inputOffset, width), + input1.Slice(inputOffset, width), + input2.Slice(inputOffset, width), + output0.Slice(outputOffset, width), + output1.Slice(outputOffset, width), + output2.Slice(outputOffset, width), + linear is null ? [] : linear0.Slice(linearOffset, width), + linear is null ? [] : linear1.Slice(linearOffset, width), + linear is null ? [] : linear2.Slice(linearOffset, width), + coefficients, + operation); + } } - private static Vector ZeroIfNegative(Vector v) => - Vector.ConditionalSelect( - Vector.LessThan(v, Vector.Zero), - Vector.Zero, - v); - - private static Vector LinearFromSRgb(Vector encoded) - => JxlSRgbTransferFunction.DisplayFromEncoded(encoded); - - /// - /// Converts linear sRGB planes to XYB in place. - /// - /// The broadcast absorbance matrix and cube-root bias coefficients. - /// The source planes, which receive XYB samples. - private static void LinearSrgbToXyb(ReadOnlySpan premulAbsorb, JxlImage3 image) + /// Fuses shared input expansion, optional linear output and XYB reconstruction over one logical row. + /// The shared input operation. + /// The finite normalized red samples within the pixel range. + /// The corresponding green samples. + /// The corresponding blue samples. + /// The equally sized X destination. + /// The equally sized Y destination. + /// The equally sized B destination. + /// The optional red destination, empty when no linear output is requested. + /// The matching optional green destination. + /// The matching optional blue destination. + /// The twelve scalar coefficients prepared before traversal. + /// The shared input component operation. + /// + /// All logical rows have equal lengths. RGB and XYB rows can start at the same address; offset overlap is not supported. + /// Input components use the normalized finite pixel range [-65504,65504]. Encoded input must use the shared bounded expansion operator. + /// Supply three empty linear rows when retention is disabled. Retained linear storage must be separate from XYB output storage. + /// + public static void RgbRowToXyb( + ReadOnlySpan inputR, + ReadOnlySpan inputG, + ReadOnlySpan inputB, + Span outputX, + Span outputY, + Span outputB, + Span linearR, + Span linearG, + Span linearB, + ReadOnlySpan coefficients, + TOperator operation) + where TOperator : struct, TensorPrimitives_.IUnaryOperator { - int xSize = image.XSize; + ref float sourceR = ref MemoryMarshal.GetReference(inputR); + ref float sourceG = ref MemoryMarshal.GetReference(inputG); + ref float sourceB = ref MemoryMarshal.GetReference(inputB); + ref float destinationX = ref MemoryMarshal.GetReference(outputX); + ref float destinationY = ref MemoryMarshal.GetReference(outputY); + ref float destinationB = ref MemoryMarshal.GetReference(outputB); + ref float retainedR = ref MemoryMarshal.GetReference(linearR); + ref float retainedG = ref MemoryMarshal.GetReference(linearG); + ref float retainedB = ref MemoryMarshal.GetReference(linearB); + bool retainLinear = !linearR.IsEmpty; + int x = 0; + + // Each register holds one component from consecutive pixels. The selected operation owns input expansion and its result safety. + // Load RGB before storing either output. Every complete SIMD block and scalar remainder uses the same coefficient layout. + if (Vector512.IsHardwareAccelerated) + { + for (; x <= inputR.Length - Vector512.Count; x += Vector512.Count) + { + Vector512 r = operation.Invoke(Vector512.LoadUnsafe(ref sourceR, (uint)x)); + Vector512 g = operation.Invoke(Vector512.LoadUnsafe(ref sourceG, (uint)x)); + Vector512 b = operation.Invoke(Vector512.LoadUnsafe(ref sourceB, (uint)x)); + if (retainLinear) + { + r.StoreUnsafe(ref retainedR, (uint)x); + g.StoreUnsafe(ref retainedG, (uint)x); + b.StoreUnsafe(ref retainedB, (uint)x); + } - void ProcessRow(int y, ReadOnlySpan premulAbsorb) + LinearRgbToXyb(r, g, b, coefficients, out Vector512 valueX, out Vector512 valueY, out Vector512 valueB); + valueX.StoreUnsafe(ref destinationX, (uint)x); + valueY.StoreUnsafe(ref destinationY, (uint)x); + valueB.StoreUnsafe(ref destinationB, (uint)x); + } + } + + if (Vector256.IsHardwareAccelerated) { - Span row0 = image.PlaneRow(0, y); - Span row1 = image.PlaneRow(1, y); - Span row2 = image.PlaneRow(2, y); + for (; x <= inputR.Length - Vector256.Count; x += Vector256.Count) + { + Vector256 r = operation.Invoke(Vector256.LoadUnsafe(ref sourceR, (uint)x)); + Vector256 g = operation.Invoke(Vector256.LoadUnsafe(ref sourceG, (uint)x)); + Vector256 b = operation.Invoke(Vector256.LoadUnsafe(ref sourceB, (uint)x)); + if (retainLinear) + { + r.StoreUnsafe(ref retainedR, (uint)x); + g.StoreUnsafe(ref retainedG, (uint)x); + b.StoreUnsafe(ref retainedB, (uint)x); + } - int lanes = Vector.Count; + LinearRgbToXyb(r, g, b, coefficients, out Vector256 valueX, out Vector256 valueY, out Vector256 valueB); + valueX.StoreUnsafe(ref destinationX, (uint)x); + valueY.StoreUnsafe(ref destinationY, (uint)x); + valueB.StoreUnsafe(ref destinationB, (uint)x); + } + } - for (int x = 0; x < xSize; x += lanes) + if (Vector128.IsHardwareAccelerated) + { + for (; x <= inputR.Length - Vector128.Count; x += Vector128.Count) { - Vector r = new(row0[x..]); - Vector g = new(row1[x..]); - Vector b = new(row2[x..]); - - LinearRgbToXyb( - r, - g, - b, - premulAbsorb, - row0[x..], - row1[x..], - row2[x..]); + Vector128 r = operation.Invoke(Vector128.LoadUnsafe(ref sourceR, (uint)x)); + Vector128 g = operation.Invoke(Vector128.LoadUnsafe(ref sourceG, (uint)x)); + Vector128 b = operation.Invoke(Vector128.LoadUnsafe(ref sourceB, (uint)x)); + if (retainLinear) + { + r.StoreUnsafe(ref retainedR, (uint)x); + g.StoreUnsafe(ref retainedG, (uint)x); + b.StoreUnsafe(ref retainedB, (uint)x); + } + + LinearRgbToXyb(r, g, b, coefficients, out Vector128 valueX, out Vector128 valueY, out Vector128 valueB); + valueX.StoreUnsafe(ref destinationX, (uint)x); + valueY.StoreUnsafe(ref destinationY, (uint)x); + valueB.StoreUnsafe(ref destinationB, (uint)x); } } - for (int y = 0; y < image.YSize; y++) + for (; x < inputR.Length; x++) { - ProcessRow(y, premulAbsorb); + float r = operation.Invoke(inputR[x]); + float g = operation.Invoke(inputG[x]); + float b = operation.Invoke(inputB[x]); + if (retainLinear) + { + linearR[x] = r; + linearG[x] = g; + linearB[x] = b; + } + + LinearRgbToXyb(r, g, b, coefficients, out float valueX, out float valueY, out float valueB); + outputX[x] = valueX; + outputY[x] = valueY; + outputB[x] = valueB; } } - /// - /// Converts encoded sRGB planes to XYB in place. - /// - /// The broadcast absorbance matrix and cube-root bias coefficients. - /// The source planes, which receive XYB samples. - private static void SRgbToXyb(ReadOnlySpan premulAbsorb, JxlImage3 image) + /// Applies the biased absorbance transform and opponent mix to one linear RGB sample. + /// The finite red component in the pixel range. + /// The corresponding green component. + /// The corresponding blue component. + /// The twelve prepared scalar coefficients. + /// The X result. + /// The Y result. + /// The B result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void LinearRgbToXyb(float r, float g, float b, ReadOnlySpan coefficients, out float x, out float y, out float blue) { - int xSize = image.XSize; + ReadOnlySpan biases = JxlOpsinConstants.OpsinAbsorbanceBias; + + // Preserve native B+bias, then G, then R multiply-add order. Moving bias outside that chain changes rounding near black. + float mixed0 = float.MultiplyAddEstimate(coefficients[2], b, biases[0]); + mixed0 = float.MultiplyAddEstimate(coefficients[1], g, mixed0); + mixed0 = float.MultiplyAddEstimate(coefficients[0], r, mixed0); + float mixed1 = float.MultiplyAddEstimate(coefficients[5], b, biases[1]); + mixed1 = float.MultiplyAddEstimate(coefficients[4], g, mixed1); + mixed1 = float.MultiplyAddEstimate(coefficients[3], r, mixed1); + float mixed2 = float.MultiplyAddEstimate(coefficients[8], b, biases[2]); + mixed2 = float.MultiplyAddEstimate(coefficients[7], g, mixed2); + mixed2 = float.MultiplyAddEstimate(coefficients[6], r, mixed2); + mixed0 = CubeRootAndAdd(MathF.Max(0F, mixed0), coefficients[9]); + mixed1 = CubeRootAndAdd(MathF.Max(0F, mixed1), coefficients[10]); + mixed2 = CubeRootAndAdd(MathF.Max(0F, mixed2), coefficients[11]); + x = 0.5F * (mixed0 - mixed1); + y = 0.5F * (mixed0 + mixed1); + blue = mixed2; + } - void ProcessRow(int y, ReadOnlySpan premulAbsorb) - { - Span row0 = image.PlaneRow(0, y); - Span row1 = image.PlaneRow(1, y); - Span row2 = image.PlaneRow(2, y); + /// Transforms 4 independent RGB samples with the same scalar coefficients and native evaluation order. + /// The linear red lanes. + /// The linear green lanes. + /// The linear blue lanes. + /// The twelve prepared scalar coefficients. + /// The X lanes. + /// The Y lanes. + /// The B lanes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void LinearRgbToXyb( + Vector128 r, + Vector128 g, + Vector128 b, + ReadOnlySpan coefficients, + out Vector128 x, + out Vector128 y, + out Vector128 blue) + { + ReadOnlySpan biases = JxlOpsinConstants.OpsinAbsorbanceBias; + Vector128 mixed0 = Vector128.MultiplyAddEstimate(Vector128.Create(coefficients[2]), b, Vector128.Create(biases[0])); + mixed0 = Vector128.MultiplyAddEstimate(Vector128.Create(coefficients[1]), g, mixed0); + mixed0 = Vector128.MultiplyAddEstimate(Vector128.Create(coefficients[0]), r, mixed0); + + Vector128 mixed1 = Vector128.MultiplyAddEstimate(Vector128.Create(coefficients[5]), b, Vector128.Create(biases[1])); + mixed1 = Vector128.MultiplyAddEstimate(Vector128.Create(coefficients[4]), g, mixed1); + mixed1 = Vector128.MultiplyAddEstimate(Vector128.Create(coefficients[3]), r, mixed1); + + Vector128 mixed2 = Vector128.MultiplyAddEstimate(Vector128.Create(coefficients[8]), b, Vector128.Create(biases[2])); + mixed2 = Vector128.MultiplyAddEstimate(Vector128.Create(coefficients[7]), g, mixed2); + mixed2 = Vector128.MultiplyAddEstimate(Vector128.Create(coefficients[6]), r, mixed2); + + mixed0 = CubeRootAndAdd(Vector128.Max(Vector128.Zero, mixed0), Vector128.Create(coefficients[9])); + mixed1 = CubeRootAndAdd(Vector128.Max(Vector128.Zero, mixed1), Vector128.Create(coefficients[10])); + mixed2 = CubeRootAndAdd(Vector128.Max(Vector128.Zero, mixed2), Vector128.Create(coefficients[11])); + Vector128 half = Vector128.Create(0.5F); + x = half * (mixed0 - mixed1); + y = half * (mixed0 + mixed1); + blue = mixed2; + } - int lanes = Vector.Count; + /// Transforms 8 independent RGB samples with the same scalar coefficients and native evaluation order. + /// The linear red lanes. + /// The linear green lanes. + /// The linear blue lanes. + /// The twelve prepared scalar coefficients. + /// The X lanes. + /// The Y lanes. + /// The B lanes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void LinearRgbToXyb( + Vector256 r, + Vector256 g, + Vector256 b, + ReadOnlySpan coefficients, + out Vector256 x, + out Vector256 y, + out Vector256 blue) + { + ReadOnlySpan biases = JxlOpsinConstants.OpsinAbsorbanceBias; + Vector256 mixed0 = Vector256.MultiplyAddEstimate(Vector256.Create(coefficients[2]), b, Vector256.Create(biases[0])); + mixed0 = Vector256.MultiplyAddEstimate(Vector256.Create(coefficients[1]), g, mixed0); + mixed0 = Vector256.MultiplyAddEstimate(Vector256.Create(coefficients[0]), r, mixed0); + + Vector256 mixed1 = Vector256.MultiplyAddEstimate(Vector256.Create(coefficients[5]), b, Vector256.Create(biases[1])); + mixed1 = Vector256.MultiplyAddEstimate(Vector256.Create(coefficients[4]), g, mixed1); + mixed1 = Vector256.MultiplyAddEstimate(Vector256.Create(coefficients[3]), r, mixed1); + + Vector256 mixed2 = Vector256.MultiplyAddEstimate(Vector256.Create(coefficients[8]), b, Vector256.Create(biases[2])); + mixed2 = Vector256.MultiplyAddEstimate(Vector256.Create(coefficients[7]), g, mixed2); + mixed2 = Vector256.MultiplyAddEstimate(Vector256.Create(coefficients[6]), r, mixed2); + + mixed0 = CubeRootAndAdd(Vector256.Max(Vector256.Zero, mixed0), Vector256.Create(coefficients[9])); + mixed1 = CubeRootAndAdd(Vector256.Max(Vector256.Zero, mixed1), Vector256.Create(coefficients[10])); + mixed2 = CubeRootAndAdd(Vector256.Max(Vector256.Zero, mixed2), Vector256.Create(coefficients[11])); + Vector256 half = Vector256.Create(0.5F); + x = half * (mixed0 - mixed1); + y = half * (mixed0 + mixed1); + blue = mixed2; + } - for (int x = 0; x < xSize; x += lanes) - { - Vector r = LinearFromSRgb(new Vector(row0[x..])); - Vector g = LinearFromSRgb(new Vector(row1[x..])); - Vector b = LinearFromSRgb(new Vector(row2[x..])); - - LinearRgbToXyb( - r, - g, - b, - premulAbsorb, - row0[x..], - row1[x..], - row2[x..]); - } - } + /// Transforms 16 independent RGB samples with the same scalar coefficients and native evaluation order. + /// The linear red lanes. + /// The linear green lanes. + /// The linear blue lanes. + /// The twelve prepared scalar coefficients. + /// The X lanes. + /// The Y lanes. + /// The B lanes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void LinearRgbToXyb( + Vector512 r, + Vector512 g, + Vector512 b, + ReadOnlySpan coefficients, + out Vector512 x, + out Vector512 y, + out Vector512 blue) + { + ReadOnlySpan biases = JxlOpsinConstants.OpsinAbsorbanceBias; + Vector512 mixed0 = Vector512.MultiplyAddEstimate(Vector512.Create(coefficients[2]), b, Vector512.Create(biases[0])); + mixed0 = Vector512.MultiplyAddEstimate(Vector512.Create(coefficients[1]), g, mixed0); + mixed0 = Vector512.MultiplyAddEstimate(Vector512.Create(coefficients[0]), r, mixed0); + + Vector512 mixed1 = Vector512.MultiplyAddEstimate(Vector512.Create(coefficients[5]), b, Vector512.Create(biases[1])); + mixed1 = Vector512.MultiplyAddEstimate(Vector512.Create(coefficients[4]), g, mixed1); + mixed1 = Vector512.MultiplyAddEstimate(Vector512.Create(coefficients[3]), r, mixed1); + + Vector512 mixed2 = Vector512.MultiplyAddEstimate(Vector512.Create(coefficients[8]), b, Vector512.Create(biases[2])); + mixed2 = Vector512.MultiplyAddEstimate(Vector512.Create(coefficients[7]), g, mixed2); + mixed2 = Vector512.MultiplyAddEstimate(Vector512.Create(coefficients[6]), r, mixed2); + + mixed0 = CubeRootAndAdd(Vector512.Max(Vector512.Zero, mixed0), Vector512.Create(coefficients[9])); + mixed1 = CubeRootAndAdd(Vector512.Max(Vector512.Zero, mixed1), Vector512.Create(coefficients[10])); + mixed2 = CubeRootAndAdd(Vector512.Max(Vector512.Zero, mixed2), Vector512.Create(coefficients[11])); + Vector512 half = Vector512.Create(0.5F); + x = half * (mixed0 - mixed1); + y = half * (mixed0 + mixed1); + blue = mixed2; + } - for (int y = 0; y < image.YSize; y++) + /// Computes a nonnegative finite cube root plus its bias without overflowing reciprocal-root intermediates. + /// The nonnegative finite absorbance, including zero and subnormal values. + /// The negative cube-root bias. + /// The biased root with the final addition performed as a multiply-add estimate. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float CubeRootAndAdd(float value, float add) + { + uint bits = BitConverter.SingleToUInt32Bits(value) & MagnitudeMask; + bool subnormal = bits < MinimumNormalBits; + float normalized = BitConverter.UInt32BitsToSingle(bits) * (subnormal ? SubnormalScale : 1F); + uint normalizedBits = BitConverter.SingleToUInt32Bits(normalized); + + // Standard subnormal preparation multiplies by 2^24, then corrects the extracted exponent. + // Zero has no exponent: substitute exponent zero before division and use final scale zero. + int exponent = bits == 0 ? 0 : (int)(normalizedBits >> MantissaBits) - ExponentBias - (subnormal ? SubnormalExponentShift : 0); + int positiveExponent = exponent + ExponentOffset; + + // Adding 150 keeps the exponent in [1,277] and preserves its remainder modulo three. + // Multiplication by ceil(2^16/3), then shifting 16 bits, gives the exact integer quotient throughout that interval. + int quotient = (positiveExponent * ThirdMultiplier) >> ThirdShift; + int remainder = positiveExponent - (3 * quotient); + float z = BitConverter.Int32BitsToSingle((int)(normalizedBits & MantissaMask) | ((ExponentBias + remainder) << MantissaBits)); + float scale = bits == 0 ? 0F : BitConverter.Int32BitsToSingle((quotient + ExponentBias - ExponentQuotientOffset) << MantissaBits); + float reciprocalRoot = BitConverter.Int32BitsToSingle(ReciprocalRootSeedBias - ((ExponentBias + remainder) * ReciprocalRootExponentMultiplier)); + float thirdZ = OneThird * z; + + // x=z*2^(3k), where 1<=z<8 and k=quotient-ExponentQuotientOffset. Reciprocal roots and their fourth powers stay in a normal, bounded range. + // Native reciprocal-root refinements now see only that range. They never see tiny normals or subnormal absorbance directly. + for (int i = 0; i < ReciprocalRootIterations; i++) { - ProcessRow(y, premulAbsorb); + float squared = reciprocalRoot * reciprocalRoot; + reciprocalRoot = float.MultiplyAddEstimate(-thirdZ, squared * squared, FourThirds * reciprocalRoot); } + + float squaredRoot = reciprocalRoot * reciprocalRoot; + reciprocalRoot = float.MultiplyAddEstimate(OneThird, float.MultiplyAddEstimate(-z, squaredRoot * squaredRoot, reciprocalRoot), reciprocalRoot); + squaredRoot = reciprocalRoot * reciprocalRoot; + + // Scaling a normal coefficient by 2^k is exact. Keep it inside the last multiply-add so bias cancellation gets no extra rounded root. + return float.MultiplyAddEstimate(squaredRoot * scale, z, add); } - /// - /// Converts encoded sRGB planes to XYB and retains the linear sRGB samples. - /// - /// The broadcast absorbance matrix and cube-root bias coefficients. - /// The source planes, which receive XYB samples. - /// The caller-owned planes that receive linear sRGB samples. - private static void SrgbToXybAndLinear(ReadOnlySpan premulAbsorb, JxlImage3 image, JxlImage3 linear) + /// Evaluates bounded reciprocal-root refinements independently in each nonnegative finite absorbance lane. + /// The absorbance lanes, including zero and subnormal values. + /// The corresponding negative cube-root biases. + /// The biased roots, with no scalar lane calls or temporary sample storage. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 CubeRootAndAdd(Vector128 value, Vector128 add) { - int xSize = image.XSize; - - void ProcessRow(int y, ReadOnlySpan premulAbsorb) + Vector128 bits = value.AsUInt32() & Vector128.Create(MagnitudeMask); + Vector128 zero = Vector128.Equals(bits, Vector128.Zero); + Vector128 subnormal = Vector128.LessThan(bits, Vector128.Create(MinimumNormalBits)); + Vector128 normalization = Vector128.ConditionalSelect(subnormal.AsSingle(), Vector128.Create(SubnormalScale), Vector128.One); + Vector128 normalizedBits = (bits.AsSingle() * normalization).AsUInt32(); + Vector128 exponent = Vector128.ShiftRightLogical(normalizedBits, MantissaBits).AsInt32() - Vector128.Create(ExponentBias); + exponent -= Vector128.ConditionalSelect(subnormal.AsInt32(), Vector128.Create(SubnormalExponentShift), Vector128.Zero); + exponent = Vector128.ConditionalSelect(zero.AsInt32(), Vector128.Zero, exponent); + Vector128 positiveExponent = exponent + Vector128.Create(ExponentOffset); + + // For positiveExponent in [1,277], floor(v/3)=(v*21846)>>16. 21846=ceil(2^16/3). + // The reciprocal error is below 1/3, so it cannot cross the next quotient. All products fit Int32 and remain SIMD. + Vector128 quotient = Vector128.ShiftRightArithmetic(positiveExponent * Vector128.Create(ThirdMultiplier), ThirdShift); + Vector128 remainder = positiveExponent - (quotient * Vector128.Create(3)); + Vector128 normalExponent = remainder + Vector128.Create(ExponentBias); + Vector128 mantissa = normalizedBits & Vector128.Create(MantissaMask); + Vector128 z = (mantissa | Vector128.ShiftLeft(normalExponent, MantissaBits).AsUInt32()).AsSingle(); + Vector128 scale = Vector128.ShiftLeft(quotient + Vector128.Create(ExponentBias - ExponentQuotientOffset), MantissaBits).AsSingle(); + scale = Vector128.ConditionalSelect(zero.AsSingle(), Vector128.Zero, scale); + Vector128 seed = Vector128.Create(ReciprocalRootSeedBias) - (normalExponent * Vector128.Create(ReciprocalRootExponentMultiplier)); + Vector128 reciprocalRoot = seed.AsSingle(); + Vector128 thirdZ = Vector128.Create(OneThird) * z; + + // Every z is in [1,8), including zero lanes whose final scale is zero. The iteration matches the scalar arithmetic order. + for (int i = 0; i < ReciprocalRootIterations; i++) { - Span rowImage0 = image.PlaneRow(0, y); - Span rowImage1 = image.PlaneRow(1, y); - Span rowImage2 = image.PlaneRow(2, y); - - Span rowLinear0 = linear.PlaneRow(0, y); - Span rowLinear1 = linear.PlaneRow(1, y); - Span rowLinear2 = linear.PlaneRow(2, y); + Vector128 squared = reciprocalRoot * reciprocalRoot; + reciprocalRoot = Vector128.MultiplyAddEstimate(-thirdZ, squared * squared, Vector128.Create(FourThirds) * reciprocalRoot); + } - int lanes = Vector.Count; + Vector128 squaredRoot = reciprocalRoot * reciprocalRoot; + reciprocalRoot = Vector128.MultiplyAddEstimate( + Vector128.Create(OneThird), + Vector128.MultiplyAddEstimate(-z, squaredRoot * squaredRoot, reciprocalRoot), + reciprocalRoot); - for (int x = 0; x < xSize; x += lanes) - { - Vector r = LinearFromSRgb(new Vector(rowImage0[x..])); - Vector g = LinearFromSRgb(new Vector(rowImage1[x..])); - Vector b = LinearFromSRgb(new Vector(rowImage2[x..])); - - r.CopyTo(rowLinear0[x..]); - g.CopyTo(rowLinear1[x..]); - b.CopyTo(rowLinear2[x..]); - - LinearRgbToXyb( - r, - g, - b, - premulAbsorb, - rowImage0[x..], - rowImage1[x..], - rowImage2[x..]); - } - } + squaredRoot = reciprocalRoot * reciprocalRoot; + return Vector128.MultiplyAddEstimate(squaredRoot * scale, z, add); + } - for (int y = 0; y < image.YSize; y++) + /// Evaluates bounded reciprocal-root refinements independently in each nonnegative finite absorbance lane. + /// The absorbance lanes, including zero and subnormal values. + /// The corresponding negative cube-root biases. + /// The biased roots, with no scalar lane calls or temporary sample storage. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 CubeRootAndAdd(Vector256 value, Vector256 add) + { + Vector256 bits = value.AsUInt32() & Vector256.Create(MagnitudeMask); + Vector256 zero = Vector256.Equals(bits, Vector256.Zero); + Vector256 subnormal = Vector256.LessThan(bits, Vector256.Create(MinimumNormalBits)); + Vector256 normalization = Vector256.ConditionalSelect(subnormal.AsSingle(), Vector256.Create(SubnormalScale), Vector256.One); + Vector256 normalizedBits = (bits.AsSingle() * normalization).AsUInt32(); + Vector256 exponent = Vector256.ShiftRightLogical(normalizedBits, MantissaBits).AsInt32() - Vector256.Create(ExponentBias); + exponent -= Vector256.ConditionalSelect(subnormal.AsInt32(), Vector256.Create(SubnormalExponentShift), Vector256.Zero); + exponent = Vector256.ConditionalSelect(zero.AsInt32(), Vector256.Zero, exponent); + Vector256 positiveExponent = exponent + Vector256.Create(ExponentOffset); + + // For positiveExponent in [1,277], floor(v/3)=(v*21846)>>16. 21846=ceil(2^16/3). + // The reciprocal error is below 1/3, so it cannot cross the next quotient. All products fit Int32 and remain SIMD. + Vector256 quotient = Vector256.ShiftRightArithmetic(positiveExponent * Vector256.Create(ThirdMultiplier), ThirdShift); + Vector256 remainder = positiveExponent - (quotient * Vector256.Create(3)); + Vector256 normalExponent = remainder + Vector256.Create(ExponentBias); + Vector256 mantissa = normalizedBits & Vector256.Create(MantissaMask); + Vector256 z = (mantissa | Vector256.ShiftLeft(normalExponent, MantissaBits).AsUInt32()).AsSingle(); + Vector256 scale = Vector256.ShiftLeft(quotient + Vector256.Create(ExponentBias - ExponentQuotientOffset), MantissaBits).AsSingle(); + scale = Vector256.ConditionalSelect(zero.AsSingle(), Vector256.Zero, scale); + Vector256 seed = Vector256.Create(ReciprocalRootSeedBias) - (normalExponent * Vector256.Create(ReciprocalRootExponentMultiplier)); + Vector256 reciprocalRoot = seed.AsSingle(); + Vector256 thirdZ = Vector256.Create(OneThird) * z; + + // Every z is in [1,8), including zero lanes whose final scale is zero. The iteration matches the scalar arithmetic order. + for (int i = 0; i < ReciprocalRootIterations; i++) { - ProcessRow(y, premulAbsorb); + Vector256 squared = reciprocalRoot * reciprocalRoot; + reciprocalRoot = Vector256.MultiplyAddEstimate(-thirdZ, squared * squared, Vector256.Create(FourThirds) * reciprocalRoot); } + + Vector256 squaredRoot = reciprocalRoot * reciprocalRoot; + reciprocalRoot = Vector256.MultiplyAddEstimate( + Vector256.Create(OneThird), + Vector256.MultiplyAddEstimate(-z, squaredRoot * squaredRoot, reciprocalRoot), + reciprocalRoot); + + squaredRoot = reciprocalRoot * reciprocalRoot; + return Vector256.MultiplyAddEstimate(squaredRoot * scale, z, add); } - private static void ComputePremulAbsorb(float intensityTarget, Span premulAbsorb) + /// Evaluates bounded reciprocal-root refinements independently in each nonnegative finite absorbance lane. + /// The absorbance lanes, including zero and subnormal values. + /// The corresponding negative cube-root biases. + /// The biased roots, with no scalar lane calls or temporary sample storage. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 CubeRootAndAdd(Vector512 value, Vector512 add) { - float mul = intensityTarget / 255.0f; - int lanes = Vector.Count; - - for (int j = 0; j < 3; j++) + Vector512 bits = value.AsUInt32() & Vector512.Create(MagnitudeMask); + Vector512 zero = Vector512.Equals(bits, Vector512.Zero); + Vector512 subnormal = Vector512.LessThan(bits, Vector512.Create(MinimumNormalBits)); + Vector512 normalization = Vector512.ConditionalSelect(subnormal.AsSingle(), Vector512.Create(SubnormalScale), Vector512.One); + Vector512 normalizedBits = (bits.AsSingle() * normalization).AsUInt32(); + Vector512 exponent = Vector512.ShiftRightLogical(normalizedBits, MantissaBits).AsInt32() - Vector512.Create(ExponentBias); + exponent -= Vector512.ConditionalSelect(subnormal.AsInt32(), Vector512.Create(SubnormalExponentShift), Vector512.Zero); + exponent = Vector512.ConditionalSelect(zero.AsInt32(), Vector512.Zero, exponent); + Vector512 positiveExponent = exponent + Vector512.Create(ExponentOffset); + + // For positiveExponent in [1,277], floor(v/3)=(v*21846)>>16. 21846=ceil(2^16/3). + // The reciprocal error is below 1/3, so it cannot cross the next quotient. All products fit Int32 and remain SIMD. + Vector512 quotient = Vector512.ShiftRightArithmetic(positiveExponent * Vector512.Create(ThirdMultiplier), ThirdShift); + Vector512 remainder = positiveExponent - (quotient * Vector512.Create(3)); + Vector512 normalExponent = remainder + Vector512.Create(ExponentBias); + Vector512 mantissa = normalizedBits & Vector512.Create(MantissaMask); + Vector512 z = (mantissa | Vector512.ShiftLeft(normalExponent, MantissaBits).AsUInt32()).AsSingle(); + Vector512 scale = Vector512.ShiftLeft(quotient + Vector512.Create(ExponentBias - ExponentQuotientOffset), MantissaBits).AsSingle(); + scale = Vector512.ConditionalSelect(zero.AsSingle(), Vector512.Zero, scale); + Vector512 seed = Vector512.Create(ReciprocalRootSeedBias) - (normalExponent * Vector512.Create(ReciprocalRootExponentMultiplier)); + Vector512 reciprocalRoot = seed.AsSingle(); + Vector512 thirdZ = Vector512.Create(OneThird) * z; + + // Every z is in [1,8), including zero lanes whose final scale is zero. The iteration matches the scalar arithmetic order. + for (int i = 0; i < ReciprocalRootIterations; i++) { - for (int i = 0; i < 3; i++) - { - float absorb = JxlOpsinAbsorbanceMatrix[j][i] * mul; - - premulAbsorb - .Slice(((j * 3) + i) * lanes, lanes) - .Fill(absorb); - } + Vector512 squared = reciprocalRoot * reciprocalRoot; + reciprocalRoot = Vector512.MultiplyAddEstimate(-thirdZ, squared * squared, Vector512.Create(FourThirds) * reciprocalRoot); } - for (int i = 0; i < 3; i++) - { - float negBiasCbrt = -MathF.Cbrt(JxlOpsinAbsorbanceBias[i]); + Vector512 squaredRoot = reciprocalRoot * reciprocalRoot; + reciprocalRoot = Vector512.MultiplyAddEstimate( + Vector512.Create(OneThird), + Vector512.MultiplyAddEstimate(-z, squaredRoot * squaredRoot, reciprocalRoot), + reciprocalRoot); - premulAbsorb - .Slice((9 + i) * lanes, lanes) - .Fill(negBiasCbrt); - } + squaredRoot = reciprocalRoot * reciprocalRoot; + return Vector512.MultiplyAddEstimate(squaredRoot * scale, z, add); } + /// Converts the image's direct sRGB samples to XYB, optionally retaining linear sRGB. + /// The configuration used by the general color conversion path. + /// The source color encoding. + /// The positive original intensity target, in nits. + /// The optional source black channel used by the general color conversion path. + /// The equally sized source planes, replaced with XYB samples. + /// The general color conversion provider. + /// The optional equally sized output that retains expanded linear sRGB. + /// Whether the source and requested output planes have matching dimensions. public static bool ToXyb( Configuration configuration, JxlColorEncoding currentEncoding, @@ -403,39 +634,34 @@ public static bool ToXyb( } } - int lanes = Vector.Count; + // Twelve scalar coefficients serve every SIMD width. Source expansion, optional linear storage and XYB conversion share one pass. + Span coefficients = stackalloc float[12]; + ComputeOpsinAbsorbanceCoefficients(intensityTarget, coefficients); + bool directSrgb = currentEncoding.HaveFields + && currentEncoding.WhitePoint == JxlWhitePoint.D65 + && (currentEncoding.ColorSpace == JxlColorSpace.Gray + || (currentEncoding.ColorSpace == JxlColorSpace.Rgb && currentEncoding.Primaries == JxlPrimaries.SRgb)); - // Twelve coefficients are broadcast across the active vector lanes. The bounded - // workspace stays on the calling thread and is reused for every row of this transform. - Span premulAbsorb = stackalloc float[lanes * 12]; - ComputePremulAbsorb(intensityTarget, premulAbsorb); - - JxlColorEncoding linearSrgb = JxlColorEncoding.LinearSRgb(currentEncoding.IsGray); - - if (linearSrgb.SameColorEncoding(currentEncoding)) + if (directSrgb && currentEncoding.TransferFunction.IsLinear) { - if (linear is not null) - { - JxlImageOperations.ForceCopyImage(image, linear); - } - - LinearSrgbToXyb(premulAbsorb, image); + RgbToXyb(coefficients, image, image, linear, default(TensorPrimitives_.IdentityOperator)); return true; } - if (currentEncoding.IsSRgb) + if (directSrgb && currentEncoding.TransferFunction.IsSrgb) { - if (linear is not null) - { - SrgbToXybAndLinear(premulAbsorb, image, linear); - return true; - } - - SRgbToXyb(premulAbsorb, image); + CompandingUtilities.BoundedOperator expand = new(new(1.055F, 0.055F, 0.04045F), PixelColorRange.Extended); + RgbToXyb(coefficients, image, image, linear, expand); return true; } - // TODO + // General profile conversion is still supplied by the codec's color-management integration. + JxlColorEncoding linearSrgb = new() + { + ColorSpace = currentEncoding.IsGray ? JxlColorSpace.Gray : JxlColorSpace.Rgb, + TransferFunction = new() { TransferFunction = JxlTransferFunction.Linear } + }; + ApplyColorTransform( configuration, currentEncoding, @@ -447,12 +673,8 @@ public static bool ToXyb( cms, linear ?? image); - if (linear is not null) - { - JxlImageOperations.ForceCopyImage(linear, image); - } - - LinearSrgbToXyb(premulAbsorb, image); + // Read the actual transformed owner directly. When linear output is retained, no full-image copy back to image is needed. + RgbToXyb(coefficients, linear ?? image, image, null, default(TensorPrimitives_.IdentityOperator)); return true; } diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/OpsinImageTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/OpsinImageTests.cs index 3dbf4b9efd..abce16e9ce 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/OpsinImageTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/OpsinImageTests.cs @@ -1,6 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; +using SixLabors.ImageSharp.ColorProfiles.Companding; +using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Jxl.Cms; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Memory; @@ -10,6 +13,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.TestUtilities; namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; @@ -314,6 +318,179 @@ static void RunTest() HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); } + /// Checks the actual forward row path, native matrix equations, shared input expansion and exact storage bounds. + [Fact] + public void ForwardOpsin_ConvertsRowsAndRetainsRequestedLinearSamples() + { + static void RunTest() + { + ReadOnlySpan widths = [1, 3, 17, 33]; + ReadOnlySpan intensities = [255F, 65504F]; + ReadOnlySpan matrix = + [ + 0.30F, 1F - 0.078F - 0.30F, 0.078F, + 0.23F, 1F - 0.078F - 0.23F, 0.078F, + 0.24342268924547819F, 0.20476744424496821F, 1F - 0.24342268924547819F - 0.20476744424496821F + ]; + + const float bias = 0.0037930732552754493F; + ReadOnlySpan sourceValues = + [ + 0F, 0F, 0F, + 1F, 1F, 1F, + 0.5F, 0.25F, 0.75F, + -bias / 0.30F, 0F, 0F, + 65504F, -65504F, 65504F, + -65504F, 65504F, -65504F, + float.Epsilon, float.Epsilon, float.Epsilon, + -0.25F, 0.125F, 0.5F + ]; + + Span coefficients = stackalloc float[12]; + coefficients[9] = coefficients[10] = coefficients[11] = -MathF.Cbrt(bias); + const float untouched = -777F; + CompandingUtilities.BoundedOperator expand = new(new(1.055F, 0.055F, 0.04045F), PixelColorRange.Extended); + foreach (int width in widths) + { + using JxlImage3 image = new(TestEnvironment.Configuration, width + 19, 1); + using JxlImage3 retained = new(TestEnvironment.Configuration, width + 35, 1); + Span storage0 = image.PlaneRow(0, 0); + Span storage1 = image.PlaneRow(1, 0); + Span storage2 = image.PlaneRow(2, 0); + Span linear0 = retained.PlaneRow(0, 0); + Span linear1 = retained.PlaneRow(1, 0); + Span linear2 = retained.PlaneRow(2, 0); + Span row0 = storage0.Slice(2, width); + Span row1 = storage1.Slice(2, width); + Span row2 = storage2.Slice(2, width); + foreach (float intensity in intensities) + { + // These are native input parameters, independent of the production coefficient preparation method. + for (int i = 0; i < matrix.Length; i++) + { + coefficients[i] = matrix[i] * (intensity / 255F); + } + + for (int mode = 0; mode < 4; mode++) + { + bool encoded = mode >= 2; + bool retainLinear = (mode & 1) != 0; + storage0.Fill(untouched); + storage1.Fill(untouched); + storage2.Fill(untouched); + linear0.Fill(untouched); + linear1.Fill(untouched); + linear2.Fill(untouched); + for (int x = 0; x < width; x++) + { + int sample = (x % 8) * 3; + row0[x] = sourceValues[sample]; + row1[x] = sourceValues[sample + 1]; + row2[x] = sourceValues[sample + 2]; + } + + Span retained0 = retainLinear ? linear0.Slice(4, width) : []; + Span retained1 = retainLinear ? linear1.Slice(4, width) : []; + Span retained2 = retainLinear ? linear2.Slice(4, width) : []; + if (encoded) + { + JxlXybEncoder.RgbRowToXyb(row0, row1, row2, row0, row1, row2, retained0, retained1, retained2, coefficients, expand); + } + else + { + JxlXybEncoder.RgbRowToXyb( + row0, + row1, + row2, + row0, + row1, + row2, + retained0, + retained1, + retained2, + coefficients, + default(TensorPrimitives_.IdentityOperator)); + } + + for (int x = 0; x < width; x++) + { + int sample = (x % 8) * 3; + Vector4 linear = new(sourceValues[sample], sourceValues[sample + 1], sourceValues[sample + 2], 1F); + if (encoded) + { + // The public shared API defines the requested input expansion and finite result range. + linear = SRgbCompanding.Expand(linear, 32, PixelColorRange.Extended); + } + + InlineArray3 roots = default; + float absoluteBound = 0F; + float scale = intensity / 255F; + + // Evaluate the frozen native equations independently of the supplied coefficient bank. + Vector3 absorbance = new( + float.MultiplyAddEstimate(0.078F * scale, linear.Z, bias), + float.MultiplyAddEstimate(0.078F * scale, linear.Z, bias), + float.MultiplyAddEstimate((1F - 0.24342268924547819F - 0.20476744424496821F) * scale, linear.Z, bias)); + + absorbance.X = float.MultiplyAddEstimate((1F - 0.078F - 0.30F) * scale, linear.Y, absorbance.X); + absorbance.X = float.MultiplyAddEstimate(0.30F * scale, linear.X, absorbance.X); + absorbance.Y = float.MultiplyAddEstimate((1F - 0.078F - 0.23F) * scale, linear.Y, absorbance.Y); + absorbance.Y = float.MultiplyAddEstimate(0.23F * scale, linear.X, absorbance.Y); + absorbance.Z = float.MultiplyAddEstimate(0.20476744424496821F * scale, linear.Y, absorbance.Z); + absorbance.Z = float.MultiplyAddEstimate(0.24342268924547819F * scale, linear.X, absorbance.Z); + for (int c = 0; c < 3; c++) + { + // MathF.Cbrt is independent of the SIMD reciprocal-root approximation. + float root = MathF.Cbrt(MathF.Max(0F, absorbance[c])); + roots[c] = root - MathF.Cbrt(bias); + + // Native documents six root ULPs. One additional root ULP covers the reference's separate bias addition. + // Use an absolute XYB bound: cancellation near black does not preserve a relative biased-result ULP bound. + absoluteBound = MathF.Max(absoluteBound, 7F * (MathF.BitIncrement(root) - root)); + } + + Assert.Equal(0.5F * (roots[0] - roots[1]), row0[x], absoluteBound); + Assert.Equal(0.5F * (roots[0] + roots[1]), row1[x], absoluteBound); + Assert.Equal(roots[2], row2[x], absoluteBound); + if (retainLinear) + { + Assert.Equal(linear.X, retained0[x]); + Assert.Equal(linear.Y, retained1[x]); + Assert.Equal(linear.Z, retained2[x]); + } + } + + // Inspect full allocations, not just logical rows. No SIMD block may overwrite either prefix or trailing storage. + for (int c = 0; c < 3; c++) + { + ReadOnlySpan pixels = c == 0 ? storage0 : c == 1 ? storage1 : storage2; + for (int i = 0; i < pixels.Length; i++) + { + if (i < 2 || i >= width + 2) + { + Assert.Equal(untouched, pixels[i]); + } + } + + pixels = c == 0 ? linear0 : c == 1 ? linear1 : linear2; + for (int i = 0; i < pixels.Length; i++) + { + if (!retainLinear || i < 4 || i >= width + 4) + { + Assert.Equal(untouched, pixels[i]); + } + } + } + } + } + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } + /// Prepares a nonsymmetric dyadic inverse matrix and exact cube-root biases for independent numerical checks. /// The relative-output intensity target: 255 or 510 nits. /// The prepared inverse transform. From 8dbb2ab1d181108c3fd11f96eedfe438ce385b1a Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Oct 2026 05:19:40 +1000 Subject: [PATCH 318/328] Remove unused JPEG XL RGB-to-YCbCr encoder utility --- .../Jxl/Processing/Encoder/JxlXybEncoder.cs | 83 ------------------- 1 file changed, 83 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlXybEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlXybEncoder.cs index 7ad16d13a7..865756fccf 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlXybEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlXybEncoder.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; @@ -10,7 +9,6 @@ using SixLabors.ImageSharp.Formats.Jxl.Cms; using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; -using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; @@ -677,85 +675,4 @@ public static bool ToXyb( RgbToXyb(coefficients, linear ?? image, image, null, default(TensorPrimitives_.IdentityOperator)); return true; } - - public static void RgbToYcbcr( - Configuration configuration, - JxlPlane rPlane, - JxlPlane gPlane, - JxlPlane bPlane, - JxlPlane yPlane, - JxlPlane cbPlane, - JxlPlane crPlane) - { - int xSize = rPlane.XSize; - int ySize = rPlane.YSize; - - if (xSize == 0 || ySize == 0) - { - return; - } - - Vector k128 = new(128.0f / 255); - Vector kR = new(0.299f); - Vector kG = new(0.587f); - Vector kB = new(0.114f); - Vector kAmpR = new(0.701f); - Vector kAmpB = new(0.886f); - - Vector kDiffR = kAmpR + kR; - Vector kDiffB = kAmpB + kB; - - Vector kNormR = Vector.One / (kAmpR + kG + kB); - Vector kNormB = Vector.One / (kR + kG + kAmpB); - - int step = Vector.Count; - - int groupArea = JxlFrameDimensions.GroupDimensions * JxlFrameDimensions.GroupDimensions; - int linesPerGroup = JxlMath.DivCeil(groupArea, xSize); - int numStripes = JxlMath.DivCeil(ySize, linesPerGroup); - - void Transform(int index) - { - int y0 = index * linesPerGroup; - int y1 = Math.Min(y0 + linesPerGroup, ySize); - - for (int y = y0; y < y1; y++) - { - ReadOnlySpan rRow = rPlane.GetRow(y); - ReadOnlySpan gRow = gPlane.GetRow(y); - ReadOnlySpan bRow = bPlane.GetRow(y); - - Span yRow = yPlane.GetRow(y); - Span cbRow = cbPlane.GetRow(y); - Span crRow = crPlane.GetRow(y); - - for (int x = 0; x < xSize; x += step) - { - Vector r = new(rRow[x..]); - Vector g = new(gRow[x..]); - Vector b = new(bRow[x..]); - - Vector rBase = r * kR; - Vector rDiff = r * kDiffR; - Vector gBase = g * kG; - Vector bBase = b * kB; - Vector bDiff = b * kDiffB; - - Vector yBase = rBase + gBase + bBase; - Vector yVec = yBase - k128; - Vector cbVec = (bDiff - yBase) * kNormB; - Vector crVec = (rDiff - yBase) * kNormR; - - yVec.CopyTo(yRow[x..]); - cbVec.CopyTo(cbRow[x..]); - crVec.CopyTo(crRow[x..]); - } - } - } - - for (int i = 0; i < numStripes; i++) - { - Transform(i); - } - } } From 09313c714780bad7bbb1b64179bff3112df3d4ac Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Oct 2026 05:25:05 +1000 Subject: [PATCH 319/328] Fuse JPEG XL comparison opsin arithmetic across scalar and SIMD paths --- .../Jxl/Processing/Butteraugli/Butteraugli.cs | 667 ++++++++++++++---- .../Jxl/Processing/ButteraugliTests.cs | 146 ++++ 2 files changed, 678 insertions(+), 135 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs index 38696d6a62..4e917b8c46 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs @@ -46,6 +46,48 @@ internal static class Butteraugli private const float GlobalScale = 1.0f / InternalGoodQualityThreshold; + /// The red contribution to the first perceptual absorbance. + private const float Absorbance0Red = 0.29956550340058319F; + + /// The green contribution to the first perceptual absorbance. + private const float Absorbance0Green = 0.63373087833825936F; + + /// The blue contribution to the first perceptual absorbance. + private const float Absorbance0Blue = 0.077705617820981968F; + + /// The red contribution to the second perceptual absorbance. + private const float Absorbance1Red = 0.22158691104574774F; + + /// The green contribution to the second perceptual absorbance. + private const float Absorbance1Green = 0.69391388044116142F; + + /// The blue contribution to the second perceptual absorbance. + private const float Absorbance1Blue = 0.0987313588422F; + + /// The red and green contributions to the third perceptual absorbance. + private const float Absorbance2RedGreen = 0.02F; + + /// The blue contribution to the third perceptual absorbance. + private const float Absorbance2Blue = 0.20480129041026129F; + + /// The native positive offset and reconstruction floor for the first two absorbances. + private const float Absorbance01Bias = 1.7557483643287353F; + + /// The native positive offset and reconstruction floor for the third absorbance. + private const float Absorbance2Bias = 12.226454707163354F; + + /// The native lower bound for the blurred image's sensitivity response. + private const float MinimumOpsinSensitivity = 1e-4F; + + /// The positive offset inside the perceptual response's logarithm. + private const float OpsinGammaInputBias = 9.9710635769299145F; + + /// The native float product that converts log2 to the scaled natural-log response. + private const float OpsinGammaSlope = 19.245013259874995F * JxlMath.InverseLog2E; + + /// The offset applied after the perceptual logarithm. + private const float OpsinGammaOffset = -23.16046239805755F; + #pragma warning disable // Just indentation warnings /// /// Underlying data for . @@ -2640,66 +2682,13 @@ public Vector512 Invoke(Vector512 first, Vector512 second, } } - public static void OpsinAbsorbance( - bool clamp, - Vector in0, - Vector in1, - Vector in2, - out Vector out0, - out Vector out1, - out Vector out2) - { - Vector mix0 = new(0.29956550340058319f); - Vector mix1 = new(0.63373087833825936f); - Vector mix2 = new(0.077705617820981968f); - Vector mix3 = new(1.7557483643287353f); - - Vector mix4 = new(0.22158691104574774f); - Vector mix5 = new(0.69391388044116142f); - Vector mix6 = new(0.0987313588422f); - Vector mix7 = new(1.7557483643287353f); - - Vector mix8 = new(0.02f); - Vector mix9 = new(0.02f); - Vector mix10 = new(0.20480129041026129f); - Vector mix11 = new(12.226454707163354f); - - out0 = (mix0 * in0) + ((mix1 * in1) + ((mix2 * in2) + mix3)); - out1 = (mix4 * in0) + ((mix5 * in1) + ((mix6 * in2) + mix7)); - out2 = (mix8 * in0) + ((mix9 * in1) + ((mix10 * in2) + mix11)); - - if (clamp) - { - out0 = Vector.Max(out0, mix3); - out1 = Vector.Max(out1, mix7); - out2 = Vector.Max(out2, mix11); - } - } - - // A simple HDR-compatible gamma function - private static Vector Gamma(Vector v) - { - Vector kRetMul = Vector.Create(19.245013259874995f * JxlMath.InverseLog2E); - Vector kRetAdd = Vector.Create(-23.16046239805755f); - - // if (value < 0) value = 0; - v = Vector.ConditionalSelect(Vector.LessThan(v, Vector.Zero), Vector.Zero, v); - - Vector biased = v + Vector.Create(9.9710635769299145f); - Vector log = Vector.Log2(biased); - - return (kRetMul * log) + kRetAdd; - } - - /// - /// Transforms borrowed RGB samples into the perceptual XYB representation using prepared blur and output storage. - /// + /// Transforms borrowed RGB samples with the prepared comparison blur and output storage. /// The comparison configuration. - /// The borrowed RGB image. It may also supply the output storage. - /// The perceptual comparison parameters. + /// The borrowed RGB image. It can also supply the output storage. + /// The perceptual comparison parameters, including the intensity of one input unit in nits. /// The distinct preallocated three-plane blur workspace. /// The prepared transposed blur workspace. - /// The preallocated transformed output. + /// The preallocated transformed output, with the same logical dimensions as the input. /// Whether all blur stages completed. public static bool OpsinDynamicsImage( Configuration configuration, @@ -2709,12 +2698,6 @@ public static bool OpsinDynamicsImage( ButteraugliBlurTemp blurTemp, JxlImage3 xyb) { - if (blurred == null) - { - return false; - } - - if (!Blur(rgb.Plane(0), OpsinDynamicsKernel, blurTemp, blurred.Plane(0))) { return false; @@ -2730,91 +2713,505 @@ public static bool OpsinDynamicsImage( return false; } - Vector intensityMultiplier = new((float)parameters.IntensityTarget); + // Blur finishes before any source samples are replaced. All nine row views borrow existing owners. + // Retained planes can have different physical strides despite equal logical dimensions. + ReadOnlySpan source0 = rgb.PlaneRow(0, 0); + ReadOnlySpan source1 = rgb.PlaneRow(1, 0); + ReadOnlySpan source2 = rgb.PlaneRow(2, 0); + ReadOnlySpan blurred0 = blurred.PlaneRow(0, 0); + ReadOnlySpan blurred1 = blurred.PlaneRow(1, 0); + ReadOnlySpan blurred2 = blurred.PlaneRow(2, 0); + Span destination0 = xyb.PlaneRow(0, 0); + Span destination1 = xyb.PlaneRow(1, 0); + Span destination2 = xyb.PlaneRow(2, 0); + int sourceStride0 = rgb.Plane(0).PixelsPerRow; + int sourceStride1 = rgb.Plane(1).PixelsPerRow; + int sourceStride2 = rgb.Plane(2).PixelsPerRow; + int blurredStride0 = blurred.Plane(0).PixelsPerRow; + int blurredStride1 = blurred.Plane(1).PixelsPerRow; + int blurredStride2 = blurred.Plane(2).PixelsPerRow; + int destinationStride0 = xyb.Plane(0).PixelsPerRow; + int destinationStride1 = xyb.Plane(1).PixelsPerRow; + int destinationStride2 = xyb.Plane(2).PixelsPerRow; + int width = rgb.XSize; + float intensity = parameters.IntensityTarget; + + for (int y = 0; y < rgb.YSize; y++) + { + ReadOnlySpan row0 = source0.Slice(y * sourceStride0, width); + ReadOnlySpan row1 = source1.Slice(y * sourceStride1, width); + ReadOnlySpan row2 = source2.Slice(y * sourceStride2, width); + ReadOnlySpan blur0 = blurred0.Slice(y * blurredStride0, width); + ReadOnlySpan blur1 = blurred1.Slice(y * blurredStride1, width); + ReadOnlySpan blur2 = blurred2.Slice(y * blurredStride2, width); + Span output0 = destination0.Slice(y * destinationStride0, width); + Span output1 = destination1.Slice(y * destinationStride1, width); + Span output2 = destination2.Slice(y * destinationStride2, width); + ref float input0 = ref MemoryMarshal.GetReference(row0); + ref float input1 = ref MemoryMarshal.GetReference(row1); + ref float input2 = ref MemoryMarshal.GetReference(row2); + ref float smoothed0 = ref MemoryMarshal.GetReference(blur0); + ref float smoothed1 = ref MemoryMarshal.GetReference(blur1); + ref float smoothed2 = ref MemoryMarshal.GetReference(blur2); + ref float result0 = ref MemoryMarshal.GetReference(output0); + ref float result1 = ref MemoryMarshal.GetReference(output1); + ref float result2 = ref MemoryMarshal.GetReference(output2); + int x = 0; - int lanes = Vector.Count; + // One register holds one component from consecutive pixels. Each block reads all six inputs before any output store. + // Only full registers enter each loop. Narrower widths and the scalar remainder never access logical row padding. + if (Vector512.IsHardwareAccelerated) + { + Vector512 multiplier = Vector512.Create(intensity); + for (; x <= width - Vector512.Count; x += Vector512.Count) + { + OpsinDynamics( + Vector512.LoadUnsafe(ref input0, (uint)x), + Vector512.LoadUnsafe(ref input1, (uint)x), + Vector512.LoadUnsafe(ref input2, (uint)x), + Vector512.LoadUnsafe(ref smoothed0, (uint)x), + Vector512.LoadUnsafe(ref smoothed1, (uint)x), + Vector512.LoadUnsafe(ref smoothed2, (uint)x), + multiplier, + out Vector512 value0, + out Vector512 value1, + out Vector512 value2); + + value0.StoreUnsafe(ref result0, (uint)x); + value1.StoreUnsafe(ref result1, (uint)x); + value2.StoreUnsafe(ref result2, (uint)x); + } + } + + if (Vector256.IsHardwareAccelerated) + { + Vector256 multiplier = Vector256.Create(intensity); + for (; x <= width - Vector256.Count; x += Vector256.Count) + { + OpsinDynamics( + Vector256.LoadUnsafe(ref input0, (uint)x), + Vector256.LoadUnsafe(ref input1, (uint)x), + Vector256.LoadUnsafe(ref input2, (uint)x), + Vector256.LoadUnsafe(ref smoothed0, (uint)x), + Vector256.LoadUnsafe(ref smoothed1, (uint)x), + Vector256.LoadUnsafe(ref smoothed2, (uint)x), + multiplier, + out Vector256 value0, + out Vector256 value1, + out Vector256 value2); + + value0.StoreUnsafe(ref result0, (uint)x); + value1.StoreUnsafe(ref result1, (uint)x); + value2.StoreUnsafe(ref result2, (uint)x); + } + } + + if (Vector128.IsHardwareAccelerated) + { + Vector128 multiplier = Vector128.Create(intensity); + for (; x <= width - Vector128.Count; x += Vector128.Count) + { + OpsinDynamics( + Vector128.LoadUnsafe(ref input0, (uint)x), + Vector128.LoadUnsafe(ref input1, (uint)x), + Vector128.LoadUnsafe(ref input2, (uint)x), + Vector128.LoadUnsafe(ref smoothed0, (uint)x), + Vector128.LoadUnsafe(ref smoothed1, (uint)x), + Vector128.LoadUnsafe(ref smoothed2, (uint)x), + multiplier, + out Vector128 value0, + out Vector128 value1, + out Vector128 value2); + + value0.StoreUnsafe(ref result0, (uint)x); + value1.StoreUnsafe(ref result1, (uint)x); + value2.StoreUnsafe(ref result2, (uint)x); + } + } - Vector minValue = new(1e-4f); + for (; x < width; x++) + { + OpsinDynamics( + row0[x], + row1[x], + row2[x], + blur0[x], + blur1[x], + blur2[x], + intensity, + out float value0, + out float value1, + out float value2); + + output0[x] = value0; + output1[x] = value1; + output2[x] = value2; + } + } - Span rgbSamples0 = rgb.Plane(0).GetRow(0); - Span rgbSamples1 = rgb.Plane(1).GetRow(0); - Span rgbSamples2 = rgb.Plane(2).GetRow(0); - Span blurredSamples0 = blurred.Plane(0).GetRow(0); - Span blurredSamples1 = blurred.Plane(1).GetRow(0); - Span blurredSamples2 = blurred.Plane(2).GetRow(0); - Span xybSamples0 = xyb.Plane(0).GetRow(0); - Span xybSamples1 = xyb.Plane(1).GetRow(0); - Span xybSamples2 = xyb.Plane(2).GetRow(0); + return true; + } - for (int y = 0; y < rgb.YSize; ++y) - { - ReadOnlySpan rowR = rgbSamples0[(y * rgb.Plane(0).PixelsPerRow)..]; - ReadOnlySpan rowG = rgbSamples1[(y * rgb.Plane(1).PixelsPerRow)..]; - ReadOnlySpan rowB = rgbSamples2[(y * rgb.Plane(2).PixelsPerRow)..]; + /// Adapts one RGB pixel using its blurred sensitivity in the native arithmetic order. + /// The red input samples. + /// The green input samples. + /// The blue input samples. + /// The blurred red samples. + /// The blurred green samples. + /// The blurred blue samples. + /// The intensity multiplier applied before both absorbance calculations. + /// The perceptual X result. + /// The perceptual Y result. + /// The perceptual B result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void OpsinDynamics( + float r, + float g, + float b, + float blurredR, + float blurredG, + float blurredB, + float intensity, + out float x, + out float y, + out float blue) + { + OpsinAbsorbance(blurredR * intensity, blurredG * intensity, blurredB * intensity, out float pre0, out float pre1, out float pre2); + + // The blurred absorbance floor keeps Gamma's input and its sensitivity divisor positive. + pre0 = MathF.Max(pre0, Absorbance01Bias); + pre1 = MathF.Max(pre1, Absorbance01Bias); + pre2 = MathF.Max(pre2, Absorbance2Bias); + float sensitivity0 = MathF.Max(Gamma(pre0) / pre0, MinimumOpsinSensitivity); + float sensitivity1 = MathF.Max(Gamma(pre1) / pre1, MinimumOpsinSensitivity); + float sensitivity2 = MathF.Max(Gamma(pre2) / pre2, MinimumOpsinSensitivity); + + OpsinAbsorbance(r * intensity, g * intensity, b * intensity, out float current0, out float current1, out float current2); + current0 = MathF.Max(current0 * sensitivity0, Absorbance01Bias); + current1 = MathF.Max(current1 * sensitivity1, Absorbance01Bias); + current2 = MathF.Max(current2 * sensitivity2, Absorbance2Bias); + x = current0 - current1; + y = current0 + current1; + blue = current2; + } - ReadOnlySpan blurredR = blurredSamples0[(y * blurred.Plane(0).PixelsPerRow)..]; - ReadOnlySpan blurredG = blurredSamples1[(y * blurred.Plane(1).PixelsPerRow)..]; - ReadOnlySpan blurredB = blurredSamples2[(y * blurred.Plane(2).PixelsPerRow)..]; + /// Mixes one RGB sample into unclamped perceptual absorbance. + /// The intensity-scaled red components. + /// The intensity-scaled green components. + /// The intensity-scaled blue components. + /// The first absorbance. + /// The second absorbance. + /// The third absorbance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void OpsinAbsorbance(float r, float g, float b, out float first, out float second, out float third) + { + // Preserve B+bias, then G, then R as nested multiply-add estimates. Clamp policy belongs to the adaptation stage. + first = float.MultiplyAddEstimate(Absorbance0Blue, b, Absorbance01Bias); + first = float.MultiplyAddEstimate(Absorbance0Green, g, first); + first = float.MultiplyAddEstimate(Absorbance0Red, r, first); + + second = float.MultiplyAddEstimate(Absorbance1Blue, b, Absorbance01Bias); + second = float.MultiplyAddEstimate(Absorbance1Green, g, second); + second = float.MultiplyAddEstimate(Absorbance1Red, r, second); + + third = float.MultiplyAddEstimate(Absorbance2Blue, b, Absorbance2Bias); + third = float.MultiplyAddEstimate(Absorbance2RedGreen, g, third); + third = float.MultiplyAddEstimate(Absorbance2RedGreen, r, third); + } - Span outX = xybSamples0[(y * xyb.Plane(0).PixelsPerRow)..]; - Span outY = xybSamples1[(y * xyb.Plane(1).PixelsPerRow)..]; - Span outB = xybSamples2[(y * xyb.Plane(2).PixelsPerRow)..]; + /// Evaluates the perceptual logarithmic response for one positive absorbance. + /// The absorbance after its native positive floor. + /// The response using the existing .NET log2 policy and the native final multiply-add order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float Gamma(float value) + => float.MultiplyAddEstimate(OpsinGammaSlope, MathF.Log2(value + OpsinGammaInputBias), OpsinGammaOffset); + + /// Adapts 4 independent RGB pixels using their blurred sensitivities, without changing pixel lane order. + /// The red input samples. + /// The green input samples. + /// The blue input samples. + /// The blurred red samples. + /// The blurred green samples. + /// The blurred blue samples. + /// The intensity multiplier applied before both absorbance calculations. + /// The perceptual X result. + /// The perceptual Y result. + /// The perceptual B result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void OpsinDynamics( + Vector128 r, + Vector128 g, + Vector128 b, + Vector128 blurredR, + Vector128 blurredG, + Vector128 blurredB, + Vector128 intensity, + out Vector128 x, + out Vector128 y, + out Vector128 blue) + { + OpsinAbsorbance( + blurredR * intensity, + blurredG * intensity, + blurredB * intensity, + out Vector128 pre0, + out Vector128 pre1, + out Vector128 pre2); + + // The blurred absorbance floor keeps Gamma's input and its sensitivity divisor positive. + pre0 = Vector128.Max(pre0, Vector128.Create(Absorbance01Bias)); + pre1 = Vector128.Max(pre1, Vector128.Create(Absorbance01Bias)); + pre2 = Vector128.Max(pre2, Vector128.Create(Absorbance2Bias)); + Vector128 sensitivity0 = Vector128.Max(Gamma(pre0) / pre0, Vector128.Create(MinimumOpsinSensitivity)); + Vector128 sensitivity1 = Vector128.Max(Gamma(pre1) / pre1, Vector128.Create(MinimumOpsinSensitivity)); + Vector128 sensitivity2 = Vector128.Max(Gamma(pre2) / pre2, Vector128.Create(MinimumOpsinSensitivity)); + + OpsinAbsorbance( + r * intensity, + g * intensity, + b * intensity, + out Vector128 current0, + out Vector128 current1, + out Vector128 current2); + + current0 = Vector128.Max(current0 * sensitivity0, Vector128.Create(Absorbance01Bias)); + current1 = Vector128.Max(current1 * sensitivity1, Vector128.Create(Absorbance01Bias)); + current2 = Vector128.Max(current2 * sensitivity2, Vector128.Create(Absorbance2Bias)); + x = current0 - current1; + y = current0 + current1; + blue = current2; + } - for (int x = 0; x < rgb.XSize; x += lanes) - { - Vector sensitivity0; - Vector sensitivity1; - Vector sensitivity2; - { - OpsinAbsorbance( - true, - new Vector(blurredR[x..]) * intensityMultiplier, - new Vector(blurredG[x..]) * intensityMultiplier, - new Vector(blurredB[x..]) * intensityMultiplier, - out Vector pre0, - out Vector pre1, - out Vector pre2); - - pre0 = Vector.Max(pre0, minValue); - pre1 = Vector.Max(pre1, minValue); - pre2 = Vector.Max(pre2, minValue); - - sensitivity0 = Gamma(pre0) / pre0; - sensitivity1 = Gamma(pre1) / pre1; - sensitivity2 = Gamma(pre2) / pre2; - - sensitivity0 = Vector.Max(sensitivity0, minValue); - sensitivity1 = Vector.Max(sensitivity1, minValue); - sensitivity2 = Vector.Max(sensitivity2, minValue); - } + /// Mixes 4 independent RGB samples into unclamped perceptual absorbance. + /// The intensity-scaled red components. + /// The intensity-scaled green components. + /// The intensity-scaled blue components. + /// The first absorbance. + /// The second absorbance. + /// The third absorbance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void OpsinAbsorbance( + Vector128 r, + Vector128 g, + Vector128 b, + out Vector128 first, + out Vector128 second, + out Vector128 third) + { + // Preserve B+bias, then G, then R as nested multiply-add estimates. Clamp policy belongs to the adaptation stage. + first = Vector128.MultiplyAddEstimate(Vector128.Create(Absorbance0Blue), b, Vector128.Create(Absorbance01Bias)); + first = Vector128.MultiplyAddEstimate(Vector128.Create(Absorbance0Green), g, first); + first = Vector128.MultiplyAddEstimate(Vector128.Create(Absorbance0Red), r, first); + + second = Vector128.MultiplyAddEstimate(Vector128.Create(Absorbance1Blue), b, Vector128.Create(Absorbance01Bias)); + second = Vector128.MultiplyAddEstimate(Vector128.Create(Absorbance1Green), g, second); + second = Vector128.MultiplyAddEstimate(Vector128.Create(Absorbance1Red), r, second); + + third = Vector128.MultiplyAddEstimate(Vector128.Create(Absorbance2Blue), b, Vector128.Create(Absorbance2Bias)); + third = Vector128.MultiplyAddEstimate(Vector128.Create(Absorbance2RedGreen), g, third); + third = Vector128.MultiplyAddEstimate(Vector128.Create(Absorbance2RedGreen), r, third); + } - OpsinAbsorbance( - false, - new Vector(rowR[x..]) * intensityMultiplier, - new Vector(rowG[x..]) * intensityMultiplier, - new Vector(rowB[x..]) * intensityMultiplier, - out Vector cur0, - out Vector cur1, - out Vector cur2); + /// Evaluates the perceptual logarithmic response for 4 positive absorbance lanes. + /// The absorbance after its native positive floor. + /// The response using the existing .NET log2 policy and the native final multiply-add order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 Gamma(Vector128 value) + { + Vector128 logarithm = Vector128.Log2(value + Vector128.Create(OpsinGammaInputBias)); + return Vector128.MultiplyAddEstimate(Vector128.Create(OpsinGammaSlope), logarithm, Vector128.Create(OpsinGammaOffset)); + } - cur0 *= sensitivity0; - cur1 *= sensitivity1; - cur2 *= sensitivity2; + /// Adapts 8 independent RGB pixels using their blurred sensitivities, without changing pixel lane order. + /// The red input samples. + /// The green input samples. + /// The blue input samples. + /// The blurred red samples. + /// The blurred green samples. + /// The blurred blue samples. + /// The intensity multiplier applied before both absorbance calculations. + /// The perceptual X result. + /// The perceptual Y result. + /// The perceptual B result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void OpsinDynamics( + Vector256 r, + Vector256 g, + Vector256 b, + Vector256 blurredR, + Vector256 blurredG, + Vector256 blurredB, + Vector256 intensity, + out Vector256 x, + out Vector256 y, + out Vector256 blue) + { + OpsinAbsorbance( + blurredR * intensity, + blurredG * intensity, + blurredB * intensity, + out Vector256 pre0, + out Vector256 pre1, + out Vector256 pre2); + + // The blurred absorbance floor keeps Gamma's input and its sensitivity divisor positive. + pre0 = Vector256.Max(pre0, Vector256.Create(Absorbance01Bias)); + pre1 = Vector256.Max(pre1, Vector256.Create(Absorbance01Bias)); + pre2 = Vector256.Max(pre2, Vector256.Create(Absorbance2Bias)); + Vector256 sensitivity0 = Vector256.Max(Gamma(pre0) / pre0, Vector256.Create(MinimumOpsinSensitivity)); + Vector256 sensitivity1 = Vector256.Max(Gamma(pre1) / pre1, Vector256.Create(MinimumOpsinSensitivity)); + Vector256 sensitivity2 = Vector256.Max(Gamma(pre2) / pre2, Vector256.Create(MinimumOpsinSensitivity)); + + OpsinAbsorbance( + r * intensity, + g * intensity, + b * intensity, + out Vector256 current0, + out Vector256 current1, + out Vector256 current2); + + current0 = Vector256.Max(current0 * sensitivity0, Vector256.Create(Absorbance01Bias)); + current1 = Vector256.Max(current1 * sensitivity1, Vector256.Create(Absorbance01Bias)); + current2 = Vector256.Max(current2 * sensitivity2, Vector256.Create(Absorbance2Bias)); + x = current0 - current1; + y = current0 + current1; + blue = current2; + } + + /// Mixes 8 independent RGB samples into unclamped perceptual absorbance. + /// The intensity-scaled red components. + /// The intensity-scaled green components. + /// The intensity-scaled blue components. + /// The first absorbance. + /// The second absorbance. + /// The third absorbance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void OpsinAbsorbance( + Vector256 r, + Vector256 g, + Vector256 b, + out Vector256 first, + out Vector256 second, + out Vector256 third) + { + // Preserve B+bias, then G, then R as nested multiply-add estimates. Clamp policy belongs to the adaptation stage. + first = Vector256.MultiplyAddEstimate(Vector256.Create(Absorbance0Blue), b, Vector256.Create(Absorbance01Bias)); + first = Vector256.MultiplyAddEstimate(Vector256.Create(Absorbance0Green), g, first); + first = Vector256.MultiplyAddEstimate(Vector256.Create(Absorbance0Red), r, first); + + second = Vector256.MultiplyAddEstimate(Vector256.Create(Absorbance1Blue), b, Vector256.Create(Absorbance01Bias)); + second = Vector256.MultiplyAddEstimate(Vector256.Create(Absorbance1Green), g, second); + second = Vector256.MultiplyAddEstimate(Vector256.Create(Absorbance1Red), r, second); + + third = Vector256.MultiplyAddEstimate(Vector256.Create(Absorbance2Blue), b, Vector256.Create(Absorbance2Bias)); + third = Vector256.MultiplyAddEstimate(Vector256.Create(Absorbance2RedGreen), g, third); + third = Vector256.MultiplyAddEstimate(Vector256.Create(Absorbance2RedGreen), r, third); + } - Vector min01 = new(1.7557483643287353f); - Vector min2 = new(12.226454707163354f); + /// Evaluates the perceptual logarithmic response for 8 positive absorbance lanes. + /// The absorbance after its native positive floor. + /// The response using the existing .NET log2 policy and the native final multiply-add order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 Gamma(Vector256 value) + { + Vector256 logarithm = Vector256.Log2(value + Vector256.Create(OpsinGammaInputBias)); + return Vector256.MultiplyAddEstimate(Vector256.Create(OpsinGammaSlope), logarithm, Vector256.Create(OpsinGammaOffset)); + } - cur0 = Vector.Max(cur0, min01); - cur1 = Vector.Max(cur1, min01); - cur2 = Vector.Max(cur2, min2); + /// Adapts 16 independent RGB pixels using their blurred sensitivities, without changing pixel lane order. + /// The red input samples. + /// The green input samples. + /// The blue input samples. + /// The blurred red samples. + /// The blurred green samples. + /// The blurred blue samples. + /// The intensity multiplier applied before both absorbance calculations. + /// The perceptual X result. + /// The perceptual Y result. + /// The perceptual B result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void OpsinDynamics( + Vector512 r, + Vector512 g, + Vector512 b, + Vector512 blurredR, + Vector512 blurredG, + Vector512 blurredB, + Vector512 intensity, + out Vector512 x, + out Vector512 y, + out Vector512 blue) + { + OpsinAbsorbance( + blurredR * intensity, + blurredG * intensity, + blurredB * intensity, + out Vector512 pre0, + out Vector512 pre1, + out Vector512 pre2); + + // The blurred absorbance floor keeps Gamma's input and its sensitivity divisor positive. + pre0 = Vector512.Max(pre0, Vector512.Create(Absorbance01Bias)); + pre1 = Vector512.Max(pre1, Vector512.Create(Absorbance01Bias)); + pre2 = Vector512.Max(pre2, Vector512.Create(Absorbance2Bias)); + Vector512 sensitivity0 = Vector512.Max(Gamma(pre0) / pre0, Vector512.Create(MinimumOpsinSensitivity)); + Vector512 sensitivity1 = Vector512.Max(Gamma(pre1) / pre1, Vector512.Create(MinimumOpsinSensitivity)); + Vector512 sensitivity2 = Vector512.Max(Gamma(pre2) / pre2, Vector512.Create(MinimumOpsinSensitivity)); + + OpsinAbsorbance( + r * intensity, + g * intensity, + b * intensity, + out Vector512 current0, + out Vector512 current1, + out Vector512 current2); + + current0 = Vector512.Max(current0 * sensitivity0, Vector512.Create(Absorbance01Bias)); + current1 = Vector512.Max(current1 * sensitivity1, Vector512.Create(Absorbance01Bias)); + current2 = Vector512.Max(current2 * sensitivity2, Vector512.Create(Absorbance2Bias)); + x = current0 - current1; + y = current0 + current1; + blue = current2; + } - (cur0 - cur1).CopyTo(outX[x..]); - (cur0 + cur1).CopyTo(outY[x..]); - cur2.CopyTo(outB[x..]); - } - } + /// Mixes 16 independent RGB samples into unclamped perceptual absorbance. + /// The intensity-scaled red components. + /// The intensity-scaled green components. + /// The intensity-scaled blue components. + /// The first absorbance. + /// The second absorbance. + /// The third absorbance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void OpsinAbsorbance( + Vector512 r, + Vector512 g, + Vector512 b, + out Vector512 first, + out Vector512 second, + out Vector512 third) + { + // Preserve B+bias, then G, then R as nested multiply-add estimates. Clamp policy belongs to the adaptation stage. + first = Vector512.MultiplyAddEstimate(Vector512.Create(Absorbance0Blue), b, Vector512.Create(Absorbance01Bias)); + first = Vector512.MultiplyAddEstimate(Vector512.Create(Absorbance0Green), g, first); + first = Vector512.MultiplyAddEstimate(Vector512.Create(Absorbance0Red), r, first); + + second = Vector512.MultiplyAddEstimate(Vector512.Create(Absorbance1Blue), b, Vector512.Create(Absorbance01Bias)); + second = Vector512.MultiplyAddEstimate(Vector512.Create(Absorbance1Green), g, second); + second = Vector512.MultiplyAddEstimate(Vector512.Create(Absorbance1Red), r, second); + + third = Vector512.MultiplyAddEstimate(Vector512.Create(Absorbance2Blue), b, Vector512.Create(Absorbance2Bias)); + third = Vector512.MultiplyAddEstimate(Vector512.Create(Absorbance2RedGreen), g, third); + third = Vector512.MultiplyAddEstimate(Vector512.Create(Absorbance2RedGreen), r, third); + } - return true; + /// Evaluates the perceptual logarithmic response for 16 positive absorbance lanes. + /// The absorbance after its native positive floor. + /// The response using the existing .NET log2 policy and the native final multiply-add order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 Gamma(Vector512 value) + { + Vector512 logarithm = Vector512.Log2(value + Vector512.Create(OpsinGammaInputBias)); + return Vector512.MultiplyAddEstimate(Vector512.Create(OpsinGammaSlope), logarithm, Vector512.Create(OpsinGammaOffset)); } /// diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs index a5e0612821..c212f9e121 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs @@ -861,4 +861,150 @@ static void CheckPlane(JxlPlane plane, float expected) RunTest, HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); } + + /// Checks native comparison-opsin equations, zero and RGB basis fields, in-place output and logical guards at all SIMD widths. + [Fact] + public void OpsinDynamics_UsesNativeResponseAndPreservesLogicalBounds() + { + static void RunTest() + { + ReadOnlySpan widths = [1, 3, 17, 33]; + ReadOnlySpan weights = + [ + 0.29956550340058319F, 0.63373087833825936F, 0.077705617820981968F, 1.7557483643287353F, + 0.22158691104574774F, 0.69391388044116142F, 0.0987313588422F, 1.7557483643287353F, + 0.02F, 0.02F, 0.20480129041026129F, 12.226454707163354F + ]; + + const int height = 3; + const float untouched = -123F; + + // Native's log2 test allows 3.1e-6 logarithm units. Two gamma terms amplify that to less than 0.000083 comparison units. + // Round the absolute component bound to 0.0001 to include the remaining float blur, division and multiplication rounding. + const float responseBound = 0.0001F; + Configuration configuration = TestEnvironment.Configuration; + ButteraugliParameters parameters = new(); + foreach (int width in widths) + { + using JxlImage3 source = new(configuration, width + 19, height + 2); + using JxlImage3 blurred = new(configuration, width + 35, height + 2); + using JxlImage3 transformed = new(configuration, width + 51, height + 2); + Assert.True(source.ShrinkTo(width, height)); + Assert.True(blurred.ShrinkTo(width, height)); + Assert.True(transformed.ShrinkTo(width, height)); + using ButteraugliBlurTemp scratch = new(configuration, width, height); + Span source0 = source.PlaneRow(0, 0); + Span source1 = source.PlaneRow(1, 0); + Span source2 = source.PlaneRow(2, 0); + Span blurred0 = blurred.PlaneRow(0, 0); + Span blurred1 = blurred.PlaneRow(1, 0); + Span blurred2 = blurred.PlaneRow(2, 0); + Span output0 = transformed.PlaneRow(0, 0); + Span output1 = transformed.PlaneRow(1, 0); + Span output2 = transformed.PlaneRow(2, 0); + + // Zero and three constant RGB basis fields remain constant under the normalized mirrored blur. + // The larger retained allocations give source, blur and output different physical row strides. + for (int basis = 0; basis < 4; basis++) + { + source0.Fill(untouched); + source1.Fill(untouched); + source2.Fill(untouched); + blurred0.Fill(untouched); + blurred1.Fill(untouched); + blurred2.Fill(untouched); + output0.Fill(untouched); + output1.Fill(untouched); + output2.Fill(untouched); + for (int c = 0; c < 3; c++) + { + Span samples = c == 0 ? source0 : c == 1 ? source1 : source2; + float value = basis == c + 1 ? 0.25F : 0F; + for (int y = 0; y < height; y++) + { + samples.Slice(y * source.PixelsPerRow, width).Fill(value); + } + } + + float red = (basis == 1 ? 0.25F : 0F) * parameters.IntensityTarget; + float green = (basis == 2 ? 0.25F : 0F) * parameters.IntensityTarget; + float blue = (basis == 3 ? 0.25F : 0F) * parameters.IntensityTarget; + InlineArray3 response = default; + for (int c = 0; c < 3; c++) + { + // Evaluate native equations with independent frozen weights and the mathematical logarithm. + // No production opsin helper or output sample contributes to this reference calculation. + float absorbance = float.MultiplyAddEstimate(weights[(c * 4) + 2], blue, weights[(c * 4) + 3]); + absorbance = float.MultiplyAddEstimate(weights[(c * 4) + 1], green, absorbance); + absorbance = float.MultiplyAddEstimate(weights[c * 4], red, absorbance); + float pre = MathF.Max(absorbance, weights[(c * 4) + 3]); + float gamma = float.MultiplyAddEstimate( + 19.245013259874995F * 0.6931471805599453F, + MathF.Log2(pre + 9.9710635769299145F), + -23.16046239805755F); + + float sensitivity = MathF.Max(gamma / pre, 1e-4F); + response[c] = MathF.Max(absorbance * sensitivity, weights[(c * 4) + 3]); + } + + Assert.True(Butteraugli.OpsinDynamicsImage(configuration, source, parameters, blurred, scratch, transformed)); + for (int c = 0; c < 3; c++) + { + float expected = c == 0 ? response[0] - response[1] : c == 1 ? response[0] + response[1] : response[2]; + ReadOnlySpan actual = c == 0 ? output0 : c == 1 ? output1 : output2; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + Assert.Equal(expected, actual[(y * transformed.PixelsPerRow) + x], responseBound); + } + } + } + + // Reuse the prepared scratch and source allocation. In-place execution must produce the same samples as disjoint output. + Assert.True(Butteraugli.OpsinDynamicsImage(configuration, source, parameters, blurred, scratch, source)); + for (int c = 0; c < 3; c++) + { + ReadOnlySpan input = c == 0 ? source0 : c == 1 ? source1 : source2; + ReadOnlySpan actual = c == 0 ? output0 : c == 1 ? output1 : output2; + ReadOnlySpan smooth = c == 0 ? blurred0 : c == 1 ? blurred1 : blurred2; + for (int i = 0; i < input.Length; i++) + { + int y = i / source.PixelsPerRow; + int x = i % source.PixelsPerRow; + if (y < height && x < width) + { + Assert.Equal(actual[(y * transformed.PixelsPerRow) + x], input[i]); + } + else + { + Assert.Equal(untouched, input[i]); + } + } + + for (int i = 0; i < actual.Length; i++) + { + if (i / transformed.PixelsPerRow >= height || i % transformed.PixelsPerRow >= width) + { + Assert.Equal(untouched, actual[i]); + } + } + + for (int i = 0; i < smooth.Length; i++) + { + if (i / blurred.PixelsPerRow >= height || i % blurred.PixelsPerRow >= width) + { + Assert.Equal(untouched, smooth[i]); + } + } + } + } + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } + } From 252963e615b5d62c7ec35ffea333c1abc3e3ebf5 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Oct 2026 05:31:40 +1000 Subject: [PATCH 320/328] Share unrounded BT.601 arithmetic and bound JPEG XL decoder rows --- .../Common/Helpers/ColorNumerics.cs | 81 +++++++++++ .../JpegColorConverter.YCbCrOperator.cs | 52 ++----- .../Processing/RenderPipeline/YCbCrStage.cs | 130 ++++++++++++------ .../Formats/Jxl/Processing/OpsinImageTests.cs | 69 ++++++++++ 4 files changed, 251 insertions(+), 81 deletions(-) diff --git a/src/ImageSharp/Common/Helpers/ColorNumerics.cs b/src/ImageSharp/Common/Helpers/ColorNumerics.cs index 3db75dcbe4..4922b08cf7 100644 --- a/src/ImageSharp/Common/Helpers/ColorNumerics.cs +++ b/src/ImageSharp/Common/Helpers/ColorNumerics.cs @@ -29,12 +29,93 @@ internal static class ColorNumerics /// public const float MinColorValue = -MaxColorValue; + // BT.601 fixes the red and blue chroma amplitudes. Derive green weights before their single conversion to float. + // These are compile-time expressions; sample conversion uses float arithmetic. + /// The BT.601 red contribution from zero-centered Cr, in the same units as Y. + public const float Bt601RedCrMultiplier = 1.402F; + + /// The magnitude of the BT.601 green contribution from zero-centered Cb. + public const float Bt601GreenCbMultiplier = (float)(0.114 * 1.772 / 0.587); + + /// The magnitude of the BT.601 green contribution from zero-centered Cr. + public const float Bt601GreenCrMultiplier = (float)(0.299 * 1.402 / 0.587); + + /// The BT.601 blue contribution from zero-centered Cb, in the same units as Y. + public const float Bt601BlueCbMultiplier = 1.772F; + /// /// Vector for converting pixel to gray value as specified by /// ITU-R Recommendation BT.709. /// public static readonly Vector4 Bt709 = new(.2126f, .7152f, .0722f, 0.0f); + /// Converts full-range BT.601 components to RGB without rounding, normalization or clipping. + /// The luma component. + /// The zero-centered blue chroma component, in the same units as luma. + /// The zero-centered red chroma component, in the same units as luma. + /// The unrounded red component. + /// The unrounded green component. + /// The unrounded blue component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertBt601YCbCrToRgb(float y, float cb, float cr, out float r, out float g, out float b) + { + // Centering and component scale belong to the caller. Keep the established scalar subtraction order; + // the destination pixel conversion applies its own bounds and final storage rounding. + r = y + (Bt601RedCrMultiplier * cr); + g = y - (Bt601GreenCbMultiplier * cb) - (Bt601GreenCrMultiplier * cr); + b = y + (Bt601BlueCbMultiplier * cb); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertBt601YCbCrToRgb( + Vector128 y, + Vector128 cb, + Vector128 cr, + out Vector128 r, + out Vector128 g, + out Vector128 b) + { + // Each lane contains one position from the three component planes. The nested green multiply-adds retain + // the existing SIMD order: blue chroma contributes first, then red chroma. + r = Vector128.MultiplyAddEstimate(cr, Vector128.Create(Bt601RedCrMultiplier), y); + Vector128 greenBase = Vector128.MultiplyAddEstimate(cb, Vector128.Create(-Bt601GreenCbMultiplier), y); + g = Vector128.MultiplyAddEstimate(cr, Vector128.Create(-Bt601GreenCrMultiplier), greenBase); + b = Vector128.MultiplyAddEstimate(cb, Vector128.Create(Bt601BlueCbMultiplier), y); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertBt601YCbCrToRgb( + Vector256 y, + Vector256 cb, + Vector256 cr, + out Vector256 r, + out Vector256 g, + out Vector256 b) + { + r = Vector256.MultiplyAddEstimate(cr, Vector256.Create(Bt601RedCrMultiplier), y); + Vector256 greenBase = Vector256.MultiplyAddEstimate(cb, Vector256.Create(-Bt601GreenCbMultiplier), y); + g = Vector256.MultiplyAddEstimate(cr, Vector256.Create(-Bt601GreenCrMultiplier), greenBase); + b = Vector256.MultiplyAddEstimate(cb, Vector256.Create(Bt601BlueCbMultiplier), y); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertBt601YCbCrToRgb( + Vector512 y, + Vector512 cb, + Vector512 cr, + out Vector512 r, + out Vector512 g, + out Vector512 b) + { + r = Vector512.MultiplyAddEstimate(cr, Vector512.Create(Bt601RedCrMultiplier), y); + Vector512 greenBase = Vector512.MultiplyAddEstimate(cb, Vector512.Create(-Bt601GreenCbMultiplier), y); + g = Vector512.MultiplyAddEstimate(cr, Vector512.Create(-Bt601GreenCrMultiplier), greenBase); + b = Vector512.MultiplyAddEstimate(cb, Vector512.Create(Bt601BlueCbMultiplier), y); + } + /// /// Gets unrounded, unsaturated luminance using ITU-R Recommendation BT.709. /// diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrOperator.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrOperator.cs index 2b128df038..8589593252 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrOperator.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrOperator.cs @@ -18,22 +18,22 @@ internal abstract partial class JpegColorConverterBase /// /// The BT.601 red contribution from centered Cr. /// - public const float RCrMult = 1.402F; + public const float RCrMult = ColorNumerics.Bt601RedCrMultiplier; /// /// The BT.601 green contribution from centered Cb. /// - public const float GCbMult = (float)(0.114 * 1.772 / 0.587); + public const float GCbMult = ColorNumerics.Bt601GreenCbMultiplier; /// /// The BT.601 green contribution from centered Cr. /// - public const float GCrMult = (float)(0.299 * 1.402 / 0.587); + public const float GCrMult = ColorNumerics.Bt601GreenCrMultiplier; /// /// The BT.601 blue contribution from centered Cb. /// - public const float BCbMult = 1.772F; + public const float BCbMult = ColorNumerics.Bt601BlueCbMultiplier; /// public static JpegColorSpace ColorSpace => JpegColorSpace.YCbCr; @@ -290,15 +290,8 @@ public static void ConvertToRgb(ref float c0, ref float c1, ref float c2, float [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void ConvertToRgb(ref float c0, ref float c1, ref float c2, float halfValue) { - float y = c0; - float cb = c1 - halfValue; - float cr = c2 - halfValue; - - // Each lane is an independent sample. ICC needs the fractional device values, while - // the ordinary JPEG wrapper above applies its existing integer rounding afterward. - c0 = y + (RCrMult * cr); - c1 = y - (GCbMult * cb) - (GCrMult * cr); - c2 = y + (BCbMult * cb); + // JPEG centers stored chroma here. Its outer conversion owns device normalization and storage rounding. + ColorNumerics.ConvertBt601YCbCrToRgb(c0, c1 - halfValue, c2 - halfValue, out c0, out c1, out c2); } /// @@ -330,15 +323,8 @@ public static void ConvertToRgb( [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void ConvertToRgb(ref Vector128 c0, ref Vector128 c1, ref Vector128 c2, Vector128 halfValue) { - Vector128 y = c0; - Vector128 cb = c1 - halfValue; - Vector128 cr = c2 - halfValue; - - // Each lane is an independent sample. ICC needs the fractional device values, while - // the ordinary JPEG wrapper above applies its existing integer rounding afterward. - c0 = Vector128.MultiplyAddEstimate(cr, Vector128.Create(RCrMult), y); - c1 = Vector128.MultiplyAddEstimate(cr, Vector128.Create(-GCrMult), Vector128.MultiplyAddEstimate(cb, Vector128.Create(-GCbMult), y)); - c2 = Vector128.MultiplyAddEstimate(cb, Vector128.Create(BCbMult), y); + // JPEG centers stored chroma here. Its outer conversion owns device normalization and storage rounding. + ColorNumerics.ConvertBt601YCbCrToRgb(c0, c1 - halfValue, c2 - halfValue, out c0, out c1, out c2); } /// @@ -370,15 +356,8 @@ public static void ConvertToRgb( [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void ConvertToRgb(ref Vector256 c0, ref Vector256 c1, ref Vector256 c2, Vector256 halfValue) { - Vector256 y = c0; - Vector256 cb = c1 - halfValue; - Vector256 cr = c2 - halfValue; - - // Each lane is an independent sample. ICC needs the fractional device values, while - // the ordinary JPEG wrapper above applies its existing integer rounding afterward. - c0 = Vector256.MultiplyAddEstimate(cr, Vector256.Create(RCrMult), y); - c1 = Vector256.MultiplyAddEstimate(cr, Vector256.Create(-GCrMult), Vector256.MultiplyAddEstimate(cb, Vector256.Create(-GCbMult), y)); - c2 = Vector256.MultiplyAddEstimate(cb, Vector256.Create(BCbMult), y); + // JPEG centers stored chroma here. Its outer conversion owns device normalization and storage rounding. + ColorNumerics.ConvertBt601YCbCrToRgb(c0, c1 - halfValue, c2 - halfValue, out c0, out c1, out c2); } /// @@ -410,15 +389,8 @@ public static void ConvertToRgb( [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void ConvertToRgb(ref Vector512 c0, ref Vector512 c1, ref Vector512 c2, Vector512 halfValue) { - Vector512 y = c0; - Vector512 cb = c1 - halfValue; - Vector512 cr = c2 - halfValue; - - // Each lane is an independent sample. ICC needs the fractional device values, while - // the ordinary JPEG wrapper above applies its existing integer rounding afterward. - c0 = Vector512.MultiplyAddEstimate(cr, Vector512.Create(RCrMult), y); - c1 = Vector512.MultiplyAddEstimate(cr, Vector512.Create(-GCrMult), Vector512.MultiplyAddEstimate(cb, Vector512.Create(-GCbMult), y)); - c2 = Vector512.MultiplyAddEstimate(cb, Vector512.Create(BCbMult), y); + // JPEG centers stored chroma here. Its outer conversion owns device normalization and storage rounding. + ColorNumerics.ConvertBt601YCbCrToRgb(c0, c1 - halfValue, c2 - halfValue, out c0, out c1, out c2); } /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/YCbCrStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/YCbCrStage.cs index 7c6ad0362b..209facd23e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/YCbCrStage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/YCbCrStage.cs @@ -1,62 +1,110 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Numerics; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; -/// -/// SIMD-based conversion from Y'Cb'Cr to RGB pixel buffers. -/// +/// Converts JPEG XL's centered Cb, Y and Cr rows to RGB in place. +/// The configuration used by the render stage. internal sealed class YCbCrStage(Configuration configuration) : RenderPipelineStageBase(configuration) { + /// The luma offset removed during JPEG XL encoding, with component units relative to RGB white at one. + private const float LumaOffset = 128F / 255F; + /// public override string Name => "YCbCr"; /// - public override void ProcessRow(Buffer2D> inputRows, Buffer2D> outputRows, int xExtraLeft, int xExtraRight, int width, int xPos, int yPos) + public override void ProcessRow( + Buffer2D> inputRows, + Buffer2D> outputRows, + int xExtraLeft, + int xExtraRight, + int width, + int xPos, + int yPos) { - // Vectors for conversion, defined by the ITU - Vector c128 = Vector.Create(128.0f / 255); - Vector crcr = Vector.Create(1.402f); - Vector cgcb = Vector.Create(-0.114f * 1.772f / 0.587f); - Vector cgcr = Vector.Create(-0.299f * 1.402f / 0.587f); - Vector cbcb = Vector.Create(1.772f); - - Span row0 = this.GetInputRow(inputRows, 0, 0); - Span row1 = this.GetInputRow(inputRows, 1, 0); - Span row2 = this.GetInputRow(inputRows, 2, 0); - - // Using refs for better performance - ref float row0Ref = ref MemoryMarshal.GetReference(row0); - ref float row1Ref = ref MemoryMarshal.GetReference(row1); - ref float row2Ref = ref MemoryMarshal.GetReference(row2); - - for (int x = 0; x < width; x += Vector.Count) + // Native stage_ycbcr consumes point-wise samples. The renderer supplies no horizontal halo for this stage. + if (xExtraLeft != 0 || xExtraRight != 0) + { + throw new InvalidOperationException("The YCbCr stage does not consume horizontal border samples."); + } + + Span row0 = this.GetInputRow(inputRows, 0, 0)[..width]; + Span row1 = this.GetInputRow(inputRows, 1, 0)[..width]; + Span row2 = this.GetInputRow(inputRows, 2, 0)[..width]; + ref float first0 = ref MemoryMarshal.GetReference(row0); + ref float first1 = ref MemoryMarshal.GetReference(row1); + ref float first2 = ref MemoryMarshal.GetReference(row2); + int x = 0; + + // JPEG XL stores Cb, Y, Cr, with centered chroma and a biased Y. Restore Y before the shared BT.601 calculation. + // Read all three components before writing R, G, B over those rows. Only full logical blocks enter SIMD; + // narrower registers and the scalar tail complete the row without reading or changing its padded suffix. + if (Vector512.IsHardwareAccelerated) + { + Vector512 offset = Vector512.Create(LumaOffset); + + for (; x <= width - Vector512.Count; x += Vector512.Count) + { + Vector512 cb = Vector512.LoadUnsafe(ref first0, (uint)x); + Vector512 y = Vector512.LoadUnsafe(ref first1, (uint)x) + offset; + Vector512 cr = Vector512.LoadUnsafe(ref first2, (uint)x); + ColorNumerics.ConvertBt601YCbCrToRgb(y, cb, cr, out Vector512 r, out Vector512 g, out Vector512 b); + r.StoreUnsafe(ref first0, (uint)x); + g.StoreUnsafe(ref first1, (uint)x); + b.StoreUnsafe(ref first2, (uint)x); + } + } + + if (Vector256.IsHardwareAccelerated) + { + Vector256 offset = Vector256.Create(LumaOffset); + + for (; x <= width - Vector256.Count; x += Vector256.Count) + { + Vector256 cb = Vector256.LoadUnsafe(ref first0, (uint)x); + Vector256 y = Vector256.LoadUnsafe(ref first1, (uint)x) + offset; + Vector256 cr = Vector256.LoadUnsafe(ref first2, (uint)x); + ColorNumerics.ConvertBt601YCbCrToRgb(y, cb, cr, out Vector256 r, out Vector256 g, out Vector256 b); + r.StoreUnsafe(ref first0, (uint)x); + g.StoreUnsafe(ref first1, (uint)x); + b.StoreUnsafe(ref first2, (uint)x); + } + } + + if (Vector128.IsHardwareAccelerated) + { + Vector128 offset = Vector128.Create(LumaOffset); + + for (; x <= width - Vector128.Count; x += Vector128.Count) + { + Vector128 cb = Vector128.LoadUnsafe(ref first0, (uint)x); + Vector128 y = Vector128.LoadUnsafe(ref first1, (uint)x) + offset; + Vector128 cr = Vector128.LoadUnsafe(ref first2, (uint)x); + ColorNumerics.ConvertBt601YCbCrToRgb(y, cb, cr, out Vector128 r, out Vector128 g, out Vector128 b); + r.StoreUnsafe(ref first0, (uint)x); + g.StoreUnsafe(ref first1, (uint)x); + b.StoreUnsafe(ref first2, (uint)x); + } + } + + for (; x < width; x++) { - // Y'Cb'Cr input vectors - Vector yVec = Vector.LoadUnsafe(ref Unsafe.Add(ref row1Ref, x)) + c128; - Vector cbVec = Vector.LoadUnsafe(ref Unsafe.Add(ref row0Ref, x)); - Vector crVec = Vector.LoadUnsafe(ref Unsafe.Add(ref row2Ref, x)); - - // RGB output vectors - Vector rVec = (crcr * crVec) + yVec; - Vector gVec = (cgcr * crVec) + ((cgcb * cbVec) + yVec); - Vector bVec = (cbcb * cbVec) + yVec; - - // Copying to the output... - rVec.StoreUnsafe(ref Unsafe.Add(ref row0Ref, x)); - gVec.StoreUnsafe(ref Unsafe.Add(ref row1Ref, x)); - bVec.StoreUnsafe(ref Unsafe.Add(ref row2Ref, x)); + float cb = row0[x]; + float y = row1[x] + LumaOffset; + float cr = row2[x]; + ColorNumerics.ConvertBt601YCbCrToRgb(y, cb, cr, out float r, out float g, out float b); + row0[x] = r; + row1[x] = g; + row2[x] = b; } } /// - public override RenderPipelineChannelMode GetChannelMode(int channel) => - channel < 3 - ? RenderPipelineChannelMode.InPlace - : RenderPipelineChannelMode.Ignored; + public override RenderPipelineChannelMode GetChannelMode(int channel) + => channel < 3 ? RenderPipelineChannelMode.InPlace : RenderPipelineChannelMode.Ignored; } diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/OpsinImageTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/OpsinImageTests.cs index abce16e9ce..a8dedfc1b9 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/OpsinImageTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/OpsinImageTests.cs @@ -505,4 +505,73 @@ private static JxlOpsinParameters CreateDyadicOpsinParameters(float intensityTar parameters.Initialize(in matrix, biases, intensityTarget); return parameters; } + + /// Checks centered chroma, luma bias, HDR values and exact in-place YCbCr row bounds at every SIMD width. + [Fact] + public void YCbCrStage_ConvertsCenteredChromaAndPreservesHdrAndPadding() + { + static void RunTest() + { + ReadOnlySpan widths = [1, 3, 17, 33]; + ReadOnlySpan chroma = [0F, 0F, 0.25F, 0F, 0F, 0.5F, 0.25F, 0.5F, -0.25F, -0.5F, 32768F, 0F, 0F, -32768F, 32768F, 32768F]; + const int origin = RenderPipelineStageBase.RenderPipelineXOffset; + const float untouched = -777F; + const float storedY = -128F / 255F; + const float greenCb = (float)(0.114 * 1.772 / 0.587); + const float greenCr = (float)(0.299 * 1.402 / 0.587); + + foreach (int width in widths) + { + int storageWidth = origin + width + 3; + using JxlPlane storage = new(TestEnvironment.Configuration, storageWidth, 4); + Span samples = storage.GetRow(0); + samples.Fill(untouched); + Memory[] table = new Memory[4]; + for (int c = 0; c < table.Length; c++) + { + table[c] = storage.GetRowMemory(c)[..storageWidth]; + if (c < 3) + { + for (int x = 0; x < width; x++) + { + int sample = (x % 8) * 2; + samples[(c * storage.PixelsPerRow) + origin + x] = c == 0 ? chroma[sample] : c == 1 ? storedY : chroma[sample + 1]; + } + } + } + + // The row table borrows the numeric allocation. JPEG XL stores Cb, biased Y and Cr in that order. + using Buffer2D> rows = new(MemoryGroup>.Wrap(table.AsMemory()), 4, 1); + using YCbCrStage stage = new(TestEnvironment.Configuration); + stage.ProcessRow(rows, rows, 0, 0, width, 0, 0); + + for (int c = 0; c < 4; c++) + { + ReadOnlySpan row = samples.Slice(c * storage.PixelsPerRow, storage.PixelsPerRow); + for (int i = 0; i < row.Length; i++) + { + int x = i - origin; + float expected = untouched; + if (c < 3 && x >= 0 && x < width) + { + int sample = (x % 8) * 2; + float cb = chroma[sample]; + float cr = chroma[sample + 1]; + + // Restored Y is exactly zero. Power-of-two chroma keeps each coefficient product exact, + // so the independent BT.601 equations agree with fused and separate float operations. + expected = c == 0 ? 1.402F * cr : c == 1 ? -(greenCb * cb) - (greenCr * cr) : 1.772F * cb; + } + + Assert.Equal(expected, row[i]); + } + } + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } + } From c279edab6f8dc10dd89ed2d5d18486f0248a73aa Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Oct 2026 05:43:33 +1000 Subject: [PATCH 321/328] Share JPEG XL Malta stencils across scalar and SIMD paths --- .../Butteraugli.Malta.Generated.cs | 1650 +++++++++++++++++ .../Butteraugli.Malta.Generated.tt | 282 +++ .../Jxl/Processing/Butteraugli/Butteraugli.cs | 1068 ++++------- src/ImageSharp/ImageSharp.csproj | 9 + .../Jxl/Processing/ButteraugliTests.cs | 278 ++- 5 files changed, 2619 insertions(+), 668 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.Malta.Generated.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.Malta.Generated.tt diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.Malta.Generated.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.Malta.Generated.cs new file mode 100644 index 0000000000..f52b624b16 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.Malta.Generated.cs @@ -0,0 +1,1650 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; + +internal static partial class Butteraugli +{ + /// + /// Evaluates the ordered full Malta rays. Each result belongs to one independent center. + /// + /// The borrowed plane or zero-padded neighborhood, starting at its logical origin. + /// The first center's sample index. Its full four-sample halo and all result lanes must exist. + /// The physical row stride in samples. + /// The sum of the 16 squared ray responses in native order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float MaltaUnitScalar(ReadOnlySpan samples, int index, int stride) + { + ref float source = ref MemoryMarshal.GetReference(samples); + float center = Unsafe.Add(ref source, index); + float sum; + float sample0; + float sample1; + float sample2; + float sample3; + float sample4; + float sample5; + float sample6; + float sample7; + float sample8; + + // Sample offsets describe the ray, not lane positions. Each load keeps adjacent result centers in lane order. + // The nested additions retain native Sum's balanced groups; only successive squared rays use multiply-add. + sample0 = Unsafe.Add(ref source, index - 4); + sample1 = Unsafe.Add(ref source, index - 3); + sample2 = Unsafe.Add(ref source, index - 2); + sample3 = Unsafe.Add(ref source, index - 1); + sample4 = center; + sample5 = Unsafe.Add(ref source, index + 1); + sample6 = Unsafe.Add(ref source, index + 2); + sample7 = Unsafe.Add(ref source, index + 3); + sample8 = Unsafe.Add(ref source, index + 4); + sum = ((((sample0 + sample1) + (sample2 + sample3)) + ((sample4 + sample5) + (sample6 + sample7))) + sample8); + float result = sum * sum; + + sample0 = Unsafe.Add(ref source, index - (4 * stride)); + sample1 = Unsafe.Add(ref source, index - (3 * stride)); + sample2 = Unsafe.Add(ref source, index - (2 * stride)); + sample3 = Unsafe.Add(ref source, index - stride); + sample4 = center; + sample5 = Unsafe.Add(ref source, index + stride); + sample6 = Unsafe.Add(ref source, index + (2 * stride)); + sample7 = Unsafe.Add(ref source, index + (3 * stride)); + sample8 = Unsafe.Add(ref source, index + (4 * stride)); + sum = ((((sample0 + sample1) + (sample2 + sample3)) + ((sample4 + sample5) + (sample6 + sample7))) + sample8); + result = float.MultiplyAddEstimate(sum, sum, result); + + sample0 = Unsafe.Add(ref source, index - (3 * stride) - 3); + sample1 = Unsafe.Add(ref source, index - (2 * stride) - 2); + sample2 = Unsafe.Add(ref source, index - stride - 1); + sample3 = center; + sample4 = Unsafe.Add(ref source, index + stride + 1); + sample5 = Unsafe.Add(ref source, index + (2 * stride) + 2); + sample6 = Unsafe.Add(ref source, index + (3 * stride) + 3); + sum = ((sample0 + sample1) + (sample2 + ((sample3 + sample4) + (sample5 + sample6)))); + result = float.MultiplyAddEstimate(sum, sum, result); + + sample0 = Unsafe.Add(ref source, index - (3 * stride) + 3); + sample1 = Unsafe.Add(ref source, index - (2 * stride) + 2); + sample2 = Unsafe.Add(ref source, index - stride + 1); + sample3 = center; + sample4 = Unsafe.Add(ref source, index + stride - 1); + sample5 = Unsafe.Add(ref source, index + (2 * stride) - 2); + sample6 = Unsafe.Add(ref source, index + (3 * stride) - 3); + sum = ((sample0 + sample1) + (sample2 + ((sample3 + sample4) + (sample5 + sample6)))); + result = float.MultiplyAddEstimate(sum, sum, result); + + sample0 = Unsafe.Add(ref source, index - (4 * stride) + 1); + sample1 = Unsafe.Add(ref source, index - (3 * stride) + 1); + sample2 = Unsafe.Add(ref source, index - (2 * stride) + 1); + sample3 = Unsafe.Add(ref source, index - stride); + sample4 = center; + sample5 = Unsafe.Add(ref source, index + stride); + sample6 = Unsafe.Add(ref source, index + (2 * stride) - 1); + sample7 = Unsafe.Add(ref source, index + (3 * stride) - 1); + sample8 = Unsafe.Add(ref source, index + (4 * stride) - 1); + sum = ((((sample0 + sample1) + (sample2 + sample3)) + ((sample4 + sample5) + (sample6 + sample7))) + sample8); + result = float.MultiplyAddEstimate(sum, sum, result); + + sample0 = Unsafe.Add(ref source, index - (4 * stride) - 1); + sample1 = Unsafe.Add(ref source, index - (3 * stride) - 1); + sample2 = Unsafe.Add(ref source, index - (2 * stride) - 1); + sample3 = Unsafe.Add(ref source, index - stride); + sample4 = center; + sample5 = Unsafe.Add(ref source, index + stride); + sample6 = Unsafe.Add(ref source, index + (2 * stride) + 1); + sample7 = Unsafe.Add(ref source, index + (3 * stride) + 1); + sample8 = Unsafe.Add(ref source, index + (4 * stride) + 1); + sum = ((((sample0 + sample1) + (sample2 + sample3)) + ((sample4 + sample5) + (sample6 + sample7))) + sample8); + result = float.MultiplyAddEstimate(sum, sum, result); + + sample0 = Unsafe.Add(ref source, index - stride - 4); + sample1 = Unsafe.Add(ref source, index - stride - 3); + sample2 = Unsafe.Add(ref source, index - stride - 2); + sample3 = Unsafe.Add(ref source, index - 1); + sample4 = center; + sample5 = Unsafe.Add(ref source, index + 1); + sample6 = Unsafe.Add(ref source, index + stride + 2); + sample7 = Unsafe.Add(ref source, index + stride + 3); + sample8 = Unsafe.Add(ref source, index + stride + 4); + sum = ((((sample0 + sample1) + (sample2 + sample3)) + ((sample4 + sample5) + (sample6 + sample7))) + sample8); + result = float.MultiplyAddEstimate(sum, sum, result); + + sample0 = Unsafe.Add(ref source, index + stride - 4); + sample1 = Unsafe.Add(ref source, index + stride - 3); + sample2 = Unsafe.Add(ref source, index + stride - 2); + sample3 = Unsafe.Add(ref source, index - 1); + sample4 = center; + sample5 = Unsafe.Add(ref source, index + 1); + sample6 = Unsafe.Add(ref source, index - stride + 2); + sample7 = Unsafe.Add(ref source, index - stride + 3); + sample8 = Unsafe.Add(ref source, index - stride + 4); + sum = ((((sample0 + sample1) + (sample2 + sample3)) + ((sample4 + sample5) + (sample6 + sample7))) + sample8); + result = float.MultiplyAddEstimate(sum, sum, result); + + sample0 = Unsafe.Add(ref source, index - (3 * stride) - 2); + sample1 = Unsafe.Add(ref source, index - (2 * stride) - 1); + sample2 = Unsafe.Add(ref source, index - stride - 1); + sample3 = center; + sample4 = Unsafe.Add(ref source, index + stride + 1); + sample5 = Unsafe.Add(ref source, index + (2 * stride) + 1); + sample6 = Unsafe.Add(ref source, index + (3 * stride) + 2); + sum = ((sample0 + sample1) + (sample2 + ((sample3 + sample4) + (sample5 + sample6)))); + result = float.MultiplyAddEstimate(sum, sum, result); + + sample0 = Unsafe.Add(ref source, index - (3 * stride) + 2); + sample1 = Unsafe.Add(ref source, index - (2 * stride) + 1); + sample2 = Unsafe.Add(ref source, index - stride + 1); + sample3 = center; + sample4 = Unsafe.Add(ref source, index + stride - 1); + sample5 = Unsafe.Add(ref source, index + (2 * stride) - 1); + sample6 = Unsafe.Add(ref source, index + (3 * stride) - 2); + sum = ((sample0 + sample1) + (sample2 + ((sample3 + sample4) + (sample5 + sample6)))); + result = float.MultiplyAddEstimate(sum, sum, result); + + sample0 = Unsafe.Add(ref source, index - (2 * stride) - 3); + sample1 = Unsafe.Add(ref source, index - stride - 2); + sample2 = Unsafe.Add(ref source, index - stride - 1); + sample3 = center; + sample4 = Unsafe.Add(ref source, index + stride + 1); + sample5 = Unsafe.Add(ref source, index + stride + 2); + sample6 = Unsafe.Add(ref source, index + (2 * stride) + 3); + sum = ((sample0 + sample1) + (sample2 + ((sample3 + sample4) + (sample5 + sample6)))); + result = float.MultiplyAddEstimate(sum, sum, result); + + sample0 = Unsafe.Add(ref source, index - (2 * stride) + 3); + sample1 = Unsafe.Add(ref source, index - stride + 2); + sample2 = Unsafe.Add(ref source, index - stride + 1); + sample3 = center; + sample4 = Unsafe.Add(ref source, index + stride - 1); + sample5 = Unsafe.Add(ref source, index + stride - 2); + sample6 = Unsafe.Add(ref source, index + (2 * stride) - 3); + sum = ((sample0 + sample1) + (sample2 + ((sample3 + sample4) + (sample5 + sample6)))); + result = float.MultiplyAddEstimate(sum, sum, result); + + sample0 = Unsafe.Add(ref source, index + stride - 4); + sample1 = Unsafe.Add(ref source, index + stride - 3); + sample2 = Unsafe.Add(ref source, index + stride - 2); + sample3 = Unsafe.Add(ref source, index - 1); + sample4 = center; + sample5 = Unsafe.Add(ref source, index + 1); + sample6 = Unsafe.Add(ref source, index - stride + 2); + sample7 = Unsafe.Add(ref source, index - stride + 3); + sample8 = Unsafe.Add(ref source, index - stride + 4); + sum = ((((sample0 + sample1) + (sample2 + sample3)) + ((sample4 + sample5) + (sample6 + sample7))) + sample8); + result = float.MultiplyAddEstimate(sum, sum, result); + + sample0 = Unsafe.Add(ref source, index - stride - 4); + sample1 = Unsafe.Add(ref source, index - stride - 3); + sample2 = Unsafe.Add(ref source, index - stride - 2); + sample3 = Unsafe.Add(ref source, index - 1); + sample4 = center; + sample5 = Unsafe.Add(ref source, index + 1); + sample6 = Unsafe.Add(ref source, index + stride + 2); + sample7 = Unsafe.Add(ref source, index + stride + 3); + sample8 = Unsafe.Add(ref source, index + stride + 4); + sum = ((((sample0 + sample1) + (sample2 + sample3)) + ((sample4 + sample5) + (sample6 + sample7))) + sample8); + result = float.MultiplyAddEstimate(sum, sum, result); + + sample0 = Unsafe.Add(ref source, index - (4 * stride) - 1); + sample1 = Unsafe.Add(ref source, index - (3 * stride) - 1); + sample2 = Unsafe.Add(ref source, index - (2 * stride) - 1); + sample3 = Unsafe.Add(ref source, index - stride); + sample4 = center; + sample5 = Unsafe.Add(ref source, index + stride); + sample6 = Unsafe.Add(ref source, index + (2 * stride) + 1); + sample7 = Unsafe.Add(ref source, index + (3 * stride) + 1); + sample8 = Unsafe.Add(ref source, index + (4 * stride) + 1); + sum = ((((sample0 + sample1) + (sample2 + sample3)) + ((sample4 + sample5) + (sample6 + sample7))) + sample8); + result = float.MultiplyAddEstimate(sum, sum, result); + + sample0 = Unsafe.Add(ref source, index - (4 * stride) + 1); + sample1 = Unsafe.Add(ref source, index - (3 * stride) + 1); + sample2 = Unsafe.Add(ref source, index - (2 * stride) + 1); + sample3 = Unsafe.Add(ref source, index - stride); + sample4 = center; + sample5 = Unsafe.Add(ref source, index + stride); + sample6 = Unsafe.Add(ref source, index + (2 * stride) - 1); + sample7 = Unsafe.Add(ref source, index + (3 * stride) - 1); + sample8 = Unsafe.Add(ref source, index + (4 * stride) - 1); + sum = ((((sample0 + sample1) + (sample2 + sample3)) + ((sample4 + sample5) + (sample6 + sample7))) + sample8); + result = float.MultiplyAddEstimate(sum, sum, result); + + return result; + } + + /// + /// Evaluates the ordered full Malta rays. Each result belongs to one independent center. + /// + /// The borrowed plane or zero-padded neighborhood, starting at its logical origin. + /// The first center's sample index. Its full four-sample halo and all result lanes must exist. + /// The physical row stride in samples. + /// The sum of the 16 squared ray responses in native order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 MaltaUnit128(ReadOnlySpan samples, int index, int stride) + { + ref float source = ref MemoryMarshal.GetReference(samples); + Vector128 center = Vector128.LoadUnsafe(ref source, (uint)(index)); + Vector128 sum; + Vector128 sample0; + Vector128 sample1; + Vector128 sample2; + Vector128 sample3; + Vector128 sample4; + Vector128 sample5; + Vector128 sample6; + Vector128 sample7; + Vector128 sample8; + + // Sample offsets describe the ray, not lane positions. Each load keeps adjacent result centers in lane order. + // The nested additions retain native Sum's balanced groups; only successive squared rays use multiply-add. + sample0 = Vector128.LoadUnsafe(ref source, (uint)(index - 4)); + sample1 = Vector128.LoadUnsafe(ref source, (uint)(index - 3)); + sample2 = Vector128.LoadUnsafe(ref source, (uint)(index - 2)); + sample3 = Vector128.LoadUnsafe(ref source, (uint)(index - 1)); + sample4 = center; + sample5 = Vector128.LoadUnsafe(ref source, (uint)(index + 1)); + sample6 = Vector128.LoadUnsafe(ref source, (uint)(index + 2)); + sample7 = Vector128.LoadUnsafe(ref source, (uint)(index + 3)); + sample8 = Vector128.LoadUnsafe(ref source, (uint)(index + 4)); + sum = ((((sample0 + sample1) + (sample2 + sample3)) + ((sample4 + sample5) + (sample6 + sample7))) + sample8); + Vector128 result = sum * sum; + + sample0 = Vector128.LoadUnsafe(ref source, (uint)(index - (4 * stride))); + sample1 = Vector128.LoadUnsafe(ref source, (uint)(index - (3 * stride))); + sample2 = Vector128.LoadUnsafe(ref source, (uint)(index - (2 * stride))); + sample3 = Vector128.LoadUnsafe(ref source, (uint)(index - stride)); + sample4 = center; + sample5 = Vector128.LoadUnsafe(ref source, (uint)(index + stride)); + sample6 = Vector128.LoadUnsafe(ref source, (uint)(index + (2 * stride))); + sample7 = Vector128.LoadUnsafe(ref source, (uint)(index + (3 * stride))); + sample8 = Vector128.LoadUnsafe(ref source, (uint)(index + (4 * stride))); + sum = ((((sample0 + sample1) + (sample2 + sample3)) + ((sample4 + sample5) + (sample6 + sample7))) + sample8); + result = Vector128.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector128.LoadUnsafe(ref source, (uint)(index - (3 * stride) - 3)); + sample1 = Vector128.LoadUnsafe(ref source, (uint)(index - (2 * stride) - 2)); + sample2 = Vector128.LoadUnsafe(ref source, (uint)(index - stride - 1)); + sample3 = center; + sample4 = Vector128.LoadUnsafe(ref source, (uint)(index + stride + 1)); + sample5 = Vector128.LoadUnsafe(ref source, (uint)(index + (2 * stride) + 2)); + sample6 = Vector128.LoadUnsafe(ref source, (uint)(index + (3 * stride) + 3)); + sum = ((sample0 + sample1) + (sample2 + ((sample3 + sample4) + (sample5 + sample6)))); + result = Vector128.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector128.LoadUnsafe(ref source, (uint)(index - (3 * stride) + 3)); + sample1 = Vector128.LoadUnsafe(ref source, (uint)(index - (2 * stride) + 2)); + sample2 = Vector128.LoadUnsafe(ref source, (uint)(index - stride + 1)); + sample3 = center; + sample4 = Vector128.LoadUnsafe(ref source, (uint)(index + stride - 1)); + sample5 = Vector128.LoadUnsafe(ref source, (uint)(index + (2 * stride) - 2)); + sample6 = Vector128.LoadUnsafe(ref source, (uint)(index + (3 * stride) - 3)); + sum = ((sample0 + sample1) + (sample2 + ((sample3 + sample4) + (sample5 + sample6)))); + result = Vector128.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector128.LoadUnsafe(ref source, (uint)(index - (4 * stride) + 1)); + sample1 = Vector128.LoadUnsafe(ref source, (uint)(index - (3 * stride) + 1)); + sample2 = Vector128.LoadUnsafe(ref source, (uint)(index - (2 * stride) + 1)); + sample3 = Vector128.LoadUnsafe(ref source, (uint)(index - stride)); + sample4 = center; + sample5 = Vector128.LoadUnsafe(ref source, (uint)(index + stride)); + sample6 = Vector128.LoadUnsafe(ref source, (uint)(index + (2 * stride) - 1)); + sample7 = Vector128.LoadUnsafe(ref source, (uint)(index + (3 * stride) - 1)); + sample8 = Vector128.LoadUnsafe(ref source, (uint)(index + (4 * stride) - 1)); + sum = ((((sample0 + sample1) + (sample2 + sample3)) + ((sample4 + sample5) + (sample6 + sample7))) + sample8); + result = Vector128.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector128.LoadUnsafe(ref source, (uint)(index - (4 * stride) - 1)); + sample1 = Vector128.LoadUnsafe(ref source, (uint)(index - (3 * stride) - 1)); + sample2 = Vector128.LoadUnsafe(ref source, (uint)(index - (2 * stride) - 1)); + sample3 = Vector128.LoadUnsafe(ref source, (uint)(index - stride)); + sample4 = center; + sample5 = Vector128.LoadUnsafe(ref source, (uint)(index + stride)); + sample6 = Vector128.LoadUnsafe(ref source, (uint)(index + (2 * stride) + 1)); + sample7 = Vector128.LoadUnsafe(ref source, (uint)(index + (3 * stride) + 1)); + sample8 = Vector128.LoadUnsafe(ref source, (uint)(index + (4 * stride) + 1)); + sum = ((((sample0 + sample1) + (sample2 + sample3)) + ((sample4 + sample5) + (sample6 + sample7))) + sample8); + result = Vector128.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector128.LoadUnsafe(ref source, (uint)(index - stride - 4)); + sample1 = Vector128.LoadUnsafe(ref source, (uint)(index - stride - 3)); + sample2 = Vector128.LoadUnsafe(ref source, (uint)(index - stride - 2)); + sample3 = Vector128.LoadUnsafe(ref source, (uint)(index - 1)); + sample4 = center; + sample5 = Vector128.LoadUnsafe(ref source, (uint)(index + 1)); + sample6 = Vector128.LoadUnsafe(ref source, (uint)(index + stride + 2)); + sample7 = Vector128.LoadUnsafe(ref source, (uint)(index + stride + 3)); + sample8 = Vector128.LoadUnsafe(ref source, (uint)(index + stride + 4)); + sum = ((((sample0 + sample1) + (sample2 + sample3)) + ((sample4 + sample5) + (sample6 + sample7))) + sample8); + result = Vector128.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector128.LoadUnsafe(ref source, (uint)(index + stride - 4)); + sample1 = Vector128.LoadUnsafe(ref source, (uint)(index + stride - 3)); + sample2 = Vector128.LoadUnsafe(ref source, (uint)(index + stride - 2)); + sample3 = Vector128.LoadUnsafe(ref source, (uint)(index - 1)); + sample4 = center; + sample5 = Vector128.LoadUnsafe(ref source, (uint)(index + 1)); + sample6 = Vector128.LoadUnsafe(ref source, (uint)(index - stride + 2)); + sample7 = Vector128.LoadUnsafe(ref source, (uint)(index - stride + 3)); + sample8 = Vector128.LoadUnsafe(ref source, (uint)(index - stride + 4)); + sum = ((((sample0 + sample1) + (sample2 + sample3)) + ((sample4 + sample5) + (sample6 + sample7))) + sample8); + result = Vector128.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector128.LoadUnsafe(ref source, (uint)(index - (3 * stride) - 2)); + sample1 = Vector128.LoadUnsafe(ref source, (uint)(index - (2 * stride) - 1)); + sample2 = Vector128.LoadUnsafe(ref source, (uint)(index - stride - 1)); + sample3 = center; + sample4 = Vector128.LoadUnsafe(ref source, (uint)(index + stride + 1)); + sample5 = Vector128.LoadUnsafe(ref source, (uint)(index + (2 * stride) + 1)); + sample6 = Vector128.LoadUnsafe(ref source, (uint)(index + (3 * stride) + 2)); + sum = ((sample0 + sample1) + (sample2 + ((sample3 + sample4) + (sample5 + sample6)))); + result = Vector128.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector128.LoadUnsafe(ref source, (uint)(index - (3 * stride) + 2)); + sample1 = Vector128.LoadUnsafe(ref source, (uint)(index - (2 * stride) + 1)); + sample2 = Vector128.LoadUnsafe(ref source, (uint)(index - stride + 1)); + sample3 = center; + sample4 = Vector128.LoadUnsafe(ref source, (uint)(index + stride - 1)); + sample5 = Vector128.LoadUnsafe(ref source, (uint)(index + (2 * stride) - 1)); + sample6 = Vector128.LoadUnsafe(ref source, (uint)(index + (3 * stride) - 2)); + sum = ((sample0 + sample1) + (sample2 + ((sample3 + sample4) + (sample5 + sample6)))); + result = Vector128.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector128.LoadUnsafe(ref source, (uint)(index - (2 * stride) - 3)); + sample1 = Vector128.LoadUnsafe(ref source, (uint)(index - stride - 2)); + sample2 = Vector128.LoadUnsafe(ref source, (uint)(index - stride - 1)); + sample3 = center; + sample4 = Vector128.LoadUnsafe(ref source, (uint)(index + stride + 1)); + sample5 = Vector128.LoadUnsafe(ref source, (uint)(index + stride + 2)); + sample6 = Vector128.LoadUnsafe(ref source, (uint)(index + (2 * stride) + 3)); + sum = ((sample0 + sample1) + (sample2 + ((sample3 + sample4) + (sample5 + sample6)))); + result = Vector128.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector128.LoadUnsafe(ref source, (uint)(index - (2 * stride) + 3)); + sample1 = Vector128.LoadUnsafe(ref source, (uint)(index - stride + 2)); + sample2 = Vector128.LoadUnsafe(ref source, (uint)(index - stride + 1)); + sample3 = center; + sample4 = Vector128.LoadUnsafe(ref source, (uint)(index + stride - 1)); + sample5 = Vector128.LoadUnsafe(ref source, (uint)(index + stride - 2)); + sample6 = Vector128.LoadUnsafe(ref source, (uint)(index + (2 * stride) - 3)); + sum = ((sample0 + sample1) + (sample2 + ((sample3 + sample4) + (sample5 + sample6)))); + result = Vector128.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector128.LoadUnsafe(ref source, (uint)(index + stride - 4)); + sample1 = Vector128.LoadUnsafe(ref source, (uint)(index + stride - 3)); + sample2 = Vector128.LoadUnsafe(ref source, (uint)(index + stride - 2)); + sample3 = Vector128.LoadUnsafe(ref source, (uint)(index - 1)); + sample4 = center; + sample5 = Vector128.LoadUnsafe(ref source, (uint)(index + 1)); + sample6 = Vector128.LoadUnsafe(ref source, (uint)(index - stride + 2)); + sample7 = Vector128.LoadUnsafe(ref source, (uint)(index - stride + 3)); + sample8 = Vector128.LoadUnsafe(ref source, (uint)(index - stride + 4)); + sum = ((((sample0 + sample1) + (sample2 + sample3)) + ((sample4 + sample5) + (sample6 + sample7))) + sample8); + result = Vector128.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector128.LoadUnsafe(ref source, (uint)(index - stride - 4)); + sample1 = Vector128.LoadUnsafe(ref source, (uint)(index - stride - 3)); + sample2 = Vector128.LoadUnsafe(ref source, (uint)(index - stride - 2)); + sample3 = Vector128.LoadUnsafe(ref source, (uint)(index - 1)); + sample4 = center; + sample5 = Vector128.LoadUnsafe(ref source, (uint)(index + 1)); + sample6 = Vector128.LoadUnsafe(ref source, (uint)(index + stride + 2)); + sample7 = Vector128.LoadUnsafe(ref source, (uint)(index + stride + 3)); + sample8 = Vector128.LoadUnsafe(ref source, (uint)(index + stride + 4)); + sum = ((((sample0 + sample1) + (sample2 + sample3)) + ((sample4 + sample5) + (sample6 + sample7))) + sample8); + result = Vector128.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector128.LoadUnsafe(ref source, (uint)(index - (4 * stride) - 1)); + sample1 = Vector128.LoadUnsafe(ref source, (uint)(index - (3 * stride) - 1)); + sample2 = Vector128.LoadUnsafe(ref source, (uint)(index - (2 * stride) - 1)); + sample3 = Vector128.LoadUnsafe(ref source, (uint)(index - stride)); + sample4 = center; + sample5 = Vector128.LoadUnsafe(ref source, (uint)(index + stride)); + sample6 = Vector128.LoadUnsafe(ref source, (uint)(index + (2 * stride) + 1)); + sample7 = Vector128.LoadUnsafe(ref source, (uint)(index + (3 * stride) + 1)); + sample8 = Vector128.LoadUnsafe(ref source, (uint)(index + (4 * stride) + 1)); + sum = ((((sample0 + sample1) + (sample2 + sample3)) + ((sample4 + sample5) + (sample6 + sample7))) + sample8); + result = Vector128.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector128.LoadUnsafe(ref source, (uint)(index - (4 * stride) + 1)); + sample1 = Vector128.LoadUnsafe(ref source, (uint)(index - (3 * stride) + 1)); + sample2 = Vector128.LoadUnsafe(ref source, (uint)(index - (2 * stride) + 1)); + sample3 = Vector128.LoadUnsafe(ref source, (uint)(index - stride)); + sample4 = center; + sample5 = Vector128.LoadUnsafe(ref source, (uint)(index + stride)); + sample6 = Vector128.LoadUnsafe(ref source, (uint)(index + (2 * stride) - 1)); + sample7 = Vector128.LoadUnsafe(ref source, (uint)(index + (3 * stride) - 1)); + sample8 = Vector128.LoadUnsafe(ref source, (uint)(index + (4 * stride) - 1)); + sum = ((((sample0 + sample1) + (sample2 + sample3)) + ((sample4 + sample5) + (sample6 + sample7))) + sample8); + result = Vector128.MultiplyAddEstimate(sum, sum, result); + + return result; + } + + /// + /// Evaluates the ordered full Malta rays. Each result belongs to one independent center. + /// + /// The borrowed plane or zero-padded neighborhood, starting at its logical origin. + /// The first center's sample index. Its full four-sample halo and all result lanes must exist. + /// The physical row stride in samples. + /// The sum of the 16 squared ray responses in native order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 MaltaUnit256(ReadOnlySpan samples, int index, int stride) + { + ref float source = ref MemoryMarshal.GetReference(samples); + Vector256 center = Vector256.LoadUnsafe(ref source, (uint)(index)); + Vector256 sum; + Vector256 sample0; + Vector256 sample1; + Vector256 sample2; + Vector256 sample3; + Vector256 sample4; + Vector256 sample5; + Vector256 sample6; + Vector256 sample7; + Vector256 sample8; + + // Sample offsets describe the ray, not lane positions. Each load keeps adjacent result centers in lane order. + // The nested additions retain native Sum's balanced groups; only successive squared rays use multiply-add. + sample0 = Vector256.LoadUnsafe(ref source, (uint)(index - 4)); + sample1 = Vector256.LoadUnsafe(ref source, (uint)(index - 3)); + sample2 = Vector256.LoadUnsafe(ref source, (uint)(index - 2)); + sample3 = Vector256.LoadUnsafe(ref source, (uint)(index - 1)); + sample4 = center; + sample5 = Vector256.LoadUnsafe(ref source, (uint)(index + 1)); + sample6 = Vector256.LoadUnsafe(ref source, (uint)(index + 2)); + sample7 = Vector256.LoadUnsafe(ref source, (uint)(index + 3)); + sample8 = Vector256.LoadUnsafe(ref source, (uint)(index + 4)); + sum = ((((sample0 + sample1) + (sample2 + sample3)) + ((sample4 + sample5) + (sample6 + sample7))) + sample8); + Vector256 result = sum * sum; + + sample0 = Vector256.LoadUnsafe(ref source, (uint)(index - (4 * stride))); + sample1 = Vector256.LoadUnsafe(ref source, (uint)(index - (3 * stride))); + sample2 = Vector256.LoadUnsafe(ref source, (uint)(index - (2 * stride))); + sample3 = Vector256.LoadUnsafe(ref source, (uint)(index - stride)); + sample4 = center; + sample5 = Vector256.LoadUnsafe(ref source, (uint)(index + stride)); + sample6 = Vector256.LoadUnsafe(ref source, (uint)(index + (2 * stride))); + sample7 = Vector256.LoadUnsafe(ref source, (uint)(index + (3 * stride))); + sample8 = Vector256.LoadUnsafe(ref source, (uint)(index + (4 * stride))); + sum = ((((sample0 + sample1) + (sample2 + sample3)) + ((sample4 + sample5) + (sample6 + sample7))) + sample8); + result = Vector256.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector256.LoadUnsafe(ref source, (uint)(index - (3 * stride) - 3)); + sample1 = Vector256.LoadUnsafe(ref source, (uint)(index - (2 * stride) - 2)); + sample2 = Vector256.LoadUnsafe(ref source, (uint)(index - stride - 1)); + sample3 = center; + sample4 = Vector256.LoadUnsafe(ref source, (uint)(index + stride + 1)); + sample5 = Vector256.LoadUnsafe(ref source, (uint)(index + (2 * stride) + 2)); + sample6 = Vector256.LoadUnsafe(ref source, (uint)(index + (3 * stride) + 3)); + sum = ((sample0 + sample1) + (sample2 + ((sample3 + sample4) + (sample5 + sample6)))); + result = Vector256.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector256.LoadUnsafe(ref source, (uint)(index - (3 * stride) + 3)); + sample1 = Vector256.LoadUnsafe(ref source, (uint)(index - (2 * stride) + 2)); + sample2 = Vector256.LoadUnsafe(ref source, (uint)(index - stride + 1)); + sample3 = center; + sample4 = Vector256.LoadUnsafe(ref source, (uint)(index + stride - 1)); + sample5 = Vector256.LoadUnsafe(ref source, (uint)(index + (2 * stride) - 2)); + sample6 = Vector256.LoadUnsafe(ref source, (uint)(index + (3 * stride) - 3)); + sum = ((sample0 + sample1) + (sample2 + ((sample3 + sample4) + (sample5 + sample6)))); + result = Vector256.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector256.LoadUnsafe(ref source, (uint)(index - (4 * stride) + 1)); + sample1 = Vector256.LoadUnsafe(ref source, (uint)(index - (3 * stride) + 1)); + sample2 = Vector256.LoadUnsafe(ref source, (uint)(index - (2 * stride) + 1)); + sample3 = Vector256.LoadUnsafe(ref source, (uint)(index - stride)); + sample4 = center; + sample5 = Vector256.LoadUnsafe(ref source, (uint)(index + stride)); + sample6 = Vector256.LoadUnsafe(ref source, (uint)(index + (2 * stride) - 1)); + sample7 = Vector256.LoadUnsafe(ref source, (uint)(index + (3 * stride) - 1)); + sample8 = Vector256.LoadUnsafe(ref source, (uint)(index + (4 * stride) - 1)); + sum = ((((sample0 + sample1) + (sample2 + sample3)) + ((sample4 + sample5) + (sample6 + sample7))) + sample8); + result = Vector256.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector256.LoadUnsafe(ref source, (uint)(index - (4 * stride) - 1)); + sample1 = Vector256.LoadUnsafe(ref source, (uint)(index - (3 * stride) - 1)); + sample2 = Vector256.LoadUnsafe(ref source, (uint)(index - (2 * stride) - 1)); + sample3 = Vector256.LoadUnsafe(ref source, (uint)(index - stride)); + sample4 = center; + sample5 = Vector256.LoadUnsafe(ref source, (uint)(index + stride)); + sample6 = Vector256.LoadUnsafe(ref source, (uint)(index + (2 * stride) + 1)); + sample7 = Vector256.LoadUnsafe(ref source, (uint)(index + (3 * stride) + 1)); + sample8 = Vector256.LoadUnsafe(ref source, (uint)(index + (4 * stride) + 1)); + sum = ((((sample0 + sample1) + (sample2 + sample3)) + ((sample4 + sample5) + (sample6 + sample7))) + sample8); + result = Vector256.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector256.LoadUnsafe(ref source, (uint)(index - stride - 4)); + sample1 = Vector256.LoadUnsafe(ref source, (uint)(index - stride - 3)); + sample2 = Vector256.LoadUnsafe(ref source, (uint)(index - stride - 2)); + sample3 = Vector256.LoadUnsafe(ref source, (uint)(index - 1)); + sample4 = center; + sample5 = Vector256.LoadUnsafe(ref source, (uint)(index + 1)); + sample6 = Vector256.LoadUnsafe(ref source, (uint)(index + stride + 2)); + sample7 = Vector256.LoadUnsafe(ref source, (uint)(index + stride + 3)); + sample8 = Vector256.LoadUnsafe(ref source, (uint)(index + stride + 4)); + sum = ((((sample0 + sample1) + (sample2 + sample3)) + ((sample4 + sample5) + (sample6 + sample7))) + sample8); + result = Vector256.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector256.LoadUnsafe(ref source, (uint)(index + stride - 4)); + sample1 = Vector256.LoadUnsafe(ref source, (uint)(index + stride - 3)); + sample2 = Vector256.LoadUnsafe(ref source, (uint)(index + stride - 2)); + sample3 = Vector256.LoadUnsafe(ref source, (uint)(index - 1)); + sample4 = center; + sample5 = Vector256.LoadUnsafe(ref source, (uint)(index + 1)); + sample6 = Vector256.LoadUnsafe(ref source, (uint)(index - stride + 2)); + sample7 = Vector256.LoadUnsafe(ref source, (uint)(index - stride + 3)); + sample8 = Vector256.LoadUnsafe(ref source, (uint)(index - stride + 4)); + sum = ((((sample0 + sample1) + (sample2 + sample3)) + ((sample4 + sample5) + (sample6 + sample7))) + sample8); + result = Vector256.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector256.LoadUnsafe(ref source, (uint)(index - (3 * stride) - 2)); + sample1 = Vector256.LoadUnsafe(ref source, (uint)(index - (2 * stride) - 1)); + sample2 = Vector256.LoadUnsafe(ref source, (uint)(index - stride - 1)); + sample3 = center; + sample4 = Vector256.LoadUnsafe(ref source, (uint)(index + stride + 1)); + sample5 = Vector256.LoadUnsafe(ref source, (uint)(index + (2 * stride) + 1)); + sample6 = Vector256.LoadUnsafe(ref source, (uint)(index + (3 * stride) + 2)); + sum = ((sample0 + sample1) + (sample2 + ((sample3 + sample4) + (sample5 + sample6)))); + result = Vector256.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector256.LoadUnsafe(ref source, (uint)(index - (3 * stride) + 2)); + sample1 = Vector256.LoadUnsafe(ref source, (uint)(index - (2 * stride) + 1)); + sample2 = Vector256.LoadUnsafe(ref source, (uint)(index - stride + 1)); + sample3 = center; + sample4 = Vector256.LoadUnsafe(ref source, (uint)(index + stride - 1)); + sample5 = Vector256.LoadUnsafe(ref source, (uint)(index + (2 * stride) - 1)); + sample6 = Vector256.LoadUnsafe(ref source, (uint)(index + (3 * stride) - 2)); + sum = ((sample0 + sample1) + (sample2 + ((sample3 + sample4) + (sample5 + sample6)))); + result = Vector256.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector256.LoadUnsafe(ref source, (uint)(index - (2 * stride) - 3)); + sample1 = Vector256.LoadUnsafe(ref source, (uint)(index - stride - 2)); + sample2 = Vector256.LoadUnsafe(ref source, (uint)(index - stride - 1)); + sample3 = center; + sample4 = Vector256.LoadUnsafe(ref source, (uint)(index + stride + 1)); + sample5 = Vector256.LoadUnsafe(ref source, (uint)(index + stride + 2)); + sample6 = Vector256.LoadUnsafe(ref source, (uint)(index + (2 * stride) + 3)); + sum = ((sample0 + sample1) + (sample2 + ((sample3 + sample4) + (sample5 + sample6)))); + result = Vector256.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector256.LoadUnsafe(ref source, (uint)(index - (2 * stride) + 3)); + sample1 = Vector256.LoadUnsafe(ref source, (uint)(index - stride + 2)); + sample2 = Vector256.LoadUnsafe(ref source, (uint)(index - stride + 1)); + sample3 = center; + sample4 = Vector256.LoadUnsafe(ref source, (uint)(index + stride - 1)); + sample5 = Vector256.LoadUnsafe(ref source, (uint)(index + stride - 2)); + sample6 = Vector256.LoadUnsafe(ref source, (uint)(index + (2 * stride) - 3)); + sum = ((sample0 + sample1) + (sample2 + ((sample3 + sample4) + (sample5 + sample6)))); + result = Vector256.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector256.LoadUnsafe(ref source, (uint)(index + stride - 4)); + sample1 = Vector256.LoadUnsafe(ref source, (uint)(index + stride - 3)); + sample2 = Vector256.LoadUnsafe(ref source, (uint)(index + stride - 2)); + sample3 = Vector256.LoadUnsafe(ref source, (uint)(index - 1)); + sample4 = center; + sample5 = Vector256.LoadUnsafe(ref source, (uint)(index + 1)); + sample6 = Vector256.LoadUnsafe(ref source, (uint)(index - stride + 2)); + sample7 = Vector256.LoadUnsafe(ref source, (uint)(index - stride + 3)); + sample8 = Vector256.LoadUnsafe(ref source, (uint)(index - stride + 4)); + sum = ((((sample0 + sample1) + (sample2 + sample3)) + ((sample4 + sample5) + (sample6 + sample7))) + sample8); + result = Vector256.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector256.LoadUnsafe(ref source, (uint)(index - stride - 4)); + sample1 = Vector256.LoadUnsafe(ref source, (uint)(index - stride - 3)); + sample2 = Vector256.LoadUnsafe(ref source, (uint)(index - stride - 2)); + sample3 = Vector256.LoadUnsafe(ref source, (uint)(index - 1)); + sample4 = center; + sample5 = Vector256.LoadUnsafe(ref source, (uint)(index + 1)); + sample6 = Vector256.LoadUnsafe(ref source, (uint)(index + stride + 2)); + sample7 = Vector256.LoadUnsafe(ref source, (uint)(index + stride + 3)); + sample8 = Vector256.LoadUnsafe(ref source, (uint)(index + stride + 4)); + sum = ((((sample0 + sample1) + (sample2 + sample3)) + ((sample4 + sample5) + (sample6 + sample7))) + sample8); + result = Vector256.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector256.LoadUnsafe(ref source, (uint)(index - (4 * stride) - 1)); + sample1 = Vector256.LoadUnsafe(ref source, (uint)(index - (3 * stride) - 1)); + sample2 = Vector256.LoadUnsafe(ref source, (uint)(index - (2 * stride) - 1)); + sample3 = Vector256.LoadUnsafe(ref source, (uint)(index - stride)); + sample4 = center; + sample5 = Vector256.LoadUnsafe(ref source, (uint)(index + stride)); + sample6 = Vector256.LoadUnsafe(ref source, (uint)(index + (2 * stride) + 1)); + sample7 = Vector256.LoadUnsafe(ref source, (uint)(index + (3 * stride) + 1)); + sample8 = Vector256.LoadUnsafe(ref source, (uint)(index + (4 * stride) + 1)); + sum = ((((sample0 + sample1) + (sample2 + sample3)) + ((sample4 + sample5) + (sample6 + sample7))) + sample8); + result = Vector256.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector256.LoadUnsafe(ref source, (uint)(index - (4 * stride) + 1)); + sample1 = Vector256.LoadUnsafe(ref source, (uint)(index - (3 * stride) + 1)); + sample2 = Vector256.LoadUnsafe(ref source, (uint)(index - (2 * stride) + 1)); + sample3 = Vector256.LoadUnsafe(ref source, (uint)(index - stride)); + sample4 = center; + sample5 = Vector256.LoadUnsafe(ref source, (uint)(index + stride)); + sample6 = Vector256.LoadUnsafe(ref source, (uint)(index + (2 * stride) - 1)); + sample7 = Vector256.LoadUnsafe(ref source, (uint)(index + (3 * stride) - 1)); + sample8 = Vector256.LoadUnsafe(ref source, (uint)(index + (4 * stride) - 1)); + sum = ((((sample0 + sample1) + (sample2 + sample3)) + ((sample4 + sample5) + (sample6 + sample7))) + sample8); + result = Vector256.MultiplyAddEstimate(sum, sum, result); + + return result; + } + + /// + /// Evaluates the ordered full Malta rays. Each result belongs to one independent center. + /// + /// The borrowed plane or zero-padded neighborhood, starting at its logical origin. + /// The first center's sample index. Its full four-sample halo and all result lanes must exist. + /// The physical row stride in samples. + /// The sum of the 16 squared ray responses in native order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 MaltaUnit512(ReadOnlySpan samples, int index, int stride) + { + ref float source = ref MemoryMarshal.GetReference(samples); + Vector512 center = Vector512.LoadUnsafe(ref source, (uint)(index)); + Vector512 sum; + Vector512 sample0; + Vector512 sample1; + Vector512 sample2; + Vector512 sample3; + Vector512 sample4; + Vector512 sample5; + Vector512 sample6; + Vector512 sample7; + Vector512 sample8; + + // Sample offsets describe the ray, not lane positions. Each load keeps adjacent result centers in lane order. + // The nested additions retain native Sum's balanced groups; only successive squared rays use multiply-add. + sample0 = Vector512.LoadUnsafe(ref source, (uint)(index - 4)); + sample1 = Vector512.LoadUnsafe(ref source, (uint)(index - 3)); + sample2 = Vector512.LoadUnsafe(ref source, (uint)(index - 2)); + sample3 = Vector512.LoadUnsafe(ref source, (uint)(index - 1)); + sample4 = center; + sample5 = Vector512.LoadUnsafe(ref source, (uint)(index + 1)); + sample6 = Vector512.LoadUnsafe(ref source, (uint)(index + 2)); + sample7 = Vector512.LoadUnsafe(ref source, (uint)(index + 3)); + sample8 = Vector512.LoadUnsafe(ref source, (uint)(index + 4)); + sum = ((((sample0 + sample1) + (sample2 + sample3)) + ((sample4 + sample5) + (sample6 + sample7))) + sample8); + Vector512 result = sum * sum; + + sample0 = Vector512.LoadUnsafe(ref source, (uint)(index - (4 * stride))); + sample1 = Vector512.LoadUnsafe(ref source, (uint)(index - (3 * stride))); + sample2 = Vector512.LoadUnsafe(ref source, (uint)(index - (2 * stride))); + sample3 = Vector512.LoadUnsafe(ref source, (uint)(index - stride)); + sample4 = center; + sample5 = Vector512.LoadUnsafe(ref source, (uint)(index + stride)); + sample6 = Vector512.LoadUnsafe(ref source, (uint)(index + (2 * stride))); + sample7 = Vector512.LoadUnsafe(ref source, (uint)(index + (3 * stride))); + sample8 = Vector512.LoadUnsafe(ref source, (uint)(index + (4 * stride))); + sum = ((((sample0 + sample1) + (sample2 + sample3)) + ((sample4 + sample5) + (sample6 + sample7))) + sample8); + result = Vector512.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector512.LoadUnsafe(ref source, (uint)(index - (3 * stride) - 3)); + sample1 = Vector512.LoadUnsafe(ref source, (uint)(index - (2 * stride) - 2)); + sample2 = Vector512.LoadUnsafe(ref source, (uint)(index - stride - 1)); + sample3 = center; + sample4 = Vector512.LoadUnsafe(ref source, (uint)(index + stride + 1)); + sample5 = Vector512.LoadUnsafe(ref source, (uint)(index + (2 * stride) + 2)); + sample6 = Vector512.LoadUnsafe(ref source, (uint)(index + (3 * stride) + 3)); + sum = ((sample0 + sample1) + (sample2 + ((sample3 + sample4) + (sample5 + sample6)))); + result = Vector512.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector512.LoadUnsafe(ref source, (uint)(index - (3 * stride) + 3)); + sample1 = Vector512.LoadUnsafe(ref source, (uint)(index - (2 * stride) + 2)); + sample2 = Vector512.LoadUnsafe(ref source, (uint)(index - stride + 1)); + sample3 = center; + sample4 = Vector512.LoadUnsafe(ref source, (uint)(index + stride - 1)); + sample5 = Vector512.LoadUnsafe(ref source, (uint)(index + (2 * stride) - 2)); + sample6 = Vector512.LoadUnsafe(ref source, (uint)(index + (3 * stride) - 3)); + sum = ((sample0 + sample1) + (sample2 + ((sample3 + sample4) + (sample5 + sample6)))); + result = Vector512.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector512.LoadUnsafe(ref source, (uint)(index - (4 * stride) + 1)); + sample1 = Vector512.LoadUnsafe(ref source, (uint)(index - (3 * stride) + 1)); + sample2 = Vector512.LoadUnsafe(ref source, (uint)(index - (2 * stride) + 1)); + sample3 = Vector512.LoadUnsafe(ref source, (uint)(index - stride)); + sample4 = center; + sample5 = Vector512.LoadUnsafe(ref source, (uint)(index + stride)); + sample6 = Vector512.LoadUnsafe(ref source, (uint)(index + (2 * stride) - 1)); + sample7 = Vector512.LoadUnsafe(ref source, (uint)(index + (3 * stride) - 1)); + sample8 = Vector512.LoadUnsafe(ref source, (uint)(index + (4 * stride) - 1)); + sum = ((((sample0 + sample1) + (sample2 + sample3)) + ((sample4 + sample5) + (sample6 + sample7))) + sample8); + result = Vector512.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector512.LoadUnsafe(ref source, (uint)(index - (4 * stride) - 1)); + sample1 = Vector512.LoadUnsafe(ref source, (uint)(index - (3 * stride) - 1)); + sample2 = Vector512.LoadUnsafe(ref source, (uint)(index - (2 * stride) - 1)); + sample3 = Vector512.LoadUnsafe(ref source, (uint)(index - stride)); + sample4 = center; + sample5 = Vector512.LoadUnsafe(ref source, (uint)(index + stride)); + sample6 = Vector512.LoadUnsafe(ref source, (uint)(index + (2 * stride) + 1)); + sample7 = Vector512.LoadUnsafe(ref source, (uint)(index + (3 * stride) + 1)); + sample8 = Vector512.LoadUnsafe(ref source, (uint)(index + (4 * stride) + 1)); + sum = ((((sample0 + sample1) + (sample2 + sample3)) + ((sample4 + sample5) + (sample6 + sample7))) + sample8); + result = Vector512.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector512.LoadUnsafe(ref source, (uint)(index - stride - 4)); + sample1 = Vector512.LoadUnsafe(ref source, (uint)(index - stride - 3)); + sample2 = Vector512.LoadUnsafe(ref source, (uint)(index - stride - 2)); + sample3 = Vector512.LoadUnsafe(ref source, (uint)(index - 1)); + sample4 = center; + sample5 = Vector512.LoadUnsafe(ref source, (uint)(index + 1)); + sample6 = Vector512.LoadUnsafe(ref source, (uint)(index + stride + 2)); + sample7 = Vector512.LoadUnsafe(ref source, (uint)(index + stride + 3)); + sample8 = Vector512.LoadUnsafe(ref source, (uint)(index + stride + 4)); + sum = ((((sample0 + sample1) + (sample2 + sample3)) + ((sample4 + sample5) + (sample6 + sample7))) + sample8); + result = Vector512.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector512.LoadUnsafe(ref source, (uint)(index + stride - 4)); + sample1 = Vector512.LoadUnsafe(ref source, (uint)(index + stride - 3)); + sample2 = Vector512.LoadUnsafe(ref source, (uint)(index + stride - 2)); + sample3 = Vector512.LoadUnsafe(ref source, (uint)(index - 1)); + sample4 = center; + sample5 = Vector512.LoadUnsafe(ref source, (uint)(index + 1)); + sample6 = Vector512.LoadUnsafe(ref source, (uint)(index - stride + 2)); + sample7 = Vector512.LoadUnsafe(ref source, (uint)(index - stride + 3)); + sample8 = Vector512.LoadUnsafe(ref source, (uint)(index - stride + 4)); + sum = ((((sample0 + sample1) + (sample2 + sample3)) + ((sample4 + sample5) + (sample6 + sample7))) + sample8); + result = Vector512.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector512.LoadUnsafe(ref source, (uint)(index - (3 * stride) - 2)); + sample1 = Vector512.LoadUnsafe(ref source, (uint)(index - (2 * stride) - 1)); + sample2 = Vector512.LoadUnsafe(ref source, (uint)(index - stride - 1)); + sample3 = center; + sample4 = Vector512.LoadUnsafe(ref source, (uint)(index + stride + 1)); + sample5 = Vector512.LoadUnsafe(ref source, (uint)(index + (2 * stride) + 1)); + sample6 = Vector512.LoadUnsafe(ref source, (uint)(index + (3 * stride) + 2)); + sum = ((sample0 + sample1) + (sample2 + ((sample3 + sample4) + (sample5 + sample6)))); + result = Vector512.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector512.LoadUnsafe(ref source, (uint)(index - (3 * stride) + 2)); + sample1 = Vector512.LoadUnsafe(ref source, (uint)(index - (2 * stride) + 1)); + sample2 = Vector512.LoadUnsafe(ref source, (uint)(index - stride + 1)); + sample3 = center; + sample4 = Vector512.LoadUnsafe(ref source, (uint)(index + stride - 1)); + sample5 = Vector512.LoadUnsafe(ref source, (uint)(index + (2 * stride) - 1)); + sample6 = Vector512.LoadUnsafe(ref source, (uint)(index + (3 * stride) - 2)); + sum = ((sample0 + sample1) + (sample2 + ((sample3 + sample4) + (sample5 + sample6)))); + result = Vector512.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector512.LoadUnsafe(ref source, (uint)(index - (2 * stride) - 3)); + sample1 = Vector512.LoadUnsafe(ref source, (uint)(index - stride - 2)); + sample2 = Vector512.LoadUnsafe(ref source, (uint)(index - stride - 1)); + sample3 = center; + sample4 = Vector512.LoadUnsafe(ref source, (uint)(index + stride + 1)); + sample5 = Vector512.LoadUnsafe(ref source, (uint)(index + stride + 2)); + sample6 = Vector512.LoadUnsafe(ref source, (uint)(index + (2 * stride) + 3)); + sum = ((sample0 + sample1) + (sample2 + ((sample3 + sample4) + (sample5 + sample6)))); + result = Vector512.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector512.LoadUnsafe(ref source, (uint)(index - (2 * stride) + 3)); + sample1 = Vector512.LoadUnsafe(ref source, (uint)(index - stride + 2)); + sample2 = Vector512.LoadUnsafe(ref source, (uint)(index - stride + 1)); + sample3 = center; + sample4 = Vector512.LoadUnsafe(ref source, (uint)(index + stride - 1)); + sample5 = Vector512.LoadUnsafe(ref source, (uint)(index + stride - 2)); + sample6 = Vector512.LoadUnsafe(ref source, (uint)(index + (2 * stride) - 3)); + sum = ((sample0 + sample1) + (sample2 + ((sample3 + sample4) + (sample5 + sample6)))); + result = Vector512.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector512.LoadUnsafe(ref source, (uint)(index + stride - 4)); + sample1 = Vector512.LoadUnsafe(ref source, (uint)(index + stride - 3)); + sample2 = Vector512.LoadUnsafe(ref source, (uint)(index + stride - 2)); + sample3 = Vector512.LoadUnsafe(ref source, (uint)(index - 1)); + sample4 = center; + sample5 = Vector512.LoadUnsafe(ref source, (uint)(index + 1)); + sample6 = Vector512.LoadUnsafe(ref source, (uint)(index - stride + 2)); + sample7 = Vector512.LoadUnsafe(ref source, (uint)(index - stride + 3)); + sample8 = Vector512.LoadUnsafe(ref source, (uint)(index - stride + 4)); + sum = ((((sample0 + sample1) + (sample2 + sample3)) + ((sample4 + sample5) + (sample6 + sample7))) + sample8); + result = Vector512.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector512.LoadUnsafe(ref source, (uint)(index - stride - 4)); + sample1 = Vector512.LoadUnsafe(ref source, (uint)(index - stride - 3)); + sample2 = Vector512.LoadUnsafe(ref source, (uint)(index - stride - 2)); + sample3 = Vector512.LoadUnsafe(ref source, (uint)(index - 1)); + sample4 = center; + sample5 = Vector512.LoadUnsafe(ref source, (uint)(index + 1)); + sample6 = Vector512.LoadUnsafe(ref source, (uint)(index + stride + 2)); + sample7 = Vector512.LoadUnsafe(ref source, (uint)(index + stride + 3)); + sample8 = Vector512.LoadUnsafe(ref source, (uint)(index + stride + 4)); + sum = ((((sample0 + sample1) + (sample2 + sample3)) + ((sample4 + sample5) + (sample6 + sample7))) + sample8); + result = Vector512.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector512.LoadUnsafe(ref source, (uint)(index - (4 * stride) - 1)); + sample1 = Vector512.LoadUnsafe(ref source, (uint)(index - (3 * stride) - 1)); + sample2 = Vector512.LoadUnsafe(ref source, (uint)(index - (2 * stride) - 1)); + sample3 = Vector512.LoadUnsafe(ref source, (uint)(index - stride)); + sample4 = center; + sample5 = Vector512.LoadUnsafe(ref source, (uint)(index + stride)); + sample6 = Vector512.LoadUnsafe(ref source, (uint)(index + (2 * stride) + 1)); + sample7 = Vector512.LoadUnsafe(ref source, (uint)(index + (3 * stride) + 1)); + sample8 = Vector512.LoadUnsafe(ref source, (uint)(index + (4 * stride) + 1)); + sum = ((((sample0 + sample1) + (sample2 + sample3)) + ((sample4 + sample5) + (sample6 + sample7))) + sample8); + result = Vector512.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector512.LoadUnsafe(ref source, (uint)(index - (4 * stride) + 1)); + sample1 = Vector512.LoadUnsafe(ref source, (uint)(index - (3 * stride) + 1)); + sample2 = Vector512.LoadUnsafe(ref source, (uint)(index - (2 * stride) + 1)); + sample3 = Vector512.LoadUnsafe(ref source, (uint)(index - stride)); + sample4 = center; + sample5 = Vector512.LoadUnsafe(ref source, (uint)(index + stride)); + sample6 = Vector512.LoadUnsafe(ref source, (uint)(index + (2 * stride) - 1)); + sample7 = Vector512.LoadUnsafe(ref source, (uint)(index + (3 * stride) - 1)); + sample8 = Vector512.LoadUnsafe(ref source, (uint)(index + (4 * stride) - 1)); + sum = ((((sample0 + sample1) + (sample2 + sample3)) + ((sample4 + sample5) + (sample6 + sample7))) + sample8); + result = Vector512.MultiplyAddEstimate(sum, sum, result); + + return result; + } + + /// + /// Accumulates full Malta responses over the prepared difference plane. + /// + /// The borrowed prepared differences at the plane's logical origin. + /// The logical width, at least eight samples as established by comparison setup. + /// The logical height, at least eight rows as established by comparison setup. + /// The physical difference-plane stride in samples. + /// The existing accumulation storage, at the plane's logical origin. + /// The physical output stride in samples. + /// The reusable 81-sample zero-padded neighborhood. + private static void AccumulateMalta( + ReadOnlySpan differences, + int width, + int height, + int differenceStride, + Span output, + int outputStride, + Span border) + { + // Comparison setup expands small inputs to at least 8 by 8. These top and bottom strips contain every center + // whose four-row halo crosses an edge. Their scalar kernel reads the same 9-by-9 zero extension as native. + int y = 0; + for (; y < 4; y++) + { + Span outputRow = output.Slice(y * outputStride, width); + for (int x = 0; x < width; x++) + { + PrepareMaltaBorder(differences, width, height, differenceStride, x, y, border); + outputRow[x] += MaltaUnitScalar(border, 40, 9); + } + } + + for (; y < height - 4; y++) + { + ref float outputRow = ref MemoryMarshal.GetReference(output.Slice(y * outputStride, width)); + int rowIndex = y * differenceStride; + int x = 0; + for (; x < 4; x++) + { + PrepareMaltaBorder(differences, width, height, differenceStride, x, y, border); + Unsafe.Add(ref outputRow, x) += MaltaUnitScalar(border, 40, 9); + } + + // Loads are unaligned. Start at the first real interior center rather than reserving an alignment strip. + // The rightmost lane and its four-column halo must remain inside the logical width, not row padding. + if (Vector512.IsHardwareAccelerated) + { + for (; x <= width - 4 - Vector512.Count; x += Vector512.Count) + { + Vector512 previous = Vector512.LoadUnsafe(ref outputRow, (uint)x); + Vector512 response = MaltaUnit512(differences, rowIndex + x, differenceStride); + (previous + response).StoreUnsafe(ref outputRow, (uint)x); + } + } + + if (Vector256.IsHardwareAccelerated) + { + for (; x <= width - 4 - Vector256.Count; x += Vector256.Count) + { + Vector256 previous = Vector256.LoadUnsafe(ref outputRow, (uint)x); + Vector256 response = MaltaUnit256(differences, rowIndex + x, differenceStride); + (previous + response).StoreUnsafe(ref outputRow, (uint)x); + } + } + + if (Vector128.IsHardwareAccelerated) + { + for (; x <= width - 4 - Vector128.Count; x += Vector128.Count) + { + Vector128 previous = Vector128.LoadUnsafe(ref outputRow, (uint)x); + Vector128 response = MaltaUnit128(differences, rowIndex + x, differenceStride); + (previous + response).StoreUnsafe(ref outputRow, (uint)x); + } + } + + for (; x < width - 4; x++) + { + Unsafe.Add(ref outputRow, x) += MaltaUnitScalar(differences, rowIndex + x, differenceStride); + } + + for (; x < width; x++) + { + PrepareMaltaBorder(differences, width, height, differenceStride, x, y, border); + Unsafe.Add(ref outputRow, x) += MaltaUnitScalar(border, 40, 9); + } + } + + for (; y < height; y++) + { + Span outputRow = output.Slice(y * outputStride, width); + for (int x = 0; x < width; x++) + { + PrepareMaltaBorder(differences, width, height, differenceStride, x, y, border); + outputRow[x] += MaltaUnitScalar(border, 40, 9); + } + } + } + + /// + /// Evaluates the ordered low-frequency Malta rays. Each result belongs to one independent center. + /// + /// The borrowed plane or zero-padded neighborhood, starting at its logical origin. + /// The first center's sample index. Its full four-sample halo and all result lanes must exist. + /// The physical row stride in samples. + /// The sum of the 16 squared ray responses in native order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float MaltaUnitLfScalar(ReadOnlySpan samples, int index, int stride) + { + ref float source = ref MemoryMarshal.GetReference(samples); + float center = Unsafe.Add(ref source, index); + float sum; + float sample0; + float sample1; + float sample2; + float sample3; + float sample4; + + // Sample offsets describe the ray, not lane positions. Each load keeps adjacent result centers in lane order. + // The nested additions retain native Sum's balanced groups; only successive squared rays use multiply-add. + sample0 = Unsafe.Add(ref source, index - 4); + sample1 = Unsafe.Add(ref source, index - 2); + sample2 = center; + sample3 = Unsafe.Add(ref source, index + 2); + sample4 = Unsafe.Add(ref source, index + 4); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + float result = sum * sum; + + sample0 = Unsafe.Add(ref source, index - (4 * stride)); + sample1 = Unsafe.Add(ref source, index - (2 * stride)); + sample2 = center; + sample3 = Unsafe.Add(ref source, index + (2 * stride)); + sample4 = Unsafe.Add(ref source, index + (4 * stride)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = float.MultiplyAddEstimate(sum, sum, result); + + sample0 = Unsafe.Add(ref source, index - (3 * stride) - 3); + sample1 = Unsafe.Add(ref source, index - (2 * stride) - 2); + sample2 = center; + sample3 = Unsafe.Add(ref source, index + (2 * stride) + 2); + sample4 = Unsafe.Add(ref source, index + (3 * stride) + 3); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = float.MultiplyAddEstimate(sum, sum, result); + + sample0 = Unsafe.Add(ref source, index - (3 * stride) + 3); + sample1 = Unsafe.Add(ref source, index - (2 * stride) + 2); + sample2 = center; + sample3 = Unsafe.Add(ref source, index + (2 * stride) - 2); + sample4 = Unsafe.Add(ref source, index + (3 * stride) - 3); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = float.MultiplyAddEstimate(sum, sum, result); + + sample0 = Unsafe.Add(ref source, index - (4 * stride) + 1); + sample1 = Unsafe.Add(ref source, index - (2 * stride) + 1); + sample2 = center; + sample3 = Unsafe.Add(ref source, index + (2 * stride) - 1); + sample4 = Unsafe.Add(ref source, index + (4 * stride) - 1); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = float.MultiplyAddEstimate(sum, sum, result); + + sample0 = Unsafe.Add(ref source, index - (4 * stride) - 1); + sample1 = Unsafe.Add(ref source, index - (2 * stride) - 1); + sample2 = center; + sample3 = Unsafe.Add(ref source, index + (2 * stride) + 1); + sample4 = Unsafe.Add(ref source, index + (4 * stride) + 1); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = float.MultiplyAddEstimate(sum, sum, result); + + sample0 = Unsafe.Add(ref source, index - stride - 4); + sample1 = Unsafe.Add(ref source, index - stride - 2); + sample2 = center; + sample3 = Unsafe.Add(ref source, index + stride + 2); + sample4 = Unsafe.Add(ref source, index + stride + 4); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = float.MultiplyAddEstimate(sum, sum, result); + + sample0 = Unsafe.Add(ref source, index + stride - 4); + sample1 = Unsafe.Add(ref source, index + stride - 2); + sample2 = center; + sample3 = Unsafe.Add(ref source, index - stride + 2); + sample4 = Unsafe.Add(ref source, index - stride + 4); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = float.MultiplyAddEstimate(sum, sum, result); + + sample0 = Unsafe.Add(ref source, index - (3 * stride) - 2); + sample1 = Unsafe.Add(ref source, index - (2 * stride) - 1); + sample2 = center; + sample3 = Unsafe.Add(ref source, index + (2 * stride) + 1); + sample4 = Unsafe.Add(ref source, index + (3 * stride) + 2); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = float.MultiplyAddEstimate(sum, sum, result); + + sample0 = Unsafe.Add(ref source, index - (3 * stride) + 2); + sample1 = Unsafe.Add(ref source, index - (2 * stride) + 1); + sample2 = center; + sample3 = Unsafe.Add(ref source, index + (2 * stride) - 1); + sample4 = Unsafe.Add(ref source, index + (3 * stride) - 2); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = float.MultiplyAddEstimate(sum, sum, result); + + sample0 = Unsafe.Add(ref source, index - (2 * stride) - 3); + sample1 = Unsafe.Add(ref source, index - stride - 2); + sample2 = center; + sample3 = Unsafe.Add(ref source, index + stride + 2); + sample4 = Unsafe.Add(ref source, index + (2 * stride) + 3); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = float.MultiplyAddEstimate(sum, sum, result); + + sample0 = Unsafe.Add(ref source, index - (2 * stride) + 3); + sample1 = Unsafe.Add(ref source, index - stride + 2); + sample2 = center; + sample3 = Unsafe.Add(ref source, index + stride - 2); + sample4 = Unsafe.Add(ref source, index + (2 * stride) - 3); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = float.MultiplyAddEstimate(sum, sum, result); + + sample0 = Unsafe.Add(ref source, index + (2 * stride) - 4); + sample1 = Unsafe.Add(ref source, index + stride - 2); + sample2 = center; + sample3 = Unsafe.Add(ref source, index - stride + 2); + sample4 = Unsafe.Add(ref source, index - (2 * stride) + 4); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = float.MultiplyAddEstimate(sum, sum, result); + + sample0 = Unsafe.Add(ref source, index - (2 * stride) - 4); + sample1 = Unsafe.Add(ref source, index - stride - 2); + sample2 = center; + sample3 = Unsafe.Add(ref source, index + stride + 2); + sample4 = Unsafe.Add(ref source, index + (2 * stride) + 4); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = float.MultiplyAddEstimate(sum, sum, result); + + sample0 = Unsafe.Add(ref source, index - (4 * stride) - 2); + sample1 = Unsafe.Add(ref source, index - (2 * stride) - 1); + sample2 = center; + sample3 = Unsafe.Add(ref source, index + (2 * stride) + 1); + sample4 = Unsafe.Add(ref source, index + (4 * stride) + 2); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = float.MultiplyAddEstimate(sum, sum, result); + + sample0 = Unsafe.Add(ref source, index - (4 * stride) + 2); + sample1 = Unsafe.Add(ref source, index - (2 * stride) + 1); + sample2 = center; + sample3 = Unsafe.Add(ref source, index + (2 * stride) - 1); + sample4 = Unsafe.Add(ref source, index + (4 * stride) - 2); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = float.MultiplyAddEstimate(sum, sum, result); + + return result; + } + + /// + /// Evaluates the ordered low-frequency Malta rays. Each result belongs to one independent center. + /// + /// The borrowed plane or zero-padded neighborhood, starting at its logical origin. + /// The first center's sample index. Its full four-sample halo and all result lanes must exist. + /// The physical row stride in samples. + /// The sum of the 16 squared ray responses in native order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 MaltaUnitLf128(ReadOnlySpan samples, int index, int stride) + { + ref float source = ref MemoryMarshal.GetReference(samples); + Vector128 center = Vector128.LoadUnsafe(ref source, (uint)(index)); + Vector128 sum; + Vector128 sample0; + Vector128 sample1; + Vector128 sample2; + Vector128 sample3; + Vector128 sample4; + + // Sample offsets describe the ray, not lane positions. Each load keeps adjacent result centers in lane order. + // The nested additions retain native Sum's balanced groups; only successive squared rays use multiply-add. + sample0 = Vector128.LoadUnsafe(ref source, (uint)(index - 4)); + sample1 = Vector128.LoadUnsafe(ref source, (uint)(index - 2)); + sample2 = center; + sample3 = Vector128.LoadUnsafe(ref source, (uint)(index + 2)); + sample4 = Vector128.LoadUnsafe(ref source, (uint)(index + 4)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + Vector128 result = sum * sum; + + sample0 = Vector128.LoadUnsafe(ref source, (uint)(index - (4 * stride))); + sample1 = Vector128.LoadUnsafe(ref source, (uint)(index - (2 * stride))); + sample2 = center; + sample3 = Vector128.LoadUnsafe(ref source, (uint)(index + (2 * stride))); + sample4 = Vector128.LoadUnsafe(ref source, (uint)(index + (4 * stride))); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector128.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector128.LoadUnsafe(ref source, (uint)(index - (3 * stride) - 3)); + sample1 = Vector128.LoadUnsafe(ref source, (uint)(index - (2 * stride) - 2)); + sample2 = center; + sample3 = Vector128.LoadUnsafe(ref source, (uint)(index + (2 * stride) + 2)); + sample4 = Vector128.LoadUnsafe(ref source, (uint)(index + (3 * stride) + 3)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector128.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector128.LoadUnsafe(ref source, (uint)(index - (3 * stride) + 3)); + sample1 = Vector128.LoadUnsafe(ref source, (uint)(index - (2 * stride) + 2)); + sample2 = center; + sample3 = Vector128.LoadUnsafe(ref source, (uint)(index + (2 * stride) - 2)); + sample4 = Vector128.LoadUnsafe(ref source, (uint)(index + (3 * stride) - 3)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector128.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector128.LoadUnsafe(ref source, (uint)(index - (4 * stride) + 1)); + sample1 = Vector128.LoadUnsafe(ref source, (uint)(index - (2 * stride) + 1)); + sample2 = center; + sample3 = Vector128.LoadUnsafe(ref source, (uint)(index + (2 * stride) - 1)); + sample4 = Vector128.LoadUnsafe(ref source, (uint)(index + (4 * stride) - 1)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector128.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector128.LoadUnsafe(ref source, (uint)(index - (4 * stride) - 1)); + sample1 = Vector128.LoadUnsafe(ref source, (uint)(index - (2 * stride) - 1)); + sample2 = center; + sample3 = Vector128.LoadUnsafe(ref source, (uint)(index + (2 * stride) + 1)); + sample4 = Vector128.LoadUnsafe(ref source, (uint)(index + (4 * stride) + 1)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector128.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector128.LoadUnsafe(ref source, (uint)(index - stride - 4)); + sample1 = Vector128.LoadUnsafe(ref source, (uint)(index - stride - 2)); + sample2 = center; + sample3 = Vector128.LoadUnsafe(ref source, (uint)(index + stride + 2)); + sample4 = Vector128.LoadUnsafe(ref source, (uint)(index + stride + 4)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector128.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector128.LoadUnsafe(ref source, (uint)(index + stride - 4)); + sample1 = Vector128.LoadUnsafe(ref source, (uint)(index + stride - 2)); + sample2 = center; + sample3 = Vector128.LoadUnsafe(ref source, (uint)(index - stride + 2)); + sample4 = Vector128.LoadUnsafe(ref source, (uint)(index - stride + 4)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector128.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector128.LoadUnsafe(ref source, (uint)(index - (3 * stride) - 2)); + sample1 = Vector128.LoadUnsafe(ref source, (uint)(index - (2 * stride) - 1)); + sample2 = center; + sample3 = Vector128.LoadUnsafe(ref source, (uint)(index + (2 * stride) + 1)); + sample4 = Vector128.LoadUnsafe(ref source, (uint)(index + (3 * stride) + 2)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector128.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector128.LoadUnsafe(ref source, (uint)(index - (3 * stride) + 2)); + sample1 = Vector128.LoadUnsafe(ref source, (uint)(index - (2 * stride) + 1)); + sample2 = center; + sample3 = Vector128.LoadUnsafe(ref source, (uint)(index + (2 * stride) - 1)); + sample4 = Vector128.LoadUnsafe(ref source, (uint)(index + (3 * stride) - 2)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector128.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector128.LoadUnsafe(ref source, (uint)(index - (2 * stride) - 3)); + sample1 = Vector128.LoadUnsafe(ref source, (uint)(index - stride - 2)); + sample2 = center; + sample3 = Vector128.LoadUnsafe(ref source, (uint)(index + stride + 2)); + sample4 = Vector128.LoadUnsafe(ref source, (uint)(index + (2 * stride) + 3)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector128.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector128.LoadUnsafe(ref source, (uint)(index - (2 * stride) + 3)); + sample1 = Vector128.LoadUnsafe(ref source, (uint)(index - stride + 2)); + sample2 = center; + sample3 = Vector128.LoadUnsafe(ref source, (uint)(index + stride - 2)); + sample4 = Vector128.LoadUnsafe(ref source, (uint)(index + (2 * stride) - 3)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector128.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector128.LoadUnsafe(ref source, (uint)(index + (2 * stride) - 4)); + sample1 = Vector128.LoadUnsafe(ref source, (uint)(index + stride - 2)); + sample2 = center; + sample3 = Vector128.LoadUnsafe(ref source, (uint)(index - stride + 2)); + sample4 = Vector128.LoadUnsafe(ref source, (uint)(index - (2 * stride) + 4)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector128.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector128.LoadUnsafe(ref source, (uint)(index - (2 * stride) - 4)); + sample1 = Vector128.LoadUnsafe(ref source, (uint)(index - stride - 2)); + sample2 = center; + sample3 = Vector128.LoadUnsafe(ref source, (uint)(index + stride + 2)); + sample4 = Vector128.LoadUnsafe(ref source, (uint)(index + (2 * stride) + 4)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector128.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector128.LoadUnsafe(ref source, (uint)(index - (4 * stride) - 2)); + sample1 = Vector128.LoadUnsafe(ref source, (uint)(index - (2 * stride) - 1)); + sample2 = center; + sample3 = Vector128.LoadUnsafe(ref source, (uint)(index + (2 * stride) + 1)); + sample4 = Vector128.LoadUnsafe(ref source, (uint)(index + (4 * stride) + 2)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector128.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector128.LoadUnsafe(ref source, (uint)(index - (4 * stride) + 2)); + sample1 = Vector128.LoadUnsafe(ref source, (uint)(index - (2 * stride) + 1)); + sample2 = center; + sample3 = Vector128.LoadUnsafe(ref source, (uint)(index + (2 * stride) - 1)); + sample4 = Vector128.LoadUnsafe(ref source, (uint)(index + (4 * stride) - 2)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector128.MultiplyAddEstimate(sum, sum, result); + + return result; + } + + /// + /// Evaluates the ordered low-frequency Malta rays. Each result belongs to one independent center. + /// + /// The borrowed plane or zero-padded neighborhood, starting at its logical origin. + /// The first center's sample index. Its full four-sample halo and all result lanes must exist. + /// The physical row stride in samples. + /// The sum of the 16 squared ray responses in native order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 MaltaUnitLf256(ReadOnlySpan samples, int index, int stride) + { + ref float source = ref MemoryMarshal.GetReference(samples); + Vector256 center = Vector256.LoadUnsafe(ref source, (uint)(index)); + Vector256 sum; + Vector256 sample0; + Vector256 sample1; + Vector256 sample2; + Vector256 sample3; + Vector256 sample4; + + // Sample offsets describe the ray, not lane positions. Each load keeps adjacent result centers in lane order. + // The nested additions retain native Sum's balanced groups; only successive squared rays use multiply-add. + sample0 = Vector256.LoadUnsafe(ref source, (uint)(index - 4)); + sample1 = Vector256.LoadUnsafe(ref source, (uint)(index - 2)); + sample2 = center; + sample3 = Vector256.LoadUnsafe(ref source, (uint)(index + 2)); + sample4 = Vector256.LoadUnsafe(ref source, (uint)(index + 4)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + Vector256 result = sum * sum; + + sample0 = Vector256.LoadUnsafe(ref source, (uint)(index - (4 * stride))); + sample1 = Vector256.LoadUnsafe(ref source, (uint)(index - (2 * stride))); + sample2 = center; + sample3 = Vector256.LoadUnsafe(ref source, (uint)(index + (2 * stride))); + sample4 = Vector256.LoadUnsafe(ref source, (uint)(index + (4 * stride))); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector256.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector256.LoadUnsafe(ref source, (uint)(index - (3 * stride) - 3)); + sample1 = Vector256.LoadUnsafe(ref source, (uint)(index - (2 * stride) - 2)); + sample2 = center; + sample3 = Vector256.LoadUnsafe(ref source, (uint)(index + (2 * stride) + 2)); + sample4 = Vector256.LoadUnsafe(ref source, (uint)(index + (3 * stride) + 3)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector256.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector256.LoadUnsafe(ref source, (uint)(index - (3 * stride) + 3)); + sample1 = Vector256.LoadUnsafe(ref source, (uint)(index - (2 * stride) + 2)); + sample2 = center; + sample3 = Vector256.LoadUnsafe(ref source, (uint)(index + (2 * stride) - 2)); + sample4 = Vector256.LoadUnsafe(ref source, (uint)(index + (3 * stride) - 3)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector256.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector256.LoadUnsafe(ref source, (uint)(index - (4 * stride) + 1)); + sample1 = Vector256.LoadUnsafe(ref source, (uint)(index - (2 * stride) + 1)); + sample2 = center; + sample3 = Vector256.LoadUnsafe(ref source, (uint)(index + (2 * stride) - 1)); + sample4 = Vector256.LoadUnsafe(ref source, (uint)(index + (4 * stride) - 1)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector256.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector256.LoadUnsafe(ref source, (uint)(index - (4 * stride) - 1)); + sample1 = Vector256.LoadUnsafe(ref source, (uint)(index - (2 * stride) - 1)); + sample2 = center; + sample3 = Vector256.LoadUnsafe(ref source, (uint)(index + (2 * stride) + 1)); + sample4 = Vector256.LoadUnsafe(ref source, (uint)(index + (4 * stride) + 1)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector256.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector256.LoadUnsafe(ref source, (uint)(index - stride - 4)); + sample1 = Vector256.LoadUnsafe(ref source, (uint)(index - stride - 2)); + sample2 = center; + sample3 = Vector256.LoadUnsafe(ref source, (uint)(index + stride + 2)); + sample4 = Vector256.LoadUnsafe(ref source, (uint)(index + stride + 4)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector256.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector256.LoadUnsafe(ref source, (uint)(index + stride - 4)); + sample1 = Vector256.LoadUnsafe(ref source, (uint)(index + stride - 2)); + sample2 = center; + sample3 = Vector256.LoadUnsafe(ref source, (uint)(index - stride + 2)); + sample4 = Vector256.LoadUnsafe(ref source, (uint)(index - stride + 4)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector256.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector256.LoadUnsafe(ref source, (uint)(index - (3 * stride) - 2)); + sample1 = Vector256.LoadUnsafe(ref source, (uint)(index - (2 * stride) - 1)); + sample2 = center; + sample3 = Vector256.LoadUnsafe(ref source, (uint)(index + (2 * stride) + 1)); + sample4 = Vector256.LoadUnsafe(ref source, (uint)(index + (3 * stride) + 2)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector256.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector256.LoadUnsafe(ref source, (uint)(index - (3 * stride) + 2)); + sample1 = Vector256.LoadUnsafe(ref source, (uint)(index - (2 * stride) + 1)); + sample2 = center; + sample3 = Vector256.LoadUnsafe(ref source, (uint)(index + (2 * stride) - 1)); + sample4 = Vector256.LoadUnsafe(ref source, (uint)(index + (3 * stride) - 2)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector256.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector256.LoadUnsafe(ref source, (uint)(index - (2 * stride) - 3)); + sample1 = Vector256.LoadUnsafe(ref source, (uint)(index - stride - 2)); + sample2 = center; + sample3 = Vector256.LoadUnsafe(ref source, (uint)(index + stride + 2)); + sample4 = Vector256.LoadUnsafe(ref source, (uint)(index + (2 * stride) + 3)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector256.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector256.LoadUnsafe(ref source, (uint)(index - (2 * stride) + 3)); + sample1 = Vector256.LoadUnsafe(ref source, (uint)(index - stride + 2)); + sample2 = center; + sample3 = Vector256.LoadUnsafe(ref source, (uint)(index + stride - 2)); + sample4 = Vector256.LoadUnsafe(ref source, (uint)(index + (2 * stride) - 3)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector256.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector256.LoadUnsafe(ref source, (uint)(index + (2 * stride) - 4)); + sample1 = Vector256.LoadUnsafe(ref source, (uint)(index + stride - 2)); + sample2 = center; + sample3 = Vector256.LoadUnsafe(ref source, (uint)(index - stride + 2)); + sample4 = Vector256.LoadUnsafe(ref source, (uint)(index - (2 * stride) + 4)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector256.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector256.LoadUnsafe(ref source, (uint)(index - (2 * stride) - 4)); + sample1 = Vector256.LoadUnsafe(ref source, (uint)(index - stride - 2)); + sample2 = center; + sample3 = Vector256.LoadUnsafe(ref source, (uint)(index + stride + 2)); + sample4 = Vector256.LoadUnsafe(ref source, (uint)(index + (2 * stride) + 4)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector256.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector256.LoadUnsafe(ref source, (uint)(index - (4 * stride) - 2)); + sample1 = Vector256.LoadUnsafe(ref source, (uint)(index - (2 * stride) - 1)); + sample2 = center; + sample3 = Vector256.LoadUnsafe(ref source, (uint)(index + (2 * stride) + 1)); + sample4 = Vector256.LoadUnsafe(ref source, (uint)(index + (4 * stride) + 2)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector256.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector256.LoadUnsafe(ref source, (uint)(index - (4 * stride) + 2)); + sample1 = Vector256.LoadUnsafe(ref source, (uint)(index - (2 * stride) + 1)); + sample2 = center; + sample3 = Vector256.LoadUnsafe(ref source, (uint)(index + (2 * stride) - 1)); + sample4 = Vector256.LoadUnsafe(ref source, (uint)(index + (4 * stride) - 2)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector256.MultiplyAddEstimate(sum, sum, result); + + return result; + } + + /// + /// Evaluates the ordered low-frequency Malta rays. Each result belongs to one independent center. + /// + /// The borrowed plane or zero-padded neighborhood, starting at its logical origin. + /// The first center's sample index. Its full four-sample halo and all result lanes must exist. + /// The physical row stride in samples. + /// The sum of the 16 squared ray responses in native order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 MaltaUnitLf512(ReadOnlySpan samples, int index, int stride) + { + ref float source = ref MemoryMarshal.GetReference(samples); + Vector512 center = Vector512.LoadUnsafe(ref source, (uint)(index)); + Vector512 sum; + Vector512 sample0; + Vector512 sample1; + Vector512 sample2; + Vector512 sample3; + Vector512 sample4; + + // Sample offsets describe the ray, not lane positions. Each load keeps adjacent result centers in lane order. + // The nested additions retain native Sum's balanced groups; only successive squared rays use multiply-add. + sample0 = Vector512.LoadUnsafe(ref source, (uint)(index - 4)); + sample1 = Vector512.LoadUnsafe(ref source, (uint)(index - 2)); + sample2 = center; + sample3 = Vector512.LoadUnsafe(ref source, (uint)(index + 2)); + sample4 = Vector512.LoadUnsafe(ref source, (uint)(index + 4)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + Vector512 result = sum * sum; + + sample0 = Vector512.LoadUnsafe(ref source, (uint)(index - (4 * stride))); + sample1 = Vector512.LoadUnsafe(ref source, (uint)(index - (2 * stride))); + sample2 = center; + sample3 = Vector512.LoadUnsafe(ref source, (uint)(index + (2 * stride))); + sample4 = Vector512.LoadUnsafe(ref source, (uint)(index + (4 * stride))); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector512.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector512.LoadUnsafe(ref source, (uint)(index - (3 * stride) - 3)); + sample1 = Vector512.LoadUnsafe(ref source, (uint)(index - (2 * stride) - 2)); + sample2 = center; + sample3 = Vector512.LoadUnsafe(ref source, (uint)(index + (2 * stride) + 2)); + sample4 = Vector512.LoadUnsafe(ref source, (uint)(index + (3 * stride) + 3)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector512.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector512.LoadUnsafe(ref source, (uint)(index - (3 * stride) + 3)); + sample1 = Vector512.LoadUnsafe(ref source, (uint)(index - (2 * stride) + 2)); + sample2 = center; + sample3 = Vector512.LoadUnsafe(ref source, (uint)(index + (2 * stride) - 2)); + sample4 = Vector512.LoadUnsafe(ref source, (uint)(index + (3 * stride) - 3)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector512.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector512.LoadUnsafe(ref source, (uint)(index - (4 * stride) + 1)); + sample1 = Vector512.LoadUnsafe(ref source, (uint)(index - (2 * stride) + 1)); + sample2 = center; + sample3 = Vector512.LoadUnsafe(ref source, (uint)(index + (2 * stride) - 1)); + sample4 = Vector512.LoadUnsafe(ref source, (uint)(index + (4 * stride) - 1)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector512.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector512.LoadUnsafe(ref source, (uint)(index - (4 * stride) - 1)); + sample1 = Vector512.LoadUnsafe(ref source, (uint)(index - (2 * stride) - 1)); + sample2 = center; + sample3 = Vector512.LoadUnsafe(ref source, (uint)(index + (2 * stride) + 1)); + sample4 = Vector512.LoadUnsafe(ref source, (uint)(index + (4 * stride) + 1)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector512.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector512.LoadUnsafe(ref source, (uint)(index - stride - 4)); + sample1 = Vector512.LoadUnsafe(ref source, (uint)(index - stride - 2)); + sample2 = center; + sample3 = Vector512.LoadUnsafe(ref source, (uint)(index + stride + 2)); + sample4 = Vector512.LoadUnsafe(ref source, (uint)(index + stride + 4)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector512.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector512.LoadUnsafe(ref source, (uint)(index + stride - 4)); + sample1 = Vector512.LoadUnsafe(ref source, (uint)(index + stride - 2)); + sample2 = center; + sample3 = Vector512.LoadUnsafe(ref source, (uint)(index - stride + 2)); + sample4 = Vector512.LoadUnsafe(ref source, (uint)(index - stride + 4)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector512.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector512.LoadUnsafe(ref source, (uint)(index - (3 * stride) - 2)); + sample1 = Vector512.LoadUnsafe(ref source, (uint)(index - (2 * stride) - 1)); + sample2 = center; + sample3 = Vector512.LoadUnsafe(ref source, (uint)(index + (2 * stride) + 1)); + sample4 = Vector512.LoadUnsafe(ref source, (uint)(index + (3 * stride) + 2)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector512.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector512.LoadUnsafe(ref source, (uint)(index - (3 * stride) + 2)); + sample1 = Vector512.LoadUnsafe(ref source, (uint)(index - (2 * stride) + 1)); + sample2 = center; + sample3 = Vector512.LoadUnsafe(ref source, (uint)(index + (2 * stride) - 1)); + sample4 = Vector512.LoadUnsafe(ref source, (uint)(index + (3 * stride) - 2)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector512.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector512.LoadUnsafe(ref source, (uint)(index - (2 * stride) - 3)); + sample1 = Vector512.LoadUnsafe(ref source, (uint)(index - stride - 2)); + sample2 = center; + sample3 = Vector512.LoadUnsafe(ref source, (uint)(index + stride + 2)); + sample4 = Vector512.LoadUnsafe(ref source, (uint)(index + (2 * stride) + 3)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector512.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector512.LoadUnsafe(ref source, (uint)(index - (2 * stride) + 3)); + sample1 = Vector512.LoadUnsafe(ref source, (uint)(index - stride + 2)); + sample2 = center; + sample3 = Vector512.LoadUnsafe(ref source, (uint)(index + stride - 2)); + sample4 = Vector512.LoadUnsafe(ref source, (uint)(index + (2 * stride) - 3)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector512.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector512.LoadUnsafe(ref source, (uint)(index + (2 * stride) - 4)); + sample1 = Vector512.LoadUnsafe(ref source, (uint)(index + stride - 2)); + sample2 = center; + sample3 = Vector512.LoadUnsafe(ref source, (uint)(index - stride + 2)); + sample4 = Vector512.LoadUnsafe(ref source, (uint)(index - (2 * stride) + 4)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector512.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector512.LoadUnsafe(ref source, (uint)(index - (2 * stride) - 4)); + sample1 = Vector512.LoadUnsafe(ref source, (uint)(index - stride - 2)); + sample2 = center; + sample3 = Vector512.LoadUnsafe(ref source, (uint)(index + stride + 2)); + sample4 = Vector512.LoadUnsafe(ref source, (uint)(index + (2 * stride) + 4)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector512.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector512.LoadUnsafe(ref source, (uint)(index - (4 * stride) - 2)); + sample1 = Vector512.LoadUnsafe(ref source, (uint)(index - (2 * stride) - 1)); + sample2 = center; + sample3 = Vector512.LoadUnsafe(ref source, (uint)(index + (2 * stride) + 1)); + sample4 = Vector512.LoadUnsafe(ref source, (uint)(index + (4 * stride) + 2)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector512.MultiplyAddEstimate(sum, sum, result); + + sample0 = Vector512.LoadUnsafe(ref source, (uint)(index - (4 * stride) + 2)); + sample1 = Vector512.LoadUnsafe(ref source, (uint)(index - (2 * stride) + 1)); + sample2 = center; + sample3 = Vector512.LoadUnsafe(ref source, (uint)(index + (2 * stride) - 1)); + sample4 = Vector512.LoadUnsafe(ref source, (uint)(index + (4 * stride) - 2)); + sum = ((sample0 + sample1) + (sample2 + (sample3 + sample4))); + result = Vector512.MultiplyAddEstimate(sum, sum, result); + + return result; + } + + /// + /// Accumulates low-frequency Malta responses over the prepared difference plane. + /// + /// The borrowed prepared differences at the plane's logical origin. + /// The logical width, at least eight samples as established by comparison setup. + /// The logical height, at least eight rows as established by comparison setup. + /// The physical difference-plane stride in samples. + /// The existing accumulation storage, at the plane's logical origin. + /// The physical output stride in samples. + /// The reusable 81-sample zero-padded neighborhood. + private static void AccumulateMaltaLf( + ReadOnlySpan differences, + int width, + int height, + int differenceStride, + Span output, + int outputStride, + Span border) + { + // Comparison setup expands small inputs to at least 8 by 8. These top and bottom strips contain every center + // whose four-row halo crosses an edge. Their scalar kernel reads the same 9-by-9 zero extension as native. + int y = 0; + for (; y < 4; y++) + { + Span outputRow = output.Slice(y * outputStride, width); + for (int x = 0; x < width; x++) + { + PrepareMaltaBorder(differences, width, height, differenceStride, x, y, border); + outputRow[x] += MaltaUnitLfScalar(border, 40, 9); + } + } + + for (; y < height - 4; y++) + { + ref float outputRow = ref MemoryMarshal.GetReference(output.Slice(y * outputStride, width)); + int rowIndex = y * differenceStride; + int x = 0; + for (; x < 4; x++) + { + PrepareMaltaBorder(differences, width, height, differenceStride, x, y, border); + Unsafe.Add(ref outputRow, x) += MaltaUnitLfScalar(border, 40, 9); + } + + // Loads are unaligned. Start at the first real interior center rather than reserving an alignment strip. + // The rightmost lane and its four-column halo must remain inside the logical width, not row padding. + if (Vector512.IsHardwareAccelerated) + { + for (; x <= width - 4 - Vector512.Count; x += Vector512.Count) + { + Vector512 previous = Vector512.LoadUnsafe(ref outputRow, (uint)x); + Vector512 response = MaltaUnitLf512(differences, rowIndex + x, differenceStride); + (previous + response).StoreUnsafe(ref outputRow, (uint)x); + } + } + + if (Vector256.IsHardwareAccelerated) + { + for (; x <= width - 4 - Vector256.Count; x += Vector256.Count) + { + Vector256 previous = Vector256.LoadUnsafe(ref outputRow, (uint)x); + Vector256 response = MaltaUnitLf256(differences, rowIndex + x, differenceStride); + (previous + response).StoreUnsafe(ref outputRow, (uint)x); + } + } + + if (Vector128.IsHardwareAccelerated) + { + for (; x <= width - 4 - Vector128.Count; x += Vector128.Count) + { + Vector128 previous = Vector128.LoadUnsafe(ref outputRow, (uint)x); + Vector128 response = MaltaUnitLf128(differences, rowIndex + x, differenceStride); + (previous + response).StoreUnsafe(ref outputRow, (uint)x); + } + } + + for (; x < width - 4; x++) + { + Unsafe.Add(ref outputRow, x) += MaltaUnitLfScalar(differences, rowIndex + x, differenceStride); + } + + for (; x < width; x++) + { + PrepareMaltaBorder(differences, width, height, differenceStride, x, y, border); + Unsafe.Add(ref outputRow, x) += MaltaUnitLfScalar(border, 40, 9); + } + } + + for (; y < height; y++) + { + Span outputRow = output.Slice(y * outputStride, width); + for (int x = 0; x < width; x++) + { + PrepareMaltaBorder(differences, width, height, differenceStride, x, y, border); + outputRow[x] += MaltaUnitLfScalar(border, 40, 9); + } + } + } + +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.Malta.Generated.tt b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.Malta.Generated.tt new file mode 100644 index 0000000000..f1dbfaa4de --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.Malta.Generated.tt @@ -0,0 +1,282 @@ +<# +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#> +<#@ template language="C#" hostspecific="true" #> +<#@ import namespace="System" #> +<#@ import namespace="System.Collections.Generic" #> +<#@ import namespace="System.Linq" #> +<#@ output extension=".cs" #> +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; + +internal static partial class Butteraugli +{ +<# + // The ordered rays are the native MaltaUnit and MaltaUnit(MaltaTagLF) neighborhoods. Each pair is (column, row), + // relative to the center. Sum grouping and ray accumulation order affect rounding and must remain unchanged. + // All lanes evaluate independent neighboring centers. Generation changes only the register width and load syntax. + // Regenerate with: t4 Butteraugli.Malta.Generated.tt -o Butteraugli.Malta.Generated.cs + (int X, int Y)[][] highFrequencyRays = + { + new[] { (-4, 0), (-3, 0), (-2, 0), (-1, 0), (0, 0), (1, 0), (2, 0), (3, 0), (4, 0) }, + new[] { (0, -4), (0, -3), (0, -2), (0, -1), (0, 0), (0, 1), (0, 2), (0, 3), (0, 4) }, + new[] { (-3, -3), (-2, -2), (-1, -1), (0, 0), (1, 1), (2, 2), (3, 3) }, + new[] { (3, -3), (2, -2), (1, -1), (0, 0), (-1, 1), (-2, 2), (-3, 3) }, + new[] { (1, -4), (1, -3), (1, -2), (0, -1), (0, 0), (0, 1), (-1, 2), (-1, 3), (-1, 4) }, + new[] { (-1, -4), (-1, -3), (-1, -2), (0, -1), (0, 0), (0, 1), (1, 2), (1, 3), (1, 4) }, + new[] { (-4, -1), (-3, -1), (-2, -1), (-1, 0), (0, 0), (1, 0), (2, 1), (3, 1), (4, 1) }, + new[] { (-4, 1), (-3, 1), (-2, 1), (-1, 0), (0, 0), (1, 0), (2, -1), (3, -1), (4, -1) }, + new[] { (-2, -3), (-1, -2), (-1, -1), (0, 0), (1, 1), (1, 2), (2, 3) }, + new[] { (2, -3), (1, -2), (1, -1), (0, 0), (-1, 1), (-1, 2), (-2, 3) }, + new[] { (-3, -2), (-2, -1), (-1, -1), (0, 0), (1, 1), (2, 1), (3, 2) }, + new[] { (3, -2), (2, -1), (1, -1), (0, 0), (-1, 1), (-2, 1), (-3, 2) }, + new[] { (-4, 1), (-3, 1), (-2, 1), (-1, 0), (0, 0), (1, 0), (2, -1), (3, -1), (4, -1) }, + new[] { (-4, -1), (-3, -1), (-2, -1), (-1, 0), (0, 0), (1, 0), (2, 1), (3, 1), (4, 1) }, + new[] { (-1, -4), (-1, -3), (-1, -2), (0, -1), (0, 0), (0, 1), (1, 2), (1, 3), (1, 4) }, + new[] { (1, -4), (1, -3), (1, -2), (0, -1), (0, 0), (0, 1), (-1, 2), (-1, 3), (-1, 4) }, + }; + + (int X, int Y)[][] lowFrequencyRays = + { + new[] { (-4, 0), (-2, 0), (0, 0), (2, 0), (4, 0) }, + new[] { (0, -4), (0, -2), (0, 0), (0, 2), (0, 4) }, + new[] { (-3, -3), (-2, -2), (0, 0), (2, 2), (3, 3) }, + new[] { (3, -3), (2, -2), (0, 0), (-2, 2), (-3, 3) }, + new[] { (1, -4), (1, -2), (0, 0), (-1, 2), (-1, 4) }, + new[] { (-1, -4), (-1, -2), (0, 0), (1, 2), (1, 4) }, + new[] { (-4, -1), (-2, -1), (0, 0), (2, 1), (4, 1) }, + new[] { (-4, 1), (-2, 1), (0, 0), (2, -1), (4, -1) }, + new[] { (-2, -3), (-1, -2), (0, 0), (1, 2), (2, 3) }, + new[] { (2, -3), (1, -2), (0, 0), (-1, 2), (-2, 3) }, + new[] { (-3, -2), (-2, -1), (0, 0), (2, 1), (3, 2) }, + new[] { (3, -2), (2, -1), (0, 0), (-2, 1), (-3, 2) }, + new[] { (-4, 2), (-2, 1), (0, 0), (2, -1), (4, -2) }, + new[] { (-4, -2), (-2, -1), (0, 0), (2, 1), (4, 2) }, + new[] { (-2, -4), (-1, -2), (0, 0), (1, 2), (2, 4) }, + new[] { (2, -4), (1, -2), (0, 0), (-1, 2), (-2, 4) }, + }; + + foreach (bool lowFrequency in new[] { false, true }) + { + string mode = lowFrequency ? "Lf" : ""; + (int X, int Y)[][] rays = lowFrequency ? lowFrequencyRays : highFrequencyRays; + foreach (int bits in new[] { 0, 128, 256, 512 }) + { + string suffix = bits == 0 ? "Scalar" : bits.ToString(); + string type = bits == 0 ? "float" : "Vector" + bits + ""; + string operation = bits == 0 ? "float" : "Vector" + bits; +#> + /// + /// Evaluates the ordered <#= lowFrequency ? "low-frequency" : "full" #> Malta rays. Each result belongs to one independent center. + /// + /// The borrowed plane or zero-padded neighborhood, starting at its logical origin. + /// The first center's sample index. Its full four-sample halo and all result lanes must exist. + /// The physical row stride in samples. + /// The sum of the 16 squared ray responses in native order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static <#= type #> MaltaUnit<#= mode #><#= suffix #>(ReadOnlySpan samples, int index, int stride) + { + ref float source = ref MemoryMarshal.GetReference(samples); + <#= type #> center = <#= Load(bits, 0, 0) #>; + <#= type #> sum; +<# + int sampleCount = rays.Max(ray => ray.Length); + for (int sample = 0; sample < sampleCount; sample++) + { +#> + <#= type #> sample<#= sample #>; +<# + } +#> + + // Sample offsets describe the ray, not lane positions. Each load keeps adjacent result centers in lane order. + // The nested additions retain native Sum's balanced groups; only successive squared rays use multiply-add. +<# + for (int rayIndex = 0; rayIndex < rays.Length; rayIndex++) + { + (int X, int Y)[] ray = rays[rayIndex]; + for (int sample = 0; sample < ray.Length; sample++) + { + (int x, int y) = ray[sample]; + string expression = x == 0 && y == 0 ? "center" : Load(bits, x, y); +#> + sample<#= sample #> = <#= expression #>; +<# + } + string sum = Sum(Enumerable.Range(0, ray.Length).Select(i => "sample" + i).ToArray()); +#> + sum = <#= sum #>; +<# + if (rayIndex == 0) + { +#> + <#= type #> result = sum * sum; +<# + } + else + { +#> + result = <#= operation #>.MultiplyAddEstimate(sum, sum, result); +<# + } +#> + +<# + } +#> + return result; + } + +<# + } +#> + /// + /// Accumulates <#= lowFrequency ? "low-frequency" : "full" #> Malta responses over the prepared difference plane. + /// + /// The borrowed prepared differences at the plane's logical origin. + /// The logical width, at least eight samples as established by comparison setup. + /// The logical height, at least eight rows as established by comparison setup. + /// The physical difference-plane stride in samples. + /// The existing accumulation storage, at the plane's logical origin. + /// The physical output stride in samples. + /// The reusable 81-sample zero-padded neighborhood. + private static void AccumulateMalta<#= mode #>( + ReadOnlySpan differences, + int width, + int height, + int differenceStride, + Span output, + int outputStride, + Span border) + { + // Comparison setup expands small inputs to at least 8 by 8. These top and bottom strips contain every center + // whose four-row halo crosses an edge. Their scalar kernel reads the same 9-by-9 zero extension as native. + int y = 0; + for (; y < 4; y++) + { + Span outputRow = output.Slice(y * outputStride, width); + for (int x = 0; x < width; x++) + { + PrepareMaltaBorder(differences, width, height, differenceStride, x, y, border); + outputRow[x] += MaltaUnit<#= mode #>Scalar(border, 40, 9); + } + } + + for (; y < height - 4; y++) + { + ref float outputRow = ref MemoryMarshal.GetReference(output.Slice(y * outputStride, width)); + int rowIndex = y * differenceStride; + int x = 0; + for (; x < 4; x++) + { + PrepareMaltaBorder(differences, width, height, differenceStride, x, y, border); + Unsafe.Add(ref outputRow, x) += MaltaUnit<#= mode #>Scalar(border, 40, 9); + } + + // Loads are unaligned. Start at the first real interior center rather than reserving an alignment strip. + // The rightmost lane and its four-column halo must remain inside the logical width, not row padding. +<# + foreach (int bits in new[] { 512, 256, 128 }) + { + string vector = "Vector" + bits; +#> + if (<#= vector #>.IsHardwareAccelerated) + { + for (; x <= width - 4 - <#= vector #>.Count; x += <#= vector #>.Count) + { + <#= vector #> previous = <#= vector #>.LoadUnsafe(ref outputRow, (uint)x); + <#= vector #> response = MaltaUnit<#= mode #><#= bits #>(differences, rowIndex + x, differenceStride); + (previous + response).StoreUnsafe(ref outputRow, (uint)x); + } + } + +<# + } +#> + for (; x < width - 4; x++) + { + Unsafe.Add(ref outputRow, x) += MaltaUnit<#= mode #>Scalar(differences, rowIndex + x, differenceStride); + } + + for (; x < width; x++) + { + PrepareMaltaBorder(differences, width, height, differenceStride, x, y, border); + Unsafe.Add(ref outputRow, x) += MaltaUnit<#= mode #>Scalar(border, 40, 9); + } + } + + for (; y < height; y++) + { + Span outputRow = output.Slice(y * outputStride, width); + for (int x = 0; x < width; x++) + { + PrepareMaltaBorder(differences, width, height, differenceStride, x, y, border); + outputRow[x] += MaltaUnit<#= mode #>Scalar(border, 40, 9); + } + } + } + +<# + } +#> +} +<#+ + /// + /// Builds one scalar or full-register sample load at a validated relative position. + /// + /// Zero for scalar loads, or the fixed SIMD register width. + /// The relative sample column. + /// The relative sample row. + /// The generated load expression. + private static string Load(int bits, int x, int y) + { + string index = "index"; + if (y != 0) + { + index += y > 0 ? " + " : " - "; + index += Math.Abs(y) == 1 ? "stride" : "(" + Math.Abs(y) + " * stride)"; + } + + if (x != 0) + { + index += (x > 0 ? " + " : " - ") + Math.Abs(x); + } + + return bits == 0 ? "Unsafe.Add(ref source, " + index + ")" + : "Vector" + bits + ".LoadUnsafe(ref source, (uint)(" + index + "))"; + } + + /// + /// Retains the native four-, five-, seven- and nine-input Sum parenthesization. + /// + /// The ordered sample expressions for one ray. + /// The native addition expression. + private static string Sum(string[] values) + { + if (values.Length == 4) + { + return "((" + values[0] + " + " + values[1] + ") + (" + values[2] + " + " + values[3] + "))"; + } + + if (values.Length == 5) + { + return Sum(new[] { values[0], values[1], values[2], "(" + values[3] + " + " + values[4] + ")" }); + } + + if (values.Length == 7) + { + return Sum(new[] { values[0], values[1], values[2], Sum(values.Skip(3).ToArray()) }); + } + + return "((" + Sum(values.Take(4).ToArray()) + " + " + Sum(values.Skip(4).Take(4).ToArray()) + ") + " + values[8] + ")"; + } +#> diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs index 4e917b8c46..45005f0170 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs @@ -14,29 +14,45 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; /// -/// Implementation of Google Butteraugli, an advanced -/// image comparison system. Unlike PSNR or SSIM, -/// Butteraugli compares images the way humans may -/// spot differences instead of pixelwise. +/// Computes perceptual difference maps and scores for linear RGB images. /// -internal static class Butteraugli +internal static partial class Butteraugli { - // NOTE: The meaning of those constants - // is not perfectly understood. - public const float WMfMalta = 37.0819870399f; - public const float Norm1Mf = 130262059.556f; - public const float WMfMaltaX = 8246.75321353f; - public const float Norm1MfX = 1009002.70582f; - - public const float WHfMalta = 18.7237414387f; - public const float Norm1Hf = 4498534.45232f; - public const float WHfMaltaX = 6923.99476109f; - public const float Norm1HfX = 8051.15833247f; - - public const float WUhfMalta = 1.10039032555f; - public const float Norm1Uhf = 71.7800275169f; - public const float WUhfMaltaX = 173.5f; - public const float Norm1UhfX = 5.0f; + /// The penalty weight for Y middle-frequency Malta differences. + public const double WMfMalta = 37.0819870399D; + + /// The Y magnitude offset used to normalize middle-frequency Malta differences. + public const double Norm1Mf = 130262059.556D; + + /// The penalty weight for X middle-frequency Malta differences. + public const double WMfMaltaX = 8246.75321353D; + + /// The X magnitude offset used to normalize middle-frequency Malta differences. + public const double Norm1MfX = 1009002.70582D; + + /// The penalty weight for Y high-frequency Malta differences. + public const double WHfMalta = 18.7237414387D; + + /// The Y magnitude offset used to normalize high-frequency Malta differences. + public const double Norm1Hf = 4498534.45232D; + + /// The penalty weight for X high-frequency Malta differences. + public const double WHfMaltaX = 6923.99476109D; + + /// The X magnitude offset used to normalize high-frequency Malta differences. + public const double Norm1HfX = 8051.15833247D; + + /// The penalty weight for Y ultra-high-frequency Malta differences. + public const double WUhfMalta = 1.10039032555D; + + /// The Y magnitude offset used to normalize ultra-high-frequency Malta differences. + public const double Norm1Uhf = 71.7800275169D; + + /// The penalty weight for X ultra-high-frequency Malta differences. + public const double WUhfMaltaX = 173.5D; + + /// The X magnitude offset used to normalize ultra-high-frequency Malta differences. + public const double Norm1UhfX = 5.0D; private const float IntensityTargetNormalizationHack = 0.79079917404f; @@ -864,483 +880,54 @@ public static bool SeparateFrequencies(Configuration configuration, ButteraugliB return true; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector Sum( - Vector a, - Vector b, - Vector c, - Vector d) - => (a + b) + (c + d); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector Sum( - Vector a, - Vector b, - Vector c, - Vector d, - Vector e) - => Sum(a, b, c, d + e); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector Sum( - Vector a, - Vector b, - Vector c, - Vector d, - Vector e, - Vector f, - Vector g) - => Sum(a, b, c, Sum(d, e, f, g)); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector Sum( - Vector a, - Vector b, - Vector c, - Vector d, - Vector e, - Vector f, - Vector g, - Vector h, - Vector i) - => (Sum(a, b, c, d) + Sum(e, f, g, h)) + i; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector MaltaUnitLF( - ReadOnlySpan row, - int index, - int xs) - { - // helper - [MethodImpl(MethodImplOptions.AggressiveInlining)] - static Vector Load(ReadOnlySpan row, int index) - => new(row.Slice(index, Vector.Count)); - - int xs3 = 3 * xs; - - Vector center = Load(row, index); - - Vector sumYConst = Sum( - Load(row, index - 4), - Load(row, index - 2), - center, - Load(row, index + 2), - Load(row, index + 4)); - - Vector retval = sumYConst * sumYConst; - - Vector sum; - - sum = Sum( - Load(row, index - xs3 - xs), - Load(row, index - xs - xs), - center, - Load(row, index + xs + xs), - Load(row, index + xs3 + xs)); - retval = (sum * sum) + retval; - - sum = Sum( - Load(row, index - xs3 - 3), - Load(row, index - xs - xs - 2), - center, - Load(row, index + xs + xs + 2), - Load(row, index + xs3 + 3)); - retval = (sum * sum) + retval; - - sum = Sum( - Load(row, index - xs3 + 3), - Load(row, index - xs - xs + 2), - center, - Load(row, index + xs + xs - 2), - Load(row, index + xs3 - 3)); - retval = (sum * sum) + retval; - - sum = Sum( - Load(row, index - xs3 - xs + 1), - Load(row, index - xs - xs + 1), - center, - Load(row, index + xs + xs - 1), - Load(row, index + xs3 + xs - 1)); - retval = (sum * sum) + retval; - - sum = Sum( - Load(row, index - xs3 - xs - 1), - Load(row, index - xs - xs - 1), - center, - Load(row, index + xs + xs + 1), - Load(row, index + xs3 + xs + 1)); - retval = (sum * sum) + retval; - - sum = Sum( - Load(row, index - 4 - xs), - Load(row, index - 2 - xs), - center, - Load(row, index + 2 + xs), - Load(row, index + 4 + xs)); - retval = (sum * sum) + retval; - - sum = Sum( - Load(row, index - 4 + xs), - Load(row, index - 2 + xs), - center, - Load(row, index + 2 - xs), - Load(row, index + 4 - xs)); - retval = (sum * sum) + retval; - - sum = Sum( - Load(row, index - xs3 - 2), - Load(row, index - xs - xs - 1), - center, - Load(row, index + xs + xs + 1), - Load(row, index + xs3 + 2)); - retval = (sum * sum) + retval; - - sum = Sum( - Load(row, index - xs3 + 2), - Load(row, index - xs - xs + 1), - center, - Load(row, index + xs + xs - 1), - Load(row, index + xs3 - 2)); - retval = (sum * sum) + retval; - - sum = Sum( - Load(row, index - xs - xs - 3), - Load(row, index - xs - 2), - center, - Load(row, index + xs + 2), - Load(row, index + xs + xs + 3)); - retval = (sum * sum) + retval; - - sum = Sum( - Load(row, index - xs - xs + 3), - Load(row, index - xs + 2), - center, - Load(row, index + xs - 2), - Load(row, index + xs + xs - 3)); - retval = (sum * sum) + retval; - - sum = Sum( - Load(row, index + xs + xs - 4), - Load(row, index + xs - 2), - center, - Load(row, index - xs + 2), - Load(row, index - xs - xs + 4)); - retval = (sum * sum) + retval; - - sum = Sum( - Load(row, index - xs - xs - 4), - Load(row, index - xs - 2), - center, - Load(row, index + xs + 2), - Load(row, index + xs + xs + 4)); - retval = (sum * sum) + retval; - - sum = Sum( - Load(row, index - xs3 - xs - 2), - Load(row, index - xs - xs - 1), - center, - Load(row, index + xs + xs + 1), - Load(row, index + xs3 + xs + 2)); - retval = (sum * sum) + retval; - - sum = Sum( - Load(row, index - xs3 - xs + 2), - Load(row, index - xs - xs + 1), - center, - Load(row, index + xs + xs - 1), - Load(row, index + xs3 + xs - 2)); - retval = (sum * sum) + retval; - - return retval; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector MaltaUnit( - ReadOnlySpan row, - int index, - int xs) - { - // helper - [MethodImpl(MethodImplOptions.AggressiveInlining)] - static Vector Load(ReadOnlySpan row, int index) - => new(row.Slice(index, Vector.Count)); - - int xs3 = 3 * xs; - - Vector center = Load(row, index); - - Vector sumYConst = Sum( - Load(row, index - 4), - Load(row, index - 3), - Load(row, index - 2), - Load(row, index - 1), - center, - Load(row, index + 1), - Load(row, index + 2), - Load(row, index + 3), - Load(row, index + 4)); - - Vector retval = sumYConst * sumYConst; - - Vector sum; - - sum = Sum( - Load(row, index - xs3 - xs), - Load(row, index - xs3), - Load(row, index - xs - xs), - Load(row, index - xs), - center, - Load(row, index + xs), - Load(row, index + xs + xs), - Load(row, index + xs3), - Load(row, index + xs3 + xs)); - retval += sum * sum; - - sum = Sum( - Load(row, index - xs3 - 3), - Load(row, index - xs - xs - 2), - Load(row, index - xs - 1), - center, - Load(row, index + xs + 1), - Load(row, index + xs + xs + 2), - Load(row, index + xs3 + 3)); - retval += sum * sum; - - sum = Sum( - Load(row, index - xs3 + 3), - Load(row, index - xs - xs + 2), - Load(row, index - xs + 1), - center, - Load(row, index + xs - 1), - Load(row, index + xs + xs - 2), - Load(row, index + xs3 - 3)); - retval += sum * sum; - - sum = Sum( - Load(row, index - xs3 - xs + 1), - Load(row, index - xs3 + 1), - Load(row, index - xs - xs + 1), - Load(row, index - xs), - center, - Load(row, index + xs), - Load(row, index + xs + xs - 1), - Load(row, index + xs3 - 1), - Load(row, index + xs3 + xs - 1)); - retval += sum * sum; - - sum = Sum( - Load(row, index - xs3 - xs - 1), - Load(row, index - xs3 - 1), - Load(row, index - xs - xs - 1), - Load(row, index - xs), - center, - Load(row, index + xs), - Load(row, index + xs + xs + 1), - Load(row, index + xs3 + 1), - Load(row, index + xs3 + xs + 1)); - retval += sum * sum; - - sum = Sum( - Load(row, index - 4 - xs), - Load(row, index - 3 - xs), - Load(row, index - 2 - xs), - Load(row, index - 1), - center, - Load(row, index + 1), - Load(row, index + 2 + xs), - Load(row, index + 3 + xs), - Load(row, index + 4 + xs)); - retval += sum * sum; - - sum = Sum( - Load(row, index - 4 + xs), - Load(row, index - 3 + xs), - Load(row, index - 2 + xs), - Load(row, index - 1), - center, - Load(row, index + 1), - Load(row, index + 2 - xs), - Load(row, index + 3 - xs), - Load(row, index + 4 - xs)); - retval += sum * sum; - - sum = Sum( - Load(row, index - xs3 - 2), - Load(row, index - xs - xs - 1), - Load(row, index - xs - 1), - center, - Load(row, index + xs + 1), - Load(row, index + xs + xs + 1), - Load(row, index + xs3 + 2)); - retval += sum * sum; - - sum = Sum( - Load(row, index - xs3 + 2), - Load(row, index - xs - xs + 1), - Load(row, index - xs + 1), - center, - Load(row, index + xs - 1), - Load(row, index + xs + xs - 1), - Load(row, index + xs3 - 2)); - retval += sum * sum; - - sum = Sum( - Load(row, index - xs - xs - 3), - Load(row, index - xs - 2), - Load(row, index - xs - 1), - center, - Load(row, index + xs + 1), - Load(row, index + xs + 2), - Load(row, index + xs + xs + 3)); - retval += sum * sum; - - sum = Sum( - Load(row, index - xs - xs + 3), - Load(row, index - xs + 2), - Load(row, index - xs + 1), - center, - Load(row, index + xs - 1), - Load(row, index + xs - 2), - Load(row, index + xs + xs - 3)); - retval += sum * sum; - - sum = Sum( - Load(row, index + xs - 4), - Load(row, index + xs - 3), - Load(row, index + xs - 2), - Load(row, index - 1), - center, - Load(row, index + 1), - Load(row, index - xs + 2), - Load(row, index - xs + 3), - Load(row, index - xs + 4)); - retval += sum * sum; - - sum = Sum( - Load(row, index - xs - 4), - Load(row, index - xs - 3), - Load(row, index - xs - 2), - Load(row, index - 1), - center, - Load(row, index + 1), - Load(row, index + xs + 2), - Load(row, index + xs + 3), - Load(row, index + xs + 4)); - retval += sum * sum; - - sum = Sum( - Load(row, index - xs3 - xs - 1), - Load(row, index - xs3 - 1), - Load(row, index - xs - xs - 1), - Load(row, index - xs), - center, - Load(row, index + xs), - Load(row, index + xs + xs + 1), - Load(row, index + xs3 + 1), - Load(row, index + xs3 + xs + 1)); - retval += sum * sum; - - sum = Sum( - Load(row, index - xs3 - xs + 1), - Load(row, index - xs3 + 1), - Load(row, index - xs - xs + 1), - Load(row, index - xs), - center, - Load(row, index + xs), - Load(row, index + xs + xs - 1), - Load(row, index + xs3 - 1), - Load(row, index + xs3 + xs - 1)); - retval += sum * sum; - - return retval; - } - /// - /// Evaluates one Malta result using a zero-padded neighborhood when a complete SIMD load would cross an image edge. + /// Accumulates a Malta difference with the selected frequency range's normalization and neighborhood. /// - /// The complete borrowed difference plane, starting at its first logical sample. - /// The logical plane width. - /// The logical plane height. - /// The physical row stride. - /// The sample column. - /// The sample row. /// Whether to use the low-frequency Malta equation. - /// The first lane of the selected Malta equation. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static float PaddedMaltaUnit(ReadOnlySpan diffs, int width, int height, int stride, int x0, int y0, bool isLf) - { - int index = (y0 * stride) + x0; - if (x0 >= 4 && y0 >= 4 && x0 + Vector.Count + 4 <= width && y0 < height - 4) - { - return isLf ? MaltaUnitLF(diffs, index, stride)[0] : MaltaUnit(diffs, index, stride)[0]; - } - - // The equation reaches four rows and columns from the center. Extra columns keep every SIMD load - // inside this local buffer even though only its first result lane is used at image borders. - int paddedStride = 8 + Vector.Count; - Span borderImage = stackalloc float[9 * paddedStride]; - borderImage.Clear(); - - for (int dy = 0; dy < 9; dy++) - { - int y = y0 + dy - 4; - if ((uint)y >= (uint)height) - { - continue; - } - - ReadOnlySpan rowDiffs = diffs[(y * stride)..]; - Span paddedRow = borderImage[(dy * paddedStride)..]; - - for (int dx = 0; dx < 9; dx++) - { - int x = x0 + dx - 4; - if ((uint)x < (uint)width) - { - paddedRow[dx] = rowDiffs[x]; - } - } - } - - int center = (4 * paddedStride) + 4; - return isLf ? MaltaUnitLF(borderImage, center, paddedStride)[0] : MaltaUnit(borderImage, center, paddedStride)[0]; - } - - /// - /// Accumulates a Malta difference with the native scaling for its frequency range. - /// - /// Whether to use the low-frequency Malta neighborhood and scaling. - /// The borrowed reference band. - /// The borrowed distorted band. - /// The penalty for the first error direction. - /// The penalty for the opposite error direction. - /// The band-specific normalization. - /// The preallocated difference workspace. - /// The existing AC error accumulation plane. - /// Whether the input and workspace dimensions match. + /// The borrowed reference band, at least eight samples wide and eight rows high. + /// The equally sized borrowed distorted band. + /// The primary symmetric-difference weight, retained in double until operation setup. + /// The secondary magnitude-penalty weight, retained in double until operation setup. + /// The magnitude normalization, retained in double until operation setup. + /// The equally sized prepared-difference workspace, separate from the input and AC planes. + /// The equally sized existing AC accumulation plane. + /// Whether input and workspace dimensions match. public static bool MaltaDiffMap( bool isLf, JxlPlane lum0, JxlPlane lum1, - float w0Gt1, - float w0Lt1, - float norm1, + double w0Gt1, + double w0Lt1, + double norm1, JxlPlane diffs, JxlPlane blockDiffAc) - // Keep neighborhood selection and its normalization together in the existing frequency-specific entry points. => isLf ? MaltaDiffMapLf(lum0, lum1, w0Gt1, w0Lt1, norm1, diffs, blockDiffAc) : MaltaDiffMap(lum0, lum1, w0Gt1, w0Lt1, norm1, diffs, blockDiffAc); + /// + /// Prepares float differences and accumulates the selected Malta neighborhood without changing either input band. + /// + /// Whether to use the low-frequency ray samples. + /// The borrowed reference band, at least eight samples wide and eight rows high. + /// The equally sized borrowed distorted band. + /// The primary symmetric-difference weight. + /// The secondary reference-magnitude penalty weight. + /// The native magnitude normalization. + /// The native effective ray half-length used to normalize the penalty. + /// The frequency-specific scale used when preparing the penalty coefficients. + /// The equally sized workspace, separate from the input and AC planes and replaced with prepared float differences. + /// The equally sized existing AC accumulation, updated only at logical samples. + /// Whether the input and workspace dimensions match. public static bool MaltaDiffMap( bool isLf, JxlPlane lum0, JxlPlane lum1, - float w0Gt1, - float w0Lt1, - float norm1, - float len, - float mulli, + double w0Gt1, + double w0Lt1, + double norm1, + double effectiveHalfLength, + double multiplier, JxlPlane diffs, JxlPlane blockDiffAc) { @@ -1351,180 +938,103 @@ public static bool MaltaDiffMap( int width = lum0.XSize; int height = lum0.YSize; - - const float weight0 = 0.5f; - const float weight1 = 0.33f; - - float norm2_0Gt1 = - (float)(mulli * MathF.Sqrt(weight0 * w0Gt1) / ((len * 2) + 1) * norm1); - - float norm2_0Lt1 = - (float)(mulli * MathF.Sqrt(weight1 * w0Lt1) / ((len * 2) + 1) * norm1); - - // Malta neighborhoods reach four rows and four columns from each center. Borrow complete planes once, - // then retain global indices so negative relative offsets never refer to a sliced-away preceding row. ReadOnlySpan samples0 = lum0.GetRow(0); ReadOnlySpan samples1 = lum1.GetRow(0); Span differenceSamples = diffs.GetRow(0); Span outputSamples = blockDiffAc.GetRow(0); + MaltaDifferenceOperator operation = new(w0Gt1, w0Lt1, norm1, effectiveHalfLength, multiplier); for (int y = 0; y < height; y++) { - ReadOnlySpan row0 = samples0[(y * lum0.PixelsPerRow)..]; - ReadOnlySpan row1 = samples1[(y * lum1.PixelsPerRow)..]; - Span rowDiffs = differenceSamples[(y * diffs.PixelsPerRow)..]; - - for (int x = 0; x < width; x++) - { - float absVal = 0.5f * - (MathF.Abs(row0[x]) + MathF.Abs(row1[x])); - - float diff = row0[x] - row1[x]; - - float scaler = norm2_0Gt1 / ((float)norm1 + absVal); - - rowDiffs[x] = scaler * diff; - - float scaler2 = norm2_0Lt1 / ((float)norm1 + absVal); - - float fabs0 = MathF.Abs(row0[x]); - - float tooSmall = 0.55f * fabs0; - float tooBig = 1.05f * fabs0; - - if (row0[x] < 0) - { - if (row1[x] > -tooSmall) - { - rowDiffs[x] -= scaler2 * (row1[x] + tooSmall); - } - else if (row1[x] < -tooBig) - { - rowDiffs[x] += scaler2 * (-row1[x] - tooBig); - } - } - else - { - if (row1[x] < tooSmall) - { - rowDiffs[x] += scaler2 * (tooSmall - row1[x]); - } - else if (row1[x] > tooBig) - { - rowDiffs[x] -= scaler2 * (row1[x] - tooBig); - } - } - } - } - - int y0 = 0; - - for (; y0 < 4; y0++) - { - Span row = outputSamples[(y0 * blockDiffAc.PixelsPerRow)..]; - - for (int x = 0; x < width; x++) - { - row[x] += PaddedMaltaUnit(differenceSamples, width, height, diffs.PixelsPerRow, x, y0, isLf); - } + ReadOnlySpan row0 = samples0.Slice(y * lum0.PixelsPerRow, width); + ReadOnlySpan row1 = samples1.Slice(y * lum1.PixelsPerRow, width); + Span differenceRow = differenceSamples.Slice(y * diffs.PixelsPerRow, width); + TensorPrimitives_.Invoke(row0, row1, differenceRow, operation); } - int lanes = Vector.Count; - int alignedX = Math.Max(4, lanes); - - int stride = diffs.PixelsPerRow; - - for (; y0 < height - 4; y0++) + // Small-image comparison expands inputs to at least 8 by 8 before reaching Malta. Every ray has a four-sample + // halo. Reuse one scalar 9-by-9 border block; SIMD never reads a halo from physical allocator padding. + Span border = stackalloc float[81]; + if (isLf) { - Span output = outputSamples[(y0 * blockDiffAc.PixelsPerRow)..]; - ref float outputReference = ref MemoryMarshal.GetReference(output); - - int x = 0; - - for (; x < Math.Min(alignedX, width); x++) - { - output[x] += PaddedMaltaUnit(differenceSamples, width, height, diffs.PixelsPerRow, x, y0, isLf); - } - - // Every lane and its four neighboring columns remain inside the logical width. Border calls use the zero-padded scalar-lane path. - for (; x + lanes + 4 <= width; x += lanes) - { - Vector value = Vector.LoadUnsafe(ref Unsafe.Add(ref outputReference, x)); - - Vector malta = isLf - ? MaltaUnitLF(differenceSamples, (y0 * stride) + x, stride) - : MaltaUnit(differenceSamples, (y0 * stride) + x, stride); - - (value + malta).CopyTo(output[x..]); - } - - for (; x < width; x++) - { - output[x] += PaddedMaltaUnit(differenceSamples, width, height, diffs.PixelsPerRow, x, y0, isLf); - } + AccumulateMaltaLf(differenceSamples, width, height, diffs.PixelsPerRow, outputSamples, blockDiffAc.PixelsPerRow, border); } - - for (; y0 < height; y0++) + else { - Span row = outputSamples[(y0 * blockDiffAc.PixelsPerRow)..]; - - for (int x = 0; x < width; x++) - { - row[x] += PaddedMaltaUnit(differenceSamples, width, height, diffs.PixelsPerRow, x, y0, isLf); - } + AccumulateMalta(differenceSamples, width, height, diffs.PixelsPerRow, outputSamples, blockDiffAc.PixelsPerRow, border); } return true; } + /// + /// Accumulates full Malta differences with the native high-frequency scale and effective ray length. + /// + /// The borrowed reference band, at least eight samples wide and eight rows high. + /// The equally sized borrowed distorted band. + /// The primary symmetric-difference weight. + /// The secondary reference-magnitude penalty weight. + /// The magnitude normalization. + /// The equally sized difference workspace, separate from the input and AC planes. + /// The equally sized existing AC accumulation. + /// Whether the input and workspace dimensions match. public static bool MaltaDiffMap( JxlPlane lum0, JxlPlane lum1, - float w0Gt1, - float w0Lt1, - float norm1, + double w0Gt1, + double w0Lt1, + double norm1, JxlPlane diffs, JxlPlane blockDiffAc) - { - const float len = 3.75f; - const float mulli = 0.39905817637f; - - return MaltaDiffMap( - false, - lum0, - lum1, - w0Gt1, - w0Lt1, - norm1, - len, - mulli, - diffs, - blockDiffAc); - } + => MaltaDiffMap(false, lum0, lum1, w0Gt1, w0Lt1, norm1, 3.75D, 0.39905817637D, diffs, blockDiffAc); + /// + /// Accumulates sparse low-frequency Malta differences with the native scale and effective ray length. + /// + /// The borrowed reference band, at least eight samples wide and eight rows high. + /// The equally sized borrowed distorted band. + /// The primary symmetric-difference weight. + /// The secondary reference-magnitude penalty weight. + /// The magnitude normalization. + /// The equally sized difference workspace, separate from the input and AC planes. + /// The equally sized existing AC accumulation. + /// Whether the input and workspace dimensions match. public static bool MaltaDiffMapLf( JxlPlane lum0, JxlPlane lum1, - float w0Gt1, - float w0Lt1, - float norm1, + double w0Gt1, + double w0Lt1, + double norm1, JxlPlane diffs, JxlPlane blockDiffAc) + => MaltaDiffMap(true, lum0, lum1, w0Gt1, w0Lt1, norm1, 3.75D, 0.611612573796D, diffs, blockDiffAc); + + /// + /// Copies a center's valid neighbors into the reused zero-extended scalar Malta block. + /// + /// The borrowed complete difference plane at its logical origin. + /// The logical source width. + /// The logical source height. + /// The physical source stride in samples. + /// The valid center column. + /// The valid center row. + /// The 81-sample block, cleared before valid source rows are copied into it. + private static void PrepareMaltaBorder(ReadOnlySpan differences, int width, int height, int stride, int x, int y, Span border) { - const float len = 3.75f; - const float mulli = 0.611612573796f; - - return MaltaDiffMap( - true, - lum0, - lum1, - w0Gt1, - w0Lt1, - norm1, - len, - mulli, - diffs, - blockDiffAc); + border.Clear(); + int firstX = Math.Max(0, x - 4); + int lastX = Math.Min(width, x + 5); + int copyWidth = lastX - firstX; + int destinationX = firstX - (x - 4); + + for (int dy = -4; dy <= 4; dy++) + { + int sourceY = y + dy; + if ((uint)sourceY < (uint)height) + { + differences.Slice((sourceY * stride) + firstX, copyWidth).CopyTo(border.Slice(((dy + 4) * 9) + destinationX, copyWidth)); + } + } } /// @@ -1532,51 +1042,69 @@ public static bool MaltaDiffMapLf( /// /// The borrowed high-frequency X and Y planes. /// The borrowed ultra-high-frequency X and Y planes. - /// The distinct preallocated masking output. + /// The distinct preallocated masking output, with the same dimensions as the four input bands. public static void CombineChannelsForMasking(InlineArray2> hf, InlineArray2> uhf, JxlPlane output) { - // Only X and Y components are involved in masking. - ReadOnlySpan muls = - [ - 2.5f, - 0.4f, - 0.4f - ]; - - int width = hf[0].XSize; - int height = hf[0].YSize; - - Span hfSamples0 = hf[0].GetRow(0); - Span hfSamples1 = hf[1].GetRow(0); - Span uhfSamples0 = uhf[0].GetRow(0); - Span uhfSamples1 = uhf[1].GetRow(0); + ReadOnlySpan hfX = hf[0].GetRow(0); + ReadOnlySpan hfY = hf[1].GetRow(0); + ReadOnlySpan uhfX = uhf[0].GetRow(0); + ReadOnlySpan uhfY = uhf[1].GetRow(0); Span outputSamples = output.GetRow(0); + int hfXStride = hf[0].PixelsPerRow; + int hfYStride = hf[1].PixelsPerRow; + int uhfXStride = uhf[0].PixelsPerRow; + int uhfYStride = uhf[1].PixelsPerRow; + int width = output.XSize; + MaskingMagnitudeOperator operation = default; + + for (int y = 0; y < output.YSize; y++) + { + ref float rowHfX = ref MemoryMarshal.GetReference(hfX.Slice(y * hfXStride, width)); + ref float rowHfY = ref MemoryMarshal.GetReference(hfY.Slice(y * hfYStride, width)); + ref float rowUhfX = ref MemoryMarshal.GetReference(uhfX.Slice(y * uhfXStride, width)); + ref float rowUhfY = ref MemoryMarshal.GetReference(uhfY.Slice(y * uhfYStride, width)); + ref float result = ref MemoryMarshal.GetReference(outputSamples.Slice(y * output.PixelsPerRow, width)); + int x = 0; - for (int y = 0; y < height; y++) - { - ReadOnlySpan rowYHf = hfSamples1[(y * hf[1].PixelsPerRow)..]; - ReadOnlySpan rowYUhf = uhfSamples1[(y * uhf[1].PixelsPerRow)..]; - ReadOnlySpan rowXHf = hfSamples0[(y * hf[0].PixelsPerRow)..]; - ReadOnlySpan rowXUhf = uhfSamples0[(y * uhf[0].PixelsPerRow)..]; - - Span row = outputSamples[(y * output.PixelsPerRow)..]; + // The four planes remain separate. Sum the X bands in registers, then use the shared ternary operator + // for the native magnitude equation. Each lane writes one logical output sample without an intermediate pass. + if (Vector512.IsHardwareAccelerated) + { + for (; x <= width - Vector512.Count; x += Vector512.Count) + { + Vector512 xSum = Vector512.LoadUnsafe(ref rowUhfX, (uint)x) + Vector512.LoadUnsafe(ref rowHfX, (uint)x); + Vector512 yUhf = Vector512.LoadUnsafe(ref rowUhfY, (uint)x); + Vector512 yHf = Vector512.LoadUnsafe(ref rowHfY, (uint)x); + operation.Invoke(xSum, yUhf, yHf).StoreUnsafe(ref result, (uint)x); + } + } - int x = 0; + if (Vector256.IsHardwareAccelerated) + { + for (; x <= width - Vector256.Count; x += Vector256.Count) + { + Vector256 xSum = Vector256.LoadUnsafe(ref rowUhfX, (uint)x) + Vector256.LoadUnsafe(ref rowHfX, (uint)x); + Vector256 yUhf = Vector256.LoadUnsafe(ref rowUhfY, (uint)x); + Vector256 yHf = Vector256.LoadUnsafe(ref rowHfY, (uint)x); + operation.Invoke(xSum, yUhf, yHf).StoreUnsafe(ref result, (uint)x); + } + } - // Each lane processes one logical sample. The scalar remainder retains the same arithmetic order. - for (; x <= width - Vector.Count; x += Vector.Count) + if (Vector128.IsHardwareAccelerated) { - Vector xDiff = (new Vector(rowXUhf[x..]) + new Vector(rowXHf[x..])) * Vector.Create(muls[0]); - Vector yDiff = (new Vector(rowYUhf[x..]) * Vector.Create(muls[1])) + (new Vector(rowYHf[x..]) * Vector.Create(muls[2])); - Vector.SquareRoot((xDiff * xDiff) + (yDiff * yDiff)).CopyTo(row[x..]); + for (; x <= width - Vector128.Count; x += Vector128.Count) + { + Vector128 xSum = Vector128.LoadUnsafe(ref rowUhfX, (uint)x) + Vector128.LoadUnsafe(ref rowHfX, (uint)x); + Vector128 yUhf = Vector128.LoadUnsafe(ref rowUhfY, (uint)x); + Vector128 yHf = Vector128.LoadUnsafe(ref rowHfY, (uint)x); + operation.Invoke(xSum, yUhf, yHf).StoreUnsafe(ref result, (uint)x); + } } for (; x < width; x++) { - float xDiff = (rowXUhf[x] + rowXHf[x]) * muls[0]; - float yDiff = (rowYUhf[x] * muls[1]) + (rowYHf[x] * muls[2]); - - row[x] = MathF.Sqrt((xDiff * xDiff) + (yDiff * yDiff)); + float xSum = Unsafe.Add(ref rowUhfX, x) + Unsafe.Add(ref rowHfX, x); + Unsafe.Add(ref result, x) = operation.Invoke(xSum, Unsafe.Add(ref rowUhfY, x), Unsafe.Add(ref rowHfY, x)); } } } @@ -2103,6 +1631,190 @@ public static void L2DiffAsymmetric(JxlPlane i0, JxlPlane i1, floa } } + /// + /// Prepares Malta differences using float samples and coefficients normalized once during setup. + /// + /// The native primary symmetric-difference weight, before narrowing. + /// The native secondary magnitude-penalty weight, before narrowing. + /// The native magnitude normalization, before narrowing. + /// The native effective ray half-length. + /// The native frequency-specific scale. + private readonly struct MaltaDifferenceOperator( + double w0Gt1, + double w0Lt1, + double norm1, + double effectiveHalfLength, + double multiplier) : TensorPrimitives_.IBinaryOperator + { + /// The lower secondary-response boundary as a fraction of the reference magnitude. + private const float LowerFraction = 0.55F; + + /// The upper secondary-response boundary as a fraction of the reference magnitude. + private const float UpperFraction = 1.05F; + + /// The native magnitude offset rounded once for the float denominator. + private readonly float normalization = (float)norm1; + + /// The primary coefficient, computed in native double precision once and then rounded to float. + private readonly float firstCoefficient = (float)(multiplier * Math.Sqrt(0.5F * w0Gt1) / ((effectiveHalfLength * 2D) + 1D) * norm1); + + /// The secondary coefficient. Native promotes its already-rounded 0.33F weight before the double product. + private readonly float secondCoefficient = (float)(multiplier * Math.Sqrt(0.33F * w0Lt1) / ((effectiveHalfLength * 2D) + 1D) * norm1); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float Invoke(float first, float second) + { + float magnitude = MathF.Abs(first); + float denominator = this.normalization + (0.5F * (magnitude + MathF.Abs(second))); + float result = (this.firstCoefficient / denominator) * (first - second); + float secondaryScale = this.secondCoefficient / denominator; + bool negative = first < 0F; + float actual = negative ? -second : second; + float lower = LowerFraction * magnitude; + float upper = UpperFraction * magnitude; + float response; + if (actual < lower) + { + response = lower - actual; + } + else if (actual > upper) + { + response = upper - actual; + } + else + { + return result; + } + + // Native evaluates the secondary threshold and impact in double for each sample. This comparison retains + // float pixel arithmetic instead. Scalar and SIMD use the same rounded fractions and one final multiply-add; + // they follow the native objective but do not claim its double-secondary rounding bits. + return float.MultiplyAddEstimate(secondaryScale, negative ? -response : response, result); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector128 Invoke(Vector128 first, Vector128 second) + { + Vector128 magnitude = Vector128.Abs(first); + Vector128 denominator = Vector128.Create(this.normalization) + (Vector128.Create(0.5F) * (magnitude + Vector128.Abs(second))); + Vector128 result = (Vector128.Create(this.firstCoefficient) / denominator) * (first - second); + Vector128 secondaryScale = Vector128.Create(this.secondCoefficient) / denominator; + Vector128 negative = Vector128.LessThan(first, Vector128.Zero); + Vector128 actual = Vector128.ConditionalSelect(negative, -second, second); + Vector128 lower = Vector128.Create(LowerFraction) * magnitude; + Vector128 upper = Vector128.Create(UpperFraction) * magnitude; + Vector128 below = Vector128.LessThan(actual, lower); + Vector128 above = Vector128.GreaterThan(actual, upper); + Vector128 response = Vector128.ConditionalSelect(below, lower - actual, upper - actual); + response = Vector128.ConditionalSelect(negative, -response, response); + Vector128 corrected = Vector128.MultiplyAddEstimate(secondaryScale, response, result); + + // Inactive lanes retain the original result, including its zero sign. Each active lane selects exactly one + // half-open secondary objective; reflecting negative references gives the scalar sign rules without branching. + return Vector128.ConditionalSelect(Vector128.BitwiseOr(below, above), corrected, result); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector256 Invoke(Vector256 first, Vector256 second) + { + Vector256 magnitude = Vector256.Abs(first); + Vector256 denominator = Vector256.Create(this.normalization) + (Vector256.Create(0.5F) * (magnitude + Vector256.Abs(second))); + Vector256 result = (Vector256.Create(this.firstCoefficient) / denominator) * (first - second); + Vector256 secondaryScale = Vector256.Create(this.secondCoefficient) / denominator; + Vector256 negative = Vector256.LessThan(first, Vector256.Zero); + Vector256 actual = Vector256.ConditionalSelect(negative, -second, second); + Vector256 lower = Vector256.Create(LowerFraction) * magnitude; + Vector256 upper = Vector256.Create(UpperFraction) * magnitude; + Vector256 below = Vector256.LessThan(actual, lower); + Vector256 above = Vector256.GreaterThan(actual, upper); + Vector256 response = Vector256.ConditionalSelect(below, lower - actual, upper - actual); + response = Vector256.ConditionalSelect(negative, -response, response); + Vector256 corrected = Vector256.MultiplyAddEstimate(secondaryScale, response, result); + + // Inactive lanes retain the original result, including its zero sign. Each active lane selects exactly one + // half-open secondary objective; reflecting negative references gives the scalar sign rules without branching. + return Vector256.ConditionalSelect(Vector256.BitwiseOr(below, above), corrected, result); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector512 Invoke(Vector512 first, Vector512 second) + { + Vector512 magnitude = Vector512.Abs(first); + Vector512 denominator = Vector512.Create(this.normalization) + (Vector512.Create(0.5F) * (magnitude + Vector512.Abs(second))); + Vector512 result = (Vector512.Create(this.firstCoefficient) / denominator) * (first - second); + Vector512 secondaryScale = Vector512.Create(this.secondCoefficient) / denominator; + Vector512 negative = Vector512.LessThan(first, Vector512.Zero); + Vector512 actual = Vector512.ConditionalSelect(negative, -second, second); + Vector512 lower = Vector512.Create(LowerFraction) * magnitude; + Vector512 upper = Vector512.Create(UpperFraction) * magnitude; + Vector512 below = Vector512.LessThan(actual, lower); + Vector512 above = Vector512.GreaterThan(actual, upper); + Vector512 response = Vector512.ConditionalSelect(below, lower - actual, upper - actual); + response = Vector512.ConditionalSelect(negative, -response, response); + Vector512 corrected = Vector512.MultiplyAddEstimate(secondaryScale, response, result); + + // Inactive lanes retain the original result, including its zero sign. Each active lane selects exactly one + // half-open secondary objective; reflecting negative references gives the scalar sign rules without branching. + return Vector512.ConditionalSelect(Vector512.BitwiseOr(below, above), corrected, result); + } + } + + /// + /// Combines summed X bands and the two Y bands into the native masking magnitude. + /// + private readonly struct MaskingMagnitudeOperator : TensorPrimitives_.ITernaryOperator + { + /// The weight for the sum of X high and ultra-high frequencies. + private const float XMultiplier = 2.5F; + + /// The weight applied separately to each Y frequency band. + private const float YMultiplier = 0.4F; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float Invoke(float first, float second, float third) + { + float x = first * XMultiplier; + float y = (second * YMultiplier) + (third * YMultiplier); + + return MathF.Sqrt((x * x) + (y * y)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector128 Invoke(Vector128 first, Vector128 second, Vector128 third) + { + Vector128 x = first * Vector128.Create(XMultiplier); + Vector128 y = (second * Vector128.Create(YMultiplier)) + (third * Vector128.Create(YMultiplier)); + + return Vector128.Sqrt((x * x) + (y * y)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector256 Invoke(Vector256 first, Vector256 second, Vector256 third) + { + Vector256 x = first * Vector256.Create(XMultiplier); + Vector256 y = (second * Vector256.Create(YMultiplier)) + (third * Vector256.Create(YMultiplier)); + + return Vector256.Sqrt((x * x) + (y * y)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector512 Invoke(Vector512 first, Vector512 second, Vector512 third) + { + Vector512 x = first * Vector512.Create(XMultiplier); + Vector512 y = (second * Vector512.Create(YMultiplier)) + (third * Vector512.Create(YMultiplier)); + + return Vector512.Sqrt((x * x) + (y * y)); + } + } + /// /// Removes the interval around zero from independent frequency samples. /// @@ -4106,23 +3818,47 @@ public static float ButteraugliScoreFromDiffmap(JxlPlane diffmap, Buttera return retval; } + /// + /// Accumulates full Malta differences into one component of an existing AC image. + /// + /// The borrowed reference band, at least eight samples wide and eight rows high. + /// The equally sized borrowed distorted band. + /// The primary symmetric-difference weight, retained until operation setup. + /// The secondary magnitude-penalty weight, retained until operation setup. + /// The native magnitude normalization, retained until operation setup. + /// The equally sized prepared-difference workspace, separate from the input and AC planes. + /// The equally sized existing AC accumulation image. + /// The accumulation component, in X, Y, B order. + /// Whether the input and workspace dimensions match. public static bool MaltaDiffMap( JxlPlane lum0, JxlPlane lum1, - float w0Gt1, - float w0Lt1, - float norm1, + double w0Gt1, + double w0Lt1, + double norm1, JxlPlane diffs, JxlImage3 blockDiffAc, int c) => MaltaDiffMap(isLf: false, lum0, lum1, w0Gt1, w0Lt1, norm1, diffs, blockDiffAc.Plane(c)); + /// + /// Accumulates low-frequency Malta differences into one component of an existing AC image. + /// + /// The borrowed reference band, at least eight samples wide and eight rows high. + /// The equally sized borrowed distorted band. + /// The primary symmetric-difference weight, retained until operation setup. + /// The secondary magnitude-penalty weight, retained until operation setup. + /// The native magnitude normalization, retained until operation setup. + /// The equally sized prepared-difference workspace, separate from the input and AC planes. + /// The equally sized existing AC accumulation image. + /// The accumulation component, in X, Y, B order. + /// Whether the input and workspace dimensions match. public static bool MaltaDiffMapLf( JxlPlane lum0, JxlPlane lum1, - float w0Gt1, - float w0Lt1, - float norm1, + double w0Gt1, + double w0Lt1, + double norm1, JxlPlane diffs, JxlImage3 blockDiffAc, int c) diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index 51e542bdcc..5434a51d7f 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -52,6 +52,15 @@ + + True + True + Butteraugli.Malta.Generated.tt + + + TextTemplatingFileGenerator + Butteraugli.Malta.Generated.cs + True True diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs index c212f9e121..7fbd8993ad 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs @@ -393,8 +393,8 @@ static void RunTest(string serialized) // Native butteraugli.cc assigns these MF penalties to both error directions, independently of HF weights. // The named single-plane LF kernel owns native scaling 0.611612573796; the retained API must select that same kernel. - float maltaWeight = changedComponent == 0 ? 8246.75321353F : 37.0819870399F; - float normalization = changedComponent == 0 ? 1009002.70582F : 130262059.556F; + double maltaWeight = changedComponent == 0 ? 8246.75321353D : 37.0819870399D; + double normalization = changedComponent == 0 ? 1009002.70582D : 130262059.556D; float squaredErrorWeight = changedComponent == 0 ? 2150F : 10.6195433239F; Assert.True(Butteraugli.MaltaDiffMapLf( referenceBands.Mf.Plane(changedComponent), @@ -1007,4 +1007,278 @@ static void RunTest() HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); } + /// + /// Checks native Malta ray responses, zero-extension, accumulation and logical bounds through the retained comparison API. + /// + [Fact] + public void MaltaDifference_AccumulatesKnownImpulseRaysAndPreservesPadding() + { + static void RunTest() + { + const float padding = -123F; + ReadOnlySpan sizes = [new(8, 8), new(13, 9), new(17, 9), new(33, 9), new(41, 10)]; + ReadOnlySpan<(int X, int Y, int HfRays, int LfRays)> checks = + [ + (0, 0, 16, 16), (4, 0, 1, 1), (1, 0, 5, 0), (3, 3, 1, 1), (4, 4, 0, 0), + (-4, 0, 1, 1), (-1, 0, 5, 0), (-3, -3, 1, 1), (-4, -4, 0, 0), (5, 0, 0, 0) + ]; + + foreach (Size size in sizes) + { + using JxlPlane reference = new(TestEnvironment.Configuration, size.Width + 3, size.Height); + using JxlPlane distorted = new(TestEnvironment.Configuration, size.Width + 19, size.Height); + using JxlPlane differences = new(TestEnvironment.Configuration, size.Width + 35, size.Height); + using JxlPlane output = new(TestEnvironment.Configuration, size.Width + 51, size.Height); + reference.ShrinkTo(size.Width, size.Height); + distorted.ShrinkTo(size.Width, size.Height); + differences.ShrinkTo(size.Width, size.Height); + output.ShrinkTo(size.Width, size.Height); + Span referenceSamples = reference.GetRow(0); + Span distortedSamples = distorted.GetRow(0); + Span differenceSamples = differences.GetRow(0); + Span outputSamples = output.GetRow(0); + + for (int mode = 0; mode < 2; mode++) + { + bool lowFrequency = mode != 0; + // With w0Gt1=2 and norm1=0.5, a reference impulse of one has denominator one. Native setup gives + // amplitude mulli/17, rounded once: HF=0x3CC04C91 and LF=0x3D135CD7. The secondary penalty is disabled. + float amplitude = BitConverter.Int32BitsToSingle(lowFrequency ? 0x3D135CD7 : 0x3CC04C91); + for (int position = 0; position < 2; position++) + { + int impulseX = position == 0 ? size.Width / 2 : 0; + int impulseY = position == 0 ? size.Height / 2 : 0; + referenceSamples.Fill(padding); + distortedSamples.Fill(padding); + differenceSamples.Fill(padding); + outputSamples.Fill(padding); + for (int y = 0; y < size.Height; y++) + { + referenceSamples.Slice(y * reference.PixelsPerRow, size.Width).Clear(); + distortedSamples.Slice(y * distorted.PixelsPerRow, size.Width).Clear(); + outputSamples.Slice(y * output.PixelsPerRow, size.Width).Fill(7F); + } + + referenceSamples[(impulseY * reference.PixelsPerRow) + impulseX] = 1F; + Assert.True(Butteraugli.MaltaDiffMap(lowFrequency, reference, distorted, 2D, 0D, 0.5D, differences, output)); + + // Every ray contains its center. Exactly one horizontal ray reaches offset (4,0); five full rays + // cross (1,0), which sparse LF rays omit. The diagonal ray ends at (3,3), not the absent (4,4). + // A single impulse therefore needs only the known ray multiplicity, not another stencil implementation. + foreach ((int dx, int dy, int hfRays, int lfRays) in checks) + { + int x = impulseX + dx; + int y = impulseY + dy; + if ((uint)x < (uint)size.Width && (uint)y < (uint)size.Height) + { + int rays = lowFrequency ? lfRays : hfRays; + float energy = 0F; + for (int ray = 0; ray < rays; ray++) + { + energy = float.MultiplyAddEstimate(amplitude, amplitude, energy); + } + + Assert.Equal(7F + energy, outputSamples[(y * output.PixelsPerRow) + x]); + } + } + + for (int y = 0; y < size.Height; y++) + { + for (int x = 0; x < size.Width; x++) + { + bool impulse = x == impulseX && y == impulseY; + Assert.Equal(impulse ? 1F : 0F, referenceSamples[(y * reference.PixelsPerRow) + x]); + Assert.Equal(0F, distortedSamples[(y * distorted.PixelsPerRow) + x]); + Assert.Equal(impulse ? amplitude : 0F, differenceSamples[(y * differences.PixelsPerRow) + x]); + } + + Assert.Equal( + -1, + referenceSamples.Slice((y * reference.PixelsPerRow) + size.Width, reference.PixelsPerRow - size.Width) + .IndexOfAnyExcept(padding)); + + Assert.Equal( + -1, + distortedSamples.Slice((y * distorted.PixelsPerRow) + size.Width, distorted.PixelsPerRow - size.Width) + .IndexOfAnyExcept(padding)); + + Assert.Equal( + -1, + differenceSamples.Slice((y * differences.PixelsPerRow) + size.Width, differences.PixelsPerRow - size.Width) + .IndexOfAnyExcept(padding)); + + Assert.Equal( + -1, + outputSamples.Slice((y * output.PixelsPerRow) + size.Width, output.PixelsPerRow - size.Width).IndexOfAnyExcept(padding)); + + } + } + } + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } + + /// + /// Checks nonzero masking magnitudes across different physical strides, all SIMD widths and scalar tails. + /// + [Fact] + public void MaskingMagnitude_CombinesFourBandsWithoutChangingPadding() + { + static void RunTest() + { + const float padding = -123F; + ReadOnlySpan widths = [1, 3, 17, 33]; + foreach (int width in widths) + { + using JxlPlane hfX = new(TestEnvironment.Configuration, width + 3, 3); + using JxlPlane hfY = new(TestEnvironment.Configuration, width + 19, 3); + using JxlPlane uhfX = new(TestEnvironment.Configuration, width + 35, 3); + using JxlPlane uhfY = new(TestEnvironment.Configuration, width + 51, 3); + using JxlPlane output = new(TestEnvironment.Configuration, width, 3); + hfX.ShrinkTo(width, 3); + hfY.ShrinkTo(width, 3); + uhfX.ShrinkTo(width, 3); + uhfY.ShrinkTo(width, 3); + InlineArray2> hf = default; + InlineArray2> uhf = default; + hf[0] = hfX; + hf[1] = hfY; + uhf[0] = uhfX; + uhf[1] = uhfY; + Span hfXSamples = hfX.GetRow(0); + Span hfYSamples = hfY.GetRow(0); + Span uhfXSamples = uhfX.GetRow(0); + Span uhfYSamples = uhfY.GetRow(0); + Span outputSamples = output.GetRow(0); + hfXSamples.Fill(padding); + hfYSamples.Fill(padding); + uhfXSamples.Fill(padding); + uhfYSamples.Fill(padding); + outputSamples.Fill(padding); + + for (int y = 0; y < 3; y++) + { + for (int x = 0; x < width; x++) + { + float xSign = (x & 1) == 0 ? 1F : -1F; + float ySign = ((x + y) & 1) == 0 ? 1F : -1F; + hfXSamples[(y * hfX.PixelsPerRow) + x] = xSign; + uhfXSamples[(y * uhfX.PixelsPerRow) + x] = xSign; + hfYSamples[(y * hfY.PixelsPerRow) + x] = 22.5F * ySign; + uhfYSamples[(y * uhfY.PixelsPerRow) + x] = 7.5F * ySign; + } + } + + Butteraugli.CombineChannelsForMasking(hf, uhf, output); + + // X gives magnitude 5; the separate Y products give 3 + 9 = 12. The known 5-12-13 triangle is independent + // of the SIMD implementation and checks both signs without copying its square/square-root expression. + for (int y = 0; y < 3; y++) + { + for (int x = 0; x < width; x++) + { + float xSign = (x & 1) == 0 ? 1F : -1F; + float ySign = ((x + y) & 1) == 0 ? 1F : -1F; + Assert.Equal(13F, outputSamples[(y * output.PixelsPerRow) + x]); + Assert.Equal(xSign, hfXSamples[(y * hfX.PixelsPerRow) + x]); + Assert.Equal(xSign, uhfXSamples[(y * uhfX.PixelsPerRow) + x]); + Assert.Equal(22.5F * ySign, hfYSamples[(y * hfY.PixelsPerRow) + x]); + Assert.Equal(7.5F * ySign, uhfYSamples[(y * uhfY.PixelsPerRow) + x]); + } + + Assert.Equal(-1, hfXSamples.Slice((y * hfX.PixelsPerRow) + width, hfX.PixelsPerRow - width).IndexOfAnyExcept(padding)); + Assert.Equal(-1, hfYSamples.Slice((y * hfY.PixelsPerRow) + width, hfY.PixelsPerRow - width).IndexOfAnyExcept(padding)); + Assert.Equal(-1, uhfXSamples.Slice((y * uhfX.PixelsPerRow) + width, uhfX.PixelsPerRow - width).IndexOfAnyExcept(padding)); + Assert.Equal(-1, uhfYSamples.Slice((y * uhfY.PixelsPerRow) + width, uhfY.PixelsPerRow - width).IndexOfAnyExcept(padding)); + Assert.Equal(-1, outputSamples.Slice((y * output.PixelsPerRow) + width, output.PixelsPerRow - width).IndexOfAnyExcept(padding)); + } + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } + + /// + /// Checks the float directional-penalty contract through both retained Malta APIs at every SIMD width. + /// + [Fact] + public void MaltaDifference_PreparesBothPenaltyDirectionsWithFloatArithmetic() + { + static void RunTest() + { + const float padding = -123F; + ReadOnlySpan widths = [13, 17, 33]; + ReadOnlySpan<(float Reference, float Actual, float Denominator, float Difference, float Penalty)> cases = + [ + (1F, 0F, 1F, 1F, 0.55F), (-1F, 0F, 1F, -1F, -0.55F), + (1F, 2F, 2F, -1F, 1.05F - 2F), (-1F, -2F, 2F, 1F, 2F - 1.05F), + (1F, 1F, 1.5F, 0F, 0F), (-1F, -1F, 1.5F, 0F, 0F), + (0F, 1F, 1F, -1F, -1F), (0F, -1F, 1F, 1F, 1F), + (1F, 0.5F, 1.25F, 0.5F, 0.55F - 0.5F), (-1F, -0.5F, 1.25F, -0.5F, 0.5F - 0.55F) + ]; + + foreach (int width in widths) + { + using JxlPlane reference = new(TestEnvironment.Configuration, width, 8); + using JxlPlane distorted = new(TestEnvironment.Configuration, width, 8); + using JxlPlane differences = new(TestEnvironment.Configuration, width, 8); + using JxlPlane output = new(TestEnvironment.Configuration, width, 8); + Span referenceSamples = reference.GetRow(0); + Span distortedSamples = distorted.GetRow(0); + Span differenceSamples = differences.GetRow(0); + Span outputSamples = output.GetRow(0); + referenceSamples.Fill(padding); + distortedSamples.Fill(padding); + for (int y = 0; y < 8; y++) + { + for (int x = 0; x < width; x++) + { + var sample = cases[(x + y) % cases.Length]; + referenceSamples[(y * reference.PixelsPerRow) + x] = sample.Reference; + distortedSamples[(y * distorted.PixelsPerRow) + x] = sample.Actual; + } + } + + for (int mode = 0; mode < 2; mode++) + { + bool lowFrequency = mode != 0; + // These bits are native double setup for weights 2,2 and normalization 0.5, rounded once to float. + // They are independent of the per-sample float thresholds and final multiply-add checked below. + float firstCoefficient = BitConverter.Int32BitsToSingle(lowFrequency ? 0x3D135CD7 : 0x3CC04C91); + float secondCoefficient = BitConverter.Int32BitsToSingle(lowFrequency ? 0x3CEF6F9C : 0x3C9C397A); + differenceSamples.Fill(padding); + outputSamples.Fill(padding); + output.Clear(); + Assert.True(Butteraugli.MaltaDiffMap(lowFrequency, reference, distorted, 2D, 2D, 0.5D, differences, output)); + + for (int y = 0; y < 8; y++) + { + for (int x = 0; x < width; x++) + { + var sample = cases[(x + y) % cases.Length]; + float primary = (firstCoefficient / sample.Denominator) * sample.Difference; + float expected = float.MultiplyAddEstimate(secondCoefficient / sample.Denominator, sample.Penalty, primary); + Assert.Equal(expected, differenceSamples[(y * differences.PixelsPerRow) + x]); + } + + Assert.Equal( + -1, + differenceSamples.Slice((y * differences.PixelsPerRow) + width, differences.PixelsPerRow - width).IndexOfAnyExcept(padding)); + + Assert.Equal(-1, outputSamples.Slice((y * output.PixelsPerRow) + width, output.PixelsPerRow - width).IndexOfAnyExcept(padding)); + } + } + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } } From 984cde8ce4492e00b0b5758322fd88d3b0f727b4 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Oct 2026 05:52:18 +1000 Subject: [PATCH 322/328] Correct inverse Gaborish weights and share bounded SIMD convolution --- .../Jxl/Processing/Encoder/JxlGaborish.cs | 19 +- .../Formats/Jxl/Processing/JxlConvolve.cs | 388 +++++++++++------- .../Formats/Jxl/Processing/GaborishTests.cs | 108 +++++ 3 files changed, 358 insertions(+), 157 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGaborish.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGaborish.cs index d07512ab5e..fa3aa3f23f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGaborish.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGaborish.cs @@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; /// -/// Gaborish transform +/// Applies the encoder's approximate inverse Gaborish filter. /// internal static class JxlGaborish { @@ -16,14 +16,25 @@ internal static class JxlGaborish -0.09495815671340026f, -0.041031725066768575f, 0.013710004822696948f, 0.006510206083837737f, -0.0014789063378272242f]; + /// + /// Filters an expanded rectangle in place while retaining the original neighboring samples in distinct planes. + /// + /// The configuration that owns temporary plane storage. + /// The image to filter. + /// The requested rectangle, expanded by three samples within the image. + /// The inverse-filter strength for each channel. public static void InverseGaborish(Configuration configuration, JxlImage3 inOut, Rectangle rect, InlineArray3 mul) { InlineArray3 weights = default; for (int i = 0; i < 3; ++i) { - double sum = 1.0 + (mul[i] * 4 * ((GaborishLookup[0] + GaborishLookup[1]) + (GaborishLookup[2] + GaborishLookup[4]) + (2 * GaborishLookup[3]))); - sum = Math.Max(sum, 1e-5); // if (sum < 1e-5) sum = 1e-5 + // Native prepares the sum in float, from left to right, before adding the double-precision unit center. + // Regrouping the lookup entries changes the normalization and every filtered sample. + float neighborSum = (((GaborishLookup[0] + GaborishLookup[1]) + GaborishLookup[2]) + GaborishLookup[4]) + (2 * GaborishLookup[3]); + double sum = 1D + (mul[i] * 4 * neighborSum); + // The native inverse filter floors the sum so normalization remains finite at large filter strengths. + sum = Math.Max(sum, 1e-5); float normalize = (float)(1.0f / sum); float normalizeMul = mul[i] * normalize; @@ -39,6 +50,8 @@ public static void InverseGaborish(Configuration configuration, JxlImage3 }; } + // Keep original blue while red and green results reuse the existing blue and red planes. Each convolution + // reads distinct storage, so later rows cannot see filtered neighbors. Swap the planes back after all three passes. using JxlPlane temp = new(configuration, inOut.Plane(2).XSize, inOut.Plane(2).YSize); JxlImageOperations.ForceCopyImage(inOut.Plane(2), temp); diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs index d6af278941..d1d7de8c4c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs @@ -1,12 +1,10 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; using SixLabors.ImageSharp.Common.Helpers; -using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; @@ -61,173 +59,193 @@ public static bool Separable5(JxlPlane input, in Rectangle rectangle, in } /// - /// Weighted sum of 1x5 pixels around ix, iy with [wx2, wx1, wx0, wx1, wx2]. + /// Applies a symmetric five-by-five kernel to distinct source and destination rectangles. /// - public static float WeightedSumBorder( - JxlPlane input, - Func wrapY, - long ix, - long iy, - int width, - int height, - float wx0, - float wx1, - float wx2) + /// The borrowed source plane. Reflection uses its full logical dimensions. + /// The source range, inside the input plane. + /// The six symmetric weights, each repeated in a four-value group. + /// The distinct destination plane. + /// The destination range, with the same dimensions as the source range. + /// Whether both rectangles have matching dimensions. + public static bool Symmetric5(JxlPlane input, in Rectangle rectangle, ref JxlWeightsSymmetric5 weights, JxlPlane output, Rectangle outputRect) { - ReadOnlySpan row = input.GetRow(wrapY(iy, height)); + if (rectangle.Width != outputRect.Width || rectangle.Height != outputRect.Height) + { + return false; + } - float inM2 = row[WrapMirror(ix - 2, width)]; - float inP2 = row[WrapMirror(ix + 2, width)]; - float inM1 = row[WrapMirror(ix - 1, width)]; - float inP1 = row[WrapMirror(ix + 1, width)]; - float in00 = row[(int)ix]; + // Each row has three horizontal weights. The adjacent and outer rows use different weights, so this kernel + // is not separable. Prepare those operations once and retain all five horizontal results in registers. + WeightedSumOperator center = new(weights.C[0], weights.R[0], weights.R2[0]); + WeightedSumOperator adjacent = new(weights.R[0], weights.D[0], weights.L[0]); + WeightedSumOperator outer = new(weights.R2[0], weights.L[0], weights.D2[0]); + ReadOnlySpan source = input.GetRow(0); + Span destination = output.GetRow(0); + int inputStride = input.PixelsPerRow; + int outputStride = output.PixelsPerRow; + + for (int y = 0; y < rectangle.Height; y++) + { + int sourceY = rectangle.Y + y; + ReadOnlySpan top2 = source.Slice(JxlImageOperations.Mirror(sourceY - 2L, input.YSize) * inputStride, input.XSize); + ReadOnlySpan top1 = source.Slice(JxlImageOperations.Mirror(sourceY - 1L, input.YSize) * inputStride, input.XSize); + ReadOnlySpan middle = source.Slice(sourceY * inputStride, input.XSize); + ReadOnlySpan bottom1 = source.Slice(JxlImageOperations.Mirror(sourceY + 1L, input.YSize) * inputStride, input.XSize); + ReadOnlySpan bottom2 = source.Slice(JxlImageOperations.Mirror(sourceY + 2L, input.YSize) * inputStride, input.XSize); + Span row = destination.Slice(((outputRect.Y + y) * outputStride) + outputRect.X, rectangle.Width); + int x = rectangle.X; + int end = rectangle.Right; + + // Only image edges need reflection. A cropped rectangle can read its two-sample halo from the input image. + // Unaligned loads keep one independent output center in each lane; physical padding is never a halo. + for (; x < Math.Min(2, end); x++) + { + row[x - rectangle.X] = Symmetric5Pixel(top2, top1, middle, bottom1, bottom2, x, center, adjacent, outer); + } - float sum2 = wx2 * (inM2 + inP2); - float sum1 = wx1 * (inM1 + inP1); - float sum0 = wx0 * in00; + int interiorEnd = Math.Min(end, input.XSize - 2); - return sum2 + (sum1 + sum0); - } + if (Vector512.IsHardwareAccelerated) + { + for (; x <= interiorEnd - Vector512.Count; x += Vector512.Count) + { + Vector512 sum0 = WeightedSum512(middle, x, center) + WeightedSum512(top2, x, outer); + Vector512 sum1 = WeightedSum512(bottom2, x, outer); + sum0 += WeightedSum512(top1, x, adjacent); + sum1 += WeightedSum512(bottom1, x, adjacent); + (sum0 + sum1).StoreUnsafe(ref MemoryMarshal.GetReference(row), (uint)(x - rectangle.X)); + } + } - public static Vector WeightedSum( - JxlPlane input, - Func wrapY, - int ix, - long iy, - int height, - Vector wx0, - Vector wx1, - Vector wx2) - { - ReadOnlySpan center = input.GetRow(wrapY(iy, height))[ix..]; - ref float centerRef = ref MemoryMarshal.GetReference(center); + if (Vector256.IsHardwareAccelerated) + { + for (; x <= interiorEnd - Vector256.Count; x += Vector256.Count) + { + Vector256 sum0 = WeightedSum256(middle, x, center) + WeightedSum256(top2, x, outer); + Vector256 sum1 = WeightedSum256(bottom2, x, outer); + sum0 += WeightedSum256(top1, x, adjacent); + sum1 += WeightedSum256(bottom1, x, adjacent); + (sum0 + sum1).StoreUnsafe(ref MemoryMarshal.GetReference(row), (uint)(x - rectangle.X)); + } + } - Vector inM2 = Vector.LoadUnsafe(ref Unsafe.Subtract(ref centerRef, 2)); - Vector inP2 = Vector.LoadUnsafe(ref Unsafe.Add(ref centerRef, 2)); - Vector inM1 = Vector.LoadUnsafe(ref Unsafe.Subtract(ref centerRef, 1)); - Vector inP1 = Vector.LoadUnsafe(ref Unsafe.Add(ref centerRef, 1)); - Vector in00 = Vector.LoadUnsafe(ref centerRef); + if (Vector128.IsHardwareAccelerated) + { + for (; x <= interiorEnd - Vector128.Count; x += Vector128.Count) + { + Vector128 sum0 = WeightedSum128(middle, x, center) + WeightedSum128(top2, x, outer); + Vector128 sum1 = WeightedSum128(bottom2, x, outer); + sum0 += WeightedSum128(top1, x, adjacent); + sum1 += WeightedSum128(bottom1, x, adjacent); + (sum0 + sum1).StoreUnsafe(ref MemoryMarshal.GetReference(row), (uint)(x - rectangle.X)); + } + } - Vector sum2 = wx2 * (inM2 + inP2); - Vector sum1 = wx1 * (inM1 + inP1); - Vector sum0 = wx0 * in00; + // Scalar tails use the identical row grouping. The last two image columns reflect their neighbors. + for (; x < end; x++) + { + row[x - rectangle.X] = Symmetric5Pixel(top2, top1, middle, bottom1, bottom2, x, center, adjacent, outer); + } + } - return sum2 + (sum1 + sum0); + return true; } - public static float Symmetric5Border(JxlPlane input, Func wrapY, long ix, long iy, ref JxlWeightsSymmetric5 weights) + /// + /// Evaluates one center with full-image reflection and the native five-row addition order. + /// + /// The logical row two positions above the center. + /// The logical row one position above the center. + /// The logical center row. + /// The logical row one position below the center. + /// The logical row two positions below the center. + /// The center column in the full input image. + /// The center row's horizontal weighting. + /// The adjacent rows' horizontal weighting. + /// The outer rows' horizontal weighting. + /// The weighted sample. + private static float Symmetric5Pixel( + ReadOnlySpan top2, + ReadOnlySpan top1, + ReadOnlySpan middle, + ReadOnlySpan bottom1, + ReadOnlySpan bottom2, + int x, + WeightedSumOperator center, + WeightedSumOperator adjacent, + WeightedSumOperator outer) { - float w0 = weights.GetCVector()[0]; - float w1 = weights.GetRVector()[0]; - float w2 = weights.GetR2Vector()[0]; - float w4 = weights.GetDVector()[0]; - float w5 = weights.GetCVector()[0]; - float w8 = weights.GetD2Vector()[0]; - - int width = input.XSize; - int height = input.YSize; - - float sum0 = WeightedSumBorder(input, wrapY, ix, iy, width, height, w0, w1, w2) - + WeightedSumBorder(input, wrapY, ix, iy - 2, width, height, w2, w5, w8); + int left1 = x - 1; + int left2 = x - 2; + int right1 = x + 1; + int right2 = x + 2; - float sum1 = WeightedSumBorder(input, wrapY, ix, iy + 2, width, height, w2, w5, w8); - - sum0 += WeightedSumBorder(input, wrapY, ix, iy + 1, width, height, w1, w4, w5); - sum1 += WeightedSumBorder(input, wrapY, ix, iy - 1, width, height, w1, w4, w5); + if (x < 2 || x >= middle.Length - 2) + { + // Reflect at the edge sample (-1 maps to 0). Compute indices once for all five rows, including width one. + left1 = JxlImageOperations.Mirror(left1, middle.Length); + left2 = JxlImageOperations.Mirror(left2, middle.Length); + right1 = JxlImageOperations.Mirror(right1, middle.Length); + right2 = JxlImageOperations.Mirror(right2, middle.Length); + } + float sum0 = center.Invoke(middle[x], middle[left1] + middle[right1], middle[left2] + middle[right2]); + sum0 += outer.Invoke(top2[x], top2[left1] + top2[right1], top2[left2] + top2[right2]); + float sum1 = outer.Invoke(bottom2[x], bottom2[left1] + bottom2[right1], bottom2[left2] + bottom2[right2]); + sum0 += adjacent.Invoke(top1[x], top1[left1] + top1[right1], top1[left2] + top1[right2]); + sum1 += adjacent.Invoke(bottom1[x], bottom1[left1] + bottom1[right1], bottom1[left2] + bottom1[right2]); return sum0 + sum1; } - public static void Symmetric5Interior( - JxlPlane image, - int ix, - Func wrapY, - int rix, - long iy, - ref JxlWeightsSymmetric5 weights, - Span rowOut) + /// + /// Weights complete interior centers without reading outside their two-sample logical halo. + /// + /// The full logical source row. + /// The first center index. Both halo samples and every result lane must exist. + /// The row's center, distance-one and distance-two weights. + /// One horizontal weighted sum per lane. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 WeightedSum128(ReadOnlySpan row, int x, WeightedSumOperator operation) { - Vector w0 = JxlSimdUtils.LoadDuplicate128(weights.GetCVector()); // c - Vector w1 = JxlSimdUtils.LoadDuplicate128(weights.GetRVector()); // r - Vector w2 = JxlSimdUtils.LoadDuplicate128(weights.GetR2Vector()); // R - Vector w4 = JxlSimdUtils.LoadDuplicate128(weights.GetDVector()); // d - Vector w5 = JxlSimdUtils.LoadDuplicate128(weights.GetLVector()); // L - Vector w8 = JxlSimdUtils.LoadDuplicate128(weights.GetD2Vector()); // D - - int height = image.YSize; - Vector sum0 = WeightedSum(image, wrapY, ix, iy, height, w0, w1, w2) - + WeightedSum(image, wrapY, ix, iy - 2, height, w2, w5, w8); - - Vector sum1 = WeightedSum(image, wrapY, ix, iy + 2, height, w2, w5, w8); - - sum0 += WeightedSum(image, wrapY, ix, iy - 1, height, w1, w4, w5); - sum1 += WeightedSum(image, wrapY, ix, iy + 1, height, w1, w4, w5); - - (sum0 + sum1).StoreUnsafe(ref Unsafe.Add(ref MemoryMarshal.GetReference(rowOut), rix)); + ref float first = ref MemoryMarshal.GetReference(row); + Vector128 center = Vector128.LoadUnsafe(ref first, (uint)x); + Vector128 near = Vector128.LoadUnsafe(ref first, (uint)(x - 1)) + Vector128.LoadUnsafe(ref first, (uint)(x + 1)); + Vector128 far = Vector128.LoadUnsafe(ref first, (uint)(x - 2)) + Vector128.LoadUnsafe(ref first, (uint)(x + 2)); + return operation.Invoke(center, near, far); } - public static void Symmetric5Row( - JxlPlane image, - Func wrapY, - in Rectangle rect, - long iy, - ref JxlWeightsSymmetric5 weights, - Span rowOut) + /// + /// Weights complete interior centers without reading outside their two-sample logical halo. + /// + /// The full logical source row. + /// The first center index. Both halo samples and every result lane must exist. + /// The row's center, distance-one and distance-two weights. + /// One horizontal weighted sum per lane. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 WeightedSum256(ReadOnlySpan row, int x, WeightedSumOperator operation) { - const int radius = 2; - int xEnd = rect.Right; - - int rix = 0; - int ix = rect.X; - - int n = Vector.Count; - int alignedX = RoundUpTo(radius, n); - - for (; ix < Math.Min(alignedX, xEnd); ix++, rix++) - { - rowOut[rix] = Symmetric5Border(image, wrapY, ix, iy, ref weights); - } - - for (; ix + n + radius <= xEnd; ix += n, rix += n) - { - Symmetric5Interior(image, ix, wrapY, rix, iy, ref weights, rowOut); - } - - for (; ix < xEnd; ix++, rix++) - { - rowOut[rix] = Symmetric5Border(image, wrapY, ix, iy, ref weights); - } + ref float first = ref MemoryMarshal.GetReference(row); + Vector256 center = Vector256.LoadUnsafe(ref first, (uint)x); + Vector256 near = Vector256.LoadUnsafe(ref first, (uint)(x - 1)) + Vector256.LoadUnsafe(ref first, (uint)(x + 1)); + Vector256 far = Vector256.LoadUnsafe(ref first, (uint)(x - 2)) + Vector256.LoadUnsafe(ref first, (uint)(x + 2)); + return operation.Invoke(center, near, far); } - public static bool Symmetric5( - JxlPlane input, - in Rectangle rectangle, - ref JxlWeightsSymmetric5 weights, - JxlPlane output, - Rectangle outputRect) + /// + /// Weights complete interior centers without reading outside their two-sample logical halo. + /// + /// The full logical source row. + /// The first center index. Both halo samples and every result lane must exist. + /// The row's center, distance-one and distance-two weights. + /// One horizontal weighted sum per lane. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 WeightedSum512(ReadOnlySpan row, int x, WeightedSumOperator operation) { - if (rectangle.Width != outputRect.Width || rectangle.Height != outputRect.Height) - { - return false; - } - - int height = rectangle.Height; - - for (int riy = 0; riy < height; riy++) - { - int iy = rectangle.Y + riy; - - if (iy < 2 || iy >= rectangle.Height - 2) - { - Symmetric5Row(input, WrapMirror, in rectangle, iy, weights, output.GetRow(outputRect, riy)); - } - else - { - Symmetric5Row(input, in rectangle, iy, weights, output.GetRow(outputRect, riy)); - } - } - - return true; + ref float first = ref MemoryMarshal.GetReference(row); + Vector512 center = Vector512.LoadUnsafe(ref first, (uint)x); + Vector512 near = Vector512.LoadUnsafe(ref first, (uint)(x - 1)) + Vector512.LoadUnsafe(ref first, (uint)(x + 1)); + Vector512 far = Vector512.LoadUnsafe(ref first, (uint)(x - 2)) + Vector512.LoadUnsafe(ref first, (uint)(x + 2)); + return operation.Invoke(center, near, far); } /// @@ -361,7 +379,7 @@ public static void SlowSymmetric3Row( width, height, weights, - WrapMirror, + JxlImageOperations.Mirror, wrapY); for (int x = 1; x < width - 1; x++) @@ -373,7 +391,7 @@ public static void SlowSymmetric3Row( width, height, weights, - WrapUnchanged, + JxlImageOperations.Mirror, wrapY); } @@ -384,7 +402,7 @@ public static void SlowSymmetric3Row( width, height, weights, - WrapMirror, + JxlImageOperations.Mirror, wrapY); } @@ -412,7 +430,7 @@ public static void SlowSymmetric3( height, weights, rowOut, - WrapMirror); + JxlImageOperations.Mirror); } else { @@ -423,7 +441,7 @@ public static void SlowSymmetric3( height, weights, rowOut, - WrapUnchanged); + JxlImageOperations.Mirror); } } } @@ -445,13 +463,13 @@ public static float SlowSeparablePixel( for (int dy = -radius; dy <= radius; dy++) { float wy = vertWeights[Math.Abs(dy) * 4]; - int sy = WrapMirror(rect.Y + y + dy, height); + int sy = JxlImageOperations.Mirror(rect.Y + y + dy, height); ReadOnlySpan row = image.GetRow(sy); for (int dx = -radius; dx <= radius; dx++) { float wx = horzWeights[Math.Abs(dx) * 4]; - int sx = WrapMirror(rect.X + x + dx, width); + int sx = JxlImageOperations.Mirror(rect.X + x + dx, width); sum += row[sx] * wx * wy; } } @@ -574,6 +592,68 @@ public Vector512 Invoke(Vector512 first, Vector512 second, } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int RoundUpTo(int value, int multiple) => ((value + multiple - 1) / multiple) * multiple; + /// + /// Weights a row's center and its two symmetric neighbor pairs without fused reassociation. + /// + private readonly struct WeightedSumOperator : TensorPrimitives_.ITernaryOperator + { + private readonly float centerWeight; + private readonly float nearWeight; + private readonly float farWeight; + + /// Retains one horizontal row's symmetric weights for all centers and SIMD widths. + /// The center weight. + /// The weight of each distance-one neighbor. + /// The weight of each distance-two neighbor. + public WeightedSumOperator(float centerWeight, float nearWeight, float farWeight) + { + this.centerWeight = centerWeight; + this.nearWeight = nearWeight; + this.farWeight = farWeight; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float Invoke(float first, float second, float third) + { + // Native convolve_symmetric5.cc adds the inner pair before the outer pair. Keep each product rounded separately. + float near = second * this.nearWeight; + float center = first * this.centerWeight; + float far = third * this.farWeight; + return far + (near + center); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector128 Invoke(Vector128 first, Vector128 second, Vector128 third) + { + // Native convolve_symmetric5.cc adds the inner pair before the outer pair. Keep each product rounded separately. + Vector128 near = second * Vector128.Create(this.nearWeight); + Vector128 center = first * Vector128.Create(this.centerWeight); + Vector128 far = third * Vector128.Create(this.farWeight); + return far + (near + center); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector256 Invoke(Vector256 first, Vector256 second, Vector256 third) + { + // Native convolve_symmetric5.cc adds the inner pair before the outer pair. Keep each product rounded separately. + Vector256 near = second * Vector256.Create(this.nearWeight); + Vector256 center = first * Vector256.Create(this.centerWeight); + Vector256 far = third * Vector256.Create(this.farWeight); + return far + (near + center); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector512 Invoke(Vector512 first, Vector512 second, Vector512 third) + { + // Native convolve_symmetric5.cc adds the inner pair before the outer pair. Keep each product rounded separately. + Vector512 near = second * Vector512.Create(this.nearWeight); + Vector512 center = first * Vector512.Create(this.centerWeight); + Vector512 far = third * Vector512.Create(this.farWeight); + return far + (near + center); + } + } } diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/GaborishTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/GaborishTests.cs index ade37cc524..7ee287b5b7 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/GaborishTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/GaborishTests.cs @@ -121,6 +121,114 @@ static void RunTest(string serialized) HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); } + /// + /// Checks all six five-by-five tap weights, full-image reflection and distinct rectangle origins at every SIMD capability. + /// + /// The requested output width, including short rows and SIMD tails. + /// Whether the source rectangle has readable neighbors outside its requested range. + [Theory] + [InlineData(1, false)] + [InlineData(3, false)] + [InlineData(17, false)] + [InlineData(33, false)] + [InlineData(17, true)] + public void Symmetric5_UsesDistinctTapWeightsAndFullImageBorders(int width, bool cropped) + { + static void RunTest(string serialized) + { + (int sampleWidth, bool useCrop) = FeatureTestRunner.Deserialize<(int, bool)>(serialized); + const float padding = -123F; + const int height = 3; + int inputWidth = useCrop ? sampleWidth + 6 : sampleWidth; + int inputHeight = useCrop ? 7 : height; + Rectangle inputRect = new(useCrop ? 3 : 0, useCrop ? 2 : 0, sampleWidth, height); + Rectangle outputRect = new(2, 1, sampleWidth, height); + using JxlPlane input = new(TestEnvironment.Configuration, inputWidth, inputHeight); + using JxlPlane output = new(TestEnvironment.Configuration, sampleWidth + 5, height + 3); + Span inputSamples = input.GetRow(0); + Span outputSamples = output.GetRow(0); + inputSamples.Fill(padding); + outputSamples.Fill(padding); + + for (int y = 0; y < inputHeight; y++) + { + for (int x = 0; x < inputWidth; x++) + { + inputSamples[(y * input.PixelsPerRow) + x] = ((13 * x) + (7 * y)) % 31 - 15; + } + } + + // Six different dyadic weights distinguish the center, axial, diagonal, outer axial, L and corner taps. + // Every product and sum remains exactly representable for these small integer samples. + JxlWeightsSymmetric5 weights = new() + { + C = JxlWeightsSymmetric5.CreateVector4(0.5F), + R = JxlWeightsSymmetric5.CreateVector4(0.0625F), + R2 = JxlWeightsSymmetric5.CreateVector4(0.03125F), + D = JxlWeightsSymmetric5.CreateVector4(0.015625F), + L = JxlWeightsSymmetric5.CreateVector4(0.00390625F), + D2 = JxlWeightsSymmetric5.CreateVector4(0.0078125F) + }; + + Assert.True(JxlConvolve.Symmetric5(input, inputRect, ref weights, output, outputRect)); + ReadOnlySpan weightBySquaredDistance = [0.5F, 0.0625F, 0.015625F, 0F, 0.03125F, 0.00390625F, 0F, 0F, 0.0078125F]; + + for (int y = 0; y < output.YSize; y++) + { + for (int x = 0; x < output.PixelsPerRow; x++) + { + float actual = outputSamples[(y * output.PixelsPerRow) + x]; + if (!outputRect.Contains(x, y)) + { + Assert.Equal(padding, actual); + continue; + } + + float expected = 0; + for (int dy = -2; dy <= 2; dy++) + { + int sourceY = inputRect.Y + y - outputRect.Y + dy; + + // The edge sample repeats. Repeated reflection also defines a one-sample image. + while ((uint)sourceY >= (uint)inputHeight) + { + sourceY = sourceY < 0 ? -sourceY - 1 : (2 * inputHeight) - sourceY - 1; + } + + for (int dx = -2; dx <= 2; dx++) + { + int sourceX = inputRect.X + x - outputRect.X + dx; + while ((uint)sourceX >= (uint)inputWidth) + { + sourceX = sourceX < 0 ? -sourceX - 1 : (2 * inputWidth) - sourceX - 1; + } + + float sample = ((13 * sourceX) + (7 * sourceY)) % 31 - 15; + expected += sample * weightBySquaredDistance[(dx * dx) + (dy * dy)]; + } + } + + Assert.Equal(expected, actual); + } + } + + // The convolution borrows the source. It must preserve its samples and allocator padding. + for (int y = 0; y < inputHeight; y++) + { + for (int x = 0; x < input.PixelsPerRow; x++) + { + float expected = x < inputWidth ? ((13 * x) + (7 * y)) % 31 - 15 : padding; + Assert.Equal(expected, inputSamples[(y * input.PixelsPerRow) + x]); + } + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + (width, cropped), + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } + private static JxlWeightsSymmetric3 GaborishKernel(float weight1, float weight2) { const float weight0 = 1.0f; From 46892eab2a443ddd0cc608eb60ba7c86d0830457 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Oct 2026 05:56:31 +1000 Subject: [PATCH 323/328] Share bounded SIMD adaptive JPEG XL block modulation --- .../Encoder/JxlAdaptiveQuantization.cs | 621 +++++++++++------- .../Jxl/Processing/QuantizationTests.cs | 174 +++++ 2 files changed, 563 insertions(+), 232 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs index 449a264e91..cf883ff366 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs @@ -36,6 +36,18 @@ internal static class JxlAdaptiveQuantization private const float SGRetMul = SGMul2 * 18.6580932135f * JxlMath.InverseLog2E; private const float SGVOffset = 7.7825991679894591f; + /// The positive regularization term that keeps the fitted derivative numerator and denominator nonzero. + private const float DerivativeEpsilon = 0.01F; + + /// The float coefficient applied to the squared photon value in the derivative numerator. + private const float DerivativeNumeratorScale = SGRetMul * 3F * SGMul; + + /// The float coefficient applied to the cubic photon value in the derivative denominator. + private const float DerivativeDenominatorScale = JxlMath.InverseLog2E * SGMul; + + /// The positive denominator offset, prepared with the native float coefficient products. + private const float DerivativeOffset = (SGVOffset * JxlMath.InverseLog2E) + DerivativeEpsilon; + private const float DcQuantPow = 0.83f; private const float DcQuant = 1.095924047623553f; private const float AcQuant = 0.765f; @@ -43,6 +55,24 @@ internal static class JxlAdaptiveQuantization private const int DefaultButteraugliIterations = 2; private const int MaxButteraugliIterations = 4; + /// The pixel width and height of one modulation block. + private const int ModulationBlockSize = 8; + + /// The positive photon offset used by the fitted derivative ratio. + private const float GammaModulationBias = 0.16F; + + /// The fitted strength of the average derivative correction in log2 units. + private const float GammaModulationStrength = 0.1005613337192697F; + + /// The maximum contribution of one horizontal or vertical Y edge. + private const float HighFrequencyLimit = 0.0206F; + + /// The largest blue-excess contribution retained from one sample. + private const float BlueModulationLimit = 0.010474084867598155F; + + /// The Y offset used when measuring blue excess over red and green activations. + private const float BlueModulationOffset = 0.0031994768654636393F; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static float ComputeMaskForAcStrategyUse(float outputValue) { @@ -52,240 +82,245 @@ public static float ComputeMaskForAcStrategyUse(float outputValue) return multiplier / (outputValue + offset); } - public static Vector ComputeMask(Vector outputValue) + /// Converts one block's erosion value to the exponent used by adaptive modulation. + /// The four-minimum erosion response for one 8-by-8 block. + /// The native masking exponent before gamma, edge and blue corrections. + private static float ComputeMask(float outputValue) { - Vector @base = Vector.Create(-0.7647f); - Vector mul4 = Vector.Create(9.4708735624378946f); - Vector mul2 = Vector.Create(17.35036561631863f); - Vector offset2 = Vector.Create(302.59587815579727f); - Vector mul3 = Vector.Create(6.7943250517376494f); - Vector offset3 = Vector.Create(3.7179635626140772f); - Vector offset4 = Vector.Create(0.25f) * offset3; - Vector mul0 = Vector.Create(0.80061762862741759f); - - Vector v1 = Vector.Max(outputValue * mul0, Vector.Create(1e-3f)); - Vector v2 = Vector.One / (v1 + offset2); - Vector v3 = Vector.One / ((v1 * v1) + offset3); - Vector v4 = Vector.One / ((v1 * v1) + offset4); - - return @base + ((mul4 * v4) + ((mul2 * v2) + (mul3 * v3))); + float value = MathF.Max(outputValue * 0.80061762862741759F, 1e-3F); + float reciprocal2 = 1F / (value + 302.59587815579727F); + float reciprocal3 = 1F / float.MultiplyAddEstimate(value, value, 3.7179635626140772F); + float reciprocal4 = 1F / float.MultiplyAddEstimate(value, value, 0.25F * 3.7179635626140772F); + + // Preserve the native nested multiply-add order. These fitted terms jointly approximate perceptual masking. + return -0.7647F + float.MultiplyAddEstimate( + 9.4708735624378946F, + reciprocal4, + float.MultiplyAddEstimate(17.35036561631863F, reciprocal2, 6.7943250517376494F * reciprocal3)); } - public static float RatioOfDerivativesOfCubicRootToSimpleGamma(float v, bool invert = false) + /// Converts between cubic-root and perceptual-gamma derivative scales. + /// The cubic-root photon values. Negative values use zero photons. + /// Whether to return the cubic-root-to-gamma ratio instead of its reciprocal. + /// The derivative ratios, with the native positive regularization terms. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float RatioOfDerivativesOfCubicRootToSimpleGamma(float value, bool invert = false) { - float epsilon = 1e-2f; - v = Math.Max(0, v); // cannot be < 0 - const float numMul = SGRetMul * 3 * SGMul; - float voffset = (SGVOffset * JxlMath.InverseLog2E) + epsilon; - const float denMul = JxlMath.InverseLog2E * SGMul; + value = MathF.Max(0F, value); + float squared = value * value; + float numerator = float.MultiplyAddEstimate(DerivativeNumeratorScale, squared, DerivativeEpsilon); + float denominator = float.MultiplyAddEstimate(DerivativeDenominatorScale * value, squared, DerivativeOffset); - float v2 = v * v; - float num = (numMul * v2) + epsilon; - float den = ((denMul * v) * v2) + voffset; - - return invert ? num / den : den / num; + return invert ? numerator / denominator : denominator / numerator; } - public static Vector RatioOfDerivativesOfCubicRootToSimpleGamma(Vector v, bool invert) + /// Converts between cubic-root and perceptual-gamma derivative scales. + /// The cubic-root photon values. Negative values use zero photons. + /// Whether to return the cubic-root-to-gamma ratio instead of its reciprocal. + /// The derivative ratios, with the native positive regularization terms. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector RatioOfDerivativesOfCubicRootToSimpleGamma(Vector value, bool invert) { - float epsilon = 1e-2f; - v = Vector.Max(Vector.Zero, v); - - Vector numMul = Vector.Create(SGRetMul * 3 * SGMul); - Vector vOffset = Vector.Create((SGVOffset * JxlMath.InverseLog2E) + epsilon); - Vector denMul = Vector.Create(JxlMath.InverseLog2E * SGMul); + value = Vector.Max(Vector.Create(0F), value); + Vector squared = value * value; + Vector numerator = Vector.MultiplyAddEstimate(Vector.Create(DerivativeNumeratorScale), squared, Vector.Create(DerivativeEpsilon)); + Vector denominator = Vector.MultiplyAddEstimate(Vector.Create(DerivativeDenominatorScale) * value, squared, Vector.Create(DerivativeOffset)); - Vector v2 = v * v; - Vector num = (numMul * v2) + Vector.Create(epsilon); - Vector den = ((denMul * v) * v2) + vOffset; - - return invert ? num / den : den / num; + return invert ? numerator / denominator : denominator / numerator; } - public static float GammaModulation(int x, int y, JxlPlane xybX, JxlPlane xybY, Rectangle rect, float outVal) + /// Converts between cubic-root and perceptual-gamma derivative scales. + /// The cubic-root photon values. Negative values use zero photons. + /// Whether to return the cubic-root-to-gamma ratio instead of its reciprocal. + /// The derivative ratios, with the native positive regularization terms. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 RatioOfDerivativesOfCubicRootToSimpleGamma(Vector128 value, bool invert) { - const float biasConst = 0.16f; - const float gamma = 0.1005613337192697f; - - Vector overallRatio = Vector.Zero; - Vector bias = new(biasConst); - - for (int dy = 0; dy < 8; dy++) - { - ReadOnlySpan rowInX = xybX.GetRow(rect, y + dy); - ReadOnlySpan rowInY = xybY.GetRow(rect, y + dy); - - for (int dx = 0; dx < 8; dx += Vector.Count) - { - Vector iny = new(rowInY[(x + dx)..]); - iny += bias; - - Vector inx = new(rowInX[(x + dx)..]); - - Vector r = iny - inx; - Vector ratioR = RatioOfDerivativesOfCubicRootToSimpleGamma(r, invert: true); - overallRatio += ratioR; - - Vector g = iny + inx; - Vector ratioG = RatioOfDerivativesOfCubicRootToSimpleGamma(g, invert: true); - overallRatio += ratioG; - } - } - - float overallRatioScalar = Vector.Sum(overallRatio) * (0.5f / 64.0f); - - return (gamma * MathF.Log2(overallRatioScalar)) + outVal; + value = Vector128.Max(Vector128.Create(0F), value); + Vector128 squared = value * value; + Vector128 numerator = Vector128.MultiplyAddEstimate(Vector128.Create(DerivativeNumeratorScale), squared, Vector128.Create(DerivativeEpsilon)); + Vector128 denominator = Vector128.MultiplyAddEstimate( + Vector128.Create(DerivativeDenominatorScale) * value, + squared, + Vector128.Create(DerivativeOffset)); + + return invert ? numerator / denominator : denominator / numerator; } - // Change precision in 8x8 blocks that have significant amounts of blue - // content (but are not close to solid blue). - // This is based on the idea that M and L cone activations saturate the - // S (blue) receptors, and the S reception becomes more important when - // both M and L levels are low. In that case M and L receptors may be - // observing S-spectra instead and viewing them with higher spatial - // accuracy, justifying spending more bits here. - public static Vector BlueModulation(int x, int y, JxlPlane planex, JxlPlane planey, JxlPlane planeb, Rectangle rect, Vector outVal) + /// Converts between cubic-root and perceptual-gamma derivative scales. + /// The cubic-root photon values. Negative values use zero photons. + /// Whether to return the cubic-root-to-gamma ratio instead of its reciprocal. + /// The derivative ratios, with the native positive regularization terms. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 RatioOfDerivativesOfCubicRootToSimpleGamma(Vector256 value, bool invert) { - const float limitConst = 0.010474084867598155f; - const float offsetConst = 0.0031994768654636393f; - const float multiplier = 0.90590804735610064f; - const float maxLimit = 15.463398341612438f; - - Vector sum = Vector.Zero; - Vector limit = new(limitConst); - Vector offset = new(offsetConst); - - for (int dy = 0; dy < 8; dy++) - { - ReadOnlySpan rowInX = planex.GetRow(rect, y + dy); - ReadOnlySpan rowInY = planey.GetRow(rect, y + dy); - ReadOnlySpan rowInB = planeb.GetRow(rect, y + dy); - - for (int dx = 0; dx < 8; dx += Vector.Count) - { - Vector pX = Vector.Create(rowInX[(x + dx)..]); - Vector pB = Vector.Create(rowInB[(x + dx)..]); - Vector pYRaw = Vector.Create(rowInY[(x + dx)..]) + offset; - - Vector pYEffective = pYRaw + Vector.Abs(pX); - Vector excess = pB - pYEffective; - - Vector contribution = Vector.Min(excess, limit); - contribution = Vector.ConditionalSelect(Vector.GreaterThan(pB, pYEffective), contribution, Vector.Zero); - - sum += contribution; - } - } - - float scalarSum = Vector.Sum(sum); - - if (scalarSum >= 32 * limitConst) - { - scalarSum = (64 * limitConst) - scalarSum; - } - - if (scalarSum >= maxLimit * limitConst) - { - scalarSum = maxLimit * limitConst; - } - - scalarSum *= multiplier; - - return new Vector(scalarSum) + outVal; + value = Vector256.Max(Vector256.Create(0F), value); + Vector256 squared = value * value; + Vector256 numerator = Vector256.MultiplyAddEstimate(Vector256.Create(DerivativeNumeratorScale), squared, Vector256.Create(DerivativeEpsilon)); + Vector256 denominator = Vector256.MultiplyAddEstimate( + Vector256.Create(DerivativeDenominatorScale) * value, + squared, + Vector256.Create(DerivativeOffset)); + + return invert ? numerator / denominator : denominator / numerator; } - private static float HfModulationScalar(int x, int y, JxlPlane xybY, Rectangle rect, float outVal) + /// Converts between cubic-root and perceptual-gamma derivative scales. + /// The cubic-root photon values. Negative values use zero photons. + /// Whether to return the cubic-root-to-gamma ratio instead of its reciprocal. + /// The derivative ratios, with the native positive regularization terms. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 RatioOfDerivativesOfCubicRootToSimpleGamma(Vector512 value, bool invert) { - const float valueMinY = 0.0206f; - const float mulY = -0.38f; - const float offset = 0.42f; - - float sumY = 0; - - for (int dy = 0; dy < 8; dy++) - { - ReadOnlySpan rowInY = xybY.GetRow(rect, y + dy)[x..]; - ReadOnlySpan rowInYNext = dy == 7 - ? rowInY - : xybY.GetRow(rect, y + dy + 1)[x..]; - - for (int dx = 0; dx < 7; dx++) - { - sumY += MathF.Min(valueMinY, MathF.Abs(rowInY[dx] - rowInY[dx + 1])); - } - - for (int dx = 0; dx < 8; dx++) - { - sumY += MathF.Min(valueMinY, MathF.Abs(rowInY[dx] - rowInYNext[dx])); - } - } - - float scalarSumY = sumY * mulY; - scalarSumY += offset; - - return scalarSumY + outVal; + value = Vector512.Max(Vector512.Create(0F), value); + Vector512 squared = value * value; + Vector512 numerator = Vector512.MultiplyAddEstimate(Vector512.Create(DerivativeNumeratorScale), squared, Vector512.Create(DerivativeEpsilon)); + Vector512 denominator = Vector512.MultiplyAddEstimate( + Vector512.Create(DerivativeDenominatorScale) * value, + squared, + Vector512.Create(DerivativeOffset)); + + return invert ? numerator / denominator : denominator / numerator; } - private static Vector HfModulationVectorized(int x, int y, JxlPlane xybY, Rectangle rect, Vector outVal) + /// Accumulates the three independent native modulation responses for one block. + /// The X sample. + /// The Y sample. + /// The B sample. + /// The next Y sample, or the same sample at the block's right edge. + /// The next Y row, or the same sample at the block's bottom edge. + /// The accumulated red and green derivative ratios. + /// The accumulated capped horizontal and vertical Y differences. + /// The accumulated excess blue response. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AccumulateModulations( + float x, + float y, + float blue, + float right, + float below, + ref float gammaSum, + ref float highFrequencySum, + ref float blueSum) { - const float valueMinY = 0.0206f; - const float mulY = -0.38f; - const float offset = 0.42f; - - Vector sumY = Vector.Zero; - Vector valueMinVY = new(valueMinY); - - ReadOnlySpan maskRight = [uint.MaxValue, uint.MaxValue, uint.MaxValue, uint.MaxValue, uint.MaxValue, uint.MaxValue, uint.MaxValue, 0]; - - for (int dy = 0; dy < 8; dy++) - { - ReadOnlySpan rowInY = xybY.GetRow(rect, y + dy)[x..]; - ReadOnlySpan rowInYNext = dy == 7 - ? rowInY - : xybY.GetRow(rect, y + dy + 1)[x..]; - - for (int dx = 0; dx < 8; dx += Vector.Count) - { - Vector mask = Vector.Create(maskRight[dx..]); - Vector maskFloat = Vector.AsVectorSingle(mask); - - Vector pY = Vector.Create(rowInY[dx..]); - Vector prY = Vector.Create(rowInY[(dx + 1)..]); - Vector pdY = Vector.Create(rowInYNext[dx..]); - - Vector horizontal = Vector.Min(valueMinVY, Vector.Abs(pY - prY)); - Vector vertical = Vector.Min(valueMinVY, Vector.Abs(pY - pdY)); - - sumY += Vector.BitwiseAnd(horizontal, maskFloat); - sumY += vertical; - } - } + float biasedY = y + GammaModulationBias; + gammaSum += RatioOfDerivativesOfCubicRootToSimpleGamma(biasedY - x, true); + gammaSum += RatioOfDerivativesOfCubicRootToSimpleGamma(biasedY + x, true); + highFrequencySum += MathF.Min(HighFrequencyLimit, MathF.Abs(y - right)); + highFrequencySum += MathF.Min(HighFrequencyLimit, MathF.Abs(y - below)); + + float effectiveY = (y + BlueModulationOffset) + MathF.Abs(x); + blueSum += blue > effectiveY ? MathF.Min(blue - effectiveY, BlueModulationLimit) : 0F; + } - float scalarSumY = Vector.Sum(sumY); - scalarSumY *= mulY; - scalarSumY += offset; + /// Accumulates modulation responses independently in each sample lane. + /// The X sample lanes. + /// The corresponding Y lanes. + /// The corresponding B lanes. + /// The right Y neighbors, repeated at each eight-column block edge. + /// The lower Y neighbors, repeated on the block's final row. + /// The lane-wise derivative sums. + /// The lane-wise capped edge sums. + /// The lane-wise blue-excess sums. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AccumulateModulations( + Vector128 x, + Vector128 y, + Vector128 blue, + Vector128 right, + Vector128 below, + ref Vector128 gammaSum, + ref Vector128 highFrequencySum, + ref Vector128 blueSum) + { + Vector128 biasedY = y + Vector128.Create(GammaModulationBias); + gammaSum += RatioOfDerivativesOfCubicRootToSimpleGamma(biasedY - x, true); + gammaSum += RatioOfDerivativesOfCubicRootToSimpleGamma(biasedY + x, true); + highFrequencySum += Vector128.Min(Vector128.Create(HighFrequencyLimit), Vector128.Abs(y - right)); + highFrequencySum += Vector128.Min(Vector128.Create(HighFrequencyLimit), Vector128.Abs(y - below)); + + Vector128 effectiveY = (y + Vector128.Create(BlueModulationOffset)) + Vector128.Abs(x); + blueSum += Vector128.ConditionalSelect( + Vector128.GreaterThan(blue, effectiveY), + Vector128.Min(blue - effectiveY, Vector128.Create(BlueModulationLimit)), + Vector128.Zero); + } - return new Vector(scalarSumY) + outVal; + /// Accumulates modulation responses independently in each sample lane. + /// The X sample lanes. + /// The corresponding Y lanes. + /// The corresponding B lanes. + /// The right Y neighbors, repeated at each eight-column block edge. + /// The lower Y neighbors, repeated on the block's final row. + /// The lane-wise derivative sums. + /// The lane-wise capped edge sums. + /// The lane-wise blue-excess sums. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AccumulateModulations( + Vector256 x, + Vector256 y, + Vector256 blue, + Vector256 right, + Vector256 below, + ref Vector256 gammaSum, + ref Vector256 highFrequencySum, + ref Vector256 blueSum) + { + Vector256 biasedY = y + Vector256.Create(GammaModulationBias); + gammaSum += RatioOfDerivativesOfCubicRootToSimpleGamma(biasedY - x, true); + gammaSum += RatioOfDerivativesOfCubicRootToSimpleGamma(biasedY + x, true); + highFrequencySum += Vector256.Min(Vector256.Create(HighFrequencyLimit), Vector256.Abs(y - right)); + highFrequencySum += Vector256.Min(Vector256.Create(HighFrequencyLimit), Vector256.Abs(y - below)); + + Vector256 effectiveY = (y + Vector256.Create(BlueModulationOffset)) + Vector256.Abs(x); + blueSum += Vector256.ConditionalSelect( + Vector256.GreaterThan(blue, effectiveY), + Vector256.Min(blue - effectiveY, Vector256.Create(BlueModulationLimit)), + Vector256.Zero); } - public static Vector HfModulation(int x, int y, JxlPlane xybY, Rectangle rect, Vector outVal) + /// Accumulates modulation responses independently in each sample lane. + /// The X sample lanes. + /// The corresponding Y lanes. + /// The corresponding B lanes. + /// The right Y neighbors, repeated at each eight-column block edge. + /// The lower Y neighbors, repeated on the block's final row. + /// The lane-wise derivative sums. + /// The lane-wise capped edge sums. + /// The lane-wise blue-excess sums. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AccumulateModulations( + Vector512 x, + Vector512 y, + Vector512 blue, + Vector512 right, + Vector512 below, + ref Vector512 gammaSum, + ref Vector512 highFrequencySum, + ref Vector512 blueSum) { - if (Vector.Count == 1) - { - // We don't have SIMD support, everything is scalar. - // Loop iterations are different in scalar HF modulation, - // so it's more efficient to go with a separate duplicated - // routine. - float result = HfModulationScalar(x, y, xybY, rect, outVal.ToScalar()); - return Vector.Create(result); - } - else - { - // We have SIMD support - return HfModulationVectorized(x, y, xybY, rect, outVal); - } + Vector512 biasedY = y + Vector512.Create(GammaModulationBias); + gammaSum += RatioOfDerivativesOfCubicRootToSimpleGamma(biasedY - x, true); + gammaSum += RatioOfDerivativesOfCubicRootToSimpleGamma(biasedY + x, true); + highFrequencySum += Vector512.Min(Vector512.Create(HighFrequencyLimit), Vector512.Abs(y - right)); + highFrequencySum += Vector512.Min(Vector512.Create(HighFrequencyLimit), Vector512.Abs(y - below)); + + Vector512 effectiveY = (y + Vector512.Create(BlueModulationOffset)) + Vector512.Abs(x); + blueSum += Vector512.ConditionalSelect( + Vector512.GreaterThan(blue, effectiveY), + Vector512.Min(blue - effectiveY, Vector512.Create(BlueModulationLimit)), + Vector512.Zero); } + /// Applies gamma, edge and blue corrections to precomputed block masking values. + /// The perceptual target that damps modulation between targets two and fourteen. + /// The source X plane. + /// The source Y plane. + /// The source B plane. + /// The source pixel region. Its dimensions are multiples of eight. + /// The multiplicative quantization scale. + /// The output block region, relative to the source region's origin. + /// The precomputed erosion responses, replaced by positive quantization multipliers. private static void PerBlockModulations( float butteraugliTarget, JxlPlane xybX, @@ -296,42 +331,164 @@ private static void PerBlockModulations( Rectangle rectOut, JxlPlane output) { - float baseLevel = 0.48f * scale; + float baseLevel = 0.48F * scale; + float dampen = butteraugliTarget < 2F ? 1F : MathF.Max(0F, 1F - ((butteraugliTarget - 2F) / 12F)); + float multiplier = scale * dampen; + float add = (1F - dampen) * baseLevel; + + // Borrow each complete plane once. The region offsets select samples; physical strides remain independent. + ReadOnlySpan samplesX = xybX.GetRow(0); + ReadOnlySpan samplesY = xybY.GetRow(0); + ReadOnlySpan samplesB = xybB.GetRow(0); + Span outputSamples = output.GetRow(0); + int xStride = xybX.PixelsPerRow; + int yStride = xybY.PixelsPerRow; + int blueStride = xybB.PixelsPerRow; + int outputStride = output.PixelsPerRow; + + for (int blockY = rectOut.Top; blockY < rectOut.Bottom; blockY++) + { + int top = rectIn.Top + (blockY * ModulationBlockSize); + Span outputRow = outputSamples.Slice(blockY * outputStride, output.XSize); - const float dampenRampStart = 2.0f; - const float dampenRampEnd = 14.0f; + for (int blockX = rectOut.Left; blockX < rectOut.Right; blockX++) + { + int left = rectIn.Left + (blockX * ModulationBlockSize); + float gammaSum; + float highFrequencySum; + float blueSum; - float dampen = 1.0f; + if (Vector512.IsHardwareAccelerated) + { + Vector512 gamma = Vector512.Zero; + Vector512 edges = Vector512.Zero; + Vector512 blue = Vector512.Zero; + Vector256 rightIndices = Vector256.Create(1, 2, 3, 4, 5, 6, 7, 7); + + // Each 256-bit half holds one complete row. Pairing rows uses all sixteen lanes without crossing an 8-by-8 block. + // Repeat each row's final Y lane for its right neighbor. The resulting difference is zero before any reduction. + // The full-row shuffle must move column four into column three; a 128-bit lane-local shuffle cannot form that neighbor. + for (int row = 0; row < ModulationBlockSize; row += 2) + { + int y = top + row; + Vector256 y0 = Vector256.Create(samplesY.Slice((y * yStride) + left, ModulationBlockSize)); + Vector256 y1 = Vector256.Create(samplesY.Slice(((y + 1) * yStride) + left, ModulationBlockSize)); + Vector256 y2 = row == ModulationBlockSize - 2 + ? y1 + : Vector256.Create(samplesY.Slice(((y + 2) * yStride) + left, ModulationBlockSize)); + + Vector512 x = Vector512.Create( + Vector256.Create(samplesX.Slice((y * xStride) + left, ModulationBlockSize)), + Vector256.Create(samplesX.Slice(((y + 1) * xStride) + left, ModulationBlockSize))); + + Vector512 b = Vector512.Create( + Vector256.Create(samplesB.Slice((y * blueStride) + left, ModulationBlockSize)), + Vector256.Create(samplesB.Slice(((y + 1) * blueStride) + left, ModulationBlockSize))); + + Vector512 right = Vector512.Create(Vector256.Shuffle(y0, rightIndices), Vector256.Shuffle(y1, rightIndices)); + AccumulateModulations(x, Vector512.Create(y0, y1), b, right, Vector512.Create(y1, y2), ref gamma, ref edges, ref blue); + } - if (butteraugliTarget >= dampenRampStart) - { - dampen = 1.0f - ((butteraugliTarget - dampenRampStart) / (dampenRampEnd - dampenRampStart)); + gammaSum = Vector512.Sum(gamma); + highFrequencySum = Vector512.Sum(edges); + blueSum = Vector512.Sum(blue); + } + else if (Vector256.IsHardwareAccelerated) + { + Vector256 gamma = Vector256.Zero; + Vector256 edges = Vector256.Zero; + Vector256 blue = Vector256.Zero; + Vector256 rightIndices = Vector256.Create(1, 2, 3, 4, 5, 6, 7, 7); + + // One register holds the block's eight columns. No load includes row padding or the following block. + // Shuffle crosses the 128-bit boundary at columns three/four. A native lane-local shuffle would select the wrong neighbor. + for (int row = 0; row < ModulationBlockSize; row++) + { + int y = top + row; + int nextY = row == ModulationBlockSize - 1 ? y : y + 1; + Vector256 x = Vector256.Create(samplesX.Slice((y * xStride) + left, ModulationBlockSize)); + Vector256 currentY = Vector256.Create(samplesY.Slice((y * yStride) + left, ModulationBlockSize)); + Vector256 b = Vector256.Create(samplesB.Slice((y * blueStride) + left, ModulationBlockSize)); + Vector256 right = Vector256.Shuffle(currentY, rightIndices); + Vector256 below = Vector256.Create(samplesY.Slice((nextY * yStride) + left, ModulationBlockSize)); + AccumulateModulations(x, currentY, b, right, below, ref gamma, ref edges, ref blue); + } - if (dampen < 0) - { - dampen = 0; - } - } + gammaSum = Vector256.Sum(gamma); + highFrequencySum = Vector256.Sum(edges); + blueSum = Vector256.Sum(blue); + } + else if (Vector128.IsHardwareAccelerated) + { + Vector128 gamma = Vector128.Zero; + Vector128 edges = Vector128.Zero; + Vector128 blue = Vector128.Zero; - float mul = scale * dampen; - float add = (1.0f - dampen) * baseLevel; + // Four lanes cover half a row. The first right-neighbor load remains inside the eight samples. + // The second half repeats its final lane, so it never loads the ninth sample of a block. + for (int row = 0; row < ModulationBlockSize; row++) + { + int y = top + row; + int nextY = row == ModulationBlockSize - 1 ? y : y + 1; - for (int iy = rectOut.Y0(); iy < rectOut.Y1(); iy++) - { - int y = iy * 8; - Span rowOut = output.GetRow(iy); + for (int column = 0; column < ModulationBlockSize; column += Vector128.Count) + { + int x = left + column; + Vector128 currentX = Vector128.Create(samplesX.Slice((y * xStride) + x, Vector128.Count)); + Vector128 currentY = Vector128.Create(samplesY.Slice((y * yStride) + x, Vector128.Count)); + Vector128 b = Vector128.Create(samplesB.Slice((y * blueStride) + x, Vector128.Count)); + Vector128 right = column == 0 + ? Vector128.Create(samplesY.Slice((y * yStride) + x + 1, Vector128.Count)) + : Vector128_.ShuffleNative(currentY, 0b11_11_10_01); + + Vector128 below = Vector128.Create(samplesY.Slice((nextY * yStride) + x, Vector128.Count)); + AccumulateModulations(currentX, currentY, b, right, below, ref gamma, ref edges, ref blue); + } + } - for (int ix = rectOut.X0(); ix < rectOut.X1(); ix++) - { - int x = ix * 8; + gammaSum = Vector128.Sum(gamma); + highFrequencySum = Vector128.Sum(edges); + blueSum = Vector128.Sum(blue); + } + else + { + gammaSum = 0F; + highFrequencySum = 0F; + blueSum = 0F; - Vector maskVal = ComputeMask(Vector.Create(rowOut[ix])); + for (int row = 0; row < ModulationBlockSize; row++) + { + int y = top + row; + int nextY = row == ModulationBlockSize - 1 ? y : y + 1; - maskVal = GammaModulation(x, y, xybX, xybY, rectIn, maskVal); - float outVal = HfModulationScalar(x, y, xybY, rectIn, maskVal); - outVal = MathF.Min(outVal, BlueModulation(x, y, xybX, xybY, xybB, rectIn, maskVal)); + for (int column = 0; column < ModulationBlockSize; column++) + { + int x = left + column; + int rightX = column == ModulationBlockSize - 1 ? x : x + 1; + AccumulateModulations( + samplesX[(y * xStride) + x], + samplesY[(y * yStride) + x], + samplesB[(y * blueStride) + x], + samplesY[(y * yStride) + rightX], + samplesY[(nextY * yStride) + x], + ref gammaSum, + ref highFrequencySum, + ref blueSum); + } + } + } - rowOut[ix] = (MathF.Pow(2.0f, outVal * 1.442695041f) * mul) + add; + // Both alternative corrections start from the same gamma-adjusted mask. Blue does not follow the HF correction. + float mask = ComputeMask(outputRow[blockX]); + mask = float.MultiplyAddEstimate(GammaModulationStrength, MathF.Log2(gammaSum * (0.5F / 64F)), mask); + float highFrequency = ((highFrequencySum * -0.38F) + 0.42F) + mask; + blueSum = blueSum >= 32F * BlueModulationLimit ? (64F * BlueModulationLimit) - blueSum : blueSum; + blueSum = MathF.Min(blueSum, 15.463398341612438F * BlueModulationLimit); + float blueCorrection = (blueSum * 0.90590804735610064F) + mask; + float exponent = MathF.Min(highFrequency, blueCorrection) * 1.442695041F; + + // The previous stages modulate an exponent. Convert it to the multiplicative field only once per block. + outputRow[blockX] = (float.Exp2(exponent) * multiplier) + add; } } } diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/QuantizationTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/QuantizationTests.cs index 3bb82fcd01..0d4b352ed5 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/QuantizationTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/QuantizationTests.cs @@ -810,4 +810,178 @@ public void DequantMatrices_TruncatedDcHeaderLeavesAllPreviousValues() Assert.Equal(JxlQuantizerConstants.InverseDcQuant[c], matrices.GetInverseDcQuant(c)); } } + + /// Checks native block modulation, final-row bounds and damping through the complete adaptive-map entry. + [Fact] + public void AdaptiveQuantizationMap_PreservesModulationAndDamping() + { + static void RunTest() + { + // A constant Y field has zero spatial differences. Four-row accumulation and two-to-one erosion give + // 16 * MaskingSqrt(0) * .29959705784054957. This closed form does not call the production filter or modulation helpers. + double erosion = 16D * (0.25D * Math.Sqrt(27.505837037000106F)) * 0.29959705784054957F; + double value = Math.Max(erosion * 0.80061762862741759F, 0.001F); + double maskExponent = -0.7647F + (9.4708735624378946F / ((value * value) + (0.25F * 3.7179635626140772F))) + + (17.35036561631863F / (value + 302.59587815579727F)) + (6.7943250517376494F / ((value * value) + 3.7179635626140772F)); + + // X=Y=0 gives the same red and green derivative ratio at the native .16 photon offset. + // Use double evaluation of the equation as an independent oracle; the production path retains float arithmetic and SIMD reductions. + double gammaExponent = maskExponent + (0.1005613337192697F * Math.Log2(1D / DerivativeRatio(0.16F))); + ReadOnlySpan widths = [8, 16, 24]; + ReadOnlySpan targets = [2F, 14F]; + const int height = 8; + const float scale = 0.75F; + const float sentinel = -123F; + + foreach (int width in widths) + { + for (int blueCase = 0; blueCase < 3; blueCase++) + { + using JxlImage3 source = new(TestEnvironment.Configuration, width, height); + Span xSamples = source.PlaneRow(0, 0); + Span ySamples = source.PlaneRow(1, 0); + Span blueSamples = source.PlaneRow(2, 0); + xSamples.Fill(sentinel); + ySamples.Fill(sentinel); + blueSamples.Fill(sentinel); + int stride = source.PixelsPerRow; + + // The three cases have 0,16,64 blue-excess samples per block. Native folds the full-blue response back to zero. + // Sixteen saturated samples reach the fitted blue cap, below the HF correction of .42 for these constant Y fields. + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + xSamples[(y * stride) + x] = 0F; + ySamples[(y * stride) + x] = 0F; + blueSamples[(y * stride) + x] = blueCase == 2 || (blueCase == 1 && y < 4 && (x % 8) < 4) ? 1F : 0F; + } + } + + double blueCorrection = blueCase == 1 ? 15.463398341612438F * 0.010474084867598155F * 0.90590804735610064F : 0D; + double expected = Math.Pow(2D, (gammaExponent + blueCorrection) * 1.442695041F) * scale; + + foreach (float target in targets) + { + using JxlPlane map = JxlAdaptiveQuantization.AdaptiveQuantizationMap( + TestEnvironment.Configuration, + target, + source, + source.GetRectangle(), + scale, + out JxlPlane mask, + out JxlPlane pixelMask); + + using (mask) + using (pixelMask) + { + ReadOnlySpan result = map.GetRow(0); + ReadOnlySpan maskSamples = mask.GetRow(0); + ReadOnlySpan pixelMaskSamples = pixelMask.GetRow(0); + for (int x = 0; x < width / 8; x++) + { + if (target == 14F) + { + // The native ramp reaches zero modulation at fourteen, leaving only the scale-dependent base level. + Assert.Equal(0.48F * scale, result[x]); + } + else + { + // The response is below one. A 128-term float reduction and the fitted exponent add less than 1e-4 absolute error. + // This bound includes different SIMD reduction trees and float multiply-add estimates, without a reference-image tolerance. + Assert.InRange(Math.Abs(result[x] - expected), 0D, 0.0001D); + } + } + + Assert.Equal(-1, result[map.XSize..map.PixelsPerRow].IndexOfAnyExcept(0F)); + Assert.Equal(-1, maskSamples[mask.XSize..mask.PixelsPerRow].IndexOfAnyExcept(0F)); + for (int y = 0; y < pixelMask.YSize; y++) + { + ReadOnlySpan padding = pixelMaskSamples.Slice( + (y * pixelMask.PixelsPerRow) + pixelMask.XSize, + pixelMask.PixelsPerRow - pixelMask.XSize); + + Assert.Equal(-1, padding.IndexOfAnyExcept(0F)); + } + } + } + + // The kernel borrows the source. It cannot change samples or consume poisoned padding at the final physical row. + for (int y = 0; y < height; y++) + { + for (int x = 0; x < stride; x++) + { + bool logical = x < width; + Assert.Equal(logical ? 0F : sentinel, xSamples[(y * stride) + x]); + Assert.Equal(logical ? 0F : sentinel, ySamples[(y * stride) + x]); + float expectedBlue = blueCase == 2 || (blueCase == 1 && y < 4 && (x % 8) < 4) ? 1F : 0F; + Assert.Equal(logical ? expectedBlue : sentinel, blueSamples[(y * stride) + x]); + } + } + } + } + + // The center block of a 24-by-24 checkerboard has 56 horizontal and 56 vertical Y edges of magnitude .25. + // Its erosion neighborhoods exclude global image edges. Each four-by-four cell contains eight samples of each Y value. + using JxlImage3 checkerboard = new(TestEnvironment.Configuration, 24, 24); + checkerboard.PlaneRow(0, 0).Clear(); + checkerboard.PlaneRow(2, 0).Clear(); + Span checkerY = checkerboard.PlaneRow(1, 0); + for (int y = 0; y < 24; y++) + { + for (int x = 0; x < 24; x++) + { + checkerY[(y * checkerboard.PixelsPerRow) + x] = ((x + y) & 1) == 0 ? 0F : 0.25F; + } + } + + // Constant four-connected neighborhoods alternate only between these two derivative-scaled differences. + // A four-row accumulation, four-column mean, and two-to-one erosion give 8*(maskLow+maskHigh)*totalWeight. + double lowDifference = 0.25D * DerivativeRatio(0.019F); + double highDifference = 0.25D * DerivativeRatio(0.25F + 0.019F); + double maskingMultiplier = Math.Sqrt(211.66567973503678F * 1e8F); + double lowMask = 0.25D * Math.Sqrt((Math.Min(lowDifference * lowDifference, 0.2F) * maskingMultiplier) + 27.505837037000106F); + double highMask = 0.25D * Math.Sqrt((Math.Min(highDifference * highDifference, 0.2F) * maskingMultiplier) + 27.505837037000106F); + double checkerErosion = 8D * (lowMask + highMask) * 0.29959705784054957F; + double checkerValue = checkerErosion * 0.80061762862741759F; + double checkerMask = -0.7647F + (9.4708735624378946F / ((checkerValue * checkerValue) + (0.25F * 3.7179635626140772F))) + + (17.35036561631863F / (checkerValue + 302.59587815579727F)) + + (6.7943250517376494F / ((checkerValue * checkerValue) + 3.7179635626140772F)); + + double averageRatio = 0.5D * ((1D / DerivativeRatio(0.16F)) + (1D / DerivativeRatio(0.25F + 0.16F))); + double checkerGamma = checkerMask + (0.1005613337192697F * Math.Log2(averageRatio)); + double highFrequencyCorrection = (112D * 0.0206F * -0.38F) + 0.42F; + double checkerExpected = Math.Pow(2D, (checkerGamma + highFrequencyCorrection) * 1.442695041F) * scale; + using JxlPlane checkerMap = JxlAdaptiveQuantization.AdaptiveQuantizationMap( + TestEnvironment.Configuration, + 2F, + checkerboard, + checkerboard.GetRectangle(), + scale, + out JxlPlane checkerMaskMap, + out JxlPlane checkerPixelMask); + + using (checkerMaskMap) + using (checkerPixelMask) + { + // Blue contributes zero. The negative HF correction must win, including exactly seven edges in each block row/column. + Assert.InRange(Math.Abs(checkerMap.GetRow(1)[1] - checkerExpected), 0D, 0.0001D); + } + + // Native coefficient products are float constants. Evaluate their continuous equation in double for the independent expected response. + static double DerivativeRatio(double photon) + { + float sgMul = 226.77216153508914F; + float inverseLog2E = 0.6931471805599453F; + float sgRetMul = (1F / 73.377132366608819F) * 18.6580932135F * inverseLog2E; + double numerator = ((sgRetMul * 3F * sgMul) * photon * photon) + 0.01F; + double denominator = ((inverseLog2E * sgMul) * photon * photon * photon) + ((7.7825991679894591F * inverseLog2E) + 0.01F); + return denominator / numerator; + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } } From c91ba50c7d6bb464c5ebfe2a44258d36027a1893 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Fri, 30 Oct 2026 00:17:24 +0400 Subject: [PATCH 324/328] Compute heuristics, and merge AV1/JXL Log1P into a shared method Both the JXL codec and the AV1 codec needed an implementation of the Log1P function. This was merged into the common MathUtils class. --- src/ImageSharp/Common/Helpers/MathUtils.cs | 46 + .../Pipeline/Lookahead/Av1FirstPass.Row.cs | 3 +- .../Av1/Pipeline/Lookahead/Av1FirstPass.cs | 5 +- .../Pipeline/Lookahead/Av1FirstPassMath.cs | 28 - .../Av1FirstPassStatisticsAccumulator.cs | 6 +- .../Av1/Pipeline/Lookahead/Av1SecondPass.cs | 6 +- .../Jxl/IO/FrameHeader/JxlFrameHeader.cs | 2 +- .../Dct/JxlDct.Kernels.Generated.cs | 5875 +---------------- .../Processing/Decoder/Ans/JxlAnsReader.cs | 1 - .../Jxl/Processing/Decoder/JxlNoiseDecoder.cs | 2 +- .../Encoder/Heuristics/JxlHeuristics.cs | 624 +- .../Jxl/Processing/JxlPassesCommonContext.cs | 27 +- 12 files changed, 670 insertions(+), 5955 deletions(-) create mode 100644 src/ImageSharp/Common/Helpers/MathUtils.cs delete mode 100644 src/ImageSharp/Formats/Heif/Av1/Pipeline/Lookahead/Av1FirstPassMath.cs diff --git a/src/ImageSharp/Common/Helpers/MathUtils.cs b/src/ImageSharp/Common/Helpers/MathUtils.cs new file mode 100644 index 0000000000..e7ff7e4ceb --- /dev/null +++ b/src/ImageSharp/Common/Helpers/MathUtils.cs @@ -0,0 +1,46 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Common.Helpers; + +/// +/// Additional math functions not provided by the .NET Runtime. +/// +internal static class MathUtils +{ + /// + /// Computes the natural logarithm of one plus a value without losing the low bits of small values. + /// + /// + /// evaluates log(1 + x), which discards the bits of that the rounded sum + /// u = 1 + x cannot hold. This method restores them with the first-order term (x - (u - 1)) / u, as the C runtime does. The + /// value u - 1 is exact, so the numerator is the exact rounding error of the sum. Then log(u + e) = log(u) + e / u to within + /// far less than one unit in the last place. The form matches the Windows C runtime bit for bit over twenty million integer and fractional + /// inputs. The correction is zero for every integer input, where the sum is exact. + /// + /// The value to add to one, at least zero. + /// The natural logarithm of one plus . + public static double Log1P(double value) + { + double sum = 1.0 + value; + return Math.Log(sum) + ((value - (sum - 1.0)) / sum); + } + + /// + /// Computes the natural logarithm of one plus a value without losing the low bits of small values. + /// + /// + /// evaluates log(1 + x), which discards the bits of that the rounded sum + /// u = 1 + x cannot hold. This method restores them with the first-order term (x - (u - 1)) / u, as the C runtime does. The + /// value u - 1 is exact, so the numerator is the exact rounding error of the sum. Then log(u + e) = log(u) + e / u to within + /// far less than one unit in the last place. The form matches the Windows C runtime bit for bit over twenty million integer and fractional + /// inputs. The correction is zero for every integer input, where the sum is exact. + /// + /// The value to add to one, at least zero. + /// The natural logarithm of one plus . + public static float Log1P(float value) + { + float sum = 1.0f + value; + return MathF.Log(sum) + ((value - (sum - 1.0f)) / sum); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Lookahead/Av1FirstPass.Row.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Lookahead/Av1FirstPass.Row.cs index 092dceb0b5..4e33f20f79 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Lookahead/Av1FirstPass.Row.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Lookahead/Av1FirstPass.Row.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers; using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; @@ -176,7 +177,7 @@ private int PredictIntra(ref FrameContext frame, int unitRow, int unitColumn, bo } // A unit with a low intra error adds more than one to the intra factor. - double logIntra = Av1FirstPassMath.Log1P(intraError); + double logIntra = MathUtils.Log1P(intraError); if (logIntra < 10.0) { record.IntraFactor += 1.0 + ((10.0 - logIntra) * 0.05); diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Lookahead/Av1FirstPass.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Lookahead/Av1FirstPass.cs index ecb8a6fad6..25574f9525 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Lookahead/Av1FirstPass.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Lookahead/Av1FirstPass.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Buffers; +using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; @@ -589,8 +590,8 @@ private static void NormalizeFirstPassStatistics( statistics.LongTermCodedError /= macroblockCount; statistics.IntraError /= macroblockCount; statistics.FrameAverageWaveletEnergy /= macroblockCount; - statistics.LogCodedError = Av1FirstPassMath.Log1P(statistics.CodedError); - statistics.LogIntraError = Av1FirstPassMath.Log1P(statistics.IntraError); + statistics.LogCodedError = MathUtils.Log1P(statistics.CodedError); + statistics.LogIntraError = MathUtils.Log1P(statistics.IntraError); statistics.MotionVectorRow /= frameHeight; statistics.MotionVectorRowAbsolute /= frameHeight; statistics.MotionVectorColumn /= frameWidth; diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Lookahead/Av1FirstPassMath.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Lookahead/Av1FirstPassMath.cs deleted file mode 100644 index 32690c2d8f..0000000000 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Lookahead/Av1FirstPassMath.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Lookahead; - -/// -/// Provides the C runtime functions that the first-pass statistics depend on bit for bit. -/// -internal static class Av1FirstPassMath -{ - /// - /// Computes the natural logarithm of one plus a value without losing the low bits of small values. - /// - /// - /// evaluates log(1 + x), which discards the bits of that the rounded sum - /// u = 1 + x cannot hold. This method restores them with the first-order term (x - (u - 1)) / u, as the C runtime does. The - /// value u - 1 is exact, so the numerator is the exact rounding error of the sum. Then log(u + e) = log(u) + e / u to within - /// far less than one unit in the last place. The form matches the Windows C runtime bit for bit over twenty million integer and fractional - /// inputs. The correction is zero for every integer input, where the sum is exact. - /// - /// The value to add to one, at least zero. - /// The natural logarithm of one plus . - public static double Log1P(double value) - { - double sum = 1.0 + value; - return Math.Log(sum) + ((value - (sum - 1.0)) / sum); - } -} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Lookahead/Av1FirstPassStatisticsAccumulator.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Lookahead/Av1FirstPassStatisticsAccumulator.cs index d264d38b9c..a20af43335 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Lookahead/Av1FirstPassStatisticsAccumulator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Lookahead/Av1FirstPassStatisticsAccumulator.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Common.Helpers; + namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Lookahead; /// @@ -19,8 +21,8 @@ public static void Accumulate(ref Av1FirstPassStatistics section, Av1FirstPassSt section.Frame += frame.Frame; section.Weight += frame.Weight; section.IntraError += frame.IntraError; - section.LogIntraError += Av1FirstPassMath.Log1P(frame.IntraError); - section.LogCodedError += Av1FirstPassMath.Log1P(frame.CodedError); + section.LogIntraError += MathUtils.Log1P(frame.IntraError); + section.LogCodedError += MathUtils.Log1P(frame.CodedError); section.FrameAverageWaveletEnergy += frame.FrameAverageWaveletEnergy; section.CodedError += frame.CodedError; section.SecondReferenceCodedError += frame.SecondReferenceCodedError; diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Lookahead/Av1SecondPass.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Lookahead/Av1SecondPass.cs index 78cce5bbf8..edc35928f5 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Lookahead/Av1SecondPass.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Lookahead/Av1SecondPass.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Common.Helpers; + namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Lookahead; /// @@ -957,10 +959,10 @@ private void SetParametersFromStatistics(int position) /// The statistics. private void SetParameters(Av1FirstPassStatistics frameStatistics) { - this.macroblockAverageEnergy = Av1FirstPassMath.Log1P(frameStatistics.IntraError); + this.macroblockAverageEnergy = MathUtils.Log1P(frameStatistics.IntraError); if (this.totalStatistics.FrameAverageWaveletEnergy >= 0) { - this.frameAverageHaarEnergy = Av1FirstPassMath.Log1P(frameStatistics.FrameAverageWaveletEnergy); + this.frameAverageHaarEnergy = MathUtils.Log1P(frameStatistics.FrameAverageWaveletEnergy); } // A frame with at least 15 percent of intra skip blocks counts as animation or graphics. diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs index fea3477a3f..a85fce9595 100644 --- a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs +++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs @@ -408,7 +408,7 @@ public static void JpegOrder(JxlColorTransform transform, bool isGray, out Inlin } } - private void UpdateFlag(bool condition, ulong flag) + public void UpdateFlag(bool condition, ulong flag) { if (condition) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDct.Kernels.Generated.cs b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDct.Kernels.Generated.cs index e54bd0aeb8..f4a2e8e1d3 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDct.Kernels.Generated.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDct.Kernels.Generated.cs @@ -1,5874 +1 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -// - -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; - -internal static partial class JxlDct -{ - /// - /// Computes independent forward transforms using two nonoverlapping borrowed vector banks. - /// - /// One vector per transform position. Its length is a power of two from one through 256. - /// At least values.Length vectors. Previous contents are not read. - private static void Forward(Span values, Span scratch) - { - // Small transforms stay in registers. The one-point transform is an identity and must not copy any trailing samples. - switch (values.Length) - { - case 1: - return; - case 2: - Forward2(values); - return; - case 4: - Forward4(values); - return; - case 8: - Forward8(values); - return; - case 16: - Forward16(values); - return; - case 32: - Forward32(values); - return; - } - - int half = values.Length / 2; - ReadOnlySpan weights = JxlDctScales.GetMultipliers(values.Length); - - // Consume both original halves before reusing their bank as recursive scratch. Neither recursive call can then destroy unread input. - for (int i = 0; i < half; i++) - { - float first = values[i]; - float last = values[values.Length - i - 1]; - scratch[i] = first + last; - scratch[half + i] = (first - last) * weights[i]; - } - - Forward(scratch[..half], values); - Forward(scratch.Slice(half, half), values); - scratch[half] = float.MultiplyAddEstimate(scratch[half], JxlDctScales.Sqrt2, scratch[half + 1]); - - for (int i = 1; i + 1 < half; i++) - { - scratch[half + i] += scratch[half + i + 1]; - } - - for (int i = 0; i < half; i++) - { - values[2 * i] = scratch[i]; - values[(2 * i) + 1] = scratch[half + i]; - } - } - - /// - /// Computes independent 2-point forward transforms without intermediate memory passes. - /// - /// Exactly 2 position vectors, replaced with the transform results. Each lane remains independent. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Forward2(Span values) - { - // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. - // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. - float input0 = values[0]; - float input1 = values[1]; - - float v0 = input0 + input1; - float v1 = input0 - input1; - - values[0] = v0; - values[1] = v1; - } - - /// - /// Computes independent 4-point forward transforms without intermediate memory passes. - /// - /// Exactly 4 position vectors, replaced with the transform results. Each lane remains independent. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Forward4(Span values) - { - // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. - // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. - float input0 = values[0]; - float input1 = values[1]; - float input2 = values[2]; - float input3 = values[3]; - - float v0 = input0 + input3; - float v1 = (input0 - input3) * 0.541196100146197f; - float v2 = input1 + input2; - float v3 = (input1 - input2) * 1.3065629648763764f; - float v4 = v0 + v2; - float v5 = v0 - v2; - float v6 = v1 + v3; - float v7 = v1 - v3; - float v8 = float.MultiplyAddEstimate(v6, JxlDctScales.Sqrt2, v7); - - values[0] = v4; - values[1] = v8; - values[2] = v5; - values[3] = v7; - } - - /// - /// Computes independent 8-point forward transforms without intermediate memory passes. - /// - /// Exactly 8 position vectors, replaced with the transform results. Each lane remains independent. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Forward8(Span values) - { - // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. - // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. - float input0 = values[0]; - float input1 = values[1]; - float input2 = values[2]; - float input3 = values[3]; - float input4 = values[4]; - float input5 = values[5]; - float input6 = values[6]; - float input7 = values[7]; - - float v0 = input0 + input7; - float v1 = (input0 - input7) * 0.5097955791041592f; - float v2 = input1 + input6; - float v3 = (input1 - input6) * 0.6013448869350453f; - float v4 = input2 + input5; - float v5 = (input2 - input5) * 0.8999762231364156f; - float v6 = input3 + input4; - float v7 = (input3 - input4) * 2.5629154477415055f; - float v8 = v0 + v6; - float v9 = (v0 - v6) * 0.541196100146197f; - float v10 = v2 + v4; - float v11 = (v2 - v4) * 1.3065629648763764f; - float v12 = v8 + v10; - float v13 = v8 - v10; - float v14 = v9 + v11; - float v15 = v9 - v11; - float v16 = float.MultiplyAddEstimate(v14, JxlDctScales.Sqrt2, v15); - float v17 = v1 + v7; - float v18 = (v1 - v7) * 0.541196100146197f; - float v19 = v3 + v5; - float v20 = (v3 - v5) * 1.3065629648763764f; - float v21 = v17 + v19; - float v22 = v17 - v19; - float v23 = v18 + v20; - float v24 = v18 - v20; - float v25 = float.MultiplyAddEstimate(v23, JxlDctScales.Sqrt2, v24); - float v26 = float.MultiplyAddEstimate(v21, JxlDctScales.Sqrt2, v25); - float v27 = v25 + v22; - float v28 = v22 + v24; - - values[0] = v12; - values[1] = v26; - values[2] = v16; - values[3] = v27; - values[4] = v13; - values[5] = v28; - values[6] = v15; - values[7] = v24; - } - - /// - /// Computes independent 16-point forward transforms without intermediate memory passes. - /// - /// Exactly 16 position vectors, replaced with the transform results. Each lane remains independent. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Forward16(Span values) - { - // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. - // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. - float input0 = values[0]; - float input1 = values[1]; - float input2 = values[2]; - float input3 = values[3]; - float input4 = values[4]; - float input5 = values[5]; - float input6 = values[6]; - float input7 = values[7]; - float input8 = values[8]; - float input9 = values[9]; - float input10 = values[10]; - float input11 = values[11]; - float input12 = values[12]; - float input13 = values[13]; - float input14 = values[14]; - float input15 = values[15]; - - float v0 = input0 + input15; - float v1 = (input0 - input15) * 0.5024192861881557f; - float v2 = input1 + input14; - float v3 = (input1 - input14) * 0.5224986149396889f; - float v4 = input2 + input13; - float v5 = (input2 - input13) * 0.5669440348163577f; - float v6 = input3 + input12; - float v7 = (input3 - input12) * 0.6468217833599901f; - float v8 = input4 + input11; - float v9 = (input4 - input11) * 0.7881546234512502f; - float v10 = input5 + input10; - float v11 = (input5 - input10) * 1.060677685990347f; - float v12 = input6 + input9; - float v13 = (input6 - input9) * 1.7224470982383342f; - float v14 = input7 + input8; - float v15 = (input7 - input8) * 5.101148618689155f; - float v16 = v0 + v14; - float v17 = (v0 - v14) * 0.5097955791041592f; - float v18 = v2 + v12; - float v19 = (v2 - v12) * 0.6013448869350453f; - float v20 = v4 + v10; - float v21 = (v4 - v10) * 0.8999762231364156f; - float v22 = v6 + v8; - float v23 = (v6 - v8) * 2.5629154477415055f; - float v24 = v16 + v22; - float v25 = (v16 - v22) * 0.541196100146197f; - float v26 = v18 + v20; - float v27 = (v18 - v20) * 1.3065629648763764f; - float v28 = v24 + v26; - float v29 = v24 - v26; - float v30 = v25 + v27; - float v31 = v25 - v27; - float v32 = float.MultiplyAddEstimate(v30, JxlDctScales.Sqrt2, v31); - float v33 = v17 + v23; - float v34 = (v17 - v23) * 0.541196100146197f; - float v35 = v19 + v21; - float v36 = (v19 - v21) * 1.3065629648763764f; - float v37 = v33 + v35; - float v38 = v33 - v35; - float v39 = v34 + v36; - float v40 = v34 - v36; - float v41 = float.MultiplyAddEstimate(v39, JxlDctScales.Sqrt2, v40); - float v42 = float.MultiplyAddEstimate(v37, JxlDctScales.Sqrt2, v41); - float v43 = v41 + v38; - float v44 = v38 + v40; - float v45 = v1 + v15; - float v46 = (v1 - v15) * 0.5097955791041592f; - float v47 = v3 + v13; - float v48 = (v3 - v13) * 0.6013448869350453f; - float v49 = v5 + v11; - float v50 = (v5 - v11) * 0.8999762231364156f; - float v51 = v7 + v9; - float v52 = (v7 - v9) * 2.5629154477415055f; - float v53 = v45 + v51; - float v54 = (v45 - v51) * 0.541196100146197f; - float v55 = v47 + v49; - float v56 = (v47 - v49) * 1.3065629648763764f; - float v57 = v53 + v55; - float v58 = v53 - v55; - float v59 = v54 + v56; - float v60 = v54 - v56; - float v61 = float.MultiplyAddEstimate(v59, JxlDctScales.Sqrt2, v60); - float v62 = v46 + v52; - float v63 = (v46 - v52) * 0.541196100146197f; - float v64 = v48 + v50; - float v65 = (v48 - v50) * 1.3065629648763764f; - float v66 = v62 + v64; - float v67 = v62 - v64; - float v68 = v63 + v65; - float v69 = v63 - v65; - float v70 = float.MultiplyAddEstimate(v68, JxlDctScales.Sqrt2, v69); - float v71 = float.MultiplyAddEstimate(v66, JxlDctScales.Sqrt2, v70); - float v72 = v70 + v67; - float v73 = v67 + v69; - float v74 = float.MultiplyAddEstimate(v57, JxlDctScales.Sqrt2, v71); - float v75 = v71 + v61; - float v76 = v61 + v72; - float v77 = v72 + v58; - float v78 = v58 + v73; - float v79 = v73 + v60; - float v80 = v60 + v69; - - values[0] = v28; - values[1] = v74; - values[2] = v42; - values[3] = v75; - values[4] = v32; - values[5] = v76; - values[6] = v43; - values[7] = v77; - values[8] = v29; - values[9] = v78; - values[10] = v44; - values[11] = v79; - values[12] = v31; - values[13] = v80; - values[14] = v40; - values[15] = v69; - } - - /// - /// Computes independent 32-point forward transforms without intermediate memory passes. - /// - /// Exactly 32 position vectors, replaced with the transform results. Each lane remains independent. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Forward32(Span values) - { - // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. - // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. - float input0 = values[0]; - float input1 = values[1]; - float input2 = values[2]; - float input3 = values[3]; - float input4 = values[4]; - float input5 = values[5]; - float input6 = values[6]; - float input7 = values[7]; - float input8 = values[8]; - float input9 = values[9]; - float input10 = values[10]; - float input11 = values[11]; - float input12 = values[12]; - float input13 = values[13]; - float input14 = values[14]; - float input15 = values[15]; - float input16 = values[16]; - float input17 = values[17]; - float input18 = values[18]; - float input19 = values[19]; - float input20 = values[20]; - float input21 = values[21]; - float input22 = values[22]; - float input23 = values[23]; - float input24 = values[24]; - float input25 = values[25]; - float input26 = values[26]; - float input27 = values[27]; - float input28 = values[28]; - float input29 = values[29]; - float input30 = values[30]; - float input31 = values[31]; - - float v0 = input0 + input31; - float v1 = (input0 - input31) * 0.5006029982351963f; - float v2 = input1 + input30; - float v3 = (input1 - input30) * 0.5054709598975436f; - float v4 = input2 + input29; - float v5 = (input2 - input29) * 0.5154473099226246f; - float v6 = input3 + input28; - float v7 = (input3 - input28) * 0.5310425910897841f; - float v8 = input4 + input27; - float v9 = (input4 - input27) * 0.5531038960344445f; - float v10 = input5 + input26; - float v11 = (input5 - input26) * 0.5829349682061339f; - float v12 = input6 + input25; - float v13 = (input6 - input25) * 0.6225041230356648f; - float v14 = input7 + input24; - float v15 = (input7 - input24) * 0.6748083414550057f; - float v16 = input8 + input23; - float v17 = (input8 - input23) * 0.7445362710022986f; - float v18 = input9 + input22; - float v19 = (input9 - input22) * 0.8393496454155268f; - float v20 = input10 + input21; - float v21 = (input10 - input21) * 0.9725682378619608f; - float v22 = input11 + input20; - float v23 = (input11 - input20) * 1.1694399334328847f; - float v24 = input12 + input19; - float v25 = (input12 - input19) * 1.4841646163141662f; - float v26 = input13 + input18; - float v27 = (input13 - input18) * 2.057781009953411f; - float v28 = input14 + input17; - float v29 = (input14 - input17) * 3.407608418468719f; - float v30 = input15 + input16; - float v31 = (input15 - input16) * 10.190008123548033f; - float v32 = v0 + v30; - float v33 = (v0 - v30) * 0.5024192861881557f; - float v34 = v2 + v28; - float v35 = (v2 - v28) * 0.5224986149396889f; - float v36 = v4 + v26; - float v37 = (v4 - v26) * 0.5669440348163577f; - float v38 = v6 + v24; - float v39 = (v6 - v24) * 0.6468217833599901f; - float v40 = v8 + v22; - float v41 = (v8 - v22) * 0.7881546234512502f; - float v42 = v10 + v20; - float v43 = (v10 - v20) * 1.060677685990347f; - float v44 = v12 + v18; - float v45 = (v12 - v18) * 1.7224470982383342f; - float v46 = v14 + v16; - float v47 = (v14 - v16) * 5.101148618689155f; - float v48 = v32 + v46; - float v49 = (v32 - v46) * 0.5097955791041592f; - float v50 = v34 + v44; - float v51 = (v34 - v44) * 0.6013448869350453f; - float v52 = v36 + v42; - float v53 = (v36 - v42) * 0.8999762231364156f; - float v54 = v38 + v40; - float v55 = (v38 - v40) * 2.5629154477415055f; - float v56 = v48 + v54; - float v57 = (v48 - v54) * 0.541196100146197f; - float v58 = v50 + v52; - float v59 = (v50 - v52) * 1.3065629648763764f; - float v60 = v56 + v58; - float v61 = v56 - v58; - float v62 = v57 + v59; - float v63 = v57 - v59; - float v64 = float.MultiplyAddEstimate(v62, JxlDctScales.Sqrt2, v63); - float v65 = v49 + v55; - float v66 = (v49 - v55) * 0.541196100146197f; - float v67 = v51 + v53; - float v68 = (v51 - v53) * 1.3065629648763764f; - float v69 = v65 + v67; - float v70 = v65 - v67; - float v71 = v66 + v68; - float v72 = v66 - v68; - float v73 = float.MultiplyAddEstimate(v71, JxlDctScales.Sqrt2, v72); - float v74 = float.MultiplyAddEstimate(v69, JxlDctScales.Sqrt2, v73); - float v75 = v73 + v70; - float v76 = v70 + v72; - float v77 = v33 + v47; - float v78 = (v33 - v47) * 0.5097955791041592f; - float v79 = v35 + v45; - float v80 = (v35 - v45) * 0.6013448869350453f; - float v81 = v37 + v43; - float v82 = (v37 - v43) * 0.8999762231364156f; - float v83 = v39 + v41; - float v84 = (v39 - v41) * 2.5629154477415055f; - float v85 = v77 + v83; - float v86 = (v77 - v83) * 0.541196100146197f; - float v87 = v79 + v81; - float v88 = (v79 - v81) * 1.3065629648763764f; - float v89 = v85 + v87; - float v90 = v85 - v87; - float v91 = v86 + v88; - float v92 = v86 - v88; - float v93 = float.MultiplyAddEstimate(v91, JxlDctScales.Sqrt2, v92); - float v94 = v78 + v84; - float v95 = (v78 - v84) * 0.541196100146197f; - float v96 = v80 + v82; - float v97 = (v80 - v82) * 1.3065629648763764f; - float v98 = v94 + v96; - float v99 = v94 - v96; - float v100 = v95 + v97; - float v101 = v95 - v97; - float v102 = float.MultiplyAddEstimate(v100, JxlDctScales.Sqrt2, v101); - float v103 = float.MultiplyAddEstimate(v98, JxlDctScales.Sqrt2, v102); - float v104 = v102 + v99; - float v105 = v99 + v101; - float v106 = float.MultiplyAddEstimate(v89, JxlDctScales.Sqrt2, v103); - float v107 = v103 + v93; - float v108 = v93 + v104; - float v109 = v104 + v90; - float v110 = v90 + v105; - float v111 = v105 + v92; - float v112 = v92 + v101; - float v113 = v1 + v31; - float v114 = (v1 - v31) * 0.5024192861881557f; - float v115 = v3 + v29; - float v116 = (v3 - v29) * 0.5224986149396889f; - float v117 = v5 + v27; - float v118 = (v5 - v27) * 0.5669440348163577f; - float v119 = v7 + v25; - float v120 = (v7 - v25) * 0.6468217833599901f; - float v121 = v9 + v23; - float v122 = (v9 - v23) * 0.7881546234512502f; - float v123 = v11 + v21; - float v124 = (v11 - v21) * 1.060677685990347f; - float v125 = v13 + v19; - float v126 = (v13 - v19) * 1.7224470982383342f; - float v127 = v15 + v17; - float v128 = (v15 - v17) * 5.101148618689155f; - float v129 = v113 + v127; - float v130 = (v113 - v127) * 0.5097955791041592f; - float v131 = v115 + v125; - float v132 = (v115 - v125) * 0.6013448869350453f; - float v133 = v117 + v123; - float v134 = (v117 - v123) * 0.8999762231364156f; - float v135 = v119 + v121; - float v136 = (v119 - v121) * 2.5629154477415055f; - float v137 = v129 + v135; - float v138 = (v129 - v135) * 0.541196100146197f; - float v139 = v131 + v133; - float v140 = (v131 - v133) * 1.3065629648763764f; - float v141 = v137 + v139; - float v142 = v137 - v139; - float v143 = v138 + v140; - float v144 = v138 - v140; - float v145 = float.MultiplyAddEstimate(v143, JxlDctScales.Sqrt2, v144); - float v146 = v130 + v136; - float v147 = (v130 - v136) * 0.541196100146197f; - float v148 = v132 + v134; - float v149 = (v132 - v134) * 1.3065629648763764f; - float v150 = v146 + v148; - float v151 = v146 - v148; - float v152 = v147 + v149; - float v153 = v147 - v149; - float v154 = float.MultiplyAddEstimate(v152, JxlDctScales.Sqrt2, v153); - float v155 = float.MultiplyAddEstimate(v150, JxlDctScales.Sqrt2, v154); - float v156 = v154 + v151; - float v157 = v151 + v153; - float v158 = v114 + v128; - float v159 = (v114 - v128) * 0.5097955791041592f; - float v160 = v116 + v126; - float v161 = (v116 - v126) * 0.6013448869350453f; - float v162 = v118 + v124; - float v163 = (v118 - v124) * 0.8999762231364156f; - float v164 = v120 + v122; - float v165 = (v120 - v122) * 2.5629154477415055f; - float v166 = v158 + v164; - float v167 = (v158 - v164) * 0.541196100146197f; - float v168 = v160 + v162; - float v169 = (v160 - v162) * 1.3065629648763764f; - float v170 = v166 + v168; - float v171 = v166 - v168; - float v172 = v167 + v169; - float v173 = v167 - v169; - float v174 = float.MultiplyAddEstimate(v172, JxlDctScales.Sqrt2, v173); - float v175 = v159 + v165; - float v176 = (v159 - v165) * 0.541196100146197f; - float v177 = v161 + v163; - float v178 = (v161 - v163) * 1.3065629648763764f; - float v179 = v175 + v177; - float v180 = v175 - v177; - float v181 = v176 + v178; - float v182 = v176 - v178; - float v183 = float.MultiplyAddEstimate(v181, JxlDctScales.Sqrt2, v182); - float v184 = float.MultiplyAddEstimate(v179, JxlDctScales.Sqrt2, v183); - float v185 = v183 + v180; - float v186 = v180 + v182; - float v187 = float.MultiplyAddEstimate(v170, JxlDctScales.Sqrt2, v184); - float v188 = v184 + v174; - float v189 = v174 + v185; - float v190 = v185 + v171; - float v191 = v171 + v186; - float v192 = v186 + v173; - float v193 = v173 + v182; - float v194 = float.MultiplyAddEstimate(v141, JxlDctScales.Sqrt2, v187); - float v195 = v187 + v155; - float v196 = v155 + v188; - float v197 = v188 + v145; - float v198 = v145 + v189; - float v199 = v189 + v156; - float v200 = v156 + v190; - float v201 = v190 + v142; - float v202 = v142 + v191; - float v203 = v191 + v157; - float v204 = v157 + v192; - float v205 = v192 + v144; - float v206 = v144 + v193; - float v207 = v193 + v153; - float v208 = v153 + v182; - - values[0] = v60; - values[1] = v194; - values[2] = v106; - values[3] = v195; - values[4] = v74; - values[5] = v196; - values[6] = v107; - values[7] = v197; - values[8] = v64; - values[9] = v198; - values[10] = v108; - values[11] = v199; - values[12] = v75; - values[13] = v200; - values[14] = v109; - values[15] = v201; - values[16] = v61; - values[17] = v202; - values[18] = v110; - values[19] = v203; - values[20] = v76; - values[21] = v204; - values[22] = v111; - values[23] = v205; - values[24] = v63; - values[25] = v206; - values[26] = v112; - values[27] = v207; - values[28] = v72; - values[29] = v208; - values[30] = v101; - values[31] = v182; - } - - /// - /// Processes complete 1-lane forward batches along input columns. - /// - /// The number of positions in each transform. - /// The number of independent transforms. - /// The borrowed source matrix. - /// The source row stride, in floats. - /// The borrowed output matrix. - /// The destination row stride, in floats. - /// The first independent transform not already processed by a wider batch. - /// Borrowed storage for two length-by-1 banks. Only the first bank needs initialized inputs. - /// The first independent transform left after the complete batches. - private static int ForwardColumnsScalar( - int length, - int count, - ReadOnlySpan source, - int sourceStride, - Span destination, - int destinationStride, - int first, - Span workspace) - { - Span values = workspace[..length]; - Span scratch = workspace.Slice(length, length); - ref float input = ref MemoryMarshal.GetReference(source); - ref float output = ref MemoryMarshal.GetReference(destination); - float scale = 1F / length; - - for (; first <= count - 1; first += 1) - { - // One vector covers adjacent columns at the same transform position. Physical strides never enter the coefficient-bank layout. - for (int i = 0; i < length; i++) - { - values[i] = Unsafe.Add(ref input, (i * sourceStride) + first); - } - - Forward(values, scratch); - for (int i = 0; i < length; i++) - { - Unsafe.Add(ref output, (i * destinationStride) + first) = values[i] * scale; - } - } - - return first; - } - - /// - /// Processes complete 1-lane forward batches along input rows. - /// - /// The number of positions in each transform. - /// The number of independent transforms. - /// The borrowed source matrix. - /// The source row stride, in floats. - /// The borrowed output matrix. - /// The destination row stride, in floats. - /// Whether transform positions become output rows and input rows become output columns. - /// The first independent transform not already processed by a wider batch. - /// Borrowed storage for two length-by-1 banks. Only the first bank needs initialized inputs. - /// The first independent transform left after the complete batches. - private static int ForwardRowsScalar( - int length, - int count, - ReadOnlySpan source, - int sourceStride, - Span destination, - int destinationStride, - bool transposed, - int first, - Span workspace) - { - Span values = workspace[..length]; - Span scratch = workspace.Slice(length, length); - ref float input = ref MemoryMarshal.GetReference(source); - ref float output = ref MemoryMarshal.GetReference(destination); - float scale = 1F / length; - - for (; first <= count - 1; first += 1) - { - source.Slice(first * sourceStride, length).CopyTo(values); - - Forward(values, scratch); - if (transposed) - { - // A complete vector is one contiguous output row. Only logical samples are stored; destination padding remains untouched. - for (int i = 0; i < length; i++) - { - Unsafe.Add(ref output, (i * destinationStride) + first) = values[i] * scale; - } - } - else - { - for (int i = 0; i < length; i++) - { - Unsafe.Add(ref output, (first * destinationStride) + i) = values[i] * scale; - } - } - } - - return first; - } - - /// - /// Computes independent inverse transforms using two nonoverlapping borrowed vector banks. - /// - /// One vector per transform position. Its length is a power of two from one through 256. - /// At least values.Length vectors. Previous contents are not read. - private static void Inverse(Span values, Span scratch) - { - // Small transforms stay in registers. The one-point transform is an identity and must not copy any trailing samples. - switch (values.Length) - { - case 1: - return; - case 2: - Inverse2(values); - return; - case 4: - Inverse4(values); - return; - case 8: - Inverse8(values); - return; - case 16: - Inverse16(values); - return; - case 32: - Inverse32(values); - return; - } - - int half = values.Length / 2; - // Separate frequency parity before either recursive call. The original bank can then serve as recursive scratch. - for (int i = 0; i < half; i++) - { - scratch[i] = values[2 * i]; - scratch[half + i] = values[(2 * i) + 1]; - } - - Inverse(scratch[..half], values); - - // Descend so every odd-frequency predecessor is read before its own entry changes. - for (int i = half - 1; i > 0; i--) - { - scratch[half + i] += scratch[half + i - 1]; - } - - scratch[half] *= JxlDctScales.Sqrt2; - Inverse(scratch.Slice(half, half), values); - ReadOnlySpan weights = JxlDctScales.GetMultipliers(values.Length); - - for (int i = 0; i < half; i++) - { - float weight = weights[i]; - float even = scratch[i]; - float odd = scratch[half + i]; - values[i] = float.MultiplyAddEstimate(weight, odd, even); - values[values.Length - i - 1] = float.MultiplyAddEstimate(-weight, odd, even); - } - } - - /// - /// Computes independent 2-point inverse transforms without intermediate memory passes. - /// - /// Exactly 2 position vectors, replaced with the transform results. Each lane remains independent. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Inverse2(Span values) - { - // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. - // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. - float input0 = values[0]; - float input1 = values[1]; - - float v0 = input0 + input1; - float v1 = input0 - input1; - - values[0] = v0; - values[1] = v1; - } - - /// - /// Computes independent 4-point inverse transforms without intermediate memory passes. - /// - /// Exactly 4 position vectors, replaced with the transform results. Each lane remains independent. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Inverse4(Span values) - { - // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. - // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. - float input0 = values[0]; - float input1 = values[1]; - float input2 = values[2]; - float input3 = values[3]; - - float v0 = input0 + input2; - float v1 = input0 - input2; - float v2 = input3 + input1; - float v3 = input1 * JxlDctScales.Sqrt2; - float v4 = v3 + v2; - float v5 = v3 - v2; - float v6 = float.MultiplyAddEstimate(0.541196100146197f, v4, v0); - float v7 = float.MultiplyAddEstimate(-0.541196100146197f, v4, v0); - float v8 = float.MultiplyAddEstimate(1.3065629648763764f, v5, v1); - float v9 = float.MultiplyAddEstimate(-1.3065629648763764f, v5, v1); - - values[0] = v6; - values[1] = v8; - values[2] = v9; - values[3] = v7; - } - - /// - /// Computes independent 8-point inverse transforms without intermediate memory passes. - /// - /// Exactly 8 position vectors, replaced with the transform results. Each lane remains independent. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Inverse8(Span values) - { - // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. - // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. - float input0 = values[0]; - float input1 = values[1]; - float input2 = values[2]; - float input3 = values[3]; - float input4 = values[4]; - float input5 = values[5]; - float input6 = values[6]; - float input7 = values[7]; - - float v0 = input0 + input4; - float v1 = input0 - input4; - float v2 = input6 + input2; - float v3 = input2 * JxlDctScales.Sqrt2; - float v4 = v3 + v2; - float v5 = v3 - v2; - float v6 = float.MultiplyAddEstimate(0.541196100146197f, v4, v0); - float v7 = float.MultiplyAddEstimate(-0.541196100146197f, v4, v0); - float v8 = float.MultiplyAddEstimate(1.3065629648763764f, v5, v1); - float v9 = float.MultiplyAddEstimate(-1.3065629648763764f, v5, v1); - float v10 = input7 + input5; - float v11 = input5 + input3; - float v12 = input3 + input1; - float v13 = input1 * JxlDctScales.Sqrt2; - float v14 = v13 + v11; - float v15 = v13 - v11; - float v16 = v10 + v12; - float v17 = v12 * JxlDctScales.Sqrt2; - float v18 = v17 + v16; - float v19 = v17 - v16; - float v20 = float.MultiplyAddEstimate(0.541196100146197f, v18, v14); - float v21 = float.MultiplyAddEstimate(-0.541196100146197f, v18, v14); - float v22 = float.MultiplyAddEstimate(1.3065629648763764f, v19, v15); - float v23 = float.MultiplyAddEstimate(-1.3065629648763764f, v19, v15); - float v24 = float.MultiplyAddEstimate(0.5097955791041592f, v20, v6); - float v25 = float.MultiplyAddEstimate(-0.5097955791041592f, v20, v6); - float v26 = float.MultiplyAddEstimate(0.6013448869350453f, v22, v8); - float v27 = float.MultiplyAddEstimate(-0.6013448869350453f, v22, v8); - float v28 = float.MultiplyAddEstimate(0.8999762231364156f, v23, v9); - float v29 = float.MultiplyAddEstimate(-0.8999762231364156f, v23, v9); - float v30 = float.MultiplyAddEstimate(2.5629154477415055f, v21, v7); - float v31 = float.MultiplyAddEstimate(-2.5629154477415055f, v21, v7); - - values[0] = v24; - values[1] = v26; - values[2] = v28; - values[3] = v30; - values[4] = v31; - values[5] = v29; - values[6] = v27; - values[7] = v25; - } - - /// - /// Computes independent 16-point inverse transforms without intermediate memory passes. - /// - /// Exactly 16 position vectors, replaced with the transform results. Each lane remains independent. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Inverse16(Span values) - { - // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. - // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. - float input0 = values[0]; - float input1 = values[1]; - float input2 = values[2]; - float input3 = values[3]; - float input4 = values[4]; - float input5 = values[5]; - float input6 = values[6]; - float input7 = values[7]; - float input8 = values[8]; - float input9 = values[9]; - float input10 = values[10]; - float input11 = values[11]; - float input12 = values[12]; - float input13 = values[13]; - float input14 = values[14]; - float input15 = values[15]; - - float v0 = input0 + input8; - float v1 = input0 - input8; - float v2 = input12 + input4; - float v3 = input4 * JxlDctScales.Sqrt2; - float v4 = v3 + v2; - float v5 = v3 - v2; - float v6 = float.MultiplyAddEstimate(0.541196100146197f, v4, v0); - float v7 = float.MultiplyAddEstimate(-0.541196100146197f, v4, v0); - float v8 = float.MultiplyAddEstimate(1.3065629648763764f, v5, v1); - float v9 = float.MultiplyAddEstimate(-1.3065629648763764f, v5, v1); - float v10 = input14 + input10; - float v11 = input10 + input6; - float v12 = input6 + input2; - float v13 = input2 * JxlDctScales.Sqrt2; - float v14 = v13 + v11; - float v15 = v13 - v11; - float v16 = v10 + v12; - float v17 = v12 * JxlDctScales.Sqrt2; - float v18 = v17 + v16; - float v19 = v17 - v16; - float v20 = float.MultiplyAddEstimate(0.541196100146197f, v18, v14); - float v21 = float.MultiplyAddEstimate(-0.541196100146197f, v18, v14); - float v22 = float.MultiplyAddEstimate(1.3065629648763764f, v19, v15); - float v23 = float.MultiplyAddEstimate(-1.3065629648763764f, v19, v15); - float v24 = float.MultiplyAddEstimate(0.5097955791041592f, v20, v6); - float v25 = float.MultiplyAddEstimate(-0.5097955791041592f, v20, v6); - float v26 = float.MultiplyAddEstimate(0.6013448869350453f, v22, v8); - float v27 = float.MultiplyAddEstimate(-0.6013448869350453f, v22, v8); - float v28 = float.MultiplyAddEstimate(0.8999762231364156f, v23, v9); - float v29 = float.MultiplyAddEstimate(-0.8999762231364156f, v23, v9); - float v30 = float.MultiplyAddEstimate(2.5629154477415055f, v21, v7); - float v31 = float.MultiplyAddEstimate(-2.5629154477415055f, v21, v7); - float v32 = input15 + input13; - float v33 = input13 + input11; - float v34 = input11 + input9; - float v35 = input9 + input7; - float v36 = input7 + input5; - float v37 = input5 + input3; - float v38 = input3 + input1; - float v39 = input1 * JxlDctScales.Sqrt2; - float v40 = v39 + v35; - float v41 = v39 - v35; - float v42 = v33 + v37; - float v43 = v37 * JxlDctScales.Sqrt2; - float v44 = v43 + v42; - float v45 = v43 - v42; - float v46 = float.MultiplyAddEstimate(0.541196100146197f, v44, v40); - float v47 = float.MultiplyAddEstimate(-0.541196100146197f, v44, v40); - float v48 = float.MultiplyAddEstimate(1.3065629648763764f, v45, v41); - float v49 = float.MultiplyAddEstimate(-1.3065629648763764f, v45, v41); - float v50 = v32 + v34; - float v51 = v34 + v36; - float v52 = v36 + v38; - float v53 = v38 * JxlDctScales.Sqrt2; - float v54 = v53 + v51; - float v55 = v53 - v51; - float v56 = v50 + v52; - float v57 = v52 * JxlDctScales.Sqrt2; - float v58 = v57 + v56; - float v59 = v57 - v56; - float v60 = float.MultiplyAddEstimate(0.541196100146197f, v58, v54); - float v61 = float.MultiplyAddEstimate(-0.541196100146197f, v58, v54); - float v62 = float.MultiplyAddEstimate(1.3065629648763764f, v59, v55); - float v63 = float.MultiplyAddEstimate(-1.3065629648763764f, v59, v55); - float v64 = float.MultiplyAddEstimate(0.5097955791041592f, v60, v46); - float v65 = float.MultiplyAddEstimate(-0.5097955791041592f, v60, v46); - float v66 = float.MultiplyAddEstimate(0.6013448869350453f, v62, v48); - float v67 = float.MultiplyAddEstimate(-0.6013448869350453f, v62, v48); - float v68 = float.MultiplyAddEstimate(0.8999762231364156f, v63, v49); - float v69 = float.MultiplyAddEstimate(-0.8999762231364156f, v63, v49); - float v70 = float.MultiplyAddEstimate(2.5629154477415055f, v61, v47); - float v71 = float.MultiplyAddEstimate(-2.5629154477415055f, v61, v47); - float v72 = float.MultiplyAddEstimate(0.5024192861881557f, v64, v24); - float v73 = float.MultiplyAddEstimate(-0.5024192861881557f, v64, v24); - float v74 = float.MultiplyAddEstimate(0.5224986149396889f, v66, v26); - float v75 = float.MultiplyAddEstimate(-0.5224986149396889f, v66, v26); - float v76 = float.MultiplyAddEstimate(0.5669440348163577f, v68, v28); - float v77 = float.MultiplyAddEstimate(-0.5669440348163577f, v68, v28); - float v78 = float.MultiplyAddEstimate(0.6468217833599901f, v70, v30); - float v79 = float.MultiplyAddEstimate(-0.6468217833599901f, v70, v30); - float v80 = float.MultiplyAddEstimate(0.7881546234512502f, v71, v31); - float v81 = float.MultiplyAddEstimate(-0.7881546234512502f, v71, v31); - float v82 = float.MultiplyAddEstimate(1.060677685990347f, v69, v29); - float v83 = float.MultiplyAddEstimate(-1.060677685990347f, v69, v29); - float v84 = float.MultiplyAddEstimate(1.7224470982383342f, v67, v27); - float v85 = float.MultiplyAddEstimate(-1.7224470982383342f, v67, v27); - float v86 = float.MultiplyAddEstimate(5.101148618689155f, v65, v25); - float v87 = float.MultiplyAddEstimate(-5.101148618689155f, v65, v25); - - values[0] = v72; - values[1] = v74; - values[2] = v76; - values[3] = v78; - values[4] = v80; - values[5] = v82; - values[6] = v84; - values[7] = v86; - values[8] = v87; - values[9] = v85; - values[10] = v83; - values[11] = v81; - values[12] = v79; - values[13] = v77; - values[14] = v75; - values[15] = v73; - } - - /// - /// Computes independent 32-point inverse transforms without intermediate memory passes. - /// - /// Exactly 32 position vectors, replaced with the transform results. Each lane remains independent. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Inverse32(Span values) - { - // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. - // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. - float input0 = values[0]; - float input1 = values[1]; - float input2 = values[2]; - float input3 = values[3]; - float input4 = values[4]; - float input5 = values[5]; - float input6 = values[6]; - float input7 = values[7]; - float input8 = values[8]; - float input9 = values[9]; - float input10 = values[10]; - float input11 = values[11]; - float input12 = values[12]; - float input13 = values[13]; - float input14 = values[14]; - float input15 = values[15]; - float input16 = values[16]; - float input17 = values[17]; - float input18 = values[18]; - float input19 = values[19]; - float input20 = values[20]; - float input21 = values[21]; - float input22 = values[22]; - float input23 = values[23]; - float input24 = values[24]; - float input25 = values[25]; - float input26 = values[26]; - float input27 = values[27]; - float input28 = values[28]; - float input29 = values[29]; - float input30 = values[30]; - float input31 = values[31]; - - float v0 = input0 + input16; - float v1 = input0 - input16; - float v2 = input24 + input8; - float v3 = input8 * JxlDctScales.Sqrt2; - float v4 = v3 + v2; - float v5 = v3 - v2; - float v6 = float.MultiplyAddEstimate(0.541196100146197f, v4, v0); - float v7 = float.MultiplyAddEstimate(-0.541196100146197f, v4, v0); - float v8 = float.MultiplyAddEstimate(1.3065629648763764f, v5, v1); - float v9 = float.MultiplyAddEstimate(-1.3065629648763764f, v5, v1); - float v10 = input28 + input20; - float v11 = input20 + input12; - float v12 = input12 + input4; - float v13 = input4 * JxlDctScales.Sqrt2; - float v14 = v13 + v11; - float v15 = v13 - v11; - float v16 = v10 + v12; - float v17 = v12 * JxlDctScales.Sqrt2; - float v18 = v17 + v16; - float v19 = v17 - v16; - float v20 = float.MultiplyAddEstimate(0.541196100146197f, v18, v14); - float v21 = float.MultiplyAddEstimate(-0.541196100146197f, v18, v14); - float v22 = float.MultiplyAddEstimate(1.3065629648763764f, v19, v15); - float v23 = float.MultiplyAddEstimate(-1.3065629648763764f, v19, v15); - float v24 = float.MultiplyAddEstimate(0.5097955791041592f, v20, v6); - float v25 = float.MultiplyAddEstimate(-0.5097955791041592f, v20, v6); - float v26 = float.MultiplyAddEstimate(0.6013448869350453f, v22, v8); - float v27 = float.MultiplyAddEstimate(-0.6013448869350453f, v22, v8); - float v28 = float.MultiplyAddEstimate(0.8999762231364156f, v23, v9); - float v29 = float.MultiplyAddEstimate(-0.8999762231364156f, v23, v9); - float v30 = float.MultiplyAddEstimate(2.5629154477415055f, v21, v7); - float v31 = float.MultiplyAddEstimate(-2.5629154477415055f, v21, v7); - float v32 = input30 + input26; - float v33 = input26 + input22; - float v34 = input22 + input18; - float v35 = input18 + input14; - float v36 = input14 + input10; - float v37 = input10 + input6; - float v38 = input6 + input2; - float v39 = input2 * JxlDctScales.Sqrt2; - float v40 = v39 + v35; - float v41 = v39 - v35; - float v42 = v33 + v37; - float v43 = v37 * JxlDctScales.Sqrt2; - float v44 = v43 + v42; - float v45 = v43 - v42; - float v46 = float.MultiplyAddEstimate(0.541196100146197f, v44, v40); - float v47 = float.MultiplyAddEstimate(-0.541196100146197f, v44, v40); - float v48 = float.MultiplyAddEstimate(1.3065629648763764f, v45, v41); - float v49 = float.MultiplyAddEstimate(-1.3065629648763764f, v45, v41); - float v50 = v32 + v34; - float v51 = v34 + v36; - float v52 = v36 + v38; - float v53 = v38 * JxlDctScales.Sqrt2; - float v54 = v53 + v51; - float v55 = v53 - v51; - float v56 = v50 + v52; - float v57 = v52 * JxlDctScales.Sqrt2; - float v58 = v57 + v56; - float v59 = v57 - v56; - float v60 = float.MultiplyAddEstimate(0.541196100146197f, v58, v54); - float v61 = float.MultiplyAddEstimate(-0.541196100146197f, v58, v54); - float v62 = float.MultiplyAddEstimate(1.3065629648763764f, v59, v55); - float v63 = float.MultiplyAddEstimate(-1.3065629648763764f, v59, v55); - float v64 = float.MultiplyAddEstimate(0.5097955791041592f, v60, v46); - float v65 = float.MultiplyAddEstimate(-0.5097955791041592f, v60, v46); - float v66 = float.MultiplyAddEstimate(0.6013448869350453f, v62, v48); - float v67 = float.MultiplyAddEstimate(-0.6013448869350453f, v62, v48); - float v68 = float.MultiplyAddEstimate(0.8999762231364156f, v63, v49); - float v69 = float.MultiplyAddEstimate(-0.8999762231364156f, v63, v49); - float v70 = float.MultiplyAddEstimate(2.5629154477415055f, v61, v47); - float v71 = float.MultiplyAddEstimate(-2.5629154477415055f, v61, v47); - float v72 = float.MultiplyAddEstimate(0.5024192861881557f, v64, v24); - float v73 = float.MultiplyAddEstimate(-0.5024192861881557f, v64, v24); - float v74 = float.MultiplyAddEstimate(0.5224986149396889f, v66, v26); - float v75 = float.MultiplyAddEstimate(-0.5224986149396889f, v66, v26); - float v76 = float.MultiplyAddEstimate(0.5669440348163577f, v68, v28); - float v77 = float.MultiplyAddEstimate(-0.5669440348163577f, v68, v28); - float v78 = float.MultiplyAddEstimate(0.6468217833599901f, v70, v30); - float v79 = float.MultiplyAddEstimate(-0.6468217833599901f, v70, v30); - float v80 = float.MultiplyAddEstimate(0.7881546234512502f, v71, v31); - float v81 = float.MultiplyAddEstimate(-0.7881546234512502f, v71, v31); - float v82 = float.MultiplyAddEstimate(1.060677685990347f, v69, v29); - float v83 = float.MultiplyAddEstimate(-1.060677685990347f, v69, v29); - float v84 = float.MultiplyAddEstimate(1.7224470982383342f, v67, v27); - float v85 = float.MultiplyAddEstimate(-1.7224470982383342f, v67, v27); - float v86 = float.MultiplyAddEstimate(5.101148618689155f, v65, v25); - float v87 = float.MultiplyAddEstimate(-5.101148618689155f, v65, v25); - float v88 = input31 + input29; - float v89 = input29 + input27; - float v90 = input27 + input25; - float v91 = input25 + input23; - float v92 = input23 + input21; - float v93 = input21 + input19; - float v94 = input19 + input17; - float v95 = input17 + input15; - float v96 = input15 + input13; - float v97 = input13 + input11; - float v98 = input11 + input9; - float v99 = input9 + input7; - float v100 = input7 + input5; - float v101 = input5 + input3; - float v102 = input3 + input1; - float v103 = input1 * JxlDctScales.Sqrt2; - float v104 = v103 + v95; - float v105 = v103 - v95; - float v106 = v91 + v99; - float v107 = v99 * JxlDctScales.Sqrt2; - float v108 = v107 + v106; - float v109 = v107 - v106; - float v110 = float.MultiplyAddEstimate(0.541196100146197f, v108, v104); - float v111 = float.MultiplyAddEstimate(-0.541196100146197f, v108, v104); - float v112 = float.MultiplyAddEstimate(1.3065629648763764f, v109, v105); - float v113 = float.MultiplyAddEstimate(-1.3065629648763764f, v109, v105); - float v114 = v89 + v93; - float v115 = v93 + v97; - float v116 = v97 + v101; - float v117 = v101 * JxlDctScales.Sqrt2; - float v118 = v117 + v115; - float v119 = v117 - v115; - float v120 = v114 + v116; - float v121 = v116 * JxlDctScales.Sqrt2; - float v122 = v121 + v120; - float v123 = v121 - v120; - float v124 = float.MultiplyAddEstimate(0.541196100146197f, v122, v118); - float v125 = float.MultiplyAddEstimate(-0.541196100146197f, v122, v118); - float v126 = float.MultiplyAddEstimate(1.3065629648763764f, v123, v119); - float v127 = float.MultiplyAddEstimate(-1.3065629648763764f, v123, v119); - float v128 = float.MultiplyAddEstimate(0.5097955791041592f, v124, v110); - float v129 = float.MultiplyAddEstimate(-0.5097955791041592f, v124, v110); - float v130 = float.MultiplyAddEstimate(0.6013448869350453f, v126, v112); - float v131 = float.MultiplyAddEstimate(-0.6013448869350453f, v126, v112); - float v132 = float.MultiplyAddEstimate(0.8999762231364156f, v127, v113); - float v133 = float.MultiplyAddEstimate(-0.8999762231364156f, v127, v113); - float v134 = float.MultiplyAddEstimate(2.5629154477415055f, v125, v111); - float v135 = float.MultiplyAddEstimate(-2.5629154477415055f, v125, v111); - float v136 = v88 + v90; - float v137 = v90 + v92; - float v138 = v92 + v94; - float v139 = v94 + v96; - float v140 = v96 + v98; - float v141 = v98 + v100; - float v142 = v100 + v102; - float v143 = v102 * JxlDctScales.Sqrt2; - float v144 = v143 + v139; - float v145 = v143 - v139; - float v146 = v137 + v141; - float v147 = v141 * JxlDctScales.Sqrt2; - float v148 = v147 + v146; - float v149 = v147 - v146; - float v150 = float.MultiplyAddEstimate(0.541196100146197f, v148, v144); - float v151 = float.MultiplyAddEstimate(-0.541196100146197f, v148, v144); - float v152 = float.MultiplyAddEstimate(1.3065629648763764f, v149, v145); - float v153 = float.MultiplyAddEstimate(-1.3065629648763764f, v149, v145); - float v154 = v136 + v138; - float v155 = v138 + v140; - float v156 = v140 + v142; - float v157 = v142 * JxlDctScales.Sqrt2; - float v158 = v157 + v155; - float v159 = v157 - v155; - float v160 = v154 + v156; - float v161 = v156 * JxlDctScales.Sqrt2; - float v162 = v161 + v160; - float v163 = v161 - v160; - float v164 = float.MultiplyAddEstimate(0.541196100146197f, v162, v158); - float v165 = float.MultiplyAddEstimate(-0.541196100146197f, v162, v158); - float v166 = float.MultiplyAddEstimate(1.3065629648763764f, v163, v159); - float v167 = float.MultiplyAddEstimate(-1.3065629648763764f, v163, v159); - float v168 = float.MultiplyAddEstimate(0.5097955791041592f, v164, v150); - float v169 = float.MultiplyAddEstimate(-0.5097955791041592f, v164, v150); - float v170 = float.MultiplyAddEstimate(0.6013448869350453f, v166, v152); - float v171 = float.MultiplyAddEstimate(-0.6013448869350453f, v166, v152); - float v172 = float.MultiplyAddEstimate(0.8999762231364156f, v167, v153); - float v173 = float.MultiplyAddEstimate(-0.8999762231364156f, v167, v153); - float v174 = float.MultiplyAddEstimate(2.5629154477415055f, v165, v151); - float v175 = float.MultiplyAddEstimate(-2.5629154477415055f, v165, v151); - float v176 = float.MultiplyAddEstimate(0.5024192861881557f, v168, v128); - float v177 = float.MultiplyAddEstimate(-0.5024192861881557f, v168, v128); - float v178 = float.MultiplyAddEstimate(0.5224986149396889f, v170, v130); - float v179 = float.MultiplyAddEstimate(-0.5224986149396889f, v170, v130); - float v180 = float.MultiplyAddEstimate(0.5669440348163577f, v172, v132); - float v181 = float.MultiplyAddEstimate(-0.5669440348163577f, v172, v132); - float v182 = float.MultiplyAddEstimate(0.6468217833599901f, v174, v134); - float v183 = float.MultiplyAddEstimate(-0.6468217833599901f, v174, v134); - float v184 = float.MultiplyAddEstimate(0.7881546234512502f, v175, v135); - float v185 = float.MultiplyAddEstimate(-0.7881546234512502f, v175, v135); - float v186 = float.MultiplyAddEstimate(1.060677685990347f, v173, v133); - float v187 = float.MultiplyAddEstimate(-1.060677685990347f, v173, v133); - float v188 = float.MultiplyAddEstimate(1.7224470982383342f, v171, v131); - float v189 = float.MultiplyAddEstimate(-1.7224470982383342f, v171, v131); - float v190 = float.MultiplyAddEstimate(5.101148618689155f, v169, v129); - float v191 = float.MultiplyAddEstimate(-5.101148618689155f, v169, v129); - float v192 = float.MultiplyAddEstimate(0.5006029982351963f, v176, v72); - float v193 = float.MultiplyAddEstimate(-0.5006029982351963f, v176, v72); - float v194 = float.MultiplyAddEstimate(0.5054709598975436f, v178, v74); - float v195 = float.MultiplyAddEstimate(-0.5054709598975436f, v178, v74); - float v196 = float.MultiplyAddEstimate(0.5154473099226246f, v180, v76); - float v197 = float.MultiplyAddEstimate(-0.5154473099226246f, v180, v76); - float v198 = float.MultiplyAddEstimate(0.5310425910897841f, v182, v78); - float v199 = float.MultiplyAddEstimate(-0.5310425910897841f, v182, v78); - float v200 = float.MultiplyAddEstimate(0.5531038960344445f, v184, v80); - float v201 = float.MultiplyAddEstimate(-0.5531038960344445f, v184, v80); - float v202 = float.MultiplyAddEstimate(0.5829349682061339f, v186, v82); - float v203 = float.MultiplyAddEstimate(-0.5829349682061339f, v186, v82); - float v204 = float.MultiplyAddEstimate(0.6225041230356648f, v188, v84); - float v205 = float.MultiplyAddEstimate(-0.6225041230356648f, v188, v84); - float v206 = float.MultiplyAddEstimate(0.6748083414550057f, v190, v86); - float v207 = float.MultiplyAddEstimate(-0.6748083414550057f, v190, v86); - float v208 = float.MultiplyAddEstimate(0.7445362710022986f, v191, v87); - float v209 = float.MultiplyAddEstimate(-0.7445362710022986f, v191, v87); - float v210 = float.MultiplyAddEstimate(0.8393496454155268f, v189, v85); - float v211 = float.MultiplyAddEstimate(-0.8393496454155268f, v189, v85); - float v212 = float.MultiplyAddEstimate(0.9725682378619608f, v187, v83); - float v213 = float.MultiplyAddEstimate(-0.9725682378619608f, v187, v83); - float v214 = float.MultiplyAddEstimate(1.1694399334328847f, v185, v81); - float v215 = float.MultiplyAddEstimate(-1.1694399334328847f, v185, v81); - float v216 = float.MultiplyAddEstimate(1.4841646163141662f, v183, v79); - float v217 = float.MultiplyAddEstimate(-1.4841646163141662f, v183, v79); - float v218 = float.MultiplyAddEstimate(2.057781009953411f, v181, v77); - float v219 = float.MultiplyAddEstimate(-2.057781009953411f, v181, v77); - float v220 = float.MultiplyAddEstimate(3.407608418468719f, v179, v75); - float v221 = float.MultiplyAddEstimate(-3.407608418468719f, v179, v75); - float v222 = float.MultiplyAddEstimate(10.190008123548033f, v177, v73); - float v223 = float.MultiplyAddEstimate(-10.190008123548033f, v177, v73); - - values[0] = v192; - values[1] = v194; - values[2] = v196; - values[3] = v198; - values[4] = v200; - values[5] = v202; - values[6] = v204; - values[7] = v206; - values[8] = v208; - values[9] = v210; - values[10] = v212; - values[11] = v214; - values[12] = v216; - values[13] = v218; - values[14] = v220; - values[15] = v222; - values[16] = v223; - values[17] = v221; - values[18] = v219; - values[19] = v217; - values[20] = v215; - values[21] = v213; - values[22] = v211; - values[23] = v209; - values[24] = v207; - values[25] = v205; - values[26] = v203; - values[27] = v201; - values[28] = v199; - values[29] = v197; - values[30] = v195; - values[31] = v193; - } - - /// - /// Processes complete 1-lane inverse batches along input columns. - /// - /// The number of positions in each transform. - /// The number of independent transforms. - /// The borrowed source matrix. - /// The source row stride, in floats. - /// The borrowed output matrix. - /// The destination row stride, in floats. - /// The first independent transform not already processed by a wider batch. - /// Borrowed storage for two length-by-1 banks. Only the first bank needs initialized inputs. - /// The first independent transform left after the complete batches. - private static int InverseColumnsScalar( - int length, - int count, - ReadOnlySpan source, - int sourceStride, - Span destination, - int destinationStride, - int first, - Span workspace) - { - Span values = workspace[..length]; - Span scratch = workspace.Slice(length, length); - ref float input = ref MemoryMarshal.GetReference(source); - ref float output = ref MemoryMarshal.GetReference(destination); - - for (; first <= count - 1; first += 1) - { - // One vector covers adjacent columns at the same transform position. Physical strides never enter the coefficient-bank layout. - for (int i = 0; i < length; i++) - { - values[i] = Unsafe.Add(ref input, (i * sourceStride) + first); - } - - Inverse(values, scratch); - for (int i = 0; i < length; i++) - { - Unsafe.Add(ref output, (i * destinationStride) + first) = values[i]; - } - } - - return first; - } - - /// - /// Processes complete 1-lane inverse batches along input rows. - /// - /// The number of positions in each transform. - /// The number of independent transforms. - /// The borrowed source matrix. - /// The source row stride, in floats. - /// The borrowed output matrix. - /// The destination row stride, in floats. - /// Whether transform positions become output rows and input rows become output columns. - /// The first independent transform not already processed by a wider batch. - /// Borrowed storage for two length-by-1 banks. Only the first bank needs initialized inputs. - /// The first independent transform left after the complete batches. - private static int InverseRowsScalar( - int length, - int count, - ReadOnlySpan source, - int sourceStride, - Span destination, - int destinationStride, - bool transposed, - int first, - Span workspace) - { - Span values = workspace[..length]; - Span scratch = workspace.Slice(length, length); - ref float input = ref MemoryMarshal.GetReference(source); - ref float output = ref MemoryMarshal.GetReference(destination); - - for (; first <= count - 1; first += 1) - { - source.Slice(first * sourceStride, length).CopyTo(values); - - Inverse(values, scratch); - if (transposed) - { - // A complete vector is one contiguous output row. Only logical samples are stored; destination padding remains untouched. - for (int i = 0; i < length; i++) - { - Unsafe.Add(ref output, (i * destinationStride) + first) = values[i]; - } - } - else - { - for (int i = 0; i < length; i++) - { - Unsafe.Add(ref output, (first * destinationStride) + i) = values[i]; - } - } - } - - return first; - } - - /// - /// Computes independent forward transforms using two nonoverlapping borrowed vector banks. - /// - /// One vector per transform position. Its length is a power of two from one through 256. - /// At least values.Length vectors. Previous contents are not read. - private static void Forward(Span> values, Span> scratch) - { - // Small transforms stay in registers. The one-point transform is an identity and must not copy any trailing samples. - switch (values.Length) - { - case 1: - return; - case 2: - Forward2(values); - return; - case 4: - Forward4(values); - return; - case 8: - Forward8(values); - return; - case 16: - Forward16(values); - return; - case 32: - Forward32(values); - return; - } - - int half = values.Length / 2; - ReadOnlySpan weights = JxlDctScales.GetMultipliers(values.Length); - - // Consume both original halves before reusing their bank as recursive scratch. Neither recursive call can then destroy unread input. - for (int i = 0; i < half; i++) - { - Vector128 first = values[i]; - Vector128 last = values[values.Length - i - 1]; - scratch[i] = first + last; - scratch[half + i] = (first - last) * Vector128.Create(weights[i]); - } - - Forward(scratch[..half], values); - Forward(scratch.Slice(half, half), values); - scratch[half] = Vector128.MultiplyAddEstimate(scratch[half], Vector128.Create(JxlDctScales.Sqrt2), scratch[half + 1]); - - for (int i = 1; i + 1 < half; i++) - { - scratch[half + i] += scratch[half + i + 1]; - } - - for (int i = 0; i < half; i++) - { - values[2 * i] = scratch[i]; - values[(2 * i) + 1] = scratch[half + i]; - } - } - - /// - /// Computes independent 2-point forward transforms without intermediate memory passes. - /// - /// Exactly 2 position vectors, replaced with the transform results. Each lane remains independent. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Forward2(Span> values) - { - // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. - // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. - Vector128 input0 = values[0]; - Vector128 input1 = values[1]; - - Vector128 v0 = input0 + input1; - Vector128 v1 = input0 - input1; - - values[0] = v0; - values[1] = v1; - } - - /// - /// Computes independent 4-point forward transforms without intermediate memory passes. - /// - /// Exactly 4 position vectors, replaced with the transform results. Each lane remains independent. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Forward4(Span> values) - { - // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. - // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. - Vector128 input0 = values[0]; - Vector128 input1 = values[1]; - Vector128 input2 = values[2]; - Vector128 input3 = values[3]; - - Vector128 v0 = input0 + input3; - Vector128 v1 = (input0 - input3) * Vector128.Create(0.541196100146197f); - Vector128 v2 = input1 + input2; - Vector128 v3 = (input1 - input2) * Vector128.Create(1.3065629648763764f); - Vector128 v4 = v0 + v2; - Vector128 v5 = v0 - v2; - Vector128 v6 = v1 + v3; - Vector128 v7 = v1 - v3; - Vector128 v8 = Vector128.MultiplyAddEstimate(v6, Vector128.Create(JxlDctScales.Sqrt2), v7); - - values[0] = v4; - values[1] = v8; - values[2] = v5; - values[3] = v7; - } - - /// - /// Computes independent 8-point forward transforms without intermediate memory passes. - /// - /// Exactly 8 position vectors, replaced with the transform results. Each lane remains independent. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Forward8(Span> values) - { - // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. - // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. - Vector128 input0 = values[0]; - Vector128 input1 = values[1]; - Vector128 input2 = values[2]; - Vector128 input3 = values[3]; - Vector128 input4 = values[4]; - Vector128 input5 = values[5]; - Vector128 input6 = values[6]; - Vector128 input7 = values[7]; - - Vector128 v0 = input0 + input7; - Vector128 v1 = (input0 - input7) * Vector128.Create(0.5097955791041592f); - Vector128 v2 = input1 + input6; - Vector128 v3 = (input1 - input6) * Vector128.Create(0.6013448869350453f); - Vector128 v4 = input2 + input5; - Vector128 v5 = (input2 - input5) * Vector128.Create(0.8999762231364156f); - Vector128 v6 = input3 + input4; - Vector128 v7 = (input3 - input4) * Vector128.Create(2.5629154477415055f); - Vector128 v8 = v0 + v6; - Vector128 v9 = (v0 - v6) * Vector128.Create(0.541196100146197f); - Vector128 v10 = v2 + v4; - Vector128 v11 = (v2 - v4) * Vector128.Create(1.3065629648763764f); - Vector128 v12 = v8 + v10; - Vector128 v13 = v8 - v10; - Vector128 v14 = v9 + v11; - Vector128 v15 = v9 - v11; - Vector128 v16 = Vector128.MultiplyAddEstimate(v14, Vector128.Create(JxlDctScales.Sqrt2), v15); - Vector128 v17 = v1 + v7; - Vector128 v18 = (v1 - v7) * Vector128.Create(0.541196100146197f); - Vector128 v19 = v3 + v5; - Vector128 v20 = (v3 - v5) * Vector128.Create(1.3065629648763764f); - Vector128 v21 = v17 + v19; - Vector128 v22 = v17 - v19; - Vector128 v23 = v18 + v20; - Vector128 v24 = v18 - v20; - Vector128 v25 = Vector128.MultiplyAddEstimate(v23, Vector128.Create(JxlDctScales.Sqrt2), v24); - Vector128 v26 = Vector128.MultiplyAddEstimate(v21, Vector128.Create(JxlDctScales.Sqrt2), v25); - Vector128 v27 = v25 + v22; - Vector128 v28 = v22 + v24; - - values[0] = v12; - values[1] = v26; - values[2] = v16; - values[3] = v27; - values[4] = v13; - values[5] = v28; - values[6] = v15; - values[7] = v24; - } - - /// - /// Computes independent 16-point forward transforms without intermediate memory passes. - /// - /// Exactly 16 position vectors, replaced with the transform results. Each lane remains independent. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Forward16(Span> values) - { - // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. - // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. - Vector128 input0 = values[0]; - Vector128 input1 = values[1]; - Vector128 input2 = values[2]; - Vector128 input3 = values[3]; - Vector128 input4 = values[4]; - Vector128 input5 = values[5]; - Vector128 input6 = values[6]; - Vector128 input7 = values[7]; - Vector128 input8 = values[8]; - Vector128 input9 = values[9]; - Vector128 input10 = values[10]; - Vector128 input11 = values[11]; - Vector128 input12 = values[12]; - Vector128 input13 = values[13]; - Vector128 input14 = values[14]; - Vector128 input15 = values[15]; - - Vector128 v0 = input0 + input15; - Vector128 v1 = (input0 - input15) * Vector128.Create(0.5024192861881557f); - Vector128 v2 = input1 + input14; - Vector128 v3 = (input1 - input14) * Vector128.Create(0.5224986149396889f); - Vector128 v4 = input2 + input13; - Vector128 v5 = (input2 - input13) * Vector128.Create(0.5669440348163577f); - Vector128 v6 = input3 + input12; - Vector128 v7 = (input3 - input12) * Vector128.Create(0.6468217833599901f); - Vector128 v8 = input4 + input11; - Vector128 v9 = (input4 - input11) * Vector128.Create(0.7881546234512502f); - Vector128 v10 = input5 + input10; - Vector128 v11 = (input5 - input10) * Vector128.Create(1.060677685990347f); - Vector128 v12 = input6 + input9; - Vector128 v13 = (input6 - input9) * Vector128.Create(1.7224470982383342f); - Vector128 v14 = input7 + input8; - Vector128 v15 = (input7 - input8) * Vector128.Create(5.101148618689155f); - Vector128 v16 = v0 + v14; - Vector128 v17 = (v0 - v14) * Vector128.Create(0.5097955791041592f); - Vector128 v18 = v2 + v12; - Vector128 v19 = (v2 - v12) * Vector128.Create(0.6013448869350453f); - Vector128 v20 = v4 + v10; - Vector128 v21 = (v4 - v10) * Vector128.Create(0.8999762231364156f); - Vector128 v22 = v6 + v8; - Vector128 v23 = (v6 - v8) * Vector128.Create(2.5629154477415055f); - Vector128 v24 = v16 + v22; - Vector128 v25 = (v16 - v22) * Vector128.Create(0.541196100146197f); - Vector128 v26 = v18 + v20; - Vector128 v27 = (v18 - v20) * Vector128.Create(1.3065629648763764f); - Vector128 v28 = v24 + v26; - Vector128 v29 = v24 - v26; - Vector128 v30 = v25 + v27; - Vector128 v31 = v25 - v27; - Vector128 v32 = Vector128.MultiplyAddEstimate(v30, Vector128.Create(JxlDctScales.Sqrt2), v31); - Vector128 v33 = v17 + v23; - Vector128 v34 = (v17 - v23) * Vector128.Create(0.541196100146197f); - Vector128 v35 = v19 + v21; - Vector128 v36 = (v19 - v21) * Vector128.Create(1.3065629648763764f); - Vector128 v37 = v33 + v35; - Vector128 v38 = v33 - v35; - Vector128 v39 = v34 + v36; - Vector128 v40 = v34 - v36; - Vector128 v41 = Vector128.MultiplyAddEstimate(v39, Vector128.Create(JxlDctScales.Sqrt2), v40); - Vector128 v42 = Vector128.MultiplyAddEstimate(v37, Vector128.Create(JxlDctScales.Sqrt2), v41); - Vector128 v43 = v41 + v38; - Vector128 v44 = v38 + v40; - Vector128 v45 = v1 + v15; - Vector128 v46 = (v1 - v15) * Vector128.Create(0.5097955791041592f); - Vector128 v47 = v3 + v13; - Vector128 v48 = (v3 - v13) * Vector128.Create(0.6013448869350453f); - Vector128 v49 = v5 + v11; - Vector128 v50 = (v5 - v11) * Vector128.Create(0.8999762231364156f); - Vector128 v51 = v7 + v9; - Vector128 v52 = (v7 - v9) * Vector128.Create(2.5629154477415055f); - Vector128 v53 = v45 + v51; - Vector128 v54 = (v45 - v51) * Vector128.Create(0.541196100146197f); - Vector128 v55 = v47 + v49; - Vector128 v56 = (v47 - v49) * Vector128.Create(1.3065629648763764f); - Vector128 v57 = v53 + v55; - Vector128 v58 = v53 - v55; - Vector128 v59 = v54 + v56; - Vector128 v60 = v54 - v56; - Vector128 v61 = Vector128.MultiplyAddEstimate(v59, Vector128.Create(JxlDctScales.Sqrt2), v60); - Vector128 v62 = v46 + v52; - Vector128 v63 = (v46 - v52) * Vector128.Create(0.541196100146197f); - Vector128 v64 = v48 + v50; - Vector128 v65 = (v48 - v50) * Vector128.Create(1.3065629648763764f); - Vector128 v66 = v62 + v64; - Vector128 v67 = v62 - v64; - Vector128 v68 = v63 + v65; - Vector128 v69 = v63 - v65; - Vector128 v70 = Vector128.MultiplyAddEstimate(v68, Vector128.Create(JxlDctScales.Sqrt2), v69); - Vector128 v71 = Vector128.MultiplyAddEstimate(v66, Vector128.Create(JxlDctScales.Sqrt2), v70); - Vector128 v72 = v70 + v67; - Vector128 v73 = v67 + v69; - Vector128 v74 = Vector128.MultiplyAddEstimate(v57, Vector128.Create(JxlDctScales.Sqrt2), v71); - Vector128 v75 = v71 + v61; - Vector128 v76 = v61 + v72; - Vector128 v77 = v72 + v58; - Vector128 v78 = v58 + v73; - Vector128 v79 = v73 + v60; - Vector128 v80 = v60 + v69; - - values[0] = v28; - values[1] = v74; - values[2] = v42; - values[3] = v75; - values[4] = v32; - values[5] = v76; - values[6] = v43; - values[7] = v77; - values[8] = v29; - values[9] = v78; - values[10] = v44; - values[11] = v79; - values[12] = v31; - values[13] = v80; - values[14] = v40; - values[15] = v69; - } - - /// - /// Computes independent 32-point forward transforms without intermediate memory passes. - /// - /// Exactly 32 position vectors, replaced with the transform results. Each lane remains independent. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Forward32(Span> values) - { - // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. - // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. - Vector128 input0 = values[0]; - Vector128 input1 = values[1]; - Vector128 input2 = values[2]; - Vector128 input3 = values[3]; - Vector128 input4 = values[4]; - Vector128 input5 = values[5]; - Vector128 input6 = values[6]; - Vector128 input7 = values[7]; - Vector128 input8 = values[8]; - Vector128 input9 = values[9]; - Vector128 input10 = values[10]; - Vector128 input11 = values[11]; - Vector128 input12 = values[12]; - Vector128 input13 = values[13]; - Vector128 input14 = values[14]; - Vector128 input15 = values[15]; - Vector128 input16 = values[16]; - Vector128 input17 = values[17]; - Vector128 input18 = values[18]; - Vector128 input19 = values[19]; - Vector128 input20 = values[20]; - Vector128 input21 = values[21]; - Vector128 input22 = values[22]; - Vector128 input23 = values[23]; - Vector128 input24 = values[24]; - Vector128 input25 = values[25]; - Vector128 input26 = values[26]; - Vector128 input27 = values[27]; - Vector128 input28 = values[28]; - Vector128 input29 = values[29]; - Vector128 input30 = values[30]; - Vector128 input31 = values[31]; - - Vector128 v0 = input0 + input31; - Vector128 v1 = (input0 - input31) * Vector128.Create(0.5006029982351963f); - Vector128 v2 = input1 + input30; - Vector128 v3 = (input1 - input30) * Vector128.Create(0.5054709598975436f); - Vector128 v4 = input2 + input29; - Vector128 v5 = (input2 - input29) * Vector128.Create(0.5154473099226246f); - Vector128 v6 = input3 + input28; - Vector128 v7 = (input3 - input28) * Vector128.Create(0.5310425910897841f); - Vector128 v8 = input4 + input27; - Vector128 v9 = (input4 - input27) * Vector128.Create(0.5531038960344445f); - Vector128 v10 = input5 + input26; - Vector128 v11 = (input5 - input26) * Vector128.Create(0.5829349682061339f); - Vector128 v12 = input6 + input25; - Vector128 v13 = (input6 - input25) * Vector128.Create(0.6225041230356648f); - Vector128 v14 = input7 + input24; - Vector128 v15 = (input7 - input24) * Vector128.Create(0.6748083414550057f); - Vector128 v16 = input8 + input23; - Vector128 v17 = (input8 - input23) * Vector128.Create(0.7445362710022986f); - Vector128 v18 = input9 + input22; - Vector128 v19 = (input9 - input22) * Vector128.Create(0.8393496454155268f); - Vector128 v20 = input10 + input21; - Vector128 v21 = (input10 - input21) * Vector128.Create(0.9725682378619608f); - Vector128 v22 = input11 + input20; - Vector128 v23 = (input11 - input20) * Vector128.Create(1.1694399334328847f); - Vector128 v24 = input12 + input19; - Vector128 v25 = (input12 - input19) * Vector128.Create(1.4841646163141662f); - Vector128 v26 = input13 + input18; - Vector128 v27 = (input13 - input18) * Vector128.Create(2.057781009953411f); - Vector128 v28 = input14 + input17; - Vector128 v29 = (input14 - input17) * Vector128.Create(3.407608418468719f); - Vector128 v30 = input15 + input16; - Vector128 v31 = (input15 - input16) * Vector128.Create(10.190008123548033f); - Vector128 v32 = v0 + v30; - Vector128 v33 = (v0 - v30) * Vector128.Create(0.5024192861881557f); - Vector128 v34 = v2 + v28; - Vector128 v35 = (v2 - v28) * Vector128.Create(0.5224986149396889f); - Vector128 v36 = v4 + v26; - Vector128 v37 = (v4 - v26) * Vector128.Create(0.5669440348163577f); - Vector128 v38 = v6 + v24; - Vector128 v39 = (v6 - v24) * Vector128.Create(0.6468217833599901f); - Vector128 v40 = v8 + v22; - Vector128 v41 = (v8 - v22) * Vector128.Create(0.7881546234512502f); - Vector128 v42 = v10 + v20; - Vector128 v43 = (v10 - v20) * Vector128.Create(1.060677685990347f); - Vector128 v44 = v12 + v18; - Vector128 v45 = (v12 - v18) * Vector128.Create(1.7224470982383342f); - Vector128 v46 = v14 + v16; - Vector128 v47 = (v14 - v16) * Vector128.Create(5.101148618689155f); - Vector128 v48 = v32 + v46; - Vector128 v49 = (v32 - v46) * Vector128.Create(0.5097955791041592f); - Vector128 v50 = v34 + v44; - Vector128 v51 = (v34 - v44) * Vector128.Create(0.6013448869350453f); - Vector128 v52 = v36 + v42; - Vector128 v53 = (v36 - v42) * Vector128.Create(0.8999762231364156f); - Vector128 v54 = v38 + v40; - Vector128 v55 = (v38 - v40) * Vector128.Create(2.5629154477415055f); - Vector128 v56 = v48 + v54; - Vector128 v57 = (v48 - v54) * Vector128.Create(0.541196100146197f); - Vector128 v58 = v50 + v52; - Vector128 v59 = (v50 - v52) * Vector128.Create(1.3065629648763764f); - Vector128 v60 = v56 + v58; - Vector128 v61 = v56 - v58; - Vector128 v62 = v57 + v59; - Vector128 v63 = v57 - v59; - Vector128 v64 = Vector128.MultiplyAddEstimate(v62, Vector128.Create(JxlDctScales.Sqrt2), v63); - Vector128 v65 = v49 + v55; - Vector128 v66 = (v49 - v55) * Vector128.Create(0.541196100146197f); - Vector128 v67 = v51 + v53; - Vector128 v68 = (v51 - v53) * Vector128.Create(1.3065629648763764f); - Vector128 v69 = v65 + v67; - Vector128 v70 = v65 - v67; - Vector128 v71 = v66 + v68; - Vector128 v72 = v66 - v68; - Vector128 v73 = Vector128.MultiplyAddEstimate(v71, Vector128.Create(JxlDctScales.Sqrt2), v72); - Vector128 v74 = Vector128.MultiplyAddEstimate(v69, Vector128.Create(JxlDctScales.Sqrt2), v73); - Vector128 v75 = v73 + v70; - Vector128 v76 = v70 + v72; - Vector128 v77 = v33 + v47; - Vector128 v78 = (v33 - v47) * Vector128.Create(0.5097955791041592f); - Vector128 v79 = v35 + v45; - Vector128 v80 = (v35 - v45) * Vector128.Create(0.6013448869350453f); - Vector128 v81 = v37 + v43; - Vector128 v82 = (v37 - v43) * Vector128.Create(0.8999762231364156f); - Vector128 v83 = v39 + v41; - Vector128 v84 = (v39 - v41) * Vector128.Create(2.5629154477415055f); - Vector128 v85 = v77 + v83; - Vector128 v86 = (v77 - v83) * Vector128.Create(0.541196100146197f); - Vector128 v87 = v79 + v81; - Vector128 v88 = (v79 - v81) * Vector128.Create(1.3065629648763764f); - Vector128 v89 = v85 + v87; - Vector128 v90 = v85 - v87; - Vector128 v91 = v86 + v88; - Vector128 v92 = v86 - v88; - Vector128 v93 = Vector128.MultiplyAddEstimate(v91, Vector128.Create(JxlDctScales.Sqrt2), v92); - Vector128 v94 = v78 + v84; - Vector128 v95 = (v78 - v84) * Vector128.Create(0.541196100146197f); - Vector128 v96 = v80 + v82; - Vector128 v97 = (v80 - v82) * Vector128.Create(1.3065629648763764f); - Vector128 v98 = v94 + v96; - Vector128 v99 = v94 - v96; - Vector128 v100 = v95 + v97; - Vector128 v101 = v95 - v97; - Vector128 v102 = Vector128.MultiplyAddEstimate(v100, Vector128.Create(JxlDctScales.Sqrt2), v101); - Vector128 v103 = Vector128.MultiplyAddEstimate(v98, Vector128.Create(JxlDctScales.Sqrt2), v102); - Vector128 v104 = v102 + v99; - Vector128 v105 = v99 + v101; - Vector128 v106 = Vector128.MultiplyAddEstimate(v89, Vector128.Create(JxlDctScales.Sqrt2), v103); - Vector128 v107 = v103 + v93; - Vector128 v108 = v93 + v104; - Vector128 v109 = v104 + v90; - Vector128 v110 = v90 + v105; - Vector128 v111 = v105 + v92; - Vector128 v112 = v92 + v101; - Vector128 v113 = v1 + v31; - Vector128 v114 = (v1 - v31) * Vector128.Create(0.5024192861881557f); - Vector128 v115 = v3 + v29; - Vector128 v116 = (v3 - v29) * Vector128.Create(0.5224986149396889f); - Vector128 v117 = v5 + v27; - Vector128 v118 = (v5 - v27) * Vector128.Create(0.5669440348163577f); - Vector128 v119 = v7 + v25; - Vector128 v120 = (v7 - v25) * Vector128.Create(0.6468217833599901f); - Vector128 v121 = v9 + v23; - Vector128 v122 = (v9 - v23) * Vector128.Create(0.7881546234512502f); - Vector128 v123 = v11 + v21; - Vector128 v124 = (v11 - v21) * Vector128.Create(1.060677685990347f); - Vector128 v125 = v13 + v19; - Vector128 v126 = (v13 - v19) * Vector128.Create(1.7224470982383342f); - Vector128 v127 = v15 + v17; - Vector128 v128 = (v15 - v17) * Vector128.Create(5.101148618689155f); - Vector128 v129 = v113 + v127; - Vector128 v130 = (v113 - v127) * Vector128.Create(0.5097955791041592f); - Vector128 v131 = v115 + v125; - Vector128 v132 = (v115 - v125) * Vector128.Create(0.6013448869350453f); - Vector128 v133 = v117 + v123; - Vector128 v134 = (v117 - v123) * Vector128.Create(0.8999762231364156f); - Vector128 v135 = v119 + v121; - Vector128 v136 = (v119 - v121) * Vector128.Create(2.5629154477415055f); - Vector128 v137 = v129 + v135; - Vector128 v138 = (v129 - v135) * Vector128.Create(0.541196100146197f); - Vector128 v139 = v131 + v133; - Vector128 v140 = (v131 - v133) * Vector128.Create(1.3065629648763764f); - Vector128 v141 = v137 + v139; - Vector128 v142 = v137 - v139; - Vector128 v143 = v138 + v140; - Vector128 v144 = v138 - v140; - Vector128 v145 = Vector128.MultiplyAddEstimate(v143, Vector128.Create(JxlDctScales.Sqrt2), v144); - Vector128 v146 = v130 + v136; - Vector128 v147 = (v130 - v136) * Vector128.Create(0.541196100146197f); - Vector128 v148 = v132 + v134; - Vector128 v149 = (v132 - v134) * Vector128.Create(1.3065629648763764f); - Vector128 v150 = v146 + v148; - Vector128 v151 = v146 - v148; - Vector128 v152 = v147 + v149; - Vector128 v153 = v147 - v149; - Vector128 v154 = Vector128.MultiplyAddEstimate(v152, Vector128.Create(JxlDctScales.Sqrt2), v153); - Vector128 v155 = Vector128.MultiplyAddEstimate(v150, Vector128.Create(JxlDctScales.Sqrt2), v154); - Vector128 v156 = v154 + v151; - Vector128 v157 = v151 + v153; - Vector128 v158 = v114 + v128; - Vector128 v159 = (v114 - v128) * Vector128.Create(0.5097955791041592f); - Vector128 v160 = v116 + v126; - Vector128 v161 = (v116 - v126) * Vector128.Create(0.6013448869350453f); - Vector128 v162 = v118 + v124; - Vector128 v163 = (v118 - v124) * Vector128.Create(0.8999762231364156f); - Vector128 v164 = v120 + v122; - Vector128 v165 = (v120 - v122) * Vector128.Create(2.5629154477415055f); - Vector128 v166 = v158 + v164; - Vector128 v167 = (v158 - v164) * Vector128.Create(0.541196100146197f); - Vector128 v168 = v160 + v162; - Vector128 v169 = (v160 - v162) * Vector128.Create(1.3065629648763764f); - Vector128 v170 = v166 + v168; - Vector128 v171 = v166 - v168; - Vector128 v172 = v167 + v169; - Vector128 v173 = v167 - v169; - Vector128 v174 = Vector128.MultiplyAddEstimate(v172, Vector128.Create(JxlDctScales.Sqrt2), v173); - Vector128 v175 = v159 + v165; - Vector128 v176 = (v159 - v165) * Vector128.Create(0.541196100146197f); - Vector128 v177 = v161 + v163; - Vector128 v178 = (v161 - v163) * Vector128.Create(1.3065629648763764f); - Vector128 v179 = v175 + v177; - Vector128 v180 = v175 - v177; - Vector128 v181 = v176 + v178; - Vector128 v182 = v176 - v178; - Vector128 v183 = Vector128.MultiplyAddEstimate(v181, Vector128.Create(JxlDctScales.Sqrt2), v182); - Vector128 v184 = Vector128.MultiplyAddEstimate(v179, Vector128.Create(JxlDctScales.Sqrt2), v183); - Vector128 v185 = v183 + v180; - Vector128 v186 = v180 + v182; - Vector128 v187 = Vector128.MultiplyAddEstimate(v170, Vector128.Create(JxlDctScales.Sqrt2), v184); - Vector128 v188 = v184 + v174; - Vector128 v189 = v174 + v185; - Vector128 v190 = v185 + v171; - Vector128 v191 = v171 + v186; - Vector128 v192 = v186 + v173; - Vector128 v193 = v173 + v182; - Vector128 v194 = Vector128.MultiplyAddEstimate(v141, Vector128.Create(JxlDctScales.Sqrt2), v187); - Vector128 v195 = v187 + v155; - Vector128 v196 = v155 + v188; - Vector128 v197 = v188 + v145; - Vector128 v198 = v145 + v189; - Vector128 v199 = v189 + v156; - Vector128 v200 = v156 + v190; - Vector128 v201 = v190 + v142; - Vector128 v202 = v142 + v191; - Vector128 v203 = v191 + v157; - Vector128 v204 = v157 + v192; - Vector128 v205 = v192 + v144; - Vector128 v206 = v144 + v193; - Vector128 v207 = v193 + v153; - Vector128 v208 = v153 + v182; - - values[0] = v60; - values[1] = v194; - values[2] = v106; - values[3] = v195; - values[4] = v74; - values[5] = v196; - values[6] = v107; - values[7] = v197; - values[8] = v64; - values[9] = v198; - values[10] = v108; - values[11] = v199; - values[12] = v75; - values[13] = v200; - values[14] = v109; - values[15] = v201; - values[16] = v61; - values[17] = v202; - values[18] = v110; - values[19] = v203; - values[20] = v76; - values[21] = v204; - values[22] = v111; - values[23] = v205; - values[24] = v63; - values[25] = v206; - values[26] = v112; - values[27] = v207; - values[28] = v72; - values[29] = v208; - values[30] = v101; - values[31] = v182; - } - - /// - /// Processes complete 4-lane forward batches along input columns. - /// - /// The number of positions in each transform. - /// The number of independent transforms. - /// The borrowed source matrix. - /// The source row stride, in floats. - /// The borrowed output matrix. - /// The destination row stride, in floats. - /// The first independent transform not already processed by a wider batch. - /// Borrowed storage for two length-by-4 banks. Only the first bank needs initialized inputs. - /// The first independent transform left after the complete batches. - private static int ForwardColumns128( - int length, - int count, - ReadOnlySpan source, - int sourceStride, - Span destination, - int destinationStride, - int first, - Span workspace) - { - Span> values = MemoryMarshal.Cast>(workspace[..(length * 4)]); - Span> scratch = MemoryMarshal.Cast>(workspace.Slice(length * 4, length * 4)); - ref float input = ref MemoryMarshal.GetReference(source); - ref float output = ref MemoryMarshal.GetReference(destination); - Vector128 scale = Vector128.Create(1F / length); - - for (; first <= count - 4; first += 4) - { - // One vector covers adjacent columns at the same transform position. Physical strides never enter the coefficient-bank layout. - for (int i = 0; i < length; i++) - { - values[i] = Vector128.LoadUnsafe(ref input, (nuint)((i * sourceStride) + first)); - } - - Forward(values, scratch); - for (int i = 0; i < length; i++) - { - (values[i] * scale).StoreUnsafe(ref output, (nuint)((i * destinationStride) + first)); - } - } - - return first; - } - - /// - /// Processes complete 4-lane forward batches along input rows. - /// - /// The number of positions in each transform. - /// The number of independent transforms. - /// The borrowed source matrix. - /// The source row stride, in floats. - /// The borrowed output matrix. - /// The destination row stride, in floats. - /// Whether transform positions become output rows and input rows become output columns. - /// The first independent transform not already processed by a wider batch. - /// Borrowed storage for two length-by-4 banks. Only the first bank needs initialized inputs. - /// The first independent transform left after the complete batches. - private static int ForwardRows128( - int length, - int count, - ReadOnlySpan source, - int sourceStride, - Span destination, - int destinationStride, - bool transposed, - int first, - Span workspace) - { - Span> values = MemoryMarshal.Cast>(workspace[..(length * 4)]); - Span> scratch = MemoryMarshal.Cast>(workspace.Slice(length * 4, length * 4)); - ref float input = ref MemoryMarshal.GetReference(source); - ref float output = ref MemoryMarshal.GetReference(destination); - Vector128 scale = Vector128.Create(1F / length); - - for (; first <= count - 4; first += 4) - { - // Each square load contains adjacent samples from independent rows. Transposition puts those rows into SIMD lanes. - // The complete row band is loaded before any same-orientation output is written, so source and destination can be the same matrix. - for (int column = 0; column < length; column += 4) - { - Vector128 row0 = Vector128.LoadUnsafe(ref input, (nuint)(((first + 0) * sourceStride) + column)).AsInt32(); - Vector128 row1 = Vector128.LoadUnsafe(ref input, (nuint)(((first + 1) * sourceStride) + column)).AsInt32(); - Vector128 row2 = Vector128.LoadUnsafe(ref input, (nuint)(((first + 2) * sourceStride) + column)).AsInt32(); - Vector128 row3 = Vector128.LoadUnsafe(ref input, (nuint)(((first + 3) * sourceStride) + column)).AsInt32(); - - SimdUtils.Transpose(ref row0, ref row1, ref row2, ref row3); - values[column + 0] = row0.AsSingle(); - values[column + 1] = row1.AsSingle(); - values[column + 2] = row2.AsSingle(); - values[column + 3] = row3.AsSingle(); - } - - Forward(values, scratch); - if (transposed) - { - // A complete vector is one contiguous output row. Only logical samples are stored; destination padding remains untouched. - for (int i = 0; i < length; i++) - { - (values[i] * scale).StoreUnsafe(ref output, (nuint)((i * destinationStride) + first)); - } - } - else - { - // Restore row-major order one register tile at a time. This replaces a separate full-block transpose and copy. - for (int column = 0; column < length; column += 4) - { - Vector128 row0 = (values[column + 0] * scale).AsInt32(); - Vector128 row1 = (values[column + 1] * scale).AsInt32(); - Vector128 row2 = (values[column + 2] * scale).AsInt32(); - Vector128 row3 = (values[column + 3] * scale).AsInt32(); - - SimdUtils.Transpose(ref row0, ref row1, ref row2, ref row3); - row0.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 0) * destinationStride) + column)); - row1.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 1) * destinationStride) + column)); - row2.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 2) * destinationStride) + column)); - row3.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 3) * destinationStride) + column)); - } - } - } - - return first; - } - - /// - /// Computes independent inverse transforms using two nonoverlapping borrowed vector banks. - /// - /// One vector per transform position. Its length is a power of two from one through 256. - /// At least values.Length vectors. Previous contents are not read. - private static void Inverse(Span> values, Span> scratch) - { - // Small transforms stay in registers. The one-point transform is an identity and must not copy any trailing samples. - switch (values.Length) - { - case 1: - return; - case 2: - Inverse2(values); - return; - case 4: - Inverse4(values); - return; - case 8: - Inverse8(values); - return; - case 16: - Inverse16(values); - return; - case 32: - Inverse32(values); - return; - } - - int half = values.Length / 2; - // Separate frequency parity before either recursive call. The original bank can then serve as recursive scratch. - for (int i = 0; i < half; i++) - { - scratch[i] = values[2 * i]; - scratch[half + i] = values[(2 * i) + 1]; - } - - Inverse(scratch[..half], values); - - // Descend so every odd-frequency predecessor is read before its own entry changes. - for (int i = half - 1; i > 0; i--) - { - scratch[half + i] += scratch[half + i - 1]; - } - - scratch[half] *= Vector128.Create(JxlDctScales.Sqrt2); - Inverse(scratch.Slice(half, half), values); - ReadOnlySpan weights = JxlDctScales.GetMultipliers(values.Length); - - for (int i = 0; i < half; i++) - { - Vector128 weight = Vector128.Create(weights[i]); - Vector128 even = scratch[i]; - Vector128 odd = scratch[half + i]; - values[i] = Vector128.MultiplyAddEstimate(weight, odd, even); - values[values.Length - i - 1] = Vector128.MultiplyAddEstimate(-weight, odd, even); - } - } - - /// - /// Computes independent 2-point inverse transforms without intermediate memory passes. - /// - /// Exactly 2 position vectors, replaced with the transform results. Each lane remains independent. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Inverse2(Span> values) - { - // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. - // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. - Vector128 input0 = values[0]; - Vector128 input1 = values[1]; - - Vector128 v0 = input0 + input1; - Vector128 v1 = input0 - input1; - - values[0] = v0; - values[1] = v1; - } - - /// - /// Computes independent 4-point inverse transforms without intermediate memory passes. - /// - /// Exactly 4 position vectors, replaced with the transform results. Each lane remains independent. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Inverse4(Span> values) - { - // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. - // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. - Vector128 input0 = values[0]; - Vector128 input1 = values[1]; - Vector128 input2 = values[2]; - Vector128 input3 = values[3]; - - Vector128 v0 = input0 + input2; - Vector128 v1 = input0 - input2; - Vector128 v2 = input3 + input1; - Vector128 v3 = input1 * Vector128.Create(JxlDctScales.Sqrt2); - Vector128 v4 = v3 + v2; - Vector128 v5 = v3 - v2; - Vector128 v6 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v4, v0); - Vector128 v7 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v4, v0); - Vector128 v8 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v5, v1); - Vector128 v9 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v5, v1); - - values[0] = v6; - values[1] = v8; - values[2] = v9; - values[3] = v7; - } - - /// - /// Computes independent 8-point inverse transforms without intermediate memory passes. - /// - /// Exactly 8 position vectors, replaced with the transform results. Each lane remains independent. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Inverse8(Span> values) - { - // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. - // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. - Vector128 input0 = values[0]; - Vector128 input1 = values[1]; - Vector128 input2 = values[2]; - Vector128 input3 = values[3]; - Vector128 input4 = values[4]; - Vector128 input5 = values[5]; - Vector128 input6 = values[6]; - Vector128 input7 = values[7]; - - Vector128 v0 = input0 + input4; - Vector128 v1 = input0 - input4; - Vector128 v2 = input6 + input2; - Vector128 v3 = input2 * Vector128.Create(JxlDctScales.Sqrt2); - Vector128 v4 = v3 + v2; - Vector128 v5 = v3 - v2; - Vector128 v6 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v4, v0); - Vector128 v7 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v4, v0); - Vector128 v8 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v5, v1); - Vector128 v9 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v5, v1); - Vector128 v10 = input7 + input5; - Vector128 v11 = input5 + input3; - Vector128 v12 = input3 + input1; - Vector128 v13 = input1 * Vector128.Create(JxlDctScales.Sqrt2); - Vector128 v14 = v13 + v11; - Vector128 v15 = v13 - v11; - Vector128 v16 = v10 + v12; - Vector128 v17 = v12 * Vector128.Create(JxlDctScales.Sqrt2); - Vector128 v18 = v17 + v16; - Vector128 v19 = v17 - v16; - Vector128 v20 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v18, v14); - Vector128 v21 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v18, v14); - Vector128 v22 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v19, v15); - Vector128 v23 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v19, v15); - Vector128 v24 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5097955791041592f), v20, v6); - Vector128 v25 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5097955791041592f), v20, v6); - Vector128 v26 = Vector128.MultiplyAddEstimate(Vector128.Create(0.6013448869350453f), v22, v8); - Vector128 v27 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.6013448869350453f), v22, v8); - Vector128 v28 = Vector128.MultiplyAddEstimate(Vector128.Create(0.8999762231364156f), v23, v9); - Vector128 v29 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.8999762231364156f), v23, v9); - Vector128 v30 = Vector128.MultiplyAddEstimate(Vector128.Create(2.5629154477415055f), v21, v7); - Vector128 v31 = Vector128.MultiplyAddEstimate(-Vector128.Create(2.5629154477415055f), v21, v7); - - values[0] = v24; - values[1] = v26; - values[2] = v28; - values[3] = v30; - values[4] = v31; - values[5] = v29; - values[6] = v27; - values[7] = v25; - } - - /// - /// Computes independent 16-point inverse transforms without intermediate memory passes. - /// - /// Exactly 16 position vectors, replaced with the transform results. Each lane remains independent. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Inverse16(Span> values) - { - // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. - // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. - Vector128 input0 = values[0]; - Vector128 input1 = values[1]; - Vector128 input2 = values[2]; - Vector128 input3 = values[3]; - Vector128 input4 = values[4]; - Vector128 input5 = values[5]; - Vector128 input6 = values[6]; - Vector128 input7 = values[7]; - Vector128 input8 = values[8]; - Vector128 input9 = values[9]; - Vector128 input10 = values[10]; - Vector128 input11 = values[11]; - Vector128 input12 = values[12]; - Vector128 input13 = values[13]; - Vector128 input14 = values[14]; - Vector128 input15 = values[15]; - - Vector128 v0 = input0 + input8; - Vector128 v1 = input0 - input8; - Vector128 v2 = input12 + input4; - Vector128 v3 = input4 * Vector128.Create(JxlDctScales.Sqrt2); - Vector128 v4 = v3 + v2; - Vector128 v5 = v3 - v2; - Vector128 v6 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v4, v0); - Vector128 v7 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v4, v0); - Vector128 v8 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v5, v1); - Vector128 v9 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v5, v1); - Vector128 v10 = input14 + input10; - Vector128 v11 = input10 + input6; - Vector128 v12 = input6 + input2; - Vector128 v13 = input2 * Vector128.Create(JxlDctScales.Sqrt2); - Vector128 v14 = v13 + v11; - Vector128 v15 = v13 - v11; - Vector128 v16 = v10 + v12; - Vector128 v17 = v12 * Vector128.Create(JxlDctScales.Sqrt2); - Vector128 v18 = v17 + v16; - Vector128 v19 = v17 - v16; - Vector128 v20 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v18, v14); - Vector128 v21 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v18, v14); - Vector128 v22 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v19, v15); - Vector128 v23 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v19, v15); - Vector128 v24 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5097955791041592f), v20, v6); - Vector128 v25 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5097955791041592f), v20, v6); - Vector128 v26 = Vector128.MultiplyAddEstimate(Vector128.Create(0.6013448869350453f), v22, v8); - Vector128 v27 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.6013448869350453f), v22, v8); - Vector128 v28 = Vector128.MultiplyAddEstimate(Vector128.Create(0.8999762231364156f), v23, v9); - Vector128 v29 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.8999762231364156f), v23, v9); - Vector128 v30 = Vector128.MultiplyAddEstimate(Vector128.Create(2.5629154477415055f), v21, v7); - Vector128 v31 = Vector128.MultiplyAddEstimate(-Vector128.Create(2.5629154477415055f), v21, v7); - Vector128 v32 = input15 + input13; - Vector128 v33 = input13 + input11; - Vector128 v34 = input11 + input9; - Vector128 v35 = input9 + input7; - Vector128 v36 = input7 + input5; - Vector128 v37 = input5 + input3; - Vector128 v38 = input3 + input1; - Vector128 v39 = input1 * Vector128.Create(JxlDctScales.Sqrt2); - Vector128 v40 = v39 + v35; - Vector128 v41 = v39 - v35; - Vector128 v42 = v33 + v37; - Vector128 v43 = v37 * Vector128.Create(JxlDctScales.Sqrt2); - Vector128 v44 = v43 + v42; - Vector128 v45 = v43 - v42; - Vector128 v46 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v44, v40); - Vector128 v47 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v44, v40); - Vector128 v48 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v45, v41); - Vector128 v49 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v45, v41); - Vector128 v50 = v32 + v34; - Vector128 v51 = v34 + v36; - Vector128 v52 = v36 + v38; - Vector128 v53 = v38 * Vector128.Create(JxlDctScales.Sqrt2); - Vector128 v54 = v53 + v51; - Vector128 v55 = v53 - v51; - Vector128 v56 = v50 + v52; - Vector128 v57 = v52 * Vector128.Create(JxlDctScales.Sqrt2); - Vector128 v58 = v57 + v56; - Vector128 v59 = v57 - v56; - Vector128 v60 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v58, v54); - Vector128 v61 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v58, v54); - Vector128 v62 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v59, v55); - Vector128 v63 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v59, v55); - Vector128 v64 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5097955791041592f), v60, v46); - Vector128 v65 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5097955791041592f), v60, v46); - Vector128 v66 = Vector128.MultiplyAddEstimate(Vector128.Create(0.6013448869350453f), v62, v48); - Vector128 v67 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.6013448869350453f), v62, v48); - Vector128 v68 = Vector128.MultiplyAddEstimate(Vector128.Create(0.8999762231364156f), v63, v49); - Vector128 v69 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.8999762231364156f), v63, v49); - Vector128 v70 = Vector128.MultiplyAddEstimate(Vector128.Create(2.5629154477415055f), v61, v47); - Vector128 v71 = Vector128.MultiplyAddEstimate(-Vector128.Create(2.5629154477415055f), v61, v47); - Vector128 v72 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5024192861881557f), v64, v24); - Vector128 v73 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5024192861881557f), v64, v24); - Vector128 v74 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5224986149396889f), v66, v26); - Vector128 v75 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5224986149396889f), v66, v26); - Vector128 v76 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5669440348163577f), v68, v28); - Vector128 v77 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5669440348163577f), v68, v28); - Vector128 v78 = Vector128.MultiplyAddEstimate(Vector128.Create(0.6468217833599901f), v70, v30); - Vector128 v79 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.6468217833599901f), v70, v30); - Vector128 v80 = Vector128.MultiplyAddEstimate(Vector128.Create(0.7881546234512502f), v71, v31); - Vector128 v81 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.7881546234512502f), v71, v31); - Vector128 v82 = Vector128.MultiplyAddEstimate(Vector128.Create(1.060677685990347f), v69, v29); - Vector128 v83 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.060677685990347f), v69, v29); - Vector128 v84 = Vector128.MultiplyAddEstimate(Vector128.Create(1.7224470982383342f), v67, v27); - Vector128 v85 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.7224470982383342f), v67, v27); - Vector128 v86 = Vector128.MultiplyAddEstimate(Vector128.Create(5.101148618689155f), v65, v25); - Vector128 v87 = Vector128.MultiplyAddEstimate(-Vector128.Create(5.101148618689155f), v65, v25); - - values[0] = v72; - values[1] = v74; - values[2] = v76; - values[3] = v78; - values[4] = v80; - values[5] = v82; - values[6] = v84; - values[7] = v86; - values[8] = v87; - values[9] = v85; - values[10] = v83; - values[11] = v81; - values[12] = v79; - values[13] = v77; - values[14] = v75; - values[15] = v73; - } - - /// - /// Computes independent 32-point inverse transforms without intermediate memory passes. - /// - /// Exactly 32 position vectors, replaced with the transform results. Each lane remains independent. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Inverse32(Span> values) - { - // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. - // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. - Vector128 input0 = values[0]; - Vector128 input1 = values[1]; - Vector128 input2 = values[2]; - Vector128 input3 = values[3]; - Vector128 input4 = values[4]; - Vector128 input5 = values[5]; - Vector128 input6 = values[6]; - Vector128 input7 = values[7]; - Vector128 input8 = values[8]; - Vector128 input9 = values[9]; - Vector128 input10 = values[10]; - Vector128 input11 = values[11]; - Vector128 input12 = values[12]; - Vector128 input13 = values[13]; - Vector128 input14 = values[14]; - Vector128 input15 = values[15]; - Vector128 input16 = values[16]; - Vector128 input17 = values[17]; - Vector128 input18 = values[18]; - Vector128 input19 = values[19]; - Vector128 input20 = values[20]; - Vector128 input21 = values[21]; - Vector128 input22 = values[22]; - Vector128 input23 = values[23]; - Vector128 input24 = values[24]; - Vector128 input25 = values[25]; - Vector128 input26 = values[26]; - Vector128 input27 = values[27]; - Vector128 input28 = values[28]; - Vector128 input29 = values[29]; - Vector128 input30 = values[30]; - Vector128 input31 = values[31]; - - Vector128 v0 = input0 + input16; - Vector128 v1 = input0 - input16; - Vector128 v2 = input24 + input8; - Vector128 v3 = input8 * Vector128.Create(JxlDctScales.Sqrt2); - Vector128 v4 = v3 + v2; - Vector128 v5 = v3 - v2; - Vector128 v6 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v4, v0); - Vector128 v7 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v4, v0); - Vector128 v8 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v5, v1); - Vector128 v9 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v5, v1); - Vector128 v10 = input28 + input20; - Vector128 v11 = input20 + input12; - Vector128 v12 = input12 + input4; - Vector128 v13 = input4 * Vector128.Create(JxlDctScales.Sqrt2); - Vector128 v14 = v13 + v11; - Vector128 v15 = v13 - v11; - Vector128 v16 = v10 + v12; - Vector128 v17 = v12 * Vector128.Create(JxlDctScales.Sqrt2); - Vector128 v18 = v17 + v16; - Vector128 v19 = v17 - v16; - Vector128 v20 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v18, v14); - Vector128 v21 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v18, v14); - Vector128 v22 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v19, v15); - Vector128 v23 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v19, v15); - Vector128 v24 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5097955791041592f), v20, v6); - Vector128 v25 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5097955791041592f), v20, v6); - Vector128 v26 = Vector128.MultiplyAddEstimate(Vector128.Create(0.6013448869350453f), v22, v8); - Vector128 v27 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.6013448869350453f), v22, v8); - Vector128 v28 = Vector128.MultiplyAddEstimate(Vector128.Create(0.8999762231364156f), v23, v9); - Vector128 v29 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.8999762231364156f), v23, v9); - Vector128 v30 = Vector128.MultiplyAddEstimate(Vector128.Create(2.5629154477415055f), v21, v7); - Vector128 v31 = Vector128.MultiplyAddEstimate(-Vector128.Create(2.5629154477415055f), v21, v7); - Vector128 v32 = input30 + input26; - Vector128 v33 = input26 + input22; - Vector128 v34 = input22 + input18; - Vector128 v35 = input18 + input14; - Vector128 v36 = input14 + input10; - Vector128 v37 = input10 + input6; - Vector128 v38 = input6 + input2; - Vector128 v39 = input2 * Vector128.Create(JxlDctScales.Sqrt2); - Vector128 v40 = v39 + v35; - Vector128 v41 = v39 - v35; - Vector128 v42 = v33 + v37; - Vector128 v43 = v37 * Vector128.Create(JxlDctScales.Sqrt2); - Vector128 v44 = v43 + v42; - Vector128 v45 = v43 - v42; - Vector128 v46 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v44, v40); - Vector128 v47 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v44, v40); - Vector128 v48 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v45, v41); - Vector128 v49 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v45, v41); - Vector128 v50 = v32 + v34; - Vector128 v51 = v34 + v36; - Vector128 v52 = v36 + v38; - Vector128 v53 = v38 * Vector128.Create(JxlDctScales.Sqrt2); - Vector128 v54 = v53 + v51; - Vector128 v55 = v53 - v51; - Vector128 v56 = v50 + v52; - Vector128 v57 = v52 * Vector128.Create(JxlDctScales.Sqrt2); - Vector128 v58 = v57 + v56; - Vector128 v59 = v57 - v56; - Vector128 v60 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v58, v54); - Vector128 v61 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v58, v54); - Vector128 v62 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v59, v55); - Vector128 v63 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v59, v55); - Vector128 v64 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5097955791041592f), v60, v46); - Vector128 v65 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5097955791041592f), v60, v46); - Vector128 v66 = Vector128.MultiplyAddEstimate(Vector128.Create(0.6013448869350453f), v62, v48); - Vector128 v67 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.6013448869350453f), v62, v48); - Vector128 v68 = Vector128.MultiplyAddEstimate(Vector128.Create(0.8999762231364156f), v63, v49); - Vector128 v69 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.8999762231364156f), v63, v49); - Vector128 v70 = Vector128.MultiplyAddEstimate(Vector128.Create(2.5629154477415055f), v61, v47); - Vector128 v71 = Vector128.MultiplyAddEstimate(-Vector128.Create(2.5629154477415055f), v61, v47); - Vector128 v72 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5024192861881557f), v64, v24); - Vector128 v73 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5024192861881557f), v64, v24); - Vector128 v74 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5224986149396889f), v66, v26); - Vector128 v75 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5224986149396889f), v66, v26); - Vector128 v76 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5669440348163577f), v68, v28); - Vector128 v77 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5669440348163577f), v68, v28); - Vector128 v78 = Vector128.MultiplyAddEstimate(Vector128.Create(0.6468217833599901f), v70, v30); - Vector128 v79 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.6468217833599901f), v70, v30); - Vector128 v80 = Vector128.MultiplyAddEstimate(Vector128.Create(0.7881546234512502f), v71, v31); - Vector128 v81 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.7881546234512502f), v71, v31); - Vector128 v82 = Vector128.MultiplyAddEstimate(Vector128.Create(1.060677685990347f), v69, v29); - Vector128 v83 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.060677685990347f), v69, v29); - Vector128 v84 = Vector128.MultiplyAddEstimate(Vector128.Create(1.7224470982383342f), v67, v27); - Vector128 v85 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.7224470982383342f), v67, v27); - Vector128 v86 = Vector128.MultiplyAddEstimate(Vector128.Create(5.101148618689155f), v65, v25); - Vector128 v87 = Vector128.MultiplyAddEstimate(-Vector128.Create(5.101148618689155f), v65, v25); - Vector128 v88 = input31 + input29; - Vector128 v89 = input29 + input27; - Vector128 v90 = input27 + input25; - Vector128 v91 = input25 + input23; - Vector128 v92 = input23 + input21; - Vector128 v93 = input21 + input19; - Vector128 v94 = input19 + input17; - Vector128 v95 = input17 + input15; - Vector128 v96 = input15 + input13; - Vector128 v97 = input13 + input11; - Vector128 v98 = input11 + input9; - Vector128 v99 = input9 + input7; - Vector128 v100 = input7 + input5; - Vector128 v101 = input5 + input3; - Vector128 v102 = input3 + input1; - Vector128 v103 = input1 * Vector128.Create(JxlDctScales.Sqrt2); - Vector128 v104 = v103 + v95; - Vector128 v105 = v103 - v95; - Vector128 v106 = v91 + v99; - Vector128 v107 = v99 * Vector128.Create(JxlDctScales.Sqrt2); - Vector128 v108 = v107 + v106; - Vector128 v109 = v107 - v106; - Vector128 v110 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v108, v104); - Vector128 v111 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v108, v104); - Vector128 v112 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v109, v105); - Vector128 v113 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v109, v105); - Vector128 v114 = v89 + v93; - Vector128 v115 = v93 + v97; - Vector128 v116 = v97 + v101; - Vector128 v117 = v101 * Vector128.Create(JxlDctScales.Sqrt2); - Vector128 v118 = v117 + v115; - Vector128 v119 = v117 - v115; - Vector128 v120 = v114 + v116; - Vector128 v121 = v116 * Vector128.Create(JxlDctScales.Sqrt2); - Vector128 v122 = v121 + v120; - Vector128 v123 = v121 - v120; - Vector128 v124 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v122, v118); - Vector128 v125 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v122, v118); - Vector128 v126 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v123, v119); - Vector128 v127 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v123, v119); - Vector128 v128 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5097955791041592f), v124, v110); - Vector128 v129 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5097955791041592f), v124, v110); - Vector128 v130 = Vector128.MultiplyAddEstimate(Vector128.Create(0.6013448869350453f), v126, v112); - Vector128 v131 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.6013448869350453f), v126, v112); - Vector128 v132 = Vector128.MultiplyAddEstimate(Vector128.Create(0.8999762231364156f), v127, v113); - Vector128 v133 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.8999762231364156f), v127, v113); - Vector128 v134 = Vector128.MultiplyAddEstimate(Vector128.Create(2.5629154477415055f), v125, v111); - Vector128 v135 = Vector128.MultiplyAddEstimate(-Vector128.Create(2.5629154477415055f), v125, v111); - Vector128 v136 = v88 + v90; - Vector128 v137 = v90 + v92; - Vector128 v138 = v92 + v94; - Vector128 v139 = v94 + v96; - Vector128 v140 = v96 + v98; - Vector128 v141 = v98 + v100; - Vector128 v142 = v100 + v102; - Vector128 v143 = v102 * Vector128.Create(JxlDctScales.Sqrt2); - Vector128 v144 = v143 + v139; - Vector128 v145 = v143 - v139; - Vector128 v146 = v137 + v141; - Vector128 v147 = v141 * Vector128.Create(JxlDctScales.Sqrt2); - Vector128 v148 = v147 + v146; - Vector128 v149 = v147 - v146; - Vector128 v150 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v148, v144); - Vector128 v151 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v148, v144); - Vector128 v152 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v149, v145); - Vector128 v153 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v149, v145); - Vector128 v154 = v136 + v138; - Vector128 v155 = v138 + v140; - Vector128 v156 = v140 + v142; - Vector128 v157 = v142 * Vector128.Create(JxlDctScales.Sqrt2); - Vector128 v158 = v157 + v155; - Vector128 v159 = v157 - v155; - Vector128 v160 = v154 + v156; - Vector128 v161 = v156 * Vector128.Create(JxlDctScales.Sqrt2); - Vector128 v162 = v161 + v160; - Vector128 v163 = v161 - v160; - Vector128 v164 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v162, v158); - Vector128 v165 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v162, v158); - Vector128 v166 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v163, v159); - Vector128 v167 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v163, v159); - Vector128 v168 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5097955791041592f), v164, v150); - Vector128 v169 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5097955791041592f), v164, v150); - Vector128 v170 = Vector128.MultiplyAddEstimate(Vector128.Create(0.6013448869350453f), v166, v152); - Vector128 v171 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.6013448869350453f), v166, v152); - Vector128 v172 = Vector128.MultiplyAddEstimate(Vector128.Create(0.8999762231364156f), v167, v153); - Vector128 v173 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.8999762231364156f), v167, v153); - Vector128 v174 = Vector128.MultiplyAddEstimate(Vector128.Create(2.5629154477415055f), v165, v151); - Vector128 v175 = Vector128.MultiplyAddEstimate(-Vector128.Create(2.5629154477415055f), v165, v151); - Vector128 v176 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5024192861881557f), v168, v128); - Vector128 v177 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5024192861881557f), v168, v128); - Vector128 v178 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5224986149396889f), v170, v130); - Vector128 v179 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5224986149396889f), v170, v130); - Vector128 v180 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5669440348163577f), v172, v132); - Vector128 v181 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5669440348163577f), v172, v132); - Vector128 v182 = Vector128.MultiplyAddEstimate(Vector128.Create(0.6468217833599901f), v174, v134); - Vector128 v183 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.6468217833599901f), v174, v134); - Vector128 v184 = Vector128.MultiplyAddEstimate(Vector128.Create(0.7881546234512502f), v175, v135); - Vector128 v185 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.7881546234512502f), v175, v135); - Vector128 v186 = Vector128.MultiplyAddEstimate(Vector128.Create(1.060677685990347f), v173, v133); - Vector128 v187 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.060677685990347f), v173, v133); - Vector128 v188 = Vector128.MultiplyAddEstimate(Vector128.Create(1.7224470982383342f), v171, v131); - Vector128 v189 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.7224470982383342f), v171, v131); - Vector128 v190 = Vector128.MultiplyAddEstimate(Vector128.Create(5.101148618689155f), v169, v129); - Vector128 v191 = Vector128.MultiplyAddEstimate(-Vector128.Create(5.101148618689155f), v169, v129); - Vector128 v192 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5006029982351963f), v176, v72); - Vector128 v193 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5006029982351963f), v176, v72); - Vector128 v194 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5054709598975436f), v178, v74); - Vector128 v195 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5054709598975436f), v178, v74); - Vector128 v196 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5154473099226246f), v180, v76); - Vector128 v197 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5154473099226246f), v180, v76); - Vector128 v198 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5310425910897841f), v182, v78); - Vector128 v199 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5310425910897841f), v182, v78); - Vector128 v200 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5531038960344445f), v184, v80); - Vector128 v201 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5531038960344445f), v184, v80); - Vector128 v202 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5829349682061339f), v186, v82); - Vector128 v203 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5829349682061339f), v186, v82); - Vector128 v204 = Vector128.MultiplyAddEstimate(Vector128.Create(0.6225041230356648f), v188, v84); - Vector128 v205 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.6225041230356648f), v188, v84); - Vector128 v206 = Vector128.MultiplyAddEstimate(Vector128.Create(0.6748083414550057f), v190, v86); - Vector128 v207 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.6748083414550057f), v190, v86); - Vector128 v208 = Vector128.MultiplyAddEstimate(Vector128.Create(0.7445362710022986f), v191, v87); - Vector128 v209 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.7445362710022986f), v191, v87); - Vector128 v210 = Vector128.MultiplyAddEstimate(Vector128.Create(0.8393496454155268f), v189, v85); - Vector128 v211 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.8393496454155268f), v189, v85); - Vector128 v212 = Vector128.MultiplyAddEstimate(Vector128.Create(0.9725682378619608f), v187, v83); - Vector128 v213 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.9725682378619608f), v187, v83); - Vector128 v214 = Vector128.MultiplyAddEstimate(Vector128.Create(1.1694399334328847f), v185, v81); - Vector128 v215 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.1694399334328847f), v185, v81); - Vector128 v216 = Vector128.MultiplyAddEstimate(Vector128.Create(1.4841646163141662f), v183, v79); - Vector128 v217 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.4841646163141662f), v183, v79); - Vector128 v218 = Vector128.MultiplyAddEstimate(Vector128.Create(2.057781009953411f), v181, v77); - Vector128 v219 = Vector128.MultiplyAddEstimate(-Vector128.Create(2.057781009953411f), v181, v77); - Vector128 v220 = Vector128.MultiplyAddEstimate(Vector128.Create(3.407608418468719f), v179, v75); - Vector128 v221 = Vector128.MultiplyAddEstimate(-Vector128.Create(3.407608418468719f), v179, v75); - Vector128 v222 = Vector128.MultiplyAddEstimate(Vector128.Create(10.190008123548033f), v177, v73); - Vector128 v223 = Vector128.MultiplyAddEstimate(-Vector128.Create(10.190008123548033f), v177, v73); - - values[0] = v192; - values[1] = v194; - values[2] = v196; - values[3] = v198; - values[4] = v200; - values[5] = v202; - values[6] = v204; - values[7] = v206; - values[8] = v208; - values[9] = v210; - values[10] = v212; - values[11] = v214; - values[12] = v216; - values[13] = v218; - values[14] = v220; - values[15] = v222; - values[16] = v223; - values[17] = v221; - values[18] = v219; - values[19] = v217; - values[20] = v215; - values[21] = v213; - values[22] = v211; - values[23] = v209; - values[24] = v207; - values[25] = v205; - values[26] = v203; - values[27] = v201; - values[28] = v199; - values[29] = v197; - values[30] = v195; - values[31] = v193; - } - - /// - /// Processes complete 4-lane inverse batches along input columns. - /// - /// The number of positions in each transform. - /// The number of independent transforms. - /// The borrowed source matrix. - /// The source row stride, in floats. - /// The borrowed output matrix. - /// The destination row stride, in floats. - /// The first independent transform not already processed by a wider batch. - /// Borrowed storage for two length-by-4 banks. Only the first bank needs initialized inputs. - /// The first independent transform left after the complete batches. - private static int InverseColumns128( - int length, - int count, - ReadOnlySpan source, - int sourceStride, - Span destination, - int destinationStride, - int first, - Span workspace) - { - Span> values = MemoryMarshal.Cast>(workspace[..(length * 4)]); - Span> scratch = MemoryMarshal.Cast>(workspace.Slice(length * 4, length * 4)); - ref float input = ref MemoryMarshal.GetReference(source); - ref float output = ref MemoryMarshal.GetReference(destination); - - for (; first <= count - 4; first += 4) - { - // One vector covers adjacent columns at the same transform position. Physical strides never enter the coefficient-bank layout. - for (int i = 0; i < length; i++) - { - values[i] = Vector128.LoadUnsafe(ref input, (nuint)((i * sourceStride) + first)); - } - - Inverse(values, scratch); - for (int i = 0; i < length; i++) - { - values[i].StoreUnsafe(ref output, (nuint)((i * destinationStride) + first)); - } - } - - return first; - } - - /// - /// Processes complete 4-lane inverse batches along input rows. - /// - /// The number of positions in each transform. - /// The number of independent transforms. - /// The borrowed source matrix. - /// The source row stride, in floats. - /// The borrowed output matrix. - /// The destination row stride, in floats. - /// Whether transform positions become output rows and input rows become output columns. - /// The first independent transform not already processed by a wider batch. - /// Borrowed storage for two length-by-4 banks. Only the first bank needs initialized inputs. - /// The first independent transform left after the complete batches. - private static int InverseRows128( - int length, - int count, - ReadOnlySpan source, - int sourceStride, - Span destination, - int destinationStride, - bool transposed, - int first, - Span workspace) - { - Span> values = MemoryMarshal.Cast>(workspace[..(length * 4)]); - Span> scratch = MemoryMarshal.Cast>(workspace.Slice(length * 4, length * 4)); - ref float input = ref MemoryMarshal.GetReference(source); - ref float output = ref MemoryMarshal.GetReference(destination); - - for (; first <= count - 4; first += 4) - { - // Each square load contains adjacent samples from independent rows. Transposition puts those rows into SIMD lanes. - // The complete row band is loaded before any same-orientation output is written, so source and destination can be the same matrix. - for (int column = 0; column < length; column += 4) - { - Vector128 row0 = Vector128.LoadUnsafe(ref input, (nuint)(((first + 0) * sourceStride) + column)).AsInt32(); - Vector128 row1 = Vector128.LoadUnsafe(ref input, (nuint)(((first + 1) * sourceStride) + column)).AsInt32(); - Vector128 row2 = Vector128.LoadUnsafe(ref input, (nuint)(((first + 2) * sourceStride) + column)).AsInt32(); - Vector128 row3 = Vector128.LoadUnsafe(ref input, (nuint)(((first + 3) * sourceStride) + column)).AsInt32(); - - SimdUtils.Transpose(ref row0, ref row1, ref row2, ref row3); - values[column + 0] = row0.AsSingle(); - values[column + 1] = row1.AsSingle(); - values[column + 2] = row2.AsSingle(); - values[column + 3] = row3.AsSingle(); - } - - Inverse(values, scratch); - if (transposed) - { - // A complete vector is one contiguous output row. Only logical samples are stored; destination padding remains untouched. - for (int i = 0; i < length; i++) - { - values[i].StoreUnsafe(ref output, (nuint)((i * destinationStride) + first)); - } - } - else - { - // Restore row-major order one register tile at a time. This replaces a separate full-block transpose and copy. - for (int column = 0; column < length; column += 4) - { - Vector128 row0 = values[column + 0].AsInt32(); - Vector128 row1 = values[column + 1].AsInt32(); - Vector128 row2 = values[column + 2].AsInt32(); - Vector128 row3 = values[column + 3].AsInt32(); - - SimdUtils.Transpose(ref row0, ref row1, ref row2, ref row3); - row0.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 0) * destinationStride) + column)); - row1.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 1) * destinationStride) + column)); - row2.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 2) * destinationStride) + column)); - row3.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 3) * destinationStride) + column)); - } - } - } - - return first; - } - - /// - /// Computes independent forward transforms using two nonoverlapping borrowed vector banks. - /// - /// One vector per transform position. Its length is a power of two from one through 256. - /// At least values.Length vectors. Previous contents are not read. - private static void Forward(Span> values, Span> scratch) - { - // Small transforms stay in registers. The one-point transform is an identity and must not copy any trailing samples. - switch (values.Length) - { - case 1: - return; - case 2: - Forward2(values); - return; - case 4: - Forward4(values); - return; - case 8: - Forward8(values); - return; - case 16: - Forward16(values); - return; - case 32: - Forward32(values); - return; - } - - int half = values.Length / 2; - ReadOnlySpan weights = JxlDctScales.GetMultipliers(values.Length); - - // Consume both original halves before reusing their bank as recursive scratch. Neither recursive call can then destroy unread input. - for (int i = 0; i < half; i++) - { - Vector256 first = values[i]; - Vector256 last = values[values.Length - i - 1]; - scratch[i] = first + last; - scratch[half + i] = (first - last) * Vector256.Create(weights[i]); - } - - Forward(scratch[..half], values); - Forward(scratch.Slice(half, half), values); - scratch[half] = Vector256.MultiplyAddEstimate(scratch[half], Vector256.Create(JxlDctScales.Sqrt2), scratch[half + 1]); - - for (int i = 1; i + 1 < half; i++) - { - scratch[half + i] += scratch[half + i + 1]; - } - - for (int i = 0; i < half; i++) - { - values[2 * i] = scratch[i]; - values[(2 * i) + 1] = scratch[half + i]; - } - } - - /// - /// Computes independent 2-point forward transforms without intermediate memory passes. - /// - /// Exactly 2 position vectors, replaced with the transform results. Each lane remains independent. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Forward2(Span> values) - { - // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. - // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. - Vector256 input0 = values[0]; - Vector256 input1 = values[1]; - - Vector256 v0 = input0 + input1; - Vector256 v1 = input0 - input1; - - values[0] = v0; - values[1] = v1; - } - - /// - /// Computes independent 4-point forward transforms without intermediate memory passes. - /// - /// Exactly 4 position vectors, replaced with the transform results. Each lane remains independent. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Forward4(Span> values) - { - // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. - // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. - Vector256 input0 = values[0]; - Vector256 input1 = values[1]; - Vector256 input2 = values[2]; - Vector256 input3 = values[3]; - - Vector256 v0 = input0 + input3; - Vector256 v1 = (input0 - input3) * Vector256.Create(0.541196100146197f); - Vector256 v2 = input1 + input2; - Vector256 v3 = (input1 - input2) * Vector256.Create(1.3065629648763764f); - Vector256 v4 = v0 + v2; - Vector256 v5 = v0 - v2; - Vector256 v6 = v1 + v3; - Vector256 v7 = v1 - v3; - Vector256 v8 = Vector256.MultiplyAddEstimate(v6, Vector256.Create(JxlDctScales.Sqrt2), v7); - - values[0] = v4; - values[1] = v8; - values[2] = v5; - values[3] = v7; - } - - /// - /// Computes independent 8-point forward transforms without intermediate memory passes. - /// - /// Exactly 8 position vectors, replaced with the transform results. Each lane remains independent. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Forward8(Span> values) - { - // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. - // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. - Vector256 input0 = values[0]; - Vector256 input1 = values[1]; - Vector256 input2 = values[2]; - Vector256 input3 = values[3]; - Vector256 input4 = values[4]; - Vector256 input5 = values[5]; - Vector256 input6 = values[6]; - Vector256 input7 = values[7]; - - Vector256 v0 = input0 + input7; - Vector256 v1 = (input0 - input7) * Vector256.Create(0.5097955791041592f); - Vector256 v2 = input1 + input6; - Vector256 v3 = (input1 - input6) * Vector256.Create(0.6013448869350453f); - Vector256 v4 = input2 + input5; - Vector256 v5 = (input2 - input5) * Vector256.Create(0.8999762231364156f); - Vector256 v6 = input3 + input4; - Vector256 v7 = (input3 - input4) * Vector256.Create(2.5629154477415055f); - Vector256 v8 = v0 + v6; - Vector256 v9 = (v0 - v6) * Vector256.Create(0.541196100146197f); - Vector256 v10 = v2 + v4; - Vector256 v11 = (v2 - v4) * Vector256.Create(1.3065629648763764f); - Vector256 v12 = v8 + v10; - Vector256 v13 = v8 - v10; - Vector256 v14 = v9 + v11; - Vector256 v15 = v9 - v11; - Vector256 v16 = Vector256.MultiplyAddEstimate(v14, Vector256.Create(JxlDctScales.Sqrt2), v15); - Vector256 v17 = v1 + v7; - Vector256 v18 = (v1 - v7) * Vector256.Create(0.541196100146197f); - Vector256 v19 = v3 + v5; - Vector256 v20 = (v3 - v5) * Vector256.Create(1.3065629648763764f); - Vector256 v21 = v17 + v19; - Vector256 v22 = v17 - v19; - Vector256 v23 = v18 + v20; - Vector256 v24 = v18 - v20; - Vector256 v25 = Vector256.MultiplyAddEstimate(v23, Vector256.Create(JxlDctScales.Sqrt2), v24); - Vector256 v26 = Vector256.MultiplyAddEstimate(v21, Vector256.Create(JxlDctScales.Sqrt2), v25); - Vector256 v27 = v25 + v22; - Vector256 v28 = v22 + v24; - - values[0] = v12; - values[1] = v26; - values[2] = v16; - values[3] = v27; - values[4] = v13; - values[5] = v28; - values[6] = v15; - values[7] = v24; - } - - /// - /// Computes independent 16-point forward transforms without intermediate memory passes. - /// - /// Exactly 16 position vectors, replaced with the transform results. Each lane remains independent. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Forward16(Span> values) - { - // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. - // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. - Vector256 input0 = values[0]; - Vector256 input1 = values[1]; - Vector256 input2 = values[2]; - Vector256 input3 = values[3]; - Vector256 input4 = values[4]; - Vector256 input5 = values[5]; - Vector256 input6 = values[6]; - Vector256 input7 = values[7]; - Vector256 input8 = values[8]; - Vector256 input9 = values[9]; - Vector256 input10 = values[10]; - Vector256 input11 = values[11]; - Vector256 input12 = values[12]; - Vector256 input13 = values[13]; - Vector256 input14 = values[14]; - Vector256 input15 = values[15]; - - Vector256 v0 = input0 + input15; - Vector256 v1 = (input0 - input15) * Vector256.Create(0.5024192861881557f); - Vector256 v2 = input1 + input14; - Vector256 v3 = (input1 - input14) * Vector256.Create(0.5224986149396889f); - Vector256 v4 = input2 + input13; - Vector256 v5 = (input2 - input13) * Vector256.Create(0.5669440348163577f); - Vector256 v6 = input3 + input12; - Vector256 v7 = (input3 - input12) * Vector256.Create(0.6468217833599901f); - Vector256 v8 = input4 + input11; - Vector256 v9 = (input4 - input11) * Vector256.Create(0.7881546234512502f); - Vector256 v10 = input5 + input10; - Vector256 v11 = (input5 - input10) * Vector256.Create(1.060677685990347f); - Vector256 v12 = input6 + input9; - Vector256 v13 = (input6 - input9) * Vector256.Create(1.7224470982383342f); - Vector256 v14 = input7 + input8; - Vector256 v15 = (input7 - input8) * Vector256.Create(5.101148618689155f); - Vector256 v16 = v0 + v14; - Vector256 v17 = (v0 - v14) * Vector256.Create(0.5097955791041592f); - Vector256 v18 = v2 + v12; - Vector256 v19 = (v2 - v12) * Vector256.Create(0.6013448869350453f); - Vector256 v20 = v4 + v10; - Vector256 v21 = (v4 - v10) * Vector256.Create(0.8999762231364156f); - Vector256 v22 = v6 + v8; - Vector256 v23 = (v6 - v8) * Vector256.Create(2.5629154477415055f); - Vector256 v24 = v16 + v22; - Vector256 v25 = (v16 - v22) * Vector256.Create(0.541196100146197f); - Vector256 v26 = v18 + v20; - Vector256 v27 = (v18 - v20) * Vector256.Create(1.3065629648763764f); - Vector256 v28 = v24 + v26; - Vector256 v29 = v24 - v26; - Vector256 v30 = v25 + v27; - Vector256 v31 = v25 - v27; - Vector256 v32 = Vector256.MultiplyAddEstimate(v30, Vector256.Create(JxlDctScales.Sqrt2), v31); - Vector256 v33 = v17 + v23; - Vector256 v34 = (v17 - v23) * Vector256.Create(0.541196100146197f); - Vector256 v35 = v19 + v21; - Vector256 v36 = (v19 - v21) * Vector256.Create(1.3065629648763764f); - Vector256 v37 = v33 + v35; - Vector256 v38 = v33 - v35; - Vector256 v39 = v34 + v36; - Vector256 v40 = v34 - v36; - Vector256 v41 = Vector256.MultiplyAddEstimate(v39, Vector256.Create(JxlDctScales.Sqrt2), v40); - Vector256 v42 = Vector256.MultiplyAddEstimate(v37, Vector256.Create(JxlDctScales.Sqrt2), v41); - Vector256 v43 = v41 + v38; - Vector256 v44 = v38 + v40; - Vector256 v45 = v1 + v15; - Vector256 v46 = (v1 - v15) * Vector256.Create(0.5097955791041592f); - Vector256 v47 = v3 + v13; - Vector256 v48 = (v3 - v13) * Vector256.Create(0.6013448869350453f); - Vector256 v49 = v5 + v11; - Vector256 v50 = (v5 - v11) * Vector256.Create(0.8999762231364156f); - Vector256 v51 = v7 + v9; - Vector256 v52 = (v7 - v9) * Vector256.Create(2.5629154477415055f); - Vector256 v53 = v45 + v51; - Vector256 v54 = (v45 - v51) * Vector256.Create(0.541196100146197f); - Vector256 v55 = v47 + v49; - Vector256 v56 = (v47 - v49) * Vector256.Create(1.3065629648763764f); - Vector256 v57 = v53 + v55; - Vector256 v58 = v53 - v55; - Vector256 v59 = v54 + v56; - Vector256 v60 = v54 - v56; - Vector256 v61 = Vector256.MultiplyAddEstimate(v59, Vector256.Create(JxlDctScales.Sqrt2), v60); - Vector256 v62 = v46 + v52; - Vector256 v63 = (v46 - v52) * Vector256.Create(0.541196100146197f); - Vector256 v64 = v48 + v50; - Vector256 v65 = (v48 - v50) * Vector256.Create(1.3065629648763764f); - Vector256 v66 = v62 + v64; - Vector256 v67 = v62 - v64; - Vector256 v68 = v63 + v65; - Vector256 v69 = v63 - v65; - Vector256 v70 = Vector256.MultiplyAddEstimate(v68, Vector256.Create(JxlDctScales.Sqrt2), v69); - Vector256 v71 = Vector256.MultiplyAddEstimate(v66, Vector256.Create(JxlDctScales.Sqrt2), v70); - Vector256 v72 = v70 + v67; - Vector256 v73 = v67 + v69; - Vector256 v74 = Vector256.MultiplyAddEstimate(v57, Vector256.Create(JxlDctScales.Sqrt2), v71); - Vector256 v75 = v71 + v61; - Vector256 v76 = v61 + v72; - Vector256 v77 = v72 + v58; - Vector256 v78 = v58 + v73; - Vector256 v79 = v73 + v60; - Vector256 v80 = v60 + v69; - - values[0] = v28; - values[1] = v74; - values[2] = v42; - values[3] = v75; - values[4] = v32; - values[5] = v76; - values[6] = v43; - values[7] = v77; - values[8] = v29; - values[9] = v78; - values[10] = v44; - values[11] = v79; - values[12] = v31; - values[13] = v80; - values[14] = v40; - values[15] = v69; - } - - /// - /// Computes independent 32-point forward transforms without intermediate memory passes. - /// - /// Exactly 32 position vectors, replaced with the transform results. Each lane remains independent. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Forward32(Span> values) - { - // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. - // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. - Vector256 input0 = values[0]; - Vector256 input1 = values[1]; - Vector256 input2 = values[2]; - Vector256 input3 = values[3]; - Vector256 input4 = values[4]; - Vector256 input5 = values[5]; - Vector256 input6 = values[6]; - Vector256 input7 = values[7]; - Vector256 input8 = values[8]; - Vector256 input9 = values[9]; - Vector256 input10 = values[10]; - Vector256 input11 = values[11]; - Vector256 input12 = values[12]; - Vector256 input13 = values[13]; - Vector256 input14 = values[14]; - Vector256 input15 = values[15]; - Vector256 input16 = values[16]; - Vector256 input17 = values[17]; - Vector256 input18 = values[18]; - Vector256 input19 = values[19]; - Vector256 input20 = values[20]; - Vector256 input21 = values[21]; - Vector256 input22 = values[22]; - Vector256 input23 = values[23]; - Vector256 input24 = values[24]; - Vector256 input25 = values[25]; - Vector256 input26 = values[26]; - Vector256 input27 = values[27]; - Vector256 input28 = values[28]; - Vector256 input29 = values[29]; - Vector256 input30 = values[30]; - Vector256 input31 = values[31]; - - Vector256 v0 = input0 + input31; - Vector256 v1 = (input0 - input31) * Vector256.Create(0.5006029982351963f); - Vector256 v2 = input1 + input30; - Vector256 v3 = (input1 - input30) * Vector256.Create(0.5054709598975436f); - Vector256 v4 = input2 + input29; - Vector256 v5 = (input2 - input29) * Vector256.Create(0.5154473099226246f); - Vector256 v6 = input3 + input28; - Vector256 v7 = (input3 - input28) * Vector256.Create(0.5310425910897841f); - Vector256 v8 = input4 + input27; - Vector256 v9 = (input4 - input27) * Vector256.Create(0.5531038960344445f); - Vector256 v10 = input5 + input26; - Vector256 v11 = (input5 - input26) * Vector256.Create(0.5829349682061339f); - Vector256 v12 = input6 + input25; - Vector256 v13 = (input6 - input25) * Vector256.Create(0.6225041230356648f); - Vector256 v14 = input7 + input24; - Vector256 v15 = (input7 - input24) * Vector256.Create(0.6748083414550057f); - Vector256 v16 = input8 + input23; - Vector256 v17 = (input8 - input23) * Vector256.Create(0.7445362710022986f); - Vector256 v18 = input9 + input22; - Vector256 v19 = (input9 - input22) * Vector256.Create(0.8393496454155268f); - Vector256 v20 = input10 + input21; - Vector256 v21 = (input10 - input21) * Vector256.Create(0.9725682378619608f); - Vector256 v22 = input11 + input20; - Vector256 v23 = (input11 - input20) * Vector256.Create(1.1694399334328847f); - Vector256 v24 = input12 + input19; - Vector256 v25 = (input12 - input19) * Vector256.Create(1.4841646163141662f); - Vector256 v26 = input13 + input18; - Vector256 v27 = (input13 - input18) * Vector256.Create(2.057781009953411f); - Vector256 v28 = input14 + input17; - Vector256 v29 = (input14 - input17) * Vector256.Create(3.407608418468719f); - Vector256 v30 = input15 + input16; - Vector256 v31 = (input15 - input16) * Vector256.Create(10.190008123548033f); - Vector256 v32 = v0 + v30; - Vector256 v33 = (v0 - v30) * Vector256.Create(0.5024192861881557f); - Vector256 v34 = v2 + v28; - Vector256 v35 = (v2 - v28) * Vector256.Create(0.5224986149396889f); - Vector256 v36 = v4 + v26; - Vector256 v37 = (v4 - v26) * Vector256.Create(0.5669440348163577f); - Vector256 v38 = v6 + v24; - Vector256 v39 = (v6 - v24) * Vector256.Create(0.6468217833599901f); - Vector256 v40 = v8 + v22; - Vector256 v41 = (v8 - v22) * Vector256.Create(0.7881546234512502f); - Vector256 v42 = v10 + v20; - Vector256 v43 = (v10 - v20) * Vector256.Create(1.060677685990347f); - Vector256 v44 = v12 + v18; - Vector256 v45 = (v12 - v18) * Vector256.Create(1.7224470982383342f); - Vector256 v46 = v14 + v16; - Vector256 v47 = (v14 - v16) * Vector256.Create(5.101148618689155f); - Vector256 v48 = v32 + v46; - Vector256 v49 = (v32 - v46) * Vector256.Create(0.5097955791041592f); - Vector256 v50 = v34 + v44; - Vector256 v51 = (v34 - v44) * Vector256.Create(0.6013448869350453f); - Vector256 v52 = v36 + v42; - Vector256 v53 = (v36 - v42) * Vector256.Create(0.8999762231364156f); - Vector256 v54 = v38 + v40; - Vector256 v55 = (v38 - v40) * Vector256.Create(2.5629154477415055f); - Vector256 v56 = v48 + v54; - Vector256 v57 = (v48 - v54) * Vector256.Create(0.541196100146197f); - Vector256 v58 = v50 + v52; - Vector256 v59 = (v50 - v52) * Vector256.Create(1.3065629648763764f); - Vector256 v60 = v56 + v58; - Vector256 v61 = v56 - v58; - Vector256 v62 = v57 + v59; - Vector256 v63 = v57 - v59; - Vector256 v64 = Vector256.MultiplyAddEstimate(v62, Vector256.Create(JxlDctScales.Sqrt2), v63); - Vector256 v65 = v49 + v55; - Vector256 v66 = (v49 - v55) * Vector256.Create(0.541196100146197f); - Vector256 v67 = v51 + v53; - Vector256 v68 = (v51 - v53) * Vector256.Create(1.3065629648763764f); - Vector256 v69 = v65 + v67; - Vector256 v70 = v65 - v67; - Vector256 v71 = v66 + v68; - Vector256 v72 = v66 - v68; - Vector256 v73 = Vector256.MultiplyAddEstimate(v71, Vector256.Create(JxlDctScales.Sqrt2), v72); - Vector256 v74 = Vector256.MultiplyAddEstimate(v69, Vector256.Create(JxlDctScales.Sqrt2), v73); - Vector256 v75 = v73 + v70; - Vector256 v76 = v70 + v72; - Vector256 v77 = v33 + v47; - Vector256 v78 = (v33 - v47) * Vector256.Create(0.5097955791041592f); - Vector256 v79 = v35 + v45; - Vector256 v80 = (v35 - v45) * Vector256.Create(0.6013448869350453f); - Vector256 v81 = v37 + v43; - Vector256 v82 = (v37 - v43) * Vector256.Create(0.8999762231364156f); - Vector256 v83 = v39 + v41; - Vector256 v84 = (v39 - v41) * Vector256.Create(2.5629154477415055f); - Vector256 v85 = v77 + v83; - Vector256 v86 = (v77 - v83) * Vector256.Create(0.541196100146197f); - Vector256 v87 = v79 + v81; - Vector256 v88 = (v79 - v81) * Vector256.Create(1.3065629648763764f); - Vector256 v89 = v85 + v87; - Vector256 v90 = v85 - v87; - Vector256 v91 = v86 + v88; - Vector256 v92 = v86 - v88; - Vector256 v93 = Vector256.MultiplyAddEstimate(v91, Vector256.Create(JxlDctScales.Sqrt2), v92); - Vector256 v94 = v78 + v84; - Vector256 v95 = (v78 - v84) * Vector256.Create(0.541196100146197f); - Vector256 v96 = v80 + v82; - Vector256 v97 = (v80 - v82) * Vector256.Create(1.3065629648763764f); - Vector256 v98 = v94 + v96; - Vector256 v99 = v94 - v96; - Vector256 v100 = v95 + v97; - Vector256 v101 = v95 - v97; - Vector256 v102 = Vector256.MultiplyAddEstimate(v100, Vector256.Create(JxlDctScales.Sqrt2), v101); - Vector256 v103 = Vector256.MultiplyAddEstimate(v98, Vector256.Create(JxlDctScales.Sqrt2), v102); - Vector256 v104 = v102 + v99; - Vector256 v105 = v99 + v101; - Vector256 v106 = Vector256.MultiplyAddEstimate(v89, Vector256.Create(JxlDctScales.Sqrt2), v103); - Vector256 v107 = v103 + v93; - Vector256 v108 = v93 + v104; - Vector256 v109 = v104 + v90; - Vector256 v110 = v90 + v105; - Vector256 v111 = v105 + v92; - Vector256 v112 = v92 + v101; - Vector256 v113 = v1 + v31; - Vector256 v114 = (v1 - v31) * Vector256.Create(0.5024192861881557f); - Vector256 v115 = v3 + v29; - Vector256 v116 = (v3 - v29) * Vector256.Create(0.5224986149396889f); - Vector256 v117 = v5 + v27; - Vector256 v118 = (v5 - v27) * Vector256.Create(0.5669440348163577f); - Vector256 v119 = v7 + v25; - Vector256 v120 = (v7 - v25) * Vector256.Create(0.6468217833599901f); - Vector256 v121 = v9 + v23; - Vector256 v122 = (v9 - v23) * Vector256.Create(0.7881546234512502f); - Vector256 v123 = v11 + v21; - Vector256 v124 = (v11 - v21) * Vector256.Create(1.060677685990347f); - Vector256 v125 = v13 + v19; - Vector256 v126 = (v13 - v19) * Vector256.Create(1.7224470982383342f); - Vector256 v127 = v15 + v17; - Vector256 v128 = (v15 - v17) * Vector256.Create(5.101148618689155f); - Vector256 v129 = v113 + v127; - Vector256 v130 = (v113 - v127) * Vector256.Create(0.5097955791041592f); - Vector256 v131 = v115 + v125; - Vector256 v132 = (v115 - v125) * Vector256.Create(0.6013448869350453f); - Vector256 v133 = v117 + v123; - Vector256 v134 = (v117 - v123) * Vector256.Create(0.8999762231364156f); - Vector256 v135 = v119 + v121; - Vector256 v136 = (v119 - v121) * Vector256.Create(2.5629154477415055f); - Vector256 v137 = v129 + v135; - Vector256 v138 = (v129 - v135) * Vector256.Create(0.541196100146197f); - Vector256 v139 = v131 + v133; - Vector256 v140 = (v131 - v133) * Vector256.Create(1.3065629648763764f); - Vector256 v141 = v137 + v139; - Vector256 v142 = v137 - v139; - Vector256 v143 = v138 + v140; - Vector256 v144 = v138 - v140; - Vector256 v145 = Vector256.MultiplyAddEstimate(v143, Vector256.Create(JxlDctScales.Sqrt2), v144); - Vector256 v146 = v130 + v136; - Vector256 v147 = (v130 - v136) * Vector256.Create(0.541196100146197f); - Vector256 v148 = v132 + v134; - Vector256 v149 = (v132 - v134) * Vector256.Create(1.3065629648763764f); - Vector256 v150 = v146 + v148; - Vector256 v151 = v146 - v148; - Vector256 v152 = v147 + v149; - Vector256 v153 = v147 - v149; - Vector256 v154 = Vector256.MultiplyAddEstimate(v152, Vector256.Create(JxlDctScales.Sqrt2), v153); - Vector256 v155 = Vector256.MultiplyAddEstimate(v150, Vector256.Create(JxlDctScales.Sqrt2), v154); - Vector256 v156 = v154 + v151; - Vector256 v157 = v151 + v153; - Vector256 v158 = v114 + v128; - Vector256 v159 = (v114 - v128) * Vector256.Create(0.5097955791041592f); - Vector256 v160 = v116 + v126; - Vector256 v161 = (v116 - v126) * Vector256.Create(0.6013448869350453f); - Vector256 v162 = v118 + v124; - Vector256 v163 = (v118 - v124) * Vector256.Create(0.8999762231364156f); - Vector256 v164 = v120 + v122; - Vector256 v165 = (v120 - v122) * Vector256.Create(2.5629154477415055f); - Vector256 v166 = v158 + v164; - Vector256 v167 = (v158 - v164) * Vector256.Create(0.541196100146197f); - Vector256 v168 = v160 + v162; - Vector256 v169 = (v160 - v162) * Vector256.Create(1.3065629648763764f); - Vector256 v170 = v166 + v168; - Vector256 v171 = v166 - v168; - Vector256 v172 = v167 + v169; - Vector256 v173 = v167 - v169; - Vector256 v174 = Vector256.MultiplyAddEstimate(v172, Vector256.Create(JxlDctScales.Sqrt2), v173); - Vector256 v175 = v159 + v165; - Vector256 v176 = (v159 - v165) * Vector256.Create(0.541196100146197f); - Vector256 v177 = v161 + v163; - Vector256 v178 = (v161 - v163) * Vector256.Create(1.3065629648763764f); - Vector256 v179 = v175 + v177; - Vector256 v180 = v175 - v177; - Vector256 v181 = v176 + v178; - Vector256 v182 = v176 - v178; - Vector256 v183 = Vector256.MultiplyAddEstimate(v181, Vector256.Create(JxlDctScales.Sqrt2), v182); - Vector256 v184 = Vector256.MultiplyAddEstimate(v179, Vector256.Create(JxlDctScales.Sqrt2), v183); - Vector256 v185 = v183 + v180; - Vector256 v186 = v180 + v182; - Vector256 v187 = Vector256.MultiplyAddEstimate(v170, Vector256.Create(JxlDctScales.Sqrt2), v184); - Vector256 v188 = v184 + v174; - Vector256 v189 = v174 + v185; - Vector256 v190 = v185 + v171; - Vector256 v191 = v171 + v186; - Vector256 v192 = v186 + v173; - Vector256 v193 = v173 + v182; - Vector256 v194 = Vector256.MultiplyAddEstimate(v141, Vector256.Create(JxlDctScales.Sqrt2), v187); - Vector256 v195 = v187 + v155; - Vector256 v196 = v155 + v188; - Vector256 v197 = v188 + v145; - Vector256 v198 = v145 + v189; - Vector256 v199 = v189 + v156; - Vector256 v200 = v156 + v190; - Vector256 v201 = v190 + v142; - Vector256 v202 = v142 + v191; - Vector256 v203 = v191 + v157; - Vector256 v204 = v157 + v192; - Vector256 v205 = v192 + v144; - Vector256 v206 = v144 + v193; - Vector256 v207 = v193 + v153; - Vector256 v208 = v153 + v182; - - values[0] = v60; - values[1] = v194; - values[2] = v106; - values[3] = v195; - values[4] = v74; - values[5] = v196; - values[6] = v107; - values[7] = v197; - values[8] = v64; - values[9] = v198; - values[10] = v108; - values[11] = v199; - values[12] = v75; - values[13] = v200; - values[14] = v109; - values[15] = v201; - values[16] = v61; - values[17] = v202; - values[18] = v110; - values[19] = v203; - values[20] = v76; - values[21] = v204; - values[22] = v111; - values[23] = v205; - values[24] = v63; - values[25] = v206; - values[26] = v112; - values[27] = v207; - values[28] = v72; - values[29] = v208; - values[30] = v101; - values[31] = v182; - } - - /// - /// Processes complete 8-lane forward batches along input columns. - /// - /// The number of positions in each transform. - /// The number of independent transforms. - /// The borrowed source matrix. - /// The source row stride, in floats. - /// The borrowed output matrix. - /// The destination row stride, in floats. - /// The first independent transform not already processed by a wider batch. - /// Borrowed storage for two length-by-8 banks. Only the first bank needs initialized inputs. - /// The first independent transform left after the complete batches. - private static int ForwardColumns256( - int length, - int count, - ReadOnlySpan source, - int sourceStride, - Span destination, - int destinationStride, - int first, - Span workspace) - { - Span> values = MemoryMarshal.Cast>(workspace[..(length * 8)]); - Span> scratch = MemoryMarshal.Cast>(workspace.Slice(length * 8, length * 8)); - ref float input = ref MemoryMarshal.GetReference(source); - ref float output = ref MemoryMarshal.GetReference(destination); - Vector256 scale = Vector256.Create(1F / length); - - for (; first <= count - 8; first += 8) - { - // One vector covers adjacent columns at the same transform position. Physical strides never enter the coefficient-bank layout. - for (int i = 0; i < length; i++) - { - values[i] = Vector256.LoadUnsafe(ref input, (nuint)((i * sourceStride) + first)); - } - - Forward(values, scratch); - for (int i = 0; i < length; i++) - { - (values[i] * scale).StoreUnsafe(ref output, (nuint)((i * destinationStride) + first)); - } - } - - return first; - } - - /// - /// Processes complete 8-lane forward batches along input rows. - /// - /// The number of positions in each transform. - /// The number of independent transforms. - /// The borrowed source matrix. - /// The source row stride, in floats. - /// The borrowed output matrix. - /// The destination row stride, in floats. - /// Whether transform positions become output rows and input rows become output columns. - /// The first independent transform not already processed by a wider batch. - /// Borrowed storage for two length-by-8 banks. Only the first bank needs initialized inputs. - /// The first independent transform left after the complete batches. - private static int ForwardRows256( - int length, - int count, - ReadOnlySpan source, - int sourceStride, - Span destination, - int destinationStride, - bool transposed, - int first, - Span workspace) - { - Span> values = MemoryMarshal.Cast>(workspace[..(length * 8)]); - Span> scratch = MemoryMarshal.Cast>(workspace.Slice(length * 8, length * 8)); - ref float input = ref MemoryMarshal.GetReference(source); - ref float output = ref MemoryMarshal.GetReference(destination); - Vector256 scale = Vector256.Create(1F / length); - - for (; first <= count - 8; first += 8) - { - // Each square load contains adjacent samples from independent rows. Transposition puts those rows into SIMD lanes. - // The complete row band is loaded before any same-orientation output is written, so source and destination can be the same matrix. - for (int column = 0; column < length; column += 8) - { - Vector256 row0 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 0) * sourceStride) + column)).AsInt32(); - Vector256 row1 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 1) * sourceStride) + column)).AsInt32(); - Vector256 row2 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 2) * sourceStride) + column)).AsInt32(); - Vector256 row3 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 3) * sourceStride) + column)).AsInt32(); - Vector256 row4 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 4) * sourceStride) + column)).AsInt32(); - Vector256 row5 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 5) * sourceStride) + column)).AsInt32(); - Vector256 row6 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 6) * sourceStride) + column)).AsInt32(); - Vector256 row7 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 7) * sourceStride) + column)).AsInt32(); - - SimdUtils.Transpose(ref row0, ref row1, ref row2, ref row3, ref row4, ref row5, ref row6, ref row7); - values[column + 0] = row0.AsSingle(); - values[column + 1] = row1.AsSingle(); - values[column + 2] = row2.AsSingle(); - values[column + 3] = row3.AsSingle(); - values[column + 4] = row4.AsSingle(); - values[column + 5] = row5.AsSingle(); - values[column + 6] = row6.AsSingle(); - values[column + 7] = row7.AsSingle(); - } - - Forward(values, scratch); - if (transposed) - { - // A complete vector is one contiguous output row. Only logical samples are stored; destination padding remains untouched. - for (int i = 0; i < length; i++) - { - (values[i] * scale).StoreUnsafe(ref output, (nuint)((i * destinationStride) + first)); - } - } - else - { - // Restore row-major order one register tile at a time. This replaces a separate full-block transpose and copy. - for (int column = 0; column < length; column += 8) - { - Vector256 row0 = (values[column + 0] * scale).AsInt32(); - Vector256 row1 = (values[column + 1] * scale).AsInt32(); - Vector256 row2 = (values[column + 2] * scale).AsInt32(); - Vector256 row3 = (values[column + 3] * scale).AsInt32(); - Vector256 row4 = (values[column + 4] * scale).AsInt32(); - Vector256 row5 = (values[column + 5] * scale).AsInt32(); - Vector256 row6 = (values[column + 6] * scale).AsInt32(); - Vector256 row7 = (values[column + 7] * scale).AsInt32(); - - SimdUtils.Transpose(ref row0, ref row1, ref row2, ref row3, ref row4, ref row5, ref row6, ref row7); - row0.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 0) * destinationStride) + column)); - row1.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 1) * destinationStride) + column)); - row2.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 2) * destinationStride) + column)); - row3.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 3) * destinationStride) + column)); - row4.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 4) * destinationStride) + column)); - row5.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 5) * destinationStride) + column)); - row6.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 6) * destinationStride) + column)); - row7.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 7) * destinationStride) + column)); - } - } - } - - return first; - } - - /// - /// Computes independent inverse transforms using two nonoverlapping borrowed vector banks. - /// - /// One vector per transform position. Its length is a power of two from one through 256. - /// At least values.Length vectors. Previous contents are not read. - private static void Inverse(Span> values, Span> scratch) - { - // Small transforms stay in registers. The one-point transform is an identity and must not copy any trailing samples. - switch (values.Length) - { - case 1: - return; - case 2: - Inverse2(values); - return; - case 4: - Inverse4(values); - return; - case 8: - Inverse8(values); - return; - case 16: - Inverse16(values); - return; - case 32: - Inverse32(values); - return; - } - - int half = values.Length / 2; - // Separate frequency parity before either recursive call. The original bank can then serve as recursive scratch. - for (int i = 0; i < half; i++) - { - scratch[i] = values[2 * i]; - scratch[half + i] = values[(2 * i) + 1]; - } - - Inverse(scratch[..half], values); - - // Descend so every odd-frequency predecessor is read before its own entry changes. - for (int i = half - 1; i > 0; i--) - { - scratch[half + i] += scratch[half + i - 1]; - } - - scratch[half] *= Vector256.Create(JxlDctScales.Sqrt2); - Inverse(scratch.Slice(half, half), values); - ReadOnlySpan weights = JxlDctScales.GetMultipliers(values.Length); - - for (int i = 0; i < half; i++) - { - Vector256 weight = Vector256.Create(weights[i]); - Vector256 even = scratch[i]; - Vector256 odd = scratch[half + i]; - values[i] = Vector256.MultiplyAddEstimate(weight, odd, even); - values[values.Length - i - 1] = Vector256.MultiplyAddEstimate(-weight, odd, even); - } - } - - /// - /// Computes independent 2-point inverse transforms without intermediate memory passes. - /// - /// Exactly 2 position vectors, replaced with the transform results. Each lane remains independent. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Inverse2(Span> values) - { - // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. - // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. - Vector256 input0 = values[0]; - Vector256 input1 = values[1]; - - Vector256 v0 = input0 + input1; - Vector256 v1 = input0 - input1; - - values[0] = v0; - values[1] = v1; - } - - /// - /// Computes independent 4-point inverse transforms without intermediate memory passes. - /// - /// Exactly 4 position vectors, replaced with the transform results. Each lane remains independent. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Inverse4(Span> values) - { - // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. - // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. - Vector256 input0 = values[0]; - Vector256 input1 = values[1]; - Vector256 input2 = values[2]; - Vector256 input3 = values[3]; - - Vector256 v0 = input0 + input2; - Vector256 v1 = input0 - input2; - Vector256 v2 = input3 + input1; - Vector256 v3 = input1 * Vector256.Create(JxlDctScales.Sqrt2); - Vector256 v4 = v3 + v2; - Vector256 v5 = v3 - v2; - Vector256 v6 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v4, v0); - Vector256 v7 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v4, v0); - Vector256 v8 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v5, v1); - Vector256 v9 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v5, v1); - - values[0] = v6; - values[1] = v8; - values[2] = v9; - values[3] = v7; - } - - /// - /// Computes independent 8-point inverse transforms without intermediate memory passes. - /// - /// Exactly 8 position vectors, replaced with the transform results. Each lane remains independent. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Inverse8(Span> values) - { - // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. - // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. - Vector256 input0 = values[0]; - Vector256 input1 = values[1]; - Vector256 input2 = values[2]; - Vector256 input3 = values[3]; - Vector256 input4 = values[4]; - Vector256 input5 = values[5]; - Vector256 input6 = values[6]; - Vector256 input7 = values[7]; - - Vector256 v0 = input0 + input4; - Vector256 v1 = input0 - input4; - Vector256 v2 = input6 + input2; - Vector256 v3 = input2 * Vector256.Create(JxlDctScales.Sqrt2); - Vector256 v4 = v3 + v2; - Vector256 v5 = v3 - v2; - Vector256 v6 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v4, v0); - Vector256 v7 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v4, v0); - Vector256 v8 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v5, v1); - Vector256 v9 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v5, v1); - Vector256 v10 = input7 + input5; - Vector256 v11 = input5 + input3; - Vector256 v12 = input3 + input1; - Vector256 v13 = input1 * Vector256.Create(JxlDctScales.Sqrt2); - Vector256 v14 = v13 + v11; - Vector256 v15 = v13 - v11; - Vector256 v16 = v10 + v12; - Vector256 v17 = v12 * Vector256.Create(JxlDctScales.Sqrt2); - Vector256 v18 = v17 + v16; - Vector256 v19 = v17 - v16; - Vector256 v20 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v18, v14); - Vector256 v21 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v18, v14); - Vector256 v22 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v19, v15); - Vector256 v23 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v19, v15); - Vector256 v24 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5097955791041592f), v20, v6); - Vector256 v25 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5097955791041592f), v20, v6); - Vector256 v26 = Vector256.MultiplyAddEstimate(Vector256.Create(0.6013448869350453f), v22, v8); - Vector256 v27 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.6013448869350453f), v22, v8); - Vector256 v28 = Vector256.MultiplyAddEstimate(Vector256.Create(0.8999762231364156f), v23, v9); - Vector256 v29 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.8999762231364156f), v23, v9); - Vector256 v30 = Vector256.MultiplyAddEstimate(Vector256.Create(2.5629154477415055f), v21, v7); - Vector256 v31 = Vector256.MultiplyAddEstimate(-Vector256.Create(2.5629154477415055f), v21, v7); - - values[0] = v24; - values[1] = v26; - values[2] = v28; - values[3] = v30; - values[4] = v31; - values[5] = v29; - values[6] = v27; - values[7] = v25; - } - - /// - /// Computes independent 16-point inverse transforms without intermediate memory passes. - /// - /// Exactly 16 position vectors, replaced with the transform results. Each lane remains independent. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Inverse16(Span> values) - { - // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. - // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. - Vector256 input0 = values[0]; - Vector256 input1 = values[1]; - Vector256 input2 = values[2]; - Vector256 input3 = values[3]; - Vector256 input4 = values[4]; - Vector256 input5 = values[5]; - Vector256 input6 = values[6]; - Vector256 input7 = values[7]; - Vector256 input8 = values[8]; - Vector256 input9 = values[9]; - Vector256 input10 = values[10]; - Vector256 input11 = values[11]; - Vector256 input12 = values[12]; - Vector256 input13 = values[13]; - Vector256 input14 = values[14]; - Vector256 input15 = values[15]; - - Vector256 v0 = input0 + input8; - Vector256 v1 = input0 - input8; - Vector256 v2 = input12 + input4; - Vector256 v3 = input4 * Vector256.Create(JxlDctScales.Sqrt2); - Vector256 v4 = v3 + v2; - Vector256 v5 = v3 - v2; - Vector256 v6 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v4, v0); - Vector256 v7 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v4, v0); - Vector256 v8 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v5, v1); - Vector256 v9 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v5, v1); - Vector256 v10 = input14 + input10; - Vector256 v11 = input10 + input6; - Vector256 v12 = input6 + input2; - Vector256 v13 = input2 * Vector256.Create(JxlDctScales.Sqrt2); - Vector256 v14 = v13 + v11; - Vector256 v15 = v13 - v11; - Vector256 v16 = v10 + v12; - Vector256 v17 = v12 * Vector256.Create(JxlDctScales.Sqrt2); - Vector256 v18 = v17 + v16; - Vector256 v19 = v17 - v16; - Vector256 v20 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v18, v14); - Vector256 v21 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v18, v14); - Vector256 v22 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v19, v15); - Vector256 v23 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v19, v15); - Vector256 v24 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5097955791041592f), v20, v6); - Vector256 v25 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5097955791041592f), v20, v6); - Vector256 v26 = Vector256.MultiplyAddEstimate(Vector256.Create(0.6013448869350453f), v22, v8); - Vector256 v27 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.6013448869350453f), v22, v8); - Vector256 v28 = Vector256.MultiplyAddEstimate(Vector256.Create(0.8999762231364156f), v23, v9); - Vector256 v29 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.8999762231364156f), v23, v9); - Vector256 v30 = Vector256.MultiplyAddEstimate(Vector256.Create(2.5629154477415055f), v21, v7); - Vector256 v31 = Vector256.MultiplyAddEstimate(-Vector256.Create(2.5629154477415055f), v21, v7); - Vector256 v32 = input15 + input13; - Vector256 v33 = input13 + input11; - Vector256 v34 = input11 + input9; - Vector256 v35 = input9 + input7; - Vector256 v36 = input7 + input5; - Vector256 v37 = input5 + input3; - Vector256 v38 = input3 + input1; - Vector256 v39 = input1 * Vector256.Create(JxlDctScales.Sqrt2); - Vector256 v40 = v39 + v35; - Vector256 v41 = v39 - v35; - Vector256 v42 = v33 + v37; - Vector256 v43 = v37 * Vector256.Create(JxlDctScales.Sqrt2); - Vector256 v44 = v43 + v42; - Vector256 v45 = v43 - v42; - Vector256 v46 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v44, v40); - Vector256 v47 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v44, v40); - Vector256 v48 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v45, v41); - Vector256 v49 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v45, v41); - Vector256 v50 = v32 + v34; - Vector256 v51 = v34 + v36; - Vector256 v52 = v36 + v38; - Vector256 v53 = v38 * Vector256.Create(JxlDctScales.Sqrt2); - Vector256 v54 = v53 + v51; - Vector256 v55 = v53 - v51; - Vector256 v56 = v50 + v52; - Vector256 v57 = v52 * Vector256.Create(JxlDctScales.Sqrt2); - Vector256 v58 = v57 + v56; - Vector256 v59 = v57 - v56; - Vector256 v60 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v58, v54); - Vector256 v61 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v58, v54); - Vector256 v62 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v59, v55); - Vector256 v63 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v59, v55); - Vector256 v64 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5097955791041592f), v60, v46); - Vector256 v65 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5097955791041592f), v60, v46); - Vector256 v66 = Vector256.MultiplyAddEstimate(Vector256.Create(0.6013448869350453f), v62, v48); - Vector256 v67 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.6013448869350453f), v62, v48); - Vector256 v68 = Vector256.MultiplyAddEstimate(Vector256.Create(0.8999762231364156f), v63, v49); - Vector256 v69 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.8999762231364156f), v63, v49); - Vector256 v70 = Vector256.MultiplyAddEstimate(Vector256.Create(2.5629154477415055f), v61, v47); - Vector256 v71 = Vector256.MultiplyAddEstimate(-Vector256.Create(2.5629154477415055f), v61, v47); - Vector256 v72 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5024192861881557f), v64, v24); - Vector256 v73 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5024192861881557f), v64, v24); - Vector256 v74 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5224986149396889f), v66, v26); - Vector256 v75 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5224986149396889f), v66, v26); - Vector256 v76 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5669440348163577f), v68, v28); - Vector256 v77 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5669440348163577f), v68, v28); - Vector256 v78 = Vector256.MultiplyAddEstimate(Vector256.Create(0.6468217833599901f), v70, v30); - Vector256 v79 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.6468217833599901f), v70, v30); - Vector256 v80 = Vector256.MultiplyAddEstimate(Vector256.Create(0.7881546234512502f), v71, v31); - Vector256 v81 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.7881546234512502f), v71, v31); - Vector256 v82 = Vector256.MultiplyAddEstimate(Vector256.Create(1.060677685990347f), v69, v29); - Vector256 v83 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.060677685990347f), v69, v29); - Vector256 v84 = Vector256.MultiplyAddEstimate(Vector256.Create(1.7224470982383342f), v67, v27); - Vector256 v85 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.7224470982383342f), v67, v27); - Vector256 v86 = Vector256.MultiplyAddEstimate(Vector256.Create(5.101148618689155f), v65, v25); - Vector256 v87 = Vector256.MultiplyAddEstimate(-Vector256.Create(5.101148618689155f), v65, v25); - - values[0] = v72; - values[1] = v74; - values[2] = v76; - values[3] = v78; - values[4] = v80; - values[5] = v82; - values[6] = v84; - values[7] = v86; - values[8] = v87; - values[9] = v85; - values[10] = v83; - values[11] = v81; - values[12] = v79; - values[13] = v77; - values[14] = v75; - values[15] = v73; - } - - /// - /// Computes independent 32-point inverse transforms without intermediate memory passes. - /// - /// Exactly 32 position vectors, replaced with the transform results. Each lane remains independent. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Inverse32(Span> values) - { - // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. - // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. - Vector256 input0 = values[0]; - Vector256 input1 = values[1]; - Vector256 input2 = values[2]; - Vector256 input3 = values[3]; - Vector256 input4 = values[4]; - Vector256 input5 = values[5]; - Vector256 input6 = values[6]; - Vector256 input7 = values[7]; - Vector256 input8 = values[8]; - Vector256 input9 = values[9]; - Vector256 input10 = values[10]; - Vector256 input11 = values[11]; - Vector256 input12 = values[12]; - Vector256 input13 = values[13]; - Vector256 input14 = values[14]; - Vector256 input15 = values[15]; - Vector256 input16 = values[16]; - Vector256 input17 = values[17]; - Vector256 input18 = values[18]; - Vector256 input19 = values[19]; - Vector256 input20 = values[20]; - Vector256 input21 = values[21]; - Vector256 input22 = values[22]; - Vector256 input23 = values[23]; - Vector256 input24 = values[24]; - Vector256 input25 = values[25]; - Vector256 input26 = values[26]; - Vector256 input27 = values[27]; - Vector256 input28 = values[28]; - Vector256 input29 = values[29]; - Vector256 input30 = values[30]; - Vector256 input31 = values[31]; - - Vector256 v0 = input0 + input16; - Vector256 v1 = input0 - input16; - Vector256 v2 = input24 + input8; - Vector256 v3 = input8 * Vector256.Create(JxlDctScales.Sqrt2); - Vector256 v4 = v3 + v2; - Vector256 v5 = v3 - v2; - Vector256 v6 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v4, v0); - Vector256 v7 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v4, v0); - Vector256 v8 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v5, v1); - Vector256 v9 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v5, v1); - Vector256 v10 = input28 + input20; - Vector256 v11 = input20 + input12; - Vector256 v12 = input12 + input4; - Vector256 v13 = input4 * Vector256.Create(JxlDctScales.Sqrt2); - Vector256 v14 = v13 + v11; - Vector256 v15 = v13 - v11; - Vector256 v16 = v10 + v12; - Vector256 v17 = v12 * Vector256.Create(JxlDctScales.Sqrt2); - Vector256 v18 = v17 + v16; - Vector256 v19 = v17 - v16; - Vector256 v20 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v18, v14); - Vector256 v21 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v18, v14); - Vector256 v22 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v19, v15); - Vector256 v23 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v19, v15); - Vector256 v24 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5097955791041592f), v20, v6); - Vector256 v25 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5097955791041592f), v20, v6); - Vector256 v26 = Vector256.MultiplyAddEstimate(Vector256.Create(0.6013448869350453f), v22, v8); - Vector256 v27 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.6013448869350453f), v22, v8); - Vector256 v28 = Vector256.MultiplyAddEstimate(Vector256.Create(0.8999762231364156f), v23, v9); - Vector256 v29 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.8999762231364156f), v23, v9); - Vector256 v30 = Vector256.MultiplyAddEstimate(Vector256.Create(2.5629154477415055f), v21, v7); - Vector256 v31 = Vector256.MultiplyAddEstimate(-Vector256.Create(2.5629154477415055f), v21, v7); - Vector256 v32 = input30 + input26; - Vector256 v33 = input26 + input22; - Vector256 v34 = input22 + input18; - Vector256 v35 = input18 + input14; - Vector256 v36 = input14 + input10; - Vector256 v37 = input10 + input6; - Vector256 v38 = input6 + input2; - Vector256 v39 = input2 * Vector256.Create(JxlDctScales.Sqrt2); - Vector256 v40 = v39 + v35; - Vector256 v41 = v39 - v35; - Vector256 v42 = v33 + v37; - Vector256 v43 = v37 * Vector256.Create(JxlDctScales.Sqrt2); - Vector256 v44 = v43 + v42; - Vector256 v45 = v43 - v42; - Vector256 v46 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v44, v40); - Vector256 v47 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v44, v40); - Vector256 v48 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v45, v41); - Vector256 v49 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v45, v41); - Vector256 v50 = v32 + v34; - Vector256 v51 = v34 + v36; - Vector256 v52 = v36 + v38; - Vector256 v53 = v38 * Vector256.Create(JxlDctScales.Sqrt2); - Vector256 v54 = v53 + v51; - Vector256 v55 = v53 - v51; - Vector256 v56 = v50 + v52; - Vector256 v57 = v52 * Vector256.Create(JxlDctScales.Sqrt2); - Vector256 v58 = v57 + v56; - Vector256 v59 = v57 - v56; - Vector256 v60 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v58, v54); - Vector256 v61 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v58, v54); - Vector256 v62 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v59, v55); - Vector256 v63 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v59, v55); - Vector256 v64 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5097955791041592f), v60, v46); - Vector256 v65 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5097955791041592f), v60, v46); - Vector256 v66 = Vector256.MultiplyAddEstimate(Vector256.Create(0.6013448869350453f), v62, v48); - Vector256 v67 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.6013448869350453f), v62, v48); - Vector256 v68 = Vector256.MultiplyAddEstimate(Vector256.Create(0.8999762231364156f), v63, v49); - Vector256 v69 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.8999762231364156f), v63, v49); - Vector256 v70 = Vector256.MultiplyAddEstimate(Vector256.Create(2.5629154477415055f), v61, v47); - Vector256 v71 = Vector256.MultiplyAddEstimate(-Vector256.Create(2.5629154477415055f), v61, v47); - Vector256 v72 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5024192861881557f), v64, v24); - Vector256 v73 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5024192861881557f), v64, v24); - Vector256 v74 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5224986149396889f), v66, v26); - Vector256 v75 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5224986149396889f), v66, v26); - Vector256 v76 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5669440348163577f), v68, v28); - Vector256 v77 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5669440348163577f), v68, v28); - Vector256 v78 = Vector256.MultiplyAddEstimate(Vector256.Create(0.6468217833599901f), v70, v30); - Vector256 v79 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.6468217833599901f), v70, v30); - Vector256 v80 = Vector256.MultiplyAddEstimate(Vector256.Create(0.7881546234512502f), v71, v31); - Vector256 v81 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.7881546234512502f), v71, v31); - Vector256 v82 = Vector256.MultiplyAddEstimate(Vector256.Create(1.060677685990347f), v69, v29); - Vector256 v83 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.060677685990347f), v69, v29); - Vector256 v84 = Vector256.MultiplyAddEstimate(Vector256.Create(1.7224470982383342f), v67, v27); - Vector256 v85 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.7224470982383342f), v67, v27); - Vector256 v86 = Vector256.MultiplyAddEstimate(Vector256.Create(5.101148618689155f), v65, v25); - Vector256 v87 = Vector256.MultiplyAddEstimate(-Vector256.Create(5.101148618689155f), v65, v25); - Vector256 v88 = input31 + input29; - Vector256 v89 = input29 + input27; - Vector256 v90 = input27 + input25; - Vector256 v91 = input25 + input23; - Vector256 v92 = input23 + input21; - Vector256 v93 = input21 + input19; - Vector256 v94 = input19 + input17; - Vector256 v95 = input17 + input15; - Vector256 v96 = input15 + input13; - Vector256 v97 = input13 + input11; - Vector256 v98 = input11 + input9; - Vector256 v99 = input9 + input7; - Vector256 v100 = input7 + input5; - Vector256 v101 = input5 + input3; - Vector256 v102 = input3 + input1; - Vector256 v103 = input1 * Vector256.Create(JxlDctScales.Sqrt2); - Vector256 v104 = v103 + v95; - Vector256 v105 = v103 - v95; - Vector256 v106 = v91 + v99; - Vector256 v107 = v99 * Vector256.Create(JxlDctScales.Sqrt2); - Vector256 v108 = v107 + v106; - Vector256 v109 = v107 - v106; - Vector256 v110 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v108, v104); - Vector256 v111 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v108, v104); - Vector256 v112 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v109, v105); - Vector256 v113 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v109, v105); - Vector256 v114 = v89 + v93; - Vector256 v115 = v93 + v97; - Vector256 v116 = v97 + v101; - Vector256 v117 = v101 * Vector256.Create(JxlDctScales.Sqrt2); - Vector256 v118 = v117 + v115; - Vector256 v119 = v117 - v115; - Vector256 v120 = v114 + v116; - Vector256 v121 = v116 * Vector256.Create(JxlDctScales.Sqrt2); - Vector256 v122 = v121 + v120; - Vector256 v123 = v121 - v120; - Vector256 v124 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v122, v118); - Vector256 v125 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v122, v118); - Vector256 v126 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v123, v119); - Vector256 v127 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v123, v119); - Vector256 v128 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5097955791041592f), v124, v110); - Vector256 v129 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5097955791041592f), v124, v110); - Vector256 v130 = Vector256.MultiplyAddEstimate(Vector256.Create(0.6013448869350453f), v126, v112); - Vector256 v131 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.6013448869350453f), v126, v112); - Vector256 v132 = Vector256.MultiplyAddEstimate(Vector256.Create(0.8999762231364156f), v127, v113); - Vector256 v133 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.8999762231364156f), v127, v113); - Vector256 v134 = Vector256.MultiplyAddEstimate(Vector256.Create(2.5629154477415055f), v125, v111); - Vector256 v135 = Vector256.MultiplyAddEstimate(-Vector256.Create(2.5629154477415055f), v125, v111); - Vector256 v136 = v88 + v90; - Vector256 v137 = v90 + v92; - Vector256 v138 = v92 + v94; - Vector256 v139 = v94 + v96; - Vector256 v140 = v96 + v98; - Vector256 v141 = v98 + v100; - Vector256 v142 = v100 + v102; - Vector256 v143 = v102 * Vector256.Create(JxlDctScales.Sqrt2); - Vector256 v144 = v143 + v139; - Vector256 v145 = v143 - v139; - Vector256 v146 = v137 + v141; - Vector256 v147 = v141 * Vector256.Create(JxlDctScales.Sqrt2); - Vector256 v148 = v147 + v146; - Vector256 v149 = v147 - v146; - Vector256 v150 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v148, v144); - Vector256 v151 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v148, v144); - Vector256 v152 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v149, v145); - Vector256 v153 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v149, v145); - Vector256 v154 = v136 + v138; - Vector256 v155 = v138 + v140; - Vector256 v156 = v140 + v142; - Vector256 v157 = v142 * Vector256.Create(JxlDctScales.Sqrt2); - Vector256 v158 = v157 + v155; - Vector256 v159 = v157 - v155; - Vector256 v160 = v154 + v156; - Vector256 v161 = v156 * Vector256.Create(JxlDctScales.Sqrt2); - Vector256 v162 = v161 + v160; - Vector256 v163 = v161 - v160; - Vector256 v164 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v162, v158); - Vector256 v165 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v162, v158); - Vector256 v166 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v163, v159); - Vector256 v167 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v163, v159); - Vector256 v168 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5097955791041592f), v164, v150); - Vector256 v169 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5097955791041592f), v164, v150); - Vector256 v170 = Vector256.MultiplyAddEstimate(Vector256.Create(0.6013448869350453f), v166, v152); - Vector256 v171 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.6013448869350453f), v166, v152); - Vector256 v172 = Vector256.MultiplyAddEstimate(Vector256.Create(0.8999762231364156f), v167, v153); - Vector256 v173 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.8999762231364156f), v167, v153); - Vector256 v174 = Vector256.MultiplyAddEstimate(Vector256.Create(2.5629154477415055f), v165, v151); - Vector256 v175 = Vector256.MultiplyAddEstimate(-Vector256.Create(2.5629154477415055f), v165, v151); - Vector256 v176 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5024192861881557f), v168, v128); - Vector256 v177 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5024192861881557f), v168, v128); - Vector256 v178 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5224986149396889f), v170, v130); - Vector256 v179 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5224986149396889f), v170, v130); - Vector256 v180 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5669440348163577f), v172, v132); - Vector256 v181 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5669440348163577f), v172, v132); - Vector256 v182 = Vector256.MultiplyAddEstimate(Vector256.Create(0.6468217833599901f), v174, v134); - Vector256 v183 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.6468217833599901f), v174, v134); - Vector256 v184 = Vector256.MultiplyAddEstimate(Vector256.Create(0.7881546234512502f), v175, v135); - Vector256 v185 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.7881546234512502f), v175, v135); - Vector256 v186 = Vector256.MultiplyAddEstimate(Vector256.Create(1.060677685990347f), v173, v133); - Vector256 v187 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.060677685990347f), v173, v133); - Vector256 v188 = Vector256.MultiplyAddEstimate(Vector256.Create(1.7224470982383342f), v171, v131); - Vector256 v189 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.7224470982383342f), v171, v131); - Vector256 v190 = Vector256.MultiplyAddEstimate(Vector256.Create(5.101148618689155f), v169, v129); - Vector256 v191 = Vector256.MultiplyAddEstimate(-Vector256.Create(5.101148618689155f), v169, v129); - Vector256 v192 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5006029982351963f), v176, v72); - Vector256 v193 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5006029982351963f), v176, v72); - Vector256 v194 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5054709598975436f), v178, v74); - Vector256 v195 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5054709598975436f), v178, v74); - Vector256 v196 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5154473099226246f), v180, v76); - Vector256 v197 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5154473099226246f), v180, v76); - Vector256 v198 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5310425910897841f), v182, v78); - Vector256 v199 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5310425910897841f), v182, v78); - Vector256 v200 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5531038960344445f), v184, v80); - Vector256 v201 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5531038960344445f), v184, v80); - Vector256 v202 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5829349682061339f), v186, v82); - Vector256 v203 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5829349682061339f), v186, v82); - Vector256 v204 = Vector256.MultiplyAddEstimate(Vector256.Create(0.6225041230356648f), v188, v84); - Vector256 v205 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.6225041230356648f), v188, v84); - Vector256 v206 = Vector256.MultiplyAddEstimate(Vector256.Create(0.6748083414550057f), v190, v86); - Vector256 v207 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.6748083414550057f), v190, v86); - Vector256 v208 = Vector256.MultiplyAddEstimate(Vector256.Create(0.7445362710022986f), v191, v87); - Vector256 v209 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.7445362710022986f), v191, v87); - Vector256 v210 = Vector256.MultiplyAddEstimate(Vector256.Create(0.8393496454155268f), v189, v85); - Vector256 v211 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.8393496454155268f), v189, v85); - Vector256 v212 = Vector256.MultiplyAddEstimate(Vector256.Create(0.9725682378619608f), v187, v83); - Vector256 v213 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.9725682378619608f), v187, v83); - Vector256 v214 = Vector256.MultiplyAddEstimate(Vector256.Create(1.1694399334328847f), v185, v81); - Vector256 v215 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.1694399334328847f), v185, v81); - Vector256 v216 = Vector256.MultiplyAddEstimate(Vector256.Create(1.4841646163141662f), v183, v79); - Vector256 v217 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.4841646163141662f), v183, v79); - Vector256 v218 = Vector256.MultiplyAddEstimate(Vector256.Create(2.057781009953411f), v181, v77); - Vector256 v219 = Vector256.MultiplyAddEstimate(-Vector256.Create(2.057781009953411f), v181, v77); - Vector256 v220 = Vector256.MultiplyAddEstimate(Vector256.Create(3.407608418468719f), v179, v75); - Vector256 v221 = Vector256.MultiplyAddEstimate(-Vector256.Create(3.407608418468719f), v179, v75); - Vector256 v222 = Vector256.MultiplyAddEstimate(Vector256.Create(10.190008123548033f), v177, v73); - Vector256 v223 = Vector256.MultiplyAddEstimate(-Vector256.Create(10.190008123548033f), v177, v73); - - values[0] = v192; - values[1] = v194; - values[2] = v196; - values[3] = v198; - values[4] = v200; - values[5] = v202; - values[6] = v204; - values[7] = v206; - values[8] = v208; - values[9] = v210; - values[10] = v212; - values[11] = v214; - values[12] = v216; - values[13] = v218; - values[14] = v220; - values[15] = v222; - values[16] = v223; - values[17] = v221; - values[18] = v219; - values[19] = v217; - values[20] = v215; - values[21] = v213; - values[22] = v211; - values[23] = v209; - values[24] = v207; - values[25] = v205; - values[26] = v203; - values[27] = v201; - values[28] = v199; - values[29] = v197; - values[30] = v195; - values[31] = v193; - } - - /// - /// Processes complete 8-lane inverse batches along input columns. - /// - /// The number of positions in each transform. - /// The number of independent transforms. - /// The borrowed source matrix. - /// The source row stride, in floats. - /// The borrowed output matrix. - /// The destination row stride, in floats. - /// The first independent transform not already processed by a wider batch. - /// Borrowed storage for two length-by-8 banks. Only the first bank needs initialized inputs. - /// The first independent transform left after the complete batches. - private static int InverseColumns256( - int length, - int count, - ReadOnlySpan source, - int sourceStride, - Span destination, - int destinationStride, - int first, - Span workspace) - { - Span> values = MemoryMarshal.Cast>(workspace[..(length * 8)]); - Span> scratch = MemoryMarshal.Cast>(workspace.Slice(length * 8, length * 8)); - ref float input = ref MemoryMarshal.GetReference(source); - ref float output = ref MemoryMarshal.GetReference(destination); - - for (; first <= count - 8; first += 8) - { - // One vector covers adjacent columns at the same transform position. Physical strides never enter the coefficient-bank layout. - for (int i = 0; i < length; i++) - { - values[i] = Vector256.LoadUnsafe(ref input, (nuint)((i * sourceStride) + first)); - } - - Inverse(values, scratch); - for (int i = 0; i < length; i++) - { - values[i].StoreUnsafe(ref output, (nuint)((i * destinationStride) + first)); - } - } - - return first; - } - - /// - /// Processes complete 8-lane inverse batches along input rows. - /// - /// The number of positions in each transform. - /// The number of independent transforms. - /// The borrowed source matrix. - /// The source row stride, in floats. - /// The borrowed output matrix. - /// The destination row stride, in floats. - /// Whether transform positions become output rows and input rows become output columns. - /// The first independent transform not already processed by a wider batch. - /// Borrowed storage for two length-by-8 banks. Only the first bank needs initialized inputs. - /// The first independent transform left after the complete batches. - private static int InverseRows256( - int length, - int count, - ReadOnlySpan source, - int sourceStride, - Span destination, - int destinationStride, - bool transposed, - int first, - Span workspace) - { - Span> values = MemoryMarshal.Cast>(workspace[..(length * 8)]); - Span> scratch = MemoryMarshal.Cast>(workspace.Slice(length * 8, length * 8)); - ref float input = ref MemoryMarshal.GetReference(source); - ref float output = ref MemoryMarshal.GetReference(destination); - - for (; first <= count - 8; first += 8) - { - // Each square load contains adjacent samples from independent rows. Transposition puts those rows into SIMD lanes. - // The complete row band is loaded before any same-orientation output is written, so source and destination can be the same matrix. - for (int column = 0; column < length; column += 8) - { - Vector256 row0 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 0) * sourceStride) + column)).AsInt32(); - Vector256 row1 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 1) * sourceStride) + column)).AsInt32(); - Vector256 row2 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 2) * sourceStride) + column)).AsInt32(); - Vector256 row3 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 3) * sourceStride) + column)).AsInt32(); - Vector256 row4 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 4) * sourceStride) + column)).AsInt32(); - Vector256 row5 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 5) * sourceStride) + column)).AsInt32(); - Vector256 row6 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 6) * sourceStride) + column)).AsInt32(); - Vector256 row7 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 7) * sourceStride) + column)).AsInt32(); - - SimdUtils.Transpose(ref row0, ref row1, ref row2, ref row3, ref row4, ref row5, ref row6, ref row7); - values[column + 0] = row0.AsSingle(); - values[column + 1] = row1.AsSingle(); - values[column + 2] = row2.AsSingle(); - values[column + 3] = row3.AsSingle(); - values[column + 4] = row4.AsSingle(); - values[column + 5] = row5.AsSingle(); - values[column + 6] = row6.AsSingle(); - values[column + 7] = row7.AsSingle(); - } - - Inverse(values, scratch); - if (transposed) - { - // A complete vector is one contiguous output row. Only logical samples are stored; destination padding remains untouched. - for (int i = 0; i < length; i++) - { - values[i].StoreUnsafe(ref output, (nuint)((i * destinationStride) + first)); - } - } - else - { - // Restore row-major order one register tile at a time. This replaces a separate full-block transpose and copy. - for (int column = 0; column < length; column += 8) - { - Vector256 row0 = values[column + 0].AsInt32(); - Vector256 row1 = values[column + 1].AsInt32(); - Vector256 row2 = values[column + 2].AsInt32(); - Vector256 row3 = values[column + 3].AsInt32(); - Vector256 row4 = values[column + 4].AsInt32(); - Vector256 row5 = values[column + 5].AsInt32(); - Vector256 row6 = values[column + 6].AsInt32(); - Vector256 row7 = values[column + 7].AsInt32(); - - SimdUtils.Transpose(ref row0, ref row1, ref row2, ref row3, ref row4, ref row5, ref row6, ref row7); - row0.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 0) * destinationStride) + column)); - row1.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 1) * destinationStride) + column)); - row2.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 2) * destinationStride) + column)); - row3.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 3) * destinationStride) + column)); - row4.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 4) * destinationStride) + column)); - row5.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 5) * destinationStride) + column)); - row6.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 6) * destinationStride) + column)); - row7.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 7) * destinationStride) + column)); - } - } - } - - return first; - } - - /// - /// Computes independent forward transforms using two nonoverlapping borrowed vector banks. - /// - /// One vector per transform position. Its length is a power of two from one through 256. - /// At least values.Length vectors. Previous contents are not read. - private static void Forward(Span> values, Span> scratch) - { - // Small transforms stay in registers. The one-point transform is an identity and must not copy any trailing samples. - switch (values.Length) - { - case 1: - return; - case 2: - Forward2(values); - return; - case 4: - Forward4(values); - return; - case 8: - Forward8(values); - return; - case 16: - Forward16(values); - return; - case 32: - Forward32(values); - return; - } - - int half = values.Length / 2; - ReadOnlySpan weights = JxlDctScales.GetMultipliers(values.Length); - - // Consume both original halves before reusing their bank as recursive scratch. Neither recursive call can then destroy unread input. - for (int i = 0; i < half; i++) - { - Vector512 first = values[i]; - Vector512 last = values[values.Length - i - 1]; - scratch[i] = first + last; - scratch[half + i] = (first - last) * Vector512.Create(weights[i]); - } - - Forward(scratch[..half], values); - Forward(scratch.Slice(half, half), values); - scratch[half] = Vector512.MultiplyAddEstimate(scratch[half], Vector512.Create(JxlDctScales.Sqrt2), scratch[half + 1]); - - for (int i = 1; i + 1 < half; i++) - { - scratch[half + i] += scratch[half + i + 1]; - } - - for (int i = 0; i < half; i++) - { - values[2 * i] = scratch[i]; - values[(2 * i) + 1] = scratch[half + i]; - } - } - - /// - /// Computes independent 2-point forward transforms without intermediate memory passes. - /// - /// Exactly 2 position vectors, replaced with the transform results. Each lane remains independent. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Forward2(Span> values) - { - // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. - // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. - Vector512 input0 = values[0]; - Vector512 input1 = values[1]; - - Vector512 v0 = input0 + input1; - Vector512 v1 = input0 - input1; - - values[0] = v0; - values[1] = v1; - } - - /// - /// Computes independent 4-point forward transforms without intermediate memory passes. - /// - /// Exactly 4 position vectors, replaced with the transform results. Each lane remains independent. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Forward4(Span> values) - { - // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. - // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. - Vector512 input0 = values[0]; - Vector512 input1 = values[1]; - Vector512 input2 = values[2]; - Vector512 input3 = values[3]; - - Vector512 v0 = input0 + input3; - Vector512 v1 = (input0 - input3) * Vector512.Create(0.541196100146197f); - Vector512 v2 = input1 + input2; - Vector512 v3 = (input1 - input2) * Vector512.Create(1.3065629648763764f); - Vector512 v4 = v0 + v2; - Vector512 v5 = v0 - v2; - Vector512 v6 = v1 + v3; - Vector512 v7 = v1 - v3; - Vector512 v8 = Vector512.MultiplyAddEstimate(v6, Vector512.Create(JxlDctScales.Sqrt2), v7); - - values[0] = v4; - values[1] = v8; - values[2] = v5; - values[3] = v7; - } - - /// - /// Computes independent 8-point forward transforms without intermediate memory passes. - /// - /// Exactly 8 position vectors, replaced with the transform results. Each lane remains independent. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Forward8(Span> values) - { - // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. - // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. - Vector512 input0 = values[0]; - Vector512 input1 = values[1]; - Vector512 input2 = values[2]; - Vector512 input3 = values[3]; - Vector512 input4 = values[4]; - Vector512 input5 = values[5]; - Vector512 input6 = values[6]; - Vector512 input7 = values[7]; - - Vector512 v0 = input0 + input7; - Vector512 v1 = (input0 - input7) * Vector512.Create(0.5097955791041592f); - Vector512 v2 = input1 + input6; - Vector512 v3 = (input1 - input6) * Vector512.Create(0.6013448869350453f); - Vector512 v4 = input2 + input5; - Vector512 v5 = (input2 - input5) * Vector512.Create(0.8999762231364156f); - Vector512 v6 = input3 + input4; - Vector512 v7 = (input3 - input4) * Vector512.Create(2.5629154477415055f); - Vector512 v8 = v0 + v6; - Vector512 v9 = (v0 - v6) * Vector512.Create(0.541196100146197f); - Vector512 v10 = v2 + v4; - Vector512 v11 = (v2 - v4) * Vector512.Create(1.3065629648763764f); - Vector512 v12 = v8 + v10; - Vector512 v13 = v8 - v10; - Vector512 v14 = v9 + v11; - Vector512 v15 = v9 - v11; - Vector512 v16 = Vector512.MultiplyAddEstimate(v14, Vector512.Create(JxlDctScales.Sqrt2), v15); - Vector512 v17 = v1 + v7; - Vector512 v18 = (v1 - v7) * Vector512.Create(0.541196100146197f); - Vector512 v19 = v3 + v5; - Vector512 v20 = (v3 - v5) * Vector512.Create(1.3065629648763764f); - Vector512 v21 = v17 + v19; - Vector512 v22 = v17 - v19; - Vector512 v23 = v18 + v20; - Vector512 v24 = v18 - v20; - Vector512 v25 = Vector512.MultiplyAddEstimate(v23, Vector512.Create(JxlDctScales.Sqrt2), v24); - Vector512 v26 = Vector512.MultiplyAddEstimate(v21, Vector512.Create(JxlDctScales.Sqrt2), v25); - Vector512 v27 = v25 + v22; - Vector512 v28 = v22 + v24; - - values[0] = v12; - values[1] = v26; - values[2] = v16; - values[3] = v27; - values[4] = v13; - values[5] = v28; - values[6] = v15; - values[7] = v24; - } - - /// - /// Computes independent 16-point forward transforms without intermediate memory passes. - /// - /// Exactly 16 position vectors, replaced with the transform results. Each lane remains independent. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Forward16(Span> values) - { - // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. - // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. - Vector512 input0 = values[0]; - Vector512 input1 = values[1]; - Vector512 input2 = values[2]; - Vector512 input3 = values[3]; - Vector512 input4 = values[4]; - Vector512 input5 = values[5]; - Vector512 input6 = values[6]; - Vector512 input7 = values[7]; - Vector512 input8 = values[8]; - Vector512 input9 = values[9]; - Vector512 input10 = values[10]; - Vector512 input11 = values[11]; - Vector512 input12 = values[12]; - Vector512 input13 = values[13]; - Vector512 input14 = values[14]; - Vector512 input15 = values[15]; - - Vector512 v0 = input0 + input15; - Vector512 v1 = (input0 - input15) * Vector512.Create(0.5024192861881557f); - Vector512 v2 = input1 + input14; - Vector512 v3 = (input1 - input14) * Vector512.Create(0.5224986149396889f); - Vector512 v4 = input2 + input13; - Vector512 v5 = (input2 - input13) * Vector512.Create(0.5669440348163577f); - Vector512 v6 = input3 + input12; - Vector512 v7 = (input3 - input12) * Vector512.Create(0.6468217833599901f); - Vector512 v8 = input4 + input11; - Vector512 v9 = (input4 - input11) * Vector512.Create(0.7881546234512502f); - Vector512 v10 = input5 + input10; - Vector512 v11 = (input5 - input10) * Vector512.Create(1.060677685990347f); - Vector512 v12 = input6 + input9; - Vector512 v13 = (input6 - input9) * Vector512.Create(1.7224470982383342f); - Vector512 v14 = input7 + input8; - Vector512 v15 = (input7 - input8) * Vector512.Create(5.101148618689155f); - Vector512 v16 = v0 + v14; - Vector512 v17 = (v0 - v14) * Vector512.Create(0.5097955791041592f); - Vector512 v18 = v2 + v12; - Vector512 v19 = (v2 - v12) * Vector512.Create(0.6013448869350453f); - Vector512 v20 = v4 + v10; - Vector512 v21 = (v4 - v10) * Vector512.Create(0.8999762231364156f); - Vector512 v22 = v6 + v8; - Vector512 v23 = (v6 - v8) * Vector512.Create(2.5629154477415055f); - Vector512 v24 = v16 + v22; - Vector512 v25 = (v16 - v22) * Vector512.Create(0.541196100146197f); - Vector512 v26 = v18 + v20; - Vector512 v27 = (v18 - v20) * Vector512.Create(1.3065629648763764f); - Vector512 v28 = v24 + v26; - Vector512 v29 = v24 - v26; - Vector512 v30 = v25 + v27; - Vector512 v31 = v25 - v27; - Vector512 v32 = Vector512.MultiplyAddEstimate(v30, Vector512.Create(JxlDctScales.Sqrt2), v31); - Vector512 v33 = v17 + v23; - Vector512 v34 = (v17 - v23) * Vector512.Create(0.541196100146197f); - Vector512 v35 = v19 + v21; - Vector512 v36 = (v19 - v21) * Vector512.Create(1.3065629648763764f); - Vector512 v37 = v33 + v35; - Vector512 v38 = v33 - v35; - Vector512 v39 = v34 + v36; - Vector512 v40 = v34 - v36; - Vector512 v41 = Vector512.MultiplyAddEstimate(v39, Vector512.Create(JxlDctScales.Sqrt2), v40); - Vector512 v42 = Vector512.MultiplyAddEstimate(v37, Vector512.Create(JxlDctScales.Sqrt2), v41); - Vector512 v43 = v41 + v38; - Vector512 v44 = v38 + v40; - Vector512 v45 = v1 + v15; - Vector512 v46 = (v1 - v15) * Vector512.Create(0.5097955791041592f); - Vector512 v47 = v3 + v13; - Vector512 v48 = (v3 - v13) * Vector512.Create(0.6013448869350453f); - Vector512 v49 = v5 + v11; - Vector512 v50 = (v5 - v11) * Vector512.Create(0.8999762231364156f); - Vector512 v51 = v7 + v9; - Vector512 v52 = (v7 - v9) * Vector512.Create(2.5629154477415055f); - Vector512 v53 = v45 + v51; - Vector512 v54 = (v45 - v51) * Vector512.Create(0.541196100146197f); - Vector512 v55 = v47 + v49; - Vector512 v56 = (v47 - v49) * Vector512.Create(1.3065629648763764f); - Vector512 v57 = v53 + v55; - Vector512 v58 = v53 - v55; - Vector512 v59 = v54 + v56; - Vector512 v60 = v54 - v56; - Vector512 v61 = Vector512.MultiplyAddEstimate(v59, Vector512.Create(JxlDctScales.Sqrt2), v60); - Vector512 v62 = v46 + v52; - Vector512 v63 = (v46 - v52) * Vector512.Create(0.541196100146197f); - Vector512 v64 = v48 + v50; - Vector512 v65 = (v48 - v50) * Vector512.Create(1.3065629648763764f); - Vector512 v66 = v62 + v64; - Vector512 v67 = v62 - v64; - Vector512 v68 = v63 + v65; - Vector512 v69 = v63 - v65; - Vector512 v70 = Vector512.MultiplyAddEstimate(v68, Vector512.Create(JxlDctScales.Sqrt2), v69); - Vector512 v71 = Vector512.MultiplyAddEstimate(v66, Vector512.Create(JxlDctScales.Sqrt2), v70); - Vector512 v72 = v70 + v67; - Vector512 v73 = v67 + v69; - Vector512 v74 = Vector512.MultiplyAddEstimate(v57, Vector512.Create(JxlDctScales.Sqrt2), v71); - Vector512 v75 = v71 + v61; - Vector512 v76 = v61 + v72; - Vector512 v77 = v72 + v58; - Vector512 v78 = v58 + v73; - Vector512 v79 = v73 + v60; - Vector512 v80 = v60 + v69; - - values[0] = v28; - values[1] = v74; - values[2] = v42; - values[3] = v75; - values[4] = v32; - values[5] = v76; - values[6] = v43; - values[7] = v77; - values[8] = v29; - values[9] = v78; - values[10] = v44; - values[11] = v79; - values[12] = v31; - values[13] = v80; - values[14] = v40; - values[15] = v69; - } - - /// - /// Computes independent 32-point forward transforms without intermediate memory passes. - /// - /// Exactly 32 position vectors, replaced with the transform results. Each lane remains independent. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Forward32(Span> values) - { - // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. - // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. - Vector512 input0 = values[0]; - Vector512 input1 = values[1]; - Vector512 input2 = values[2]; - Vector512 input3 = values[3]; - Vector512 input4 = values[4]; - Vector512 input5 = values[5]; - Vector512 input6 = values[6]; - Vector512 input7 = values[7]; - Vector512 input8 = values[8]; - Vector512 input9 = values[9]; - Vector512 input10 = values[10]; - Vector512 input11 = values[11]; - Vector512 input12 = values[12]; - Vector512 input13 = values[13]; - Vector512 input14 = values[14]; - Vector512 input15 = values[15]; - Vector512 input16 = values[16]; - Vector512 input17 = values[17]; - Vector512 input18 = values[18]; - Vector512 input19 = values[19]; - Vector512 input20 = values[20]; - Vector512 input21 = values[21]; - Vector512 input22 = values[22]; - Vector512 input23 = values[23]; - Vector512 input24 = values[24]; - Vector512 input25 = values[25]; - Vector512 input26 = values[26]; - Vector512 input27 = values[27]; - Vector512 input28 = values[28]; - Vector512 input29 = values[29]; - Vector512 input30 = values[30]; - Vector512 input31 = values[31]; - - Vector512 v0 = input0 + input31; - Vector512 v1 = (input0 - input31) * Vector512.Create(0.5006029982351963f); - Vector512 v2 = input1 + input30; - Vector512 v3 = (input1 - input30) * Vector512.Create(0.5054709598975436f); - Vector512 v4 = input2 + input29; - Vector512 v5 = (input2 - input29) * Vector512.Create(0.5154473099226246f); - Vector512 v6 = input3 + input28; - Vector512 v7 = (input3 - input28) * Vector512.Create(0.5310425910897841f); - Vector512 v8 = input4 + input27; - Vector512 v9 = (input4 - input27) * Vector512.Create(0.5531038960344445f); - Vector512 v10 = input5 + input26; - Vector512 v11 = (input5 - input26) * Vector512.Create(0.5829349682061339f); - Vector512 v12 = input6 + input25; - Vector512 v13 = (input6 - input25) * Vector512.Create(0.6225041230356648f); - Vector512 v14 = input7 + input24; - Vector512 v15 = (input7 - input24) * Vector512.Create(0.6748083414550057f); - Vector512 v16 = input8 + input23; - Vector512 v17 = (input8 - input23) * Vector512.Create(0.7445362710022986f); - Vector512 v18 = input9 + input22; - Vector512 v19 = (input9 - input22) * Vector512.Create(0.8393496454155268f); - Vector512 v20 = input10 + input21; - Vector512 v21 = (input10 - input21) * Vector512.Create(0.9725682378619608f); - Vector512 v22 = input11 + input20; - Vector512 v23 = (input11 - input20) * Vector512.Create(1.1694399334328847f); - Vector512 v24 = input12 + input19; - Vector512 v25 = (input12 - input19) * Vector512.Create(1.4841646163141662f); - Vector512 v26 = input13 + input18; - Vector512 v27 = (input13 - input18) * Vector512.Create(2.057781009953411f); - Vector512 v28 = input14 + input17; - Vector512 v29 = (input14 - input17) * Vector512.Create(3.407608418468719f); - Vector512 v30 = input15 + input16; - Vector512 v31 = (input15 - input16) * Vector512.Create(10.190008123548033f); - Vector512 v32 = v0 + v30; - Vector512 v33 = (v0 - v30) * Vector512.Create(0.5024192861881557f); - Vector512 v34 = v2 + v28; - Vector512 v35 = (v2 - v28) * Vector512.Create(0.5224986149396889f); - Vector512 v36 = v4 + v26; - Vector512 v37 = (v4 - v26) * Vector512.Create(0.5669440348163577f); - Vector512 v38 = v6 + v24; - Vector512 v39 = (v6 - v24) * Vector512.Create(0.6468217833599901f); - Vector512 v40 = v8 + v22; - Vector512 v41 = (v8 - v22) * Vector512.Create(0.7881546234512502f); - Vector512 v42 = v10 + v20; - Vector512 v43 = (v10 - v20) * Vector512.Create(1.060677685990347f); - Vector512 v44 = v12 + v18; - Vector512 v45 = (v12 - v18) * Vector512.Create(1.7224470982383342f); - Vector512 v46 = v14 + v16; - Vector512 v47 = (v14 - v16) * Vector512.Create(5.101148618689155f); - Vector512 v48 = v32 + v46; - Vector512 v49 = (v32 - v46) * Vector512.Create(0.5097955791041592f); - Vector512 v50 = v34 + v44; - Vector512 v51 = (v34 - v44) * Vector512.Create(0.6013448869350453f); - Vector512 v52 = v36 + v42; - Vector512 v53 = (v36 - v42) * Vector512.Create(0.8999762231364156f); - Vector512 v54 = v38 + v40; - Vector512 v55 = (v38 - v40) * Vector512.Create(2.5629154477415055f); - Vector512 v56 = v48 + v54; - Vector512 v57 = (v48 - v54) * Vector512.Create(0.541196100146197f); - Vector512 v58 = v50 + v52; - Vector512 v59 = (v50 - v52) * Vector512.Create(1.3065629648763764f); - Vector512 v60 = v56 + v58; - Vector512 v61 = v56 - v58; - Vector512 v62 = v57 + v59; - Vector512 v63 = v57 - v59; - Vector512 v64 = Vector512.MultiplyAddEstimate(v62, Vector512.Create(JxlDctScales.Sqrt2), v63); - Vector512 v65 = v49 + v55; - Vector512 v66 = (v49 - v55) * Vector512.Create(0.541196100146197f); - Vector512 v67 = v51 + v53; - Vector512 v68 = (v51 - v53) * Vector512.Create(1.3065629648763764f); - Vector512 v69 = v65 + v67; - Vector512 v70 = v65 - v67; - Vector512 v71 = v66 + v68; - Vector512 v72 = v66 - v68; - Vector512 v73 = Vector512.MultiplyAddEstimate(v71, Vector512.Create(JxlDctScales.Sqrt2), v72); - Vector512 v74 = Vector512.MultiplyAddEstimate(v69, Vector512.Create(JxlDctScales.Sqrt2), v73); - Vector512 v75 = v73 + v70; - Vector512 v76 = v70 + v72; - Vector512 v77 = v33 + v47; - Vector512 v78 = (v33 - v47) * Vector512.Create(0.5097955791041592f); - Vector512 v79 = v35 + v45; - Vector512 v80 = (v35 - v45) * Vector512.Create(0.6013448869350453f); - Vector512 v81 = v37 + v43; - Vector512 v82 = (v37 - v43) * Vector512.Create(0.8999762231364156f); - Vector512 v83 = v39 + v41; - Vector512 v84 = (v39 - v41) * Vector512.Create(2.5629154477415055f); - Vector512 v85 = v77 + v83; - Vector512 v86 = (v77 - v83) * Vector512.Create(0.541196100146197f); - Vector512 v87 = v79 + v81; - Vector512 v88 = (v79 - v81) * Vector512.Create(1.3065629648763764f); - Vector512 v89 = v85 + v87; - Vector512 v90 = v85 - v87; - Vector512 v91 = v86 + v88; - Vector512 v92 = v86 - v88; - Vector512 v93 = Vector512.MultiplyAddEstimate(v91, Vector512.Create(JxlDctScales.Sqrt2), v92); - Vector512 v94 = v78 + v84; - Vector512 v95 = (v78 - v84) * Vector512.Create(0.541196100146197f); - Vector512 v96 = v80 + v82; - Vector512 v97 = (v80 - v82) * Vector512.Create(1.3065629648763764f); - Vector512 v98 = v94 + v96; - Vector512 v99 = v94 - v96; - Vector512 v100 = v95 + v97; - Vector512 v101 = v95 - v97; - Vector512 v102 = Vector512.MultiplyAddEstimate(v100, Vector512.Create(JxlDctScales.Sqrt2), v101); - Vector512 v103 = Vector512.MultiplyAddEstimate(v98, Vector512.Create(JxlDctScales.Sqrt2), v102); - Vector512 v104 = v102 + v99; - Vector512 v105 = v99 + v101; - Vector512 v106 = Vector512.MultiplyAddEstimate(v89, Vector512.Create(JxlDctScales.Sqrt2), v103); - Vector512 v107 = v103 + v93; - Vector512 v108 = v93 + v104; - Vector512 v109 = v104 + v90; - Vector512 v110 = v90 + v105; - Vector512 v111 = v105 + v92; - Vector512 v112 = v92 + v101; - Vector512 v113 = v1 + v31; - Vector512 v114 = (v1 - v31) * Vector512.Create(0.5024192861881557f); - Vector512 v115 = v3 + v29; - Vector512 v116 = (v3 - v29) * Vector512.Create(0.5224986149396889f); - Vector512 v117 = v5 + v27; - Vector512 v118 = (v5 - v27) * Vector512.Create(0.5669440348163577f); - Vector512 v119 = v7 + v25; - Vector512 v120 = (v7 - v25) * Vector512.Create(0.6468217833599901f); - Vector512 v121 = v9 + v23; - Vector512 v122 = (v9 - v23) * Vector512.Create(0.7881546234512502f); - Vector512 v123 = v11 + v21; - Vector512 v124 = (v11 - v21) * Vector512.Create(1.060677685990347f); - Vector512 v125 = v13 + v19; - Vector512 v126 = (v13 - v19) * Vector512.Create(1.7224470982383342f); - Vector512 v127 = v15 + v17; - Vector512 v128 = (v15 - v17) * Vector512.Create(5.101148618689155f); - Vector512 v129 = v113 + v127; - Vector512 v130 = (v113 - v127) * Vector512.Create(0.5097955791041592f); - Vector512 v131 = v115 + v125; - Vector512 v132 = (v115 - v125) * Vector512.Create(0.6013448869350453f); - Vector512 v133 = v117 + v123; - Vector512 v134 = (v117 - v123) * Vector512.Create(0.8999762231364156f); - Vector512 v135 = v119 + v121; - Vector512 v136 = (v119 - v121) * Vector512.Create(2.5629154477415055f); - Vector512 v137 = v129 + v135; - Vector512 v138 = (v129 - v135) * Vector512.Create(0.541196100146197f); - Vector512 v139 = v131 + v133; - Vector512 v140 = (v131 - v133) * Vector512.Create(1.3065629648763764f); - Vector512 v141 = v137 + v139; - Vector512 v142 = v137 - v139; - Vector512 v143 = v138 + v140; - Vector512 v144 = v138 - v140; - Vector512 v145 = Vector512.MultiplyAddEstimate(v143, Vector512.Create(JxlDctScales.Sqrt2), v144); - Vector512 v146 = v130 + v136; - Vector512 v147 = (v130 - v136) * Vector512.Create(0.541196100146197f); - Vector512 v148 = v132 + v134; - Vector512 v149 = (v132 - v134) * Vector512.Create(1.3065629648763764f); - Vector512 v150 = v146 + v148; - Vector512 v151 = v146 - v148; - Vector512 v152 = v147 + v149; - Vector512 v153 = v147 - v149; - Vector512 v154 = Vector512.MultiplyAddEstimate(v152, Vector512.Create(JxlDctScales.Sqrt2), v153); - Vector512 v155 = Vector512.MultiplyAddEstimate(v150, Vector512.Create(JxlDctScales.Sqrt2), v154); - Vector512 v156 = v154 + v151; - Vector512 v157 = v151 + v153; - Vector512 v158 = v114 + v128; - Vector512 v159 = (v114 - v128) * Vector512.Create(0.5097955791041592f); - Vector512 v160 = v116 + v126; - Vector512 v161 = (v116 - v126) * Vector512.Create(0.6013448869350453f); - Vector512 v162 = v118 + v124; - Vector512 v163 = (v118 - v124) * Vector512.Create(0.8999762231364156f); - Vector512 v164 = v120 + v122; - Vector512 v165 = (v120 - v122) * Vector512.Create(2.5629154477415055f); - Vector512 v166 = v158 + v164; - Vector512 v167 = (v158 - v164) * Vector512.Create(0.541196100146197f); - Vector512 v168 = v160 + v162; - Vector512 v169 = (v160 - v162) * Vector512.Create(1.3065629648763764f); - Vector512 v170 = v166 + v168; - Vector512 v171 = v166 - v168; - Vector512 v172 = v167 + v169; - Vector512 v173 = v167 - v169; - Vector512 v174 = Vector512.MultiplyAddEstimate(v172, Vector512.Create(JxlDctScales.Sqrt2), v173); - Vector512 v175 = v159 + v165; - Vector512 v176 = (v159 - v165) * Vector512.Create(0.541196100146197f); - Vector512 v177 = v161 + v163; - Vector512 v178 = (v161 - v163) * Vector512.Create(1.3065629648763764f); - Vector512 v179 = v175 + v177; - Vector512 v180 = v175 - v177; - Vector512 v181 = v176 + v178; - Vector512 v182 = v176 - v178; - Vector512 v183 = Vector512.MultiplyAddEstimate(v181, Vector512.Create(JxlDctScales.Sqrt2), v182); - Vector512 v184 = Vector512.MultiplyAddEstimate(v179, Vector512.Create(JxlDctScales.Sqrt2), v183); - Vector512 v185 = v183 + v180; - Vector512 v186 = v180 + v182; - Vector512 v187 = Vector512.MultiplyAddEstimate(v170, Vector512.Create(JxlDctScales.Sqrt2), v184); - Vector512 v188 = v184 + v174; - Vector512 v189 = v174 + v185; - Vector512 v190 = v185 + v171; - Vector512 v191 = v171 + v186; - Vector512 v192 = v186 + v173; - Vector512 v193 = v173 + v182; - Vector512 v194 = Vector512.MultiplyAddEstimate(v141, Vector512.Create(JxlDctScales.Sqrt2), v187); - Vector512 v195 = v187 + v155; - Vector512 v196 = v155 + v188; - Vector512 v197 = v188 + v145; - Vector512 v198 = v145 + v189; - Vector512 v199 = v189 + v156; - Vector512 v200 = v156 + v190; - Vector512 v201 = v190 + v142; - Vector512 v202 = v142 + v191; - Vector512 v203 = v191 + v157; - Vector512 v204 = v157 + v192; - Vector512 v205 = v192 + v144; - Vector512 v206 = v144 + v193; - Vector512 v207 = v193 + v153; - Vector512 v208 = v153 + v182; - - values[0] = v60; - values[1] = v194; - values[2] = v106; - values[3] = v195; - values[4] = v74; - values[5] = v196; - values[6] = v107; - values[7] = v197; - values[8] = v64; - values[9] = v198; - values[10] = v108; - values[11] = v199; - values[12] = v75; - values[13] = v200; - values[14] = v109; - values[15] = v201; - values[16] = v61; - values[17] = v202; - values[18] = v110; - values[19] = v203; - values[20] = v76; - values[21] = v204; - values[22] = v111; - values[23] = v205; - values[24] = v63; - values[25] = v206; - values[26] = v112; - values[27] = v207; - values[28] = v72; - values[29] = v208; - values[30] = v101; - values[31] = v182; - } - - /// - /// Processes complete 16-lane forward batches along input columns. - /// - /// The number of positions in each transform. - /// The number of independent transforms. - /// The borrowed source matrix. - /// The source row stride, in floats. - /// The borrowed output matrix. - /// The destination row stride, in floats. - /// The first independent transform not already processed by a wider batch. - /// Borrowed storage for two length-by-16 banks. Only the first bank needs initialized inputs. - /// The first independent transform left after the complete batches. - private static int ForwardColumns512( - int length, - int count, - ReadOnlySpan source, - int sourceStride, - Span destination, - int destinationStride, - int first, - Span workspace) - { - Span> values = MemoryMarshal.Cast>(workspace[..(length * 16)]); - Span> scratch = MemoryMarshal.Cast>(workspace.Slice(length * 16, length * 16)); - ref float input = ref MemoryMarshal.GetReference(source); - ref float output = ref MemoryMarshal.GetReference(destination); - Vector512 scale = Vector512.Create(1F / length); - - for (; first <= count - 16; first += 16) - { - // One vector covers adjacent columns at the same transform position. Physical strides never enter the coefficient-bank layout. - for (int i = 0; i < length; i++) - { - values[i] = Vector512.LoadUnsafe(ref input, (nuint)((i * sourceStride) + first)); - } - - Forward(values, scratch); - for (int i = 0; i < length; i++) - { - (values[i] * scale).StoreUnsafe(ref output, (nuint)((i * destinationStride) + first)); - } - } - - return first; - } - - /// - /// Processes complete 16-lane forward batches along input rows. - /// - /// The number of positions in each transform. - /// The number of independent transforms. - /// The borrowed source matrix. - /// The source row stride, in floats. - /// The borrowed output matrix. - /// The destination row stride, in floats. - /// Whether transform positions become output rows and input rows become output columns. - /// The first independent transform not already processed by a wider batch. - /// Borrowed storage for two length-by-16 banks. Only the first bank needs initialized inputs. - /// The first independent transform left after the complete batches. - private static int ForwardRows512( - int length, - int count, - ReadOnlySpan source, - int sourceStride, - Span destination, - int destinationStride, - bool transposed, - int first, - Span workspace) - { - Span> values = MemoryMarshal.Cast>(workspace[..(length * 16)]); - Span> scratch = MemoryMarshal.Cast>(workspace.Slice(length * 16, length * 16)); - ref float input = ref MemoryMarshal.GetReference(source); - ref float output = ref MemoryMarshal.GetReference(destination); - Vector512 scale = Vector512.Create(1F / length); - - for (; first <= count - 16; first += 16) - { - // Each square load contains adjacent samples from independent rows. Transposition puts those rows into SIMD lanes. - // The complete row band is loaded before any same-orientation output is written, so source and destination can be the same matrix. - for (int column = 0; column < length; column += 16) - { - Vector512 row0 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 0) * sourceStride) + column)).AsInt32(); - Vector512 row1 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 1) * sourceStride) + column)).AsInt32(); - Vector512 row2 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 2) * sourceStride) + column)).AsInt32(); - Vector512 row3 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 3) * sourceStride) + column)).AsInt32(); - Vector512 row4 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 4) * sourceStride) + column)).AsInt32(); - Vector512 row5 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 5) * sourceStride) + column)).AsInt32(); - Vector512 row6 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 6) * sourceStride) + column)).AsInt32(); - Vector512 row7 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 7) * sourceStride) + column)).AsInt32(); - Vector512 row8 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 8) * sourceStride) + column)).AsInt32(); - Vector512 row9 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 9) * sourceStride) + column)).AsInt32(); - Vector512 row10 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 10) * sourceStride) + column)).AsInt32(); - Vector512 row11 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 11) * sourceStride) + column)).AsInt32(); - Vector512 row12 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 12) * sourceStride) + column)).AsInt32(); - Vector512 row13 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 13) * sourceStride) + column)).AsInt32(); - Vector512 row14 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 14) * sourceStride) + column)).AsInt32(); - Vector512 row15 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 15) * sourceStride) + column)).AsInt32(); - - SimdUtils.Transpose( - ref row0, - ref row1, - ref row2, - ref row3, - ref row4, - ref row5, - ref row6, - ref row7, - ref row8, - ref row9, - ref row10, - ref row11, - ref row12, - ref row13, - ref row14, - ref row15); - - values[column + 0] = row0.AsSingle(); - values[column + 1] = row1.AsSingle(); - values[column + 2] = row2.AsSingle(); - values[column + 3] = row3.AsSingle(); - values[column + 4] = row4.AsSingle(); - values[column + 5] = row5.AsSingle(); - values[column + 6] = row6.AsSingle(); - values[column + 7] = row7.AsSingle(); - values[column + 8] = row8.AsSingle(); - values[column + 9] = row9.AsSingle(); - values[column + 10] = row10.AsSingle(); - values[column + 11] = row11.AsSingle(); - values[column + 12] = row12.AsSingle(); - values[column + 13] = row13.AsSingle(); - values[column + 14] = row14.AsSingle(); - values[column + 15] = row15.AsSingle(); - } - - Forward(values, scratch); - if (transposed) - { - // A complete vector is one contiguous output row. Only logical samples are stored; destination padding remains untouched. - for (int i = 0; i < length; i++) - { - (values[i] * scale).StoreUnsafe(ref output, (nuint)((i * destinationStride) + first)); - } - } - else - { - // Restore row-major order one register tile at a time. This replaces a separate full-block transpose and copy. - for (int column = 0; column < length; column += 16) - { - Vector512 row0 = (values[column + 0] * scale).AsInt32(); - Vector512 row1 = (values[column + 1] * scale).AsInt32(); - Vector512 row2 = (values[column + 2] * scale).AsInt32(); - Vector512 row3 = (values[column + 3] * scale).AsInt32(); - Vector512 row4 = (values[column + 4] * scale).AsInt32(); - Vector512 row5 = (values[column + 5] * scale).AsInt32(); - Vector512 row6 = (values[column + 6] * scale).AsInt32(); - Vector512 row7 = (values[column + 7] * scale).AsInt32(); - Vector512 row8 = (values[column + 8] * scale).AsInt32(); - Vector512 row9 = (values[column + 9] * scale).AsInt32(); - Vector512 row10 = (values[column + 10] * scale).AsInt32(); - Vector512 row11 = (values[column + 11] * scale).AsInt32(); - Vector512 row12 = (values[column + 12] * scale).AsInt32(); - Vector512 row13 = (values[column + 13] * scale).AsInt32(); - Vector512 row14 = (values[column + 14] * scale).AsInt32(); - Vector512 row15 = (values[column + 15] * scale).AsInt32(); - - SimdUtils.Transpose( - ref row0, - ref row1, - ref row2, - ref row3, - ref row4, - ref row5, - ref row6, - ref row7, - ref row8, - ref row9, - ref row10, - ref row11, - ref row12, - ref row13, - ref row14, - ref row15); - - row0.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 0) * destinationStride) + column)); - row1.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 1) * destinationStride) + column)); - row2.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 2) * destinationStride) + column)); - row3.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 3) * destinationStride) + column)); - row4.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 4) * destinationStride) + column)); - row5.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 5) * destinationStride) + column)); - row6.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 6) * destinationStride) + column)); - row7.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 7) * destinationStride) + column)); - row8.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 8) * destinationStride) + column)); - row9.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 9) * destinationStride) + column)); - row10.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 10) * destinationStride) + column)); - row11.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 11) * destinationStride) + column)); - row12.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 12) * destinationStride) + column)); - row13.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 13) * destinationStride) + column)); - row14.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 14) * destinationStride) + column)); - row15.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 15) * destinationStride) + column)); - } - } - } - - return first; - } - - /// - /// Computes independent inverse transforms using two nonoverlapping borrowed vector banks. - /// - /// One vector per transform position. Its length is a power of two from one through 256. - /// At least values.Length vectors. Previous contents are not read. - private static void Inverse(Span> values, Span> scratch) - { - // Small transforms stay in registers. The one-point transform is an identity and must not copy any trailing samples. - switch (values.Length) - { - case 1: - return; - case 2: - Inverse2(values); - return; - case 4: - Inverse4(values); - return; - case 8: - Inverse8(values); - return; - case 16: - Inverse16(values); - return; - case 32: - Inverse32(values); - return; - } - - int half = values.Length / 2; - // Separate frequency parity before either recursive call. The original bank can then serve as recursive scratch. - for (int i = 0; i < half; i++) - { - scratch[i] = values[2 * i]; - scratch[half + i] = values[(2 * i) + 1]; - } - - Inverse(scratch[..half], values); - - // Descend so every odd-frequency predecessor is read before its own entry changes. - for (int i = half - 1; i > 0; i--) - { - scratch[half + i] += scratch[half + i - 1]; - } - - scratch[half] *= Vector512.Create(JxlDctScales.Sqrt2); - Inverse(scratch.Slice(half, half), values); - ReadOnlySpan weights = JxlDctScales.GetMultipliers(values.Length); - - for (int i = 0; i < half; i++) - { - Vector512 weight = Vector512.Create(weights[i]); - Vector512 even = scratch[i]; - Vector512 odd = scratch[half + i]; - values[i] = Vector512.MultiplyAddEstimate(weight, odd, even); - values[values.Length - i - 1] = Vector512.MultiplyAddEstimate(-weight, odd, even); - } - } - - /// - /// Computes independent 2-point inverse transforms without intermediate memory passes. - /// - /// Exactly 2 position vectors, replaced with the transform results. Each lane remains independent. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Inverse2(Span> values) - { - // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. - // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. - Vector512 input0 = values[0]; - Vector512 input1 = values[1]; - - Vector512 v0 = input0 + input1; - Vector512 v1 = input0 - input1; - - values[0] = v0; - values[1] = v1; - } - - /// - /// Computes independent 4-point inverse transforms without intermediate memory passes. - /// - /// Exactly 4 position vectors, replaced with the transform results. Each lane remains independent. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Inverse4(Span> values) - { - // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. - // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. - Vector512 input0 = values[0]; - Vector512 input1 = values[1]; - Vector512 input2 = values[2]; - Vector512 input3 = values[3]; - - Vector512 v0 = input0 + input2; - Vector512 v1 = input0 - input2; - Vector512 v2 = input3 + input1; - Vector512 v3 = input1 * Vector512.Create(JxlDctScales.Sqrt2); - Vector512 v4 = v3 + v2; - Vector512 v5 = v3 - v2; - Vector512 v6 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v4, v0); - Vector512 v7 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v4, v0); - Vector512 v8 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v5, v1); - Vector512 v9 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v5, v1); - - values[0] = v6; - values[1] = v8; - values[2] = v9; - values[3] = v7; - } - - /// - /// Computes independent 8-point inverse transforms without intermediate memory passes. - /// - /// Exactly 8 position vectors, replaced with the transform results. Each lane remains independent. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Inverse8(Span> values) - { - // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. - // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. - Vector512 input0 = values[0]; - Vector512 input1 = values[1]; - Vector512 input2 = values[2]; - Vector512 input3 = values[3]; - Vector512 input4 = values[4]; - Vector512 input5 = values[5]; - Vector512 input6 = values[6]; - Vector512 input7 = values[7]; - - Vector512 v0 = input0 + input4; - Vector512 v1 = input0 - input4; - Vector512 v2 = input6 + input2; - Vector512 v3 = input2 * Vector512.Create(JxlDctScales.Sqrt2); - Vector512 v4 = v3 + v2; - Vector512 v5 = v3 - v2; - Vector512 v6 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v4, v0); - Vector512 v7 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v4, v0); - Vector512 v8 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v5, v1); - Vector512 v9 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v5, v1); - Vector512 v10 = input7 + input5; - Vector512 v11 = input5 + input3; - Vector512 v12 = input3 + input1; - Vector512 v13 = input1 * Vector512.Create(JxlDctScales.Sqrt2); - Vector512 v14 = v13 + v11; - Vector512 v15 = v13 - v11; - Vector512 v16 = v10 + v12; - Vector512 v17 = v12 * Vector512.Create(JxlDctScales.Sqrt2); - Vector512 v18 = v17 + v16; - Vector512 v19 = v17 - v16; - Vector512 v20 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v18, v14); - Vector512 v21 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v18, v14); - Vector512 v22 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v19, v15); - Vector512 v23 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v19, v15); - Vector512 v24 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5097955791041592f), v20, v6); - Vector512 v25 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5097955791041592f), v20, v6); - Vector512 v26 = Vector512.MultiplyAddEstimate(Vector512.Create(0.6013448869350453f), v22, v8); - Vector512 v27 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.6013448869350453f), v22, v8); - Vector512 v28 = Vector512.MultiplyAddEstimate(Vector512.Create(0.8999762231364156f), v23, v9); - Vector512 v29 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.8999762231364156f), v23, v9); - Vector512 v30 = Vector512.MultiplyAddEstimate(Vector512.Create(2.5629154477415055f), v21, v7); - Vector512 v31 = Vector512.MultiplyAddEstimate(-Vector512.Create(2.5629154477415055f), v21, v7); - - values[0] = v24; - values[1] = v26; - values[2] = v28; - values[3] = v30; - values[4] = v31; - values[5] = v29; - values[6] = v27; - values[7] = v25; - } - - /// - /// Computes independent 16-point inverse transforms without intermediate memory passes. - /// - /// Exactly 16 position vectors, replaced with the transform results. Each lane remains independent. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Inverse16(Span> values) - { - // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. - // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. - Vector512 input0 = values[0]; - Vector512 input1 = values[1]; - Vector512 input2 = values[2]; - Vector512 input3 = values[3]; - Vector512 input4 = values[4]; - Vector512 input5 = values[5]; - Vector512 input6 = values[6]; - Vector512 input7 = values[7]; - Vector512 input8 = values[8]; - Vector512 input9 = values[9]; - Vector512 input10 = values[10]; - Vector512 input11 = values[11]; - Vector512 input12 = values[12]; - Vector512 input13 = values[13]; - Vector512 input14 = values[14]; - Vector512 input15 = values[15]; - - Vector512 v0 = input0 + input8; - Vector512 v1 = input0 - input8; - Vector512 v2 = input12 + input4; - Vector512 v3 = input4 * Vector512.Create(JxlDctScales.Sqrt2); - Vector512 v4 = v3 + v2; - Vector512 v5 = v3 - v2; - Vector512 v6 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v4, v0); - Vector512 v7 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v4, v0); - Vector512 v8 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v5, v1); - Vector512 v9 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v5, v1); - Vector512 v10 = input14 + input10; - Vector512 v11 = input10 + input6; - Vector512 v12 = input6 + input2; - Vector512 v13 = input2 * Vector512.Create(JxlDctScales.Sqrt2); - Vector512 v14 = v13 + v11; - Vector512 v15 = v13 - v11; - Vector512 v16 = v10 + v12; - Vector512 v17 = v12 * Vector512.Create(JxlDctScales.Sqrt2); - Vector512 v18 = v17 + v16; - Vector512 v19 = v17 - v16; - Vector512 v20 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v18, v14); - Vector512 v21 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v18, v14); - Vector512 v22 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v19, v15); - Vector512 v23 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v19, v15); - Vector512 v24 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5097955791041592f), v20, v6); - Vector512 v25 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5097955791041592f), v20, v6); - Vector512 v26 = Vector512.MultiplyAddEstimate(Vector512.Create(0.6013448869350453f), v22, v8); - Vector512 v27 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.6013448869350453f), v22, v8); - Vector512 v28 = Vector512.MultiplyAddEstimate(Vector512.Create(0.8999762231364156f), v23, v9); - Vector512 v29 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.8999762231364156f), v23, v9); - Vector512 v30 = Vector512.MultiplyAddEstimate(Vector512.Create(2.5629154477415055f), v21, v7); - Vector512 v31 = Vector512.MultiplyAddEstimate(-Vector512.Create(2.5629154477415055f), v21, v7); - Vector512 v32 = input15 + input13; - Vector512 v33 = input13 + input11; - Vector512 v34 = input11 + input9; - Vector512 v35 = input9 + input7; - Vector512 v36 = input7 + input5; - Vector512 v37 = input5 + input3; - Vector512 v38 = input3 + input1; - Vector512 v39 = input1 * Vector512.Create(JxlDctScales.Sqrt2); - Vector512 v40 = v39 + v35; - Vector512 v41 = v39 - v35; - Vector512 v42 = v33 + v37; - Vector512 v43 = v37 * Vector512.Create(JxlDctScales.Sqrt2); - Vector512 v44 = v43 + v42; - Vector512 v45 = v43 - v42; - Vector512 v46 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v44, v40); - Vector512 v47 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v44, v40); - Vector512 v48 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v45, v41); - Vector512 v49 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v45, v41); - Vector512 v50 = v32 + v34; - Vector512 v51 = v34 + v36; - Vector512 v52 = v36 + v38; - Vector512 v53 = v38 * Vector512.Create(JxlDctScales.Sqrt2); - Vector512 v54 = v53 + v51; - Vector512 v55 = v53 - v51; - Vector512 v56 = v50 + v52; - Vector512 v57 = v52 * Vector512.Create(JxlDctScales.Sqrt2); - Vector512 v58 = v57 + v56; - Vector512 v59 = v57 - v56; - Vector512 v60 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v58, v54); - Vector512 v61 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v58, v54); - Vector512 v62 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v59, v55); - Vector512 v63 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v59, v55); - Vector512 v64 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5097955791041592f), v60, v46); - Vector512 v65 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5097955791041592f), v60, v46); - Vector512 v66 = Vector512.MultiplyAddEstimate(Vector512.Create(0.6013448869350453f), v62, v48); - Vector512 v67 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.6013448869350453f), v62, v48); - Vector512 v68 = Vector512.MultiplyAddEstimate(Vector512.Create(0.8999762231364156f), v63, v49); - Vector512 v69 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.8999762231364156f), v63, v49); - Vector512 v70 = Vector512.MultiplyAddEstimate(Vector512.Create(2.5629154477415055f), v61, v47); - Vector512 v71 = Vector512.MultiplyAddEstimate(-Vector512.Create(2.5629154477415055f), v61, v47); - Vector512 v72 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5024192861881557f), v64, v24); - Vector512 v73 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5024192861881557f), v64, v24); - Vector512 v74 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5224986149396889f), v66, v26); - Vector512 v75 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5224986149396889f), v66, v26); - Vector512 v76 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5669440348163577f), v68, v28); - Vector512 v77 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5669440348163577f), v68, v28); - Vector512 v78 = Vector512.MultiplyAddEstimate(Vector512.Create(0.6468217833599901f), v70, v30); - Vector512 v79 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.6468217833599901f), v70, v30); - Vector512 v80 = Vector512.MultiplyAddEstimate(Vector512.Create(0.7881546234512502f), v71, v31); - Vector512 v81 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.7881546234512502f), v71, v31); - Vector512 v82 = Vector512.MultiplyAddEstimate(Vector512.Create(1.060677685990347f), v69, v29); - Vector512 v83 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.060677685990347f), v69, v29); - Vector512 v84 = Vector512.MultiplyAddEstimate(Vector512.Create(1.7224470982383342f), v67, v27); - Vector512 v85 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.7224470982383342f), v67, v27); - Vector512 v86 = Vector512.MultiplyAddEstimate(Vector512.Create(5.101148618689155f), v65, v25); - Vector512 v87 = Vector512.MultiplyAddEstimate(-Vector512.Create(5.101148618689155f), v65, v25); - - values[0] = v72; - values[1] = v74; - values[2] = v76; - values[3] = v78; - values[4] = v80; - values[5] = v82; - values[6] = v84; - values[7] = v86; - values[8] = v87; - values[9] = v85; - values[10] = v83; - values[11] = v81; - values[12] = v79; - values[13] = v77; - values[14] = v75; - values[15] = v73; - } - - /// - /// Computes independent 32-point inverse transforms without intermediate memory passes. - /// - /// Exactly 32 position vectors, replaced with the transform results. Each lane remains independent. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Inverse32(Span> values) - { - // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. - // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. - Vector512 input0 = values[0]; - Vector512 input1 = values[1]; - Vector512 input2 = values[2]; - Vector512 input3 = values[3]; - Vector512 input4 = values[4]; - Vector512 input5 = values[5]; - Vector512 input6 = values[6]; - Vector512 input7 = values[7]; - Vector512 input8 = values[8]; - Vector512 input9 = values[9]; - Vector512 input10 = values[10]; - Vector512 input11 = values[11]; - Vector512 input12 = values[12]; - Vector512 input13 = values[13]; - Vector512 input14 = values[14]; - Vector512 input15 = values[15]; - Vector512 input16 = values[16]; - Vector512 input17 = values[17]; - Vector512 input18 = values[18]; - Vector512 input19 = values[19]; - Vector512 input20 = values[20]; - Vector512 input21 = values[21]; - Vector512 input22 = values[22]; - Vector512 input23 = values[23]; - Vector512 input24 = values[24]; - Vector512 input25 = values[25]; - Vector512 input26 = values[26]; - Vector512 input27 = values[27]; - Vector512 input28 = values[28]; - Vector512 input29 = values[29]; - Vector512 input30 = values[30]; - Vector512 input31 = values[31]; - - Vector512 v0 = input0 + input16; - Vector512 v1 = input0 - input16; - Vector512 v2 = input24 + input8; - Vector512 v3 = input8 * Vector512.Create(JxlDctScales.Sqrt2); - Vector512 v4 = v3 + v2; - Vector512 v5 = v3 - v2; - Vector512 v6 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v4, v0); - Vector512 v7 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v4, v0); - Vector512 v8 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v5, v1); - Vector512 v9 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v5, v1); - Vector512 v10 = input28 + input20; - Vector512 v11 = input20 + input12; - Vector512 v12 = input12 + input4; - Vector512 v13 = input4 * Vector512.Create(JxlDctScales.Sqrt2); - Vector512 v14 = v13 + v11; - Vector512 v15 = v13 - v11; - Vector512 v16 = v10 + v12; - Vector512 v17 = v12 * Vector512.Create(JxlDctScales.Sqrt2); - Vector512 v18 = v17 + v16; - Vector512 v19 = v17 - v16; - Vector512 v20 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v18, v14); - Vector512 v21 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v18, v14); - Vector512 v22 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v19, v15); - Vector512 v23 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v19, v15); - Vector512 v24 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5097955791041592f), v20, v6); - Vector512 v25 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5097955791041592f), v20, v6); - Vector512 v26 = Vector512.MultiplyAddEstimate(Vector512.Create(0.6013448869350453f), v22, v8); - Vector512 v27 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.6013448869350453f), v22, v8); - Vector512 v28 = Vector512.MultiplyAddEstimate(Vector512.Create(0.8999762231364156f), v23, v9); - Vector512 v29 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.8999762231364156f), v23, v9); - Vector512 v30 = Vector512.MultiplyAddEstimate(Vector512.Create(2.5629154477415055f), v21, v7); - Vector512 v31 = Vector512.MultiplyAddEstimate(-Vector512.Create(2.5629154477415055f), v21, v7); - Vector512 v32 = input30 + input26; - Vector512 v33 = input26 + input22; - Vector512 v34 = input22 + input18; - Vector512 v35 = input18 + input14; - Vector512 v36 = input14 + input10; - Vector512 v37 = input10 + input6; - Vector512 v38 = input6 + input2; - Vector512 v39 = input2 * Vector512.Create(JxlDctScales.Sqrt2); - Vector512 v40 = v39 + v35; - Vector512 v41 = v39 - v35; - Vector512 v42 = v33 + v37; - Vector512 v43 = v37 * Vector512.Create(JxlDctScales.Sqrt2); - Vector512 v44 = v43 + v42; - Vector512 v45 = v43 - v42; - Vector512 v46 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v44, v40); - Vector512 v47 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v44, v40); - Vector512 v48 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v45, v41); - Vector512 v49 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v45, v41); - Vector512 v50 = v32 + v34; - Vector512 v51 = v34 + v36; - Vector512 v52 = v36 + v38; - Vector512 v53 = v38 * Vector512.Create(JxlDctScales.Sqrt2); - Vector512 v54 = v53 + v51; - Vector512 v55 = v53 - v51; - Vector512 v56 = v50 + v52; - Vector512 v57 = v52 * Vector512.Create(JxlDctScales.Sqrt2); - Vector512 v58 = v57 + v56; - Vector512 v59 = v57 - v56; - Vector512 v60 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v58, v54); - Vector512 v61 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v58, v54); - Vector512 v62 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v59, v55); - Vector512 v63 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v59, v55); - Vector512 v64 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5097955791041592f), v60, v46); - Vector512 v65 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5097955791041592f), v60, v46); - Vector512 v66 = Vector512.MultiplyAddEstimate(Vector512.Create(0.6013448869350453f), v62, v48); - Vector512 v67 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.6013448869350453f), v62, v48); - Vector512 v68 = Vector512.MultiplyAddEstimate(Vector512.Create(0.8999762231364156f), v63, v49); - Vector512 v69 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.8999762231364156f), v63, v49); - Vector512 v70 = Vector512.MultiplyAddEstimate(Vector512.Create(2.5629154477415055f), v61, v47); - Vector512 v71 = Vector512.MultiplyAddEstimate(-Vector512.Create(2.5629154477415055f), v61, v47); - Vector512 v72 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5024192861881557f), v64, v24); - Vector512 v73 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5024192861881557f), v64, v24); - Vector512 v74 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5224986149396889f), v66, v26); - Vector512 v75 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5224986149396889f), v66, v26); - Vector512 v76 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5669440348163577f), v68, v28); - Vector512 v77 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5669440348163577f), v68, v28); - Vector512 v78 = Vector512.MultiplyAddEstimate(Vector512.Create(0.6468217833599901f), v70, v30); - Vector512 v79 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.6468217833599901f), v70, v30); - Vector512 v80 = Vector512.MultiplyAddEstimate(Vector512.Create(0.7881546234512502f), v71, v31); - Vector512 v81 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.7881546234512502f), v71, v31); - Vector512 v82 = Vector512.MultiplyAddEstimate(Vector512.Create(1.060677685990347f), v69, v29); - Vector512 v83 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.060677685990347f), v69, v29); - Vector512 v84 = Vector512.MultiplyAddEstimate(Vector512.Create(1.7224470982383342f), v67, v27); - Vector512 v85 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.7224470982383342f), v67, v27); - Vector512 v86 = Vector512.MultiplyAddEstimate(Vector512.Create(5.101148618689155f), v65, v25); - Vector512 v87 = Vector512.MultiplyAddEstimate(-Vector512.Create(5.101148618689155f), v65, v25); - Vector512 v88 = input31 + input29; - Vector512 v89 = input29 + input27; - Vector512 v90 = input27 + input25; - Vector512 v91 = input25 + input23; - Vector512 v92 = input23 + input21; - Vector512 v93 = input21 + input19; - Vector512 v94 = input19 + input17; - Vector512 v95 = input17 + input15; - Vector512 v96 = input15 + input13; - Vector512 v97 = input13 + input11; - Vector512 v98 = input11 + input9; - Vector512 v99 = input9 + input7; - Vector512 v100 = input7 + input5; - Vector512 v101 = input5 + input3; - Vector512 v102 = input3 + input1; - Vector512 v103 = input1 * Vector512.Create(JxlDctScales.Sqrt2); - Vector512 v104 = v103 + v95; - Vector512 v105 = v103 - v95; - Vector512 v106 = v91 + v99; - Vector512 v107 = v99 * Vector512.Create(JxlDctScales.Sqrt2); - Vector512 v108 = v107 + v106; - Vector512 v109 = v107 - v106; - Vector512 v110 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v108, v104); - Vector512 v111 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v108, v104); - Vector512 v112 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v109, v105); - Vector512 v113 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v109, v105); - Vector512 v114 = v89 + v93; - Vector512 v115 = v93 + v97; - Vector512 v116 = v97 + v101; - Vector512 v117 = v101 * Vector512.Create(JxlDctScales.Sqrt2); - Vector512 v118 = v117 + v115; - Vector512 v119 = v117 - v115; - Vector512 v120 = v114 + v116; - Vector512 v121 = v116 * Vector512.Create(JxlDctScales.Sqrt2); - Vector512 v122 = v121 + v120; - Vector512 v123 = v121 - v120; - Vector512 v124 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v122, v118); - Vector512 v125 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v122, v118); - Vector512 v126 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v123, v119); - Vector512 v127 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v123, v119); - Vector512 v128 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5097955791041592f), v124, v110); - Vector512 v129 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5097955791041592f), v124, v110); - Vector512 v130 = Vector512.MultiplyAddEstimate(Vector512.Create(0.6013448869350453f), v126, v112); - Vector512 v131 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.6013448869350453f), v126, v112); - Vector512 v132 = Vector512.MultiplyAddEstimate(Vector512.Create(0.8999762231364156f), v127, v113); - Vector512 v133 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.8999762231364156f), v127, v113); - Vector512 v134 = Vector512.MultiplyAddEstimate(Vector512.Create(2.5629154477415055f), v125, v111); - Vector512 v135 = Vector512.MultiplyAddEstimate(-Vector512.Create(2.5629154477415055f), v125, v111); - Vector512 v136 = v88 + v90; - Vector512 v137 = v90 + v92; - Vector512 v138 = v92 + v94; - Vector512 v139 = v94 + v96; - Vector512 v140 = v96 + v98; - Vector512 v141 = v98 + v100; - Vector512 v142 = v100 + v102; - Vector512 v143 = v102 * Vector512.Create(JxlDctScales.Sqrt2); - Vector512 v144 = v143 + v139; - Vector512 v145 = v143 - v139; - Vector512 v146 = v137 + v141; - Vector512 v147 = v141 * Vector512.Create(JxlDctScales.Sqrt2); - Vector512 v148 = v147 + v146; - Vector512 v149 = v147 - v146; - Vector512 v150 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v148, v144); - Vector512 v151 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v148, v144); - Vector512 v152 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v149, v145); - Vector512 v153 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v149, v145); - Vector512 v154 = v136 + v138; - Vector512 v155 = v138 + v140; - Vector512 v156 = v140 + v142; - Vector512 v157 = v142 * Vector512.Create(JxlDctScales.Sqrt2); - Vector512 v158 = v157 + v155; - Vector512 v159 = v157 - v155; - Vector512 v160 = v154 + v156; - Vector512 v161 = v156 * Vector512.Create(JxlDctScales.Sqrt2); - Vector512 v162 = v161 + v160; - Vector512 v163 = v161 - v160; - Vector512 v164 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v162, v158); - Vector512 v165 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v162, v158); - Vector512 v166 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v163, v159); - Vector512 v167 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v163, v159); - Vector512 v168 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5097955791041592f), v164, v150); - Vector512 v169 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5097955791041592f), v164, v150); - Vector512 v170 = Vector512.MultiplyAddEstimate(Vector512.Create(0.6013448869350453f), v166, v152); - Vector512 v171 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.6013448869350453f), v166, v152); - Vector512 v172 = Vector512.MultiplyAddEstimate(Vector512.Create(0.8999762231364156f), v167, v153); - Vector512 v173 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.8999762231364156f), v167, v153); - Vector512 v174 = Vector512.MultiplyAddEstimate(Vector512.Create(2.5629154477415055f), v165, v151); - Vector512 v175 = Vector512.MultiplyAddEstimate(-Vector512.Create(2.5629154477415055f), v165, v151); - Vector512 v176 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5024192861881557f), v168, v128); - Vector512 v177 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5024192861881557f), v168, v128); - Vector512 v178 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5224986149396889f), v170, v130); - Vector512 v179 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5224986149396889f), v170, v130); - Vector512 v180 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5669440348163577f), v172, v132); - Vector512 v181 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5669440348163577f), v172, v132); - Vector512 v182 = Vector512.MultiplyAddEstimate(Vector512.Create(0.6468217833599901f), v174, v134); - Vector512 v183 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.6468217833599901f), v174, v134); - Vector512 v184 = Vector512.MultiplyAddEstimate(Vector512.Create(0.7881546234512502f), v175, v135); - Vector512 v185 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.7881546234512502f), v175, v135); - Vector512 v186 = Vector512.MultiplyAddEstimate(Vector512.Create(1.060677685990347f), v173, v133); - Vector512 v187 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.060677685990347f), v173, v133); - Vector512 v188 = Vector512.MultiplyAddEstimate(Vector512.Create(1.7224470982383342f), v171, v131); - Vector512 v189 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.7224470982383342f), v171, v131); - Vector512 v190 = Vector512.MultiplyAddEstimate(Vector512.Create(5.101148618689155f), v169, v129); - Vector512 v191 = Vector512.MultiplyAddEstimate(-Vector512.Create(5.101148618689155f), v169, v129); - Vector512 v192 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5006029982351963f), v176, v72); - Vector512 v193 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5006029982351963f), v176, v72); - Vector512 v194 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5054709598975436f), v178, v74); - Vector512 v195 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5054709598975436f), v178, v74); - Vector512 v196 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5154473099226246f), v180, v76); - Vector512 v197 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5154473099226246f), v180, v76); - Vector512 v198 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5310425910897841f), v182, v78); - Vector512 v199 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5310425910897841f), v182, v78); - Vector512 v200 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5531038960344445f), v184, v80); - Vector512 v201 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5531038960344445f), v184, v80); - Vector512 v202 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5829349682061339f), v186, v82); - Vector512 v203 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5829349682061339f), v186, v82); - Vector512 v204 = Vector512.MultiplyAddEstimate(Vector512.Create(0.6225041230356648f), v188, v84); - Vector512 v205 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.6225041230356648f), v188, v84); - Vector512 v206 = Vector512.MultiplyAddEstimate(Vector512.Create(0.6748083414550057f), v190, v86); - Vector512 v207 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.6748083414550057f), v190, v86); - Vector512 v208 = Vector512.MultiplyAddEstimate(Vector512.Create(0.7445362710022986f), v191, v87); - Vector512 v209 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.7445362710022986f), v191, v87); - Vector512 v210 = Vector512.MultiplyAddEstimate(Vector512.Create(0.8393496454155268f), v189, v85); - Vector512 v211 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.8393496454155268f), v189, v85); - Vector512 v212 = Vector512.MultiplyAddEstimate(Vector512.Create(0.9725682378619608f), v187, v83); - Vector512 v213 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.9725682378619608f), v187, v83); - Vector512 v214 = Vector512.MultiplyAddEstimate(Vector512.Create(1.1694399334328847f), v185, v81); - Vector512 v215 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.1694399334328847f), v185, v81); - Vector512 v216 = Vector512.MultiplyAddEstimate(Vector512.Create(1.4841646163141662f), v183, v79); - Vector512 v217 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.4841646163141662f), v183, v79); - Vector512 v218 = Vector512.MultiplyAddEstimate(Vector512.Create(2.057781009953411f), v181, v77); - Vector512 v219 = Vector512.MultiplyAddEstimate(-Vector512.Create(2.057781009953411f), v181, v77); - Vector512 v220 = Vector512.MultiplyAddEstimate(Vector512.Create(3.407608418468719f), v179, v75); - Vector512 v221 = Vector512.MultiplyAddEstimate(-Vector512.Create(3.407608418468719f), v179, v75); - Vector512 v222 = Vector512.MultiplyAddEstimate(Vector512.Create(10.190008123548033f), v177, v73); - Vector512 v223 = Vector512.MultiplyAddEstimate(-Vector512.Create(10.190008123548033f), v177, v73); - - values[0] = v192; - values[1] = v194; - values[2] = v196; - values[3] = v198; - values[4] = v200; - values[5] = v202; - values[6] = v204; - values[7] = v206; - values[8] = v208; - values[9] = v210; - values[10] = v212; - values[11] = v214; - values[12] = v216; - values[13] = v218; - values[14] = v220; - values[15] = v222; - values[16] = v223; - values[17] = v221; - values[18] = v219; - values[19] = v217; - values[20] = v215; - values[21] = v213; - values[22] = v211; - values[23] = v209; - values[24] = v207; - values[25] = v205; - values[26] = v203; - values[27] = v201; - values[28] = v199; - values[29] = v197; - values[30] = v195; - values[31] = v193; - } - - /// - /// Processes complete 16-lane inverse batches along input columns. - /// - /// The number of positions in each transform. - /// The number of independent transforms. - /// The borrowed source matrix. - /// The source row stride, in floats. - /// The borrowed output matrix. - /// The destination row stride, in floats. - /// The first independent transform not already processed by a wider batch. - /// Borrowed storage for two length-by-16 banks. Only the first bank needs initialized inputs. - /// The first independent transform left after the complete batches. - private static int InverseColumns512( - int length, - int count, - ReadOnlySpan source, - int sourceStride, - Span destination, - int destinationStride, - int first, - Span workspace) - { - Span> values = MemoryMarshal.Cast>(workspace[..(length * 16)]); - Span> scratch = MemoryMarshal.Cast>(workspace.Slice(length * 16, length * 16)); - ref float input = ref MemoryMarshal.GetReference(source); - ref float output = ref MemoryMarshal.GetReference(destination); - - for (; first <= count - 16; first += 16) - { - // One vector covers adjacent columns at the same transform position. Physical strides never enter the coefficient-bank layout. - for (int i = 0; i < length; i++) - { - values[i] = Vector512.LoadUnsafe(ref input, (nuint)((i * sourceStride) + first)); - } - - Inverse(values, scratch); - for (int i = 0; i < length; i++) - { - values[i].StoreUnsafe(ref output, (nuint)((i * destinationStride) + first)); - } - } - - return first; - } - - /// - /// Processes complete 16-lane inverse batches along input rows. - /// - /// The number of positions in each transform. - /// The number of independent transforms. - /// The borrowed source matrix. - /// The source row stride, in floats. - /// The borrowed output matrix. - /// The destination row stride, in floats. - /// Whether transform positions become output rows and input rows become output columns. - /// The first independent transform not already processed by a wider batch. - /// Borrowed storage for two length-by-16 banks. Only the first bank needs initialized inputs. - /// The first independent transform left after the complete batches. - private static int InverseRows512( - int length, - int count, - ReadOnlySpan source, - int sourceStride, - Span destination, - int destinationStride, - bool transposed, - int first, - Span workspace) - { - Span> values = MemoryMarshal.Cast>(workspace[..(length * 16)]); - Span> scratch = MemoryMarshal.Cast>(workspace.Slice(length * 16, length * 16)); - ref float input = ref MemoryMarshal.GetReference(source); - ref float output = ref MemoryMarshal.GetReference(destination); - - for (; first <= count - 16; first += 16) - { - // Each square load contains adjacent samples from independent rows. Transposition puts those rows into SIMD lanes. - // The complete row band is loaded before any same-orientation output is written, so source and destination can be the same matrix. - for (int column = 0; column < length; column += 16) - { - Vector512 row0 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 0) * sourceStride) + column)).AsInt32(); - Vector512 row1 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 1) * sourceStride) + column)).AsInt32(); - Vector512 row2 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 2) * sourceStride) + column)).AsInt32(); - Vector512 row3 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 3) * sourceStride) + column)).AsInt32(); - Vector512 row4 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 4) * sourceStride) + column)).AsInt32(); - Vector512 row5 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 5) * sourceStride) + column)).AsInt32(); - Vector512 row6 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 6) * sourceStride) + column)).AsInt32(); - Vector512 row7 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 7) * sourceStride) + column)).AsInt32(); - Vector512 row8 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 8) * sourceStride) + column)).AsInt32(); - Vector512 row9 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 9) * sourceStride) + column)).AsInt32(); - Vector512 row10 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 10) * sourceStride) + column)).AsInt32(); - Vector512 row11 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 11) * sourceStride) + column)).AsInt32(); - Vector512 row12 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 12) * sourceStride) + column)).AsInt32(); - Vector512 row13 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 13) * sourceStride) + column)).AsInt32(); - Vector512 row14 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 14) * sourceStride) + column)).AsInt32(); - Vector512 row15 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 15) * sourceStride) + column)).AsInt32(); - - SimdUtils.Transpose( - ref row0, - ref row1, - ref row2, - ref row3, - ref row4, - ref row5, - ref row6, - ref row7, - ref row8, - ref row9, - ref row10, - ref row11, - ref row12, - ref row13, - ref row14, - ref row15); - - values[column + 0] = row0.AsSingle(); - values[column + 1] = row1.AsSingle(); - values[column + 2] = row2.AsSingle(); - values[column + 3] = row3.AsSingle(); - values[column + 4] = row4.AsSingle(); - values[column + 5] = row5.AsSingle(); - values[column + 6] = row6.AsSingle(); - values[column + 7] = row7.AsSingle(); - values[column + 8] = row8.AsSingle(); - values[column + 9] = row9.AsSingle(); - values[column + 10] = row10.AsSingle(); - values[column + 11] = row11.AsSingle(); - values[column + 12] = row12.AsSingle(); - values[column + 13] = row13.AsSingle(); - values[column + 14] = row14.AsSingle(); - values[column + 15] = row15.AsSingle(); - } - - Inverse(values, scratch); - if (transposed) - { - // A complete vector is one contiguous output row. Only logical samples are stored; destination padding remains untouched. - for (int i = 0; i < length; i++) - { - values[i].StoreUnsafe(ref output, (nuint)((i * destinationStride) + first)); - } - } - else - { - // Restore row-major order one register tile at a time. This replaces a separate full-block transpose and copy. - for (int column = 0; column < length; column += 16) - { - Vector512 row0 = values[column + 0].AsInt32(); - Vector512 row1 = values[column + 1].AsInt32(); - Vector512 row2 = values[column + 2].AsInt32(); - Vector512 row3 = values[column + 3].AsInt32(); - Vector512 row4 = values[column + 4].AsInt32(); - Vector512 row5 = values[column + 5].AsInt32(); - Vector512 row6 = values[column + 6].AsInt32(); - Vector512 row7 = values[column + 7].AsInt32(); - Vector512 row8 = values[column + 8].AsInt32(); - Vector512 row9 = values[column + 9].AsInt32(); - Vector512 row10 = values[column + 10].AsInt32(); - Vector512 row11 = values[column + 11].AsInt32(); - Vector512 row12 = values[column + 12].AsInt32(); - Vector512 row13 = values[column + 13].AsInt32(); - Vector512 row14 = values[column + 14].AsInt32(); - Vector512 row15 = values[column + 15].AsInt32(); - - SimdUtils.Transpose( - ref row0, - ref row1, - ref row2, - ref row3, - ref row4, - ref row5, - ref row6, - ref row7, - ref row8, - ref row9, - ref row10, - ref row11, - ref row12, - ref row13, - ref row14, - ref row15); - - row0.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 0) * destinationStride) + column)); - row1.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 1) * destinationStride) + column)); - row2.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 2) * destinationStride) + column)); - row3.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 3) * destinationStride) + column)); - row4.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 4) * destinationStride) + column)); - row5.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 5) * destinationStride) + column)); - row6.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 6) * destinationStride) + column)); - row7.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 7) * destinationStride) + column)); - row8.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 8) * destinationStride) + column)); - row9.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 9) * destinationStride) + column)); - row10.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 10) * destinationStride) + column)); - row11.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 11) * destinationStride) + column)); - row12.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 12) * destinationStride) + column)); - row13.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 13) * destinationStride) + column)); - row14.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 14) * destinationStride) + column)); - row15.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 15) * destinationStride) + column)); - } - } - } - - return first; - } - -} +ErrorGeneratingOutput \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsReader.cs index 4644fbff3d..e56e3d25ec 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsReader.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Buffers; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.IO; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs index 836df27a38..46f5e1357b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs @@ -126,7 +126,7 @@ public static void Random3Planes( GenerateRandomImage(rng, plane2.Bounds, plane2.Image); } - private static void PrepareNoiseInput( + public static void PrepareNoiseInput( JxlPassesDecoderContext decState, JxlFrameDimensions frameDim, JxlFrameHeader frameHeader, diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs index 044858cb33..9760feee4b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs @@ -4,14 +4,29 @@ using System.Buffers; using System.Numerics; using System.Numerics.Tensors; -using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Formats.Jxl.Cms; +using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; +using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Group; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Modular; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; +using SixLabors.ImageSharp.Memory; + +// For a 'stackalloc byte[128].Slice(0, n)' this suggests +// to update to '(stackalloc byte[128])[..n]', then suggests +// to remove the parentheses, producing invalid syntax. +#pragma warning disable IDE0057 // Use range operator namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Heuristics; @@ -123,6 +138,110 @@ internal static class JxlHeuristics -0.00624645f, -0.02921014f, -0.04022174f, -0.03452303f, -0.01716200f, ]; + public static void DownsampleImage2Sharper(Configuration configuration, ref JxlImage3 opsin) + { + using JxlImage3 downsampled = new( + configuration, + JxlMath.DivCeil(opsin.XSize, 2) + JxlFrameDimensions.BlockDimensions, + JxlMath.DivCeil(opsin.YSize, 2) + JxlFrameDimensions.BlockDimensions); + + if (!downsampled.ShrinkTo( + downsampled.XSize - JxlFrameDimensions.BlockDimensions, + downsampled.YSize - JxlFrameDimensions.BlockDimensions)) + { + throw new InvalidOperationException("Could not shrink the downsampled image."); + } + + DownsampleImage2Sharper(configuration, opsin.Plane(0), downsampled.Plane(0)); + DownsampleImage2Sharper(configuration, opsin.Plane(1), downsampled.Plane(1)); + DownsampleImage2Sharper(configuration, opsin.Plane(2), downsampled.Plane(2)); + + opsin = downsampled; + } + + public static void DownsampleImage2Iterative(Configuration configuration, JxlPlane original, JxlPlane output) + { + // Get the image sizes. + int xsize = original.XSize; + int ysize = original.YSize; + int xsize2 = JxlMath.DivCeil(original.XSize, 2); + int ysize2 = JxlMath.DivCeil(original.YSize, 2); + + // oldBoxDownsample is there so it can be disposed. + JxlPlane oldBoxDownsample = new(configuration, xsize, ysize); + JxlPlane boxDownsample = oldBoxDownsample; + JxlImageOperations.ForceCopyImage(original, boxDownsample); + + // Downsample. + boxDownsample = DownsampleImage(boxDownsample, 2); + oldBoxDownsample.Dispose(); + + // Create a mask. + JxlPlane mask = new(configuration, boxDownsample.XSize, boxDownsample.YSize)); + CreateMask(boxDownsample, mask); + + // Try to shrink the output down to the width/height divided by 2. + if (!output.ShrinkTo(xsize2, ysize2)) + { + throw new InvalidOperationException("Could not shrink output image"); + } + + // Contains the result of sharper image downscale. + JxlPlane initial = new( + configuration, + JxlMath.DivCeil(original.XSize, 2) + JxlFrameDimensions.BlockDimensions, + JxlMath.DivCeil(original.YSize, 2) + JxlFrameDimensions.BlockDimensions); + + if (!initial.ShrinkTo(initial.XSize - JxlFrameDimensions.BlockDimensions, initial.YSize - JxlFrameDimensions.BlockDimensions)) + { + throw new InvalidOperationException("Could not shrink initial image"); + } + + // Downscale the image and store into initial. + DownsampleImage2Sharper(configuration, original, initial); + + JxlPlane down = new(configuration, initial.XSize, initial.YSize); + JxlImageOperations.ForceCopyImage(initial, down); + + JxlPlane up = new(configuration, xsize, ysize); + JxlPlane corr = new(configuration, xsize, ysize); + JxlPlane corr2 = new(configuration, xsize2, ysize2); + JxlPlane weights = new(configuration, xsize, ysize); + + for (int y = 0; y < weights.YSize; y++) + { + Span row = weights.GetRow(y); + row[..weights.XSize].Fill(1); + } + + JxlPlane weights2 = new(configuration, xsize2, ysize2); + AntiUpsample(weights, weights2); + + // Three iterations. + const int numberOfIterations = 3; + for (int it = 0; it < numberOfIterations; ++it) + { + UpsampleImage(down, up); + corr = JxlImageOperations.LinComb(configuration, 1, original, -1, up) ?? throw new InvalidOperationException("Linear combination failed"); + ElementwiseMultiply(corr, weights, corr); + AntiUpsample(corr, corr2); + ElementwiseDivide(corr2, weights2, corr2); + + down = JxlImageOperations.LinComb(configuration, 1, down, 1, corr2) ?? throw new InvalidOperationException("Linear combination failed"); + } + + // Reduce ringing. + ReduceRinging(initial, mask, down); + + for (int y = 0; y < down.YSize; y++) + { + Span downRow = down.GetRow(y); + Span outputRow = output.GetRow(y); + + downRow[..down.XSize].CopyTo(outputRow); + } + } + public static void FindBestBlockEntropyModel( JxlCompressParameters cparameters, JxlPlane rqf, @@ -569,20 +688,18 @@ public static void UpsampleImage(JxlPlane input, JxlPlane output) { xi = 0; } - - if (xi >= xsize) + else if (xi >= xsize) { - xi = input.XSize - 1; + xi = xsize - 1; } if (yi < 0) { yi = 0; } - - if (yi >= ysize) + else if (yi >= ysize) { - yi = input.YSize - 1; + yi = ysize - 1; } min = MathF.Min(min, input.GetRow(yi)[xi]); @@ -601,36 +718,35 @@ public static void UpsampleImage(JxlPlane input, JxlPlane output) { xi = 0; } - - if (xi >= xsize) + else if (xi >= xsize) { - xi = input.XSize - 1; + xi = xsize - 1; } if (yi < 0) { yi = 0; } - - if (yi >= ysize) + else if (yi >= ysize) { - yi = input.YSize - 1; + yi = ysize - 1; } sum += input.GetRow(yi)[xi] * kernel[(ky * Size) + kx]; } } - output.GetRow(y)[x] = sum; + ref float rowYX = ref output.GetRow(y)[x]; - if (output.GetRow(y)[x] < min) + rowYX = sum; + + if (rowYX < min) { - output.GetRow(y)[x] = min; + rowYX = min; } - - if (output.GetRow(y)[x] > max) + else if (rowYX > max) { - output.GetRow(y)[x] = max; + rowYX = max; } } } @@ -695,17 +811,7 @@ public static void DownsampleImage2Sharper(Configuration configuration, JxlPlane for (int ky = 0; ky < kernelY; ky++) { - int iy = (y * 2) + ky - ((kernelY - 1) / 2); - - if (iy < 0) - { - iy = 0; - } - else if (iy >= ySize) - { - iy = ySize - 1; - } - + int iy = Math.Clamp((y * 2) + ky - ((kernelY - 1) / 2), 0, ySize - 1); rowIn[ky] = input.GetRowMemory(iy); } @@ -892,6 +998,464 @@ public static float ComputeBlockL2Distance(JxlImage3 a, JxlImage3 return result; } + public static void ComputeARHeuristics(Configuration configuration, JxlFrameHeader frameHeader, JxlPassesEncoderContext encoderState, JxlImage3 originalOpsin, Rectangle rect) + { + JxlCompressParameters cparams = encoderState.CompressionParameters; + JxlPassesCommonContext shared = encoderState.PassesCommonContext; + JxlFrameDimensions frameDimensions = shared.FrameDimensions; + JxlPlane initialQuantMasking1x1 = encoderState.InitialQuantMasking1x1; + JxlPlane epfSharpness = shared.EpfSharpness; + + float clampedButteraugli = MathF.Min(5.0f, cparams.ButteraugliDistance); + if (cparams.ButteraugliDistance < MinimumButteraugliForDynamicAR || + cparams.SpeedTier > JxlSpeedTier.Wombat || + frameHeader.LoopFilter.EpfIterations == 0) + { + JxlImageOperations.FillPlane((byte)4, epfSharpness, epfSharpness.GetRectangle()); + return; + } + + // Steps for the edge preserving filter + Span epfSteps = cparams.ButteraugliDistance > 4.5f ? [0, 4] : [0, 2, 7]; + + const int numberOfEPFValues = 8; + Span epfStepsLookupTable = stackalloc int[numberOfEPFValues]; + epfStepsLookupTable.Clear(); + + for (int i = 0; i < epfSteps.Length; i++) + { + epfStepsLookupTable[epfSteps[i]] = i; + } + + // Contains the L2 distance for each coefficient. This will be + // initialized. + JxlPlane[] errorImages = new JxlPlane[numberOfEPFValues]; + + for (int i = 0; i < epfSteps.Length; i++) + { + // Allocate and reconstruct the images. + byte value = epfSteps[i]; + JxlImage3 decoded = ReconstructImage(configuration, frameHeader, shared, encoderState.Coefficients); + errorImages[value] = new(configuration, frameDimensions.XSizeBlocks, frameDimensions.YSizeBlocks); + + // Compute the L2 distance between original opsin and the decoded image. + for (int by = 0; by < frameDimensions.YSizeBlocks; by++) + { + Span errorRow = errorImages[value].GetRow(by); + for (int bx = 0; bx < frameDimensions.XSize; bx++) + { + errorRow[bx] = ComputeBlockL2Distance(originalOpsin, decoded, initialQuantMasking1x1, by, bx); + } + } + } + + using Buffer2D histo = configuration.MemoryAllocator.Allocate2D(9, numberOfEPFValues); + Span totals = stackalloc int[16].Slice(0, 9); + totals.Fill(1); + const float favorNoSmoothing = 0.99f; + + for (int by = 0; by < frameDimensions.YSizeBlocks; by++) + { + // Get the current (output) and previous row. If Y + // is 0, then the previous row is also row 0. + Span outputRow = epfSharpness.GetRow(by); + Span previousRow = epfSharpness.GetRow(by > 0 ? by - 1 : 0); + + for (int bx = 0; bx < frameDimensions.XSizeBlocks; bx++) + { + byte bestValue = 0; + float bestError = float.MaxValue; + + // Neighboring values + byte topValue = by > 0 ? previousRow[bx] : (byte)0; + byte leftValue = bx > 0 ? outputRow[bx - 1] : (byte)0; + + // Errors of neighboring values + float topError = errorImages[topValue].GetRow(by)[bx]; + float leftError = errorImages[leftValue].GetRow(by)[bx]; + + for (int i = 0; i < epfSteps.Length; i++) + { + // Current EPF step + byte val = epfSteps[i]; + + // Current error + float error = errorImages[val].GetRow(by)[bx]; + + if (val == 0) + { + error *= favorNoSmoothing; + } + + if (error < bestError) + { + bestValue = val; + bestError = error; + } + } + + if (bestError < MathF.Min(topError, leftError)) + { + outputRow[bx] = bestValue; + } + else if (topError < leftError) + { + outputRow[bx] = topValue; + } + else + { + outputRow[bx] = leftValue; + } + + int context = (epfStepsLookupTable[topValue] * 3) + epfStepsLookupTable[leftValue]; + histo[context, outputRow[bx]]++; + totals[context]++; + } + } + + const float c3base = 0.98017198824148288f; + const float c3clamp = 0.85970338919928291f; + float c3 = MathF.Max(c3clamp, MathF.Pow(c3base, clampedButteraugli)); + const float c5 = 0.1087690359555803f; + + Span mul = stackalloc float[32].Slice(0, 3 * 3 * 3); + mul.Clear(); + + foreach (byte topValue in epfSteps) + { + foreach (byte leftValue in epfSteps) + { + foreach (byte val in epfSteps) + { + int context = (epfStepsLookupTable[topValue] * 3) + epfStepsLookupTable[leftValue]; + Span contextHisto = histo.DangerousGetRowSpan(context); + + int multiplierIndex = epfStepsLookupTable[val] + (3 * context); + mul[multiplierIndex] = 1.0f / (1.0f + (c5 * MathUtils.Log1P(contextHisto[val] / totals[context]) / clampedButteraugli)); + + if (val == 0) + { + mul[multiplierIndex] *= c3; + } + } + } + } + + for (int by = 0; by < frameDimensions.YSizeBlocks; by++) + { + // Get the current (output) and previous row. If Y + // is 0, then the previous row is also row 0. + Span outputRow = epfSharpness.GetRow(by); + Span previousRow = epfSharpness.GetRow(by > 0 ? by - 1 : 0); + + for (int bx = 0; bx < frameDimensions.XSizeBlocks; bx++) + { + byte bestValue = 0; + float bestError = float.MaxValue; + + // Neighbors + byte topValue = by > 0 ? previousRow[bx] : (byte)0; + byte leftValue = bx > 0 ? outputRow[bx - 1] : (byte)0; + + int context = (epfStepsLookupTable[topValue] * 3) + epfStepsLookupTable[leftValue]; + + foreach (byte val in epfSteps) + { + int multiplierIndex = epfStepsLookupTable[val] + (3 * context); + float error = errorImages[val].GetRow(by)[bx] * mul[multiplierIndex]; + + if (error < bestError) + { + bestValue = val; + bestError = error; + } + } + + outputRow[bx] = bestValue; + } + } + } + + public static void DownsampleImage2Iterative(Configuration configuration, ref JxlImage3 opsin) + { + JxlImage3 downsampled = new( + configuration, + JxlMath.DivCeil(opsin.XSize, 2) + JxlFrameDimensions.BlockDimensions, + JxlMath.DivCeil(opsin.YSize, 2) + JxlFrameDimensions.BlockDimensions); + + if (!downsampled.ShrinkTo(downsampled.XSize - JxlFrameDimensions.BlockDimensions, downsampled.YSize - JxlFrameDimensions.BlockDimensions)) + { + throw new InvalidOperationException("Could not shrink image"); + } + + // Temporary conversion data + using JxlImage3 rgb = new(configuration, opsin.XSize, opsin.YSize); + + JxlOpsinParameters opsinParameters = new(); + opsinParameters.Initialize(JxlMath.DefaultIntensityTarget); + + // Convert Opsin XYB to linear + if (!JxlXybDecoder.OpsinToLinear(opsin, rgb.GetRectangle(), rgb, opsinParameters)) + { + throw new InvalidOperationException("Failed to convert Opsin color-space to RGB"); + } + + // Compute the Butteraugli mask for the image. + JxlPlane mask = new(configuration, opsin.XSize, opsin.YSize); + ButteraugliParameters butterParameters = new(); + ButteraugliComparator butter = ButteraugliComparator.Make(configuration, rgb, butterParameters); + + if (!butter.Mask(configuration, mask)) + { + throw new InvalidOperationException("Could not compute the Butteraugli mask."); + } + + JxlPlane maskFuzzy = new(configuration, opsin.XSize, opsin.YSize); + + for (int c = 0; c < 3; c++) + { + // This calls into a separate method overload. + DownsampleImage2Iterative(configuration, opsin.Plane(c), downsampled.Plane(c)); + } + + opsin = downsampled; + } + + public static JxlImage3 ReconstructImage(Configuration configuration, JxlFrameHeader origFrameHeader, JxlPassesCommonContext shared, IReadOnlyList coeffs) + { + JxlFrameDimensions frameDim = shared.FrameDimensions; + + JxlFrameHeader frameHeader = origFrameHeader; + frameHeader.UpdateFlag(shared.ImageFeatures.PatchDictionary.HasAny, (ulong)JxlFrameHeaderFlags.Patches); + frameHeader.UpdateFlag(shared.ImageFeatures.Splines.HasAny, (ulong)JxlFrameHeaderFlags.Splines); + frameHeader.ColorTransform = JxlColorTransform.None; + + JxlCodecMetadata metadata = frameHeader.Metadata!.DeepClone(); + metadata.ImageMetadata!.ExtraChannels.Clear(); + metadata.ImageMetadata!.ExtraChannelCount = metadata.ImageMetadata.ExtraChannels.Count; + frameHeader.Metadata = metadata; + frameHeader.ExtraChannelUpsampling.Clear(); + + bool isGray = shared.CodecMetadata.ImageMetadata.ColorEncoding.IsGray; + + JxlPassesDecoderContext decState = new(frameHeader, configuration); + + decState.OutputEncodingInfo.SetFromMetadata(shared.CodecMetadata); + decState.OutputEncodingInfo.MaybeSetColorEncoding(JxlColorEncoding.LinearSRgb(isGray)); + + decState.Shared = shared; + decState.Initialize(frameHeader); + + JxlImageBundle decoded = new(shared.CodecMetadata.ImageMetadata) + { + Origin = frameHeader.FrameOrigin + }; + + JxlImage3 tmp = new(configuration, frameDim.XSize, frameDim.YSize); + decoded.SetFromImage(tmp, decState.OutputEncodingInfo.ColorEncoding); + + JxlPassesDecoderContext.PipelineOptions options = new() + { + UseSlowRenderPipeline = false, + Coalescing = false, + RenderSpotcolors = false, + RenderNoise = true + }; + + decState.PreparePipeline(frameHeader, shared.CodecMetadata.ImageMetadata, decoded, options); + + decState.RenderPipeline.PrepareForThreads(1, false); + JxlGroupDecoderCache groupDecCaches = new(configuration); + + void ProcessGroup(uint groupIndex, int thread) + { + if (frameHeader.LoopFilter.EpfIterations > 0) + { + frameHeader.LoopFilter.ComputeSigma(frameDim.BlockGroupRect(groupIndex), decState); + } + + JxlRenderPipelineInput input = decState.RenderPipeline.GetInputBuffers(groupIndex, thread); + JxlGroupDecoder.DecodeGroupForRoundtrip(frameHeader, coeffs, groupIndex, decState, ref groupDecCaches, thread, input, null, null); + + if ((frameHeader.Flags & (ulong)JxlFrameHeaderFlags.Noise) != 0) + { + JxlNoiseDecoder.PrepareNoiseInput(decState, shared.FrameDimensions, frameHeader, (int)groupIndex, thread); + } + + input.Done(); + } + + for (int i = 0; i < frameDim.NumGroups; i++) + { + ProcessGroup((uint)i, i); + } + + return decoded.Color; + } + + public static void LossyFrameHeuristics( + JxlFrameHeader frameHeader, + JxlPassesEncoderState encState, + JxlModularFrameEncoder modularFrameEncoder, + JxlImage3 linear, + JxlImage3 opsin, + Rectangle rect, + JxlCmsInterface cms, + JxlAuxiliaryOutput? auxOut) + { + JxlCompressParams cparams = encState.CompressParams; + bool streamingMode = encState.StreamingMode; + bool initializeGlobalState = encState.InitializeGlobalState; + JxlPassesCommonContext shared = encState.Shared; + JxlFrameDimensions frameDim = shared.FrameDimensions; + JxlImageFeatures imageFeatures = shared.ImageFeatures; + JxlDequantMatrices matrices = shared.Matrices; + JxlQuantizer quantizer = shared.Quantizer; + JxlPlane initialQuantMasking1x1 = encState.InitialQuantMasking1x1; + JxlPlane rawQuantField = shared.RawQuantField; + JxlColorCorrelationMap cmap = shared.ColorMap; + JxlAcStrategyImage acStrategy = shared.AcStrategy; + JxlBlockContextMap blockCtxMap = shared.BlockContextMap; + Configuration configuration = encState.Configuration; + + bool overrideSplines = cparams.CustomSplines.HasAny; + if (overrideSplines) + { + imageFeatures.Splines.SetData(cparams.CustomSplines); + } + + if (!streamingMode && cparams.SpeedTier <= JxlSpeedTier.Squirrel) + { + if (!overrideSplines) + { + imageFeatures.Splines = FindSplines(configuration, opsin); + } + + imageFeatures.Splines.InitializeDrawCache(configuration, opsin.XSize, opsin.YSize, cmap.Base); + imageFeatures.Splines.SubtractFrom(opsin); + } + + if (!streamingMode && JxlOverrideHelpers.ApplyOverride(cparams.Patches, cparams.SpeedTier <= JxlSpeedTier.Squirrel)) + { + JxlPatchDictionaryEncoder.FindBestPatchDictionary(configuration, opsin, encState, cms, pool, auxOut); + JxlPatchDictionaryEncoder.SubtractFrom(imageFeatures.Patches, opsin); + } + + float quantDc = JxlAdaptiveQuantization.InitialQuantDc(cparams.ButteraugliDistance); + + JxlAcStrategyHeuristics acsHeuristics = new(configuration, cparams, matrices); + JxlCflHeuristics cflHeuristics = new(configuration, rect); + JxlPlane initialQuantField; + JxlPlane initialQuantMasking; + + if (cparams.SpeedTier > JxlSpeedTier.Hare || cparams.DisablePerceptualOptimizations) + { + initialQuantField = new(configuration, frameDim.XSizeBlocks, frameDim.YSizeBlocks); + initialQuantMasking = new(configuration, frameDim.XSizeBlocks, frameDim.YSizeBlocks); + + // Fill the quant field with 0.79 / butteraugliDistance. + float q = 0.79f / cparams.ButteraugliDistance; + JxlImageOperations.FillImage(q, initialQuantField); + + // Fill initial quant masking with the inverse of q+0.001. + float masking = 1.0f / (q + 0.001f); + JxlImageOperations.FillImage(masking, initialQuantMasking); + + if (cparams.DisablePerceptualOptimizations) + { + initialQuantMasking1x1 = new(configuration, frameDim.XSize, frameDim.YSize); + JxlImageOperations.FillImage(masking, initialQuantMasking1x1); + } + + quantizer.ComputeGlobalScaleAndQuant(quantDc, q, 0); + } + else + { + float butteraugliDistanceForIqf = cparams.ButteraugliDistance; + if (!frameHeader.LoopFilter.UseGaborishConvolution) + { + butteraugliDistanceForIqf *= 0.62f; + } + + initialQuantField = JxlAdaptiveQuantization.InitialQuantField(configuration, butteraugliDistanceForIqf, opsin, rect, pool, 1.0f, out initialQuantMasking, ref initialQuantMasking1x1); + + float q = 0.39f / cparams.ButteraugliDistance; + quantizer.ComputeGlobalScaleAndQuant(quantDc, q, 0); + } + + if (frameHeader.LoopFilter.UseGaborishConvolution) + { + ReadOnlySpan weight = [1.0f, 1.0f, 1.0f]; + JxlGaborish.InverseGaborish(opsin, rect, weight, pool); + } + + if (initializeGlobalState) + { + JxlDequantMatrices.FindBestDequantMatrices(configuration, cparams, modularFrameEncoder, matrices); + } + + acsHeuristics.Initialize(opsin, rect, initialQuantField, initialQuantMasking, initialQuantMasking1x1, matrices); + + void ProcessTile(uint tid, int thread) + { + // Find the tile offset. + int nEncTiles = JxlMath.DivCeil(frameDim.XSizeBlocks, JxlChromaFromLuma.ColorTileDimensionInBlocks); + int tx = (int)(tid % nEncTiles); + int ty = (int)(tid / nEncTiles); + + // Find the block ranges + int by0 = ty * JxlChromaFromLuma.ColorTileDimensionInBlocks; + int by1 = Math.Min((ty + 1) * JxlChromaFromLuma.ColorTileDimensionInBlocks, frameDim.YSizeBlocks); + int bx0 = tx * JxlChromaFromLuma.ColorTileDimensionInBlocks; + int bx1 = Math.Min((tx + 1) * JxlChromaFromLuma.ColorTileDimensionInBlocks, frameDim.XSizeBlocks); + + // Create the rectangle + Rectangle r = new(bx0, by0, bx1 - bx0, by1 - by0); + + // If the user settings allow worse performance, + // compute the CFL tile. + if (cparams.SpeedTier <= JxlSpeedTier.Squirrel) + { + cflHeuristics.ComputeTile(r, opsin, rect, matrices, null, null, null, false, cmap); + } + + // Process the AC Strategy rectangle. + acsHeuristics.ProcessRectangle(r, cmap, acStrategy); + + // Quantize. + JxlAdaptiveQuantization.AdjustQuantField(acStrategy, r, cparams.ButteraugliDistance, initialQuantField); + quantizer.SetQuantFieldRect(initialQuantField, r, rawQuantField); + + // Compute the CFL tile for the quant field. + if (cparams.SpeedTier <= JxlSpeedTier.Hare) + { + cflHeuristics.ComputeTile(r, opsin, rect, matrices, acStrategy, rawQuantField, quantizer, cparams.SpeedTier >= JxlSpeedTier.Wombat, thread, cmap); + } + } + + int numTiles = JxlMath.DivCeil(frameDim.XSizeBlocks, JxlChromaFromLuma.ColorTileDimensionInBlocks) * JxlMath.DivCeil(frameDim.YSizeBlocks, JxlChromaFromLuma.ColorTileDimensionInBlocks); + + for (int i = 0; i < numTiles; i++) + { + ProcessTile((uint)i, i); + } + + acsHeuristics.Finalize(acStrategy, auxOut); + + if (!streamingMode && !cparams.DisablePerceptualOptimizations) + { + JxlPlane epfSharpness = shared.EpfSharpness; + JxlImageOperations.FillPlane((byte)4, epfSharpness, epfSharpness.GetRectangle()); + + JxlAdaptiveQuantization.FindBestQuantizer(frameHeader, linear, opsin, initialQuantField, encState, cms, pool, auxOut); + } + + if (cparams.SpeedTier < JxlSpeedTier.Falcon && initializeGlobalState) + { + FindBestBlockEntropyModel(cparams, rawQuantField, acStrategy, blockCtxMap); + } + } + private sealed class OccCounters : IDisposable { // These are pooled using Array.Shared.Rent. diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlPassesCommonContext.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlPassesCommonContext.cs index a1ba01ce66..25088b5f08 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlPassesCommonContext.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlPassesCommonContext.cs @@ -5,6 +5,7 @@ using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Patch; @@ -34,9 +35,9 @@ internal class JxlPassesCommonContext public JxlQuantizer Quantizer { get; set; } - public JxlImageI RawQuantField { get; set; } + public JxlPlane RawQuantField { get; set; } - public JxlImageB EpfSharpness { get; set; } + public JxlPlane EpfSharpness { get; set; } public JxlColorCorrelationMap ColorMap { get; set; } @@ -46,13 +47,13 @@ internal class JxlPassesCommonContext public List CoeffOrders { get; set; } = []; - public JxlImageB QuantDc { get; set; } + public JxlPlane QuantDc { get; set; } - public JxlImage3F DcStorage { get; set; } + public JxlImage3 DcStorage { get; set; } - public JxlImage3F Dc { get; set; } + public JxlImage3 Dc { get; set; } - public JxlImage3F[] DcFrames { get; set; } = new JxlImage3F[4]; + public JxlImage3[] DcFrames { get; set; } = new JxlImage3[4]; public JxlReferenceFrame[] ReferenceFrames { get; set; } = new JxlReferenceFrame[4]; @@ -72,18 +73,18 @@ public JxlPassesCommonContext(Configuration configuration, JxlFrameHeader frameH JxlFrameDimensions dimensions = frameHeader.FrameDimensions; this.AcStrategy = JxlAcStrategyImage.Create(configuration, dimensions.XSizeBlocks, dimensions.YSizeBlocks); - this.RawQuantField = new JxlImageI(configuration, dimensions.XSizeBlocks, dimensions.YSizeBlocks); - this.EpfSharpness = new JxlImageB(configuration, dimensions.XSizeBlocks, dimensions.YSizeBlocks); + this.RawQuantField = new(configuration, dimensions.XSizeBlocks, dimensions.YSizeBlocks); + this.EpfSharpness = new(configuration, dimensions.XSizeBlocks, dimensions.YSizeBlocks); this.ColorMap = JxlColorCorrelationMap.Create(configuration, dimensions.XSize, dimensions.YSize); this.CoeffOrderSize = JxlCoefficientOrder.CoefficientOrderMaxSize; if (encoder && - this.CoeffOrders.Count < (frameHeader.Passes.NumPasses & JxlCoefficientOrder.CoefficientOrderMaxSize) && + this.CoeffOrders.Count < (frameHeader.Passes.NumberOfPasses & JxlCoefficientOrder.CoefficientOrderMaxSize) && frameHeader.Encoding == JxlFrameEncoding.VarDct) { // we add the padding to CoeffOrders so its length is equal to the variable upperBound - int upperBound = frameHeader.Passes.NumPasses & JxlCoefficientOrder.CoefficientOrderMaxSize; + int upperBound = (int)(frameHeader.Passes.NumberOfPasses & JxlCoefficientOrder.CoefficientOrderMaxSize); int length = this.CoeffOrders.Count; int delta = upperBound - length; @@ -93,7 +94,7 @@ public JxlPassesCommonContext(Configuration configuration, JxlFrameHeader frameH } } - this.QuantDc = new JxlImageB(configuration, dimensions.XSizeBlocks, dimensions.YSizeBlocks); + this.QuantDc = new(configuration, dimensions.XSizeBlocks, dimensions.YSizeBlocks); bool useDcFrame = (frameHeader.Flags & (ulong)JxlFrameHeaderFlags.Dc) != 0; if (!encoder && useDcFrame) @@ -103,7 +104,7 @@ public JxlPassesCommonContext(Configuration configuration, JxlFrameHeader frameH throw new InvalidOperationException("DC level for DC frames cannot be equal to 4"); } - this.DcStorage = new JxlImage3F(); + this.DcStorage = new(); this.Dc = this.DcFrames[(int)frameHeader.DcLevel]; if (this.Dc.XSize == 0) @@ -115,7 +116,7 @@ public JxlPassesCommonContext(Configuration configuration, JxlFrameHeader frameH } else { - this.DcStorage = new JxlImage3F(configuration, dimensions.XSizeBlocks, dimensions.YSizeBlocks); + this.DcStorage = new(configuration, dimensions.XSizeBlocks, dimensions.YSizeBlocks); this.Dc = this.DcStorage; } From 58489313ee76c861b8388cbb5d52030e8248cfd3 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Fri, 30 Oct 2026 00:22:02 +0400 Subject: [PATCH 325/328] Undo changes made to JxlDct.Kernels When I pulled the repo, T4 templates have run automatically, but it seems like Visual Studio has issues with some T4 templates, leading to the file being replaced with nothing but ErrorGeneratingOutput. --- .../Dct/JxlDct.Kernels.Generated.cs | 5875 ++++++++++++++++- 1 file changed, 5874 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDct.Kernels.Generated.cs b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDct.Kernels.Generated.cs index f4a2e8e1d3..e54bd0aeb8 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDct.Kernels.Generated.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDct.Kernels.Generated.cs @@ -1 +1,5874 @@ -ErrorGeneratingOutput \ No newline at end of file +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; + +internal static partial class JxlDct +{ + /// + /// Computes independent forward transforms using two nonoverlapping borrowed vector banks. + /// + /// One vector per transform position. Its length is a power of two from one through 256. + /// At least values.Length vectors. Previous contents are not read. + private static void Forward(Span values, Span scratch) + { + // Small transforms stay in registers. The one-point transform is an identity and must not copy any trailing samples. + switch (values.Length) + { + case 1: + return; + case 2: + Forward2(values); + return; + case 4: + Forward4(values); + return; + case 8: + Forward8(values); + return; + case 16: + Forward16(values); + return; + case 32: + Forward32(values); + return; + } + + int half = values.Length / 2; + ReadOnlySpan weights = JxlDctScales.GetMultipliers(values.Length); + + // Consume both original halves before reusing their bank as recursive scratch. Neither recursive call can then destroy unread input. + for (int i = 0; i < half; i++) + { + float first = values[i]; + float last = values[values.Length - i - 1]; + scratch[i] = first + last; + scratch[half + i] = (first - last) * weights[i]; + } + + Forward(scratch[..half], values); + Forward(scratch.Slice(half, half), values); + scratch[half] = float.MultiplyAddEstimate(scratch[half], JxlDctScales.Sqrt2, scratch[half + 1]); + + for (int i = 1; i + 1 < half; i++) + { + scratch[half + i] += scratch[half + i + 1]; + } + + for (int i = 0; i < half; i++) + { + values[2 * i] = scratch[i]; + values[(2 * i) + 1] = scratch[half + i]; + } + } + + /// + /// Computes independent 2-point forward transforms without intermediate memory passes. + /// + /// Exactly 2 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Forward2(Span values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + float input0 = values[0]; + float input1 = values[1]; + + float v0 = input0 + input1; + float v1 = input0 - input1; + + values[0] = v0; + values[1] = v1; + } + + /// + /// Computes independent 4-point forward transforms without intermediate memory passes. + /// + /// Exactly 4 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Forward4(Span values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + float input0 = values[0]; + float input1 = values[1]; + float input2 = values[2]; + float input3 = values[3]; + + float v0 = input0 + input3; + float v1 = (input0 - input3) * 0.541196100146197f; + float v2 = input1 + input2; + float v3 = (input1 - input2) * 1.3065629648763764f; + float v4 = v0 + v2; + float v5 = v0 - v2; + float v6 = v1 + v3; + float v7 = v1 - v3; + float v8 = float.MultiplyAddEstimate(v6, JxlDctScales.Sqrt2, v7); + + values[0] = v4; + values[1] = v8; + values[2] = v5; + values[3] = v7; + } + + /// + /// Computes independent 8-point forward transforms without intermediate memory passes. + /// + /// Exactly 8 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Forward8(Span values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + float input0 = values[0]; + float input1 = values[1]; + float input2 = values[2]; + float input3 = values[3]; + float input4 = values[4]; + float input5 = values[5]; + float input6 = values[6]; + float input7 = values[7]; + + float v0 = input0 + input7; + float v1 = (input0 - input7) * 0.5097955791041592f; + float v2 = input1 + input6; + float v3 = (input1 - input6) * 0.6013448869350453f; + float v4 = input2 + input5; + float v5 = (input2 - input5) * 0.8999762231364156f; + float v6 = input3 + input4; + float v7 = (input3 - input4) * 2.5629154477415055f; + float v8 = v0 + v6; + float v9 = (v0 - v6) * 0.541196100146197f; + float v10 = v2 + v4; + float v11 = (v2 - v4) * 1.3065629648763764f; + float v12 = v8 + v10; + float v13 = v8 - v10; + float v14 = v9 + v11; + float v15 = v9 - v11; + float v16 = float.MultiplyAddEstimate(v14, JxlDctScales.Sqrt2, v15); + float v17 = v1 + v7; + float v18 = (v1 - v7) * 0.541196100146197f; + float v19 = v3 + v5; + float v20 = (v3 - v5) * 1.3065629648763764f; + float v21 = v17 + v19; + float v22 = v17 - v19; + float v23 = v18 + v20; + float v24 = v18 - v20; + float v25 = float.MultiplyAddEstimate(v23, JxlDctScales.Sqrt2, v24); + float v26 = float.MultiplyAddEstimate(v21, JxlDctScales.Sqrt2, v25); + float v27 = v25 + v22; + float v28 = v22 + v24; + + values[0] = v12; + values[1] = v26; + values[2] = v16; + values[3] = v27; + values[4] = v13; + values[5] = v28; + values[6] = v15; + values[7] = v24; + } + + /// + /// Computes independent 16-point forward transforms without intermediate memory passes. + /// + /// Exactly 16 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Forward16(Span values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + float input0 = values[0]; + float input1 = values[1]; + float input2 = values[2]; + float input3 = values[3]; + float input4 = values[4]; + float input5 = values[5]; + float input6 = values[6]; + float input7 = values[7]; + float input8 = values[8]; + float input9 = values[9]; + float input10 = values[10]; + float input11 = values[11]; + float input12 = values[12]; + float input13 = values[13]; + float input14 = values[14]; + float input15 = values[15]; + + float v0 = input0 + input15; + float v1 = (input0 - input15) * 0.5024192861881557f; + float v2 = input1 + input14; + float v3 = (input1 - input14) * 0.5224986149396889f; + float v4 = input2 + input13; + float v5 = (input2 - input13) * 0.5669440348163577f; + float v6 = input3 + input12; + float v7 = (input3 - input12) * 0.6468217833599901f; + float v8 = input4 + input11; + float v9 = (input4 - input11) * 0.7881546234512502f; + float v10 = input5 + input10; + float v11 = (input5 - input10) * 1.060677685990347f; + float v12 = input6 + input9; + float v13 = (input6 - input9) * 1.7224470982383342f; + float v14 = input7 + input8; + float v15 = (input7 - input8) * 5.101148618689155f; + float v16 = v0 + v14; + float v17 = (v0 - v14) * 0.5097955791041592f; + float v18 = v2 + v12; + float v19 = (v2 - v12) * 0.6013448869350453f; + float v20 = v4 + v10; + float v21 = (v4 - v10) * 0.8999762231364156f; + float v22 = v6 + v8; + float v23 = (v6 - v8) * 2.5629154477415055f; + float v24 = v16 + v22; + float v25 = (v16 - v22) * 0.541196100146197f; + float v26 = v18 + v20; + float v27 = (v18 - v20) * 1.3065629648763764f; + float v28 = v24 + v26; + float v29 = v24 - v26; + float v30 = v25 + v27; + float v31 = v25 - v27; + float v32 = float.MultiplyAddEstimate(v30, JxlDctScales.Sqrt2, v31); + float v33 = v17 + v23; + float v34 = (v17 - v23) * 0.541196100146197f; + float v35 = v19 + v21; + float v36 = (v19 - v21) * 1.3065629648763764f; + float v37 = v33 + v35; + float v38 = v33 - v35; + float v39 = v34 + v36; + float v40 = v34 - v36; + float v41 = float.MultiplyAddEstimate(v39, JxlDctScales.Sqrt2, v40); + float v42 = float.MultiplyAddEstimate(v37, JxlDctScales.Sqrt2, v41); + float v43 = v41 + v38; + float v44 = v38 + v40; + float v45 = v1 + v15; + float v46 = (v1 - v15) * 0.5097955791041592f; + float v47 = v3 + v13; + float v48 = (v3 - v13) * 0.6013448869350453f; + float v49 = v5 + v11; + float v50 = (v5 - v11) * 0.8999762231364156f; + float v51 = v7 + v9; + float v52 = (v7 - v9) * 2.5629154477415055f; + float v53 = v45 + v51; + float v54 = (v45 - v51) * 0.541196100146197f; + float v55 = v47 + v49; + float v56 = (v47 - v49) * 1.3065629648763764f; + float v57 = v53 + v55; + float v58 = v53 - v55; + float v59 = v54 + v56; + float v60 = v54 - v56; + float v61 = float.MultiplyAddEstimate(v59, JxlDctScales.Sqrt2, v60); + float v62 = v46 + v52; + float v63 = (v46 - v52) * 0.541196100146197f; + float v64 = v48 + v50; + float v65 = (v48 - v50) * 1.3065629648763764f; + float v66 = v62 + v64; + float v67 = v62 - v64; + float v68 = v63 + v65; + float v69 = v63 - v65; + float v70 = float.MultiplyAddEstimate(v68, JxlDctScales.Sqrt2, v69); + float v71 = float.MultiplyAddEstimate(v66, JxlDctScales.Sqrt2, v70); + float v72 = v70 + v67; + float v73 = v67 + v69; + float v74 = float.MultiplyAddEstimate(v57, JxlDctScales.Sqrt2, v71); + float v75 = v71 + v61; + float v76 = v61 + v72; + float v77 = v72 + v58; + float v78 = v58 + v73; + float v79 = v73 + v60; + float v80 = v60 + v69; + + values[0] = v28; + values[1] = v74; + values[2] = v42; + values[3] = v75; + values[4] = v32; + values[5] = v76; + values[6] = v43; + values[7] = v77; + values[8] = v29; + values[9] = v78; + values[10] = v44; + values[11] = v79; + values[12] = v31; + values[13] = v80; + values[14] = v40; + values[15] = v69; + } + + /// + /// Computes independent 32-point forward transforms without intermediate memory passes. + /// + /// Exactly 32 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Forward32(Span values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + float input0 = values[0]; + float input1 = values[1]; + float input2 = values[2]; + float input3 = values[3]; + float input4 = values[4]; + float input5 = values[5]; + float input6 = values[6]; + float input7 = values[7]; + float input8 = values[8]; + float input9 = values[9]; + float input10 = values[10]; + float input11 = values[11]; + float input12 = values[12]; + float input13 = values[13]; + float input14 = values[14]; + float input15 = values[15]; + float input16 = values[16]; + float input17 = values[17]; + float input18 = values[18]; + float input19 = values[19]; + float input20 = values[20]; + float input21 = values[21]; + float input22 = values[22]; + float input23 = values[23]; + float input24 = values[24]; + float input25 = values[25]; + float input26 = values[26]; + float input27 = values[27]; + float input28 = values[28]; + float input29 = values[29]; + float input30 = values[30]; + float input31 = values[31]; + + float v0 = input0 + input31; + float v1 = (input0 - input31) * 0.5006029982351963f; + float v2 = input1 + input30; + float v3 = (input1 - input30) * 0.5054709598975436f; + float v4 = input2 + input29; + float v5 = (input2 - input29) * 0.5154473099226246f; + float v6 = input3 + input28; + float v7 = (input3 - input28) * 0.5310425910897841f; + float v8 = input4 + input27; + float v9 = (input4 - input27) * 0.5531038960344445f; + float v10 = input5 + input26; + float v11 = (input5 - input26) * 0.5829349682061339f; + float v12 = input6 + input25; + float v13 = (input6 - input25) * 0.6225041230356648f; + float v14 = input7 + input24; + float v15 = (input7 - input24) * 0.6748083414550057f; + float v16 = input8 + input23; + float v17 = (input8 - input23) * 0.7445362710022986f; + float v18 = input9 + input22; + float v19 = (input9 - input22) * 0.8393496454155268f; + float v20 = input10 + input21; + float v21 = (input10 - input21) * 0.9725682378619608f; + float v22 = input11 + input20; + float v23 = (input11 - input20) * 1.1694399334328847f; + float v24 = input12 + input19; + float v25 = (input12 - input19) * 1.4841646163141662f; + float v26 = input13 + input18; + float v27 = (input13 - input18) * 2.057781009953411f; + float v28 = input14 + input17; + float v29 = (input14 - input17) * 3.407608418468719f; + float v30 = input15 + input16; + float v31 = (input15 - input16) * 10.190008123548033f; + float v32 = v0 + v30; + float v33 = (v0 - v30) * 0.5024192861881557f; + float v34 = v2 + v28; + float v35 = (v2 - v28) * 0.5224986149396889f; + float v36 = v4 + v26; + float v37 = (v4 - v26) * 0.5669440348163577f; + float v38 = v6 + v24; + float v39 = (v6 - v24) * 0.6468217833599901f; + float v40 = v8 + v22; + float v41 = (v8 - v22) * 0.7881546234512502f; + float v42 = v10 + v20; + float v43 = (v10 - v20) * 1.060677685990347f; + float v44 = v12 + v18; + float v45 = (v12 - v18) * 1.7224470982383342f; + float v46 = v14 + v16; + float v47 = (v14 - v16) * 5.101148618689155f; + float v48 = v32 + v46; + float v49 = (v32 - v46) * 0.5097955791041592f; + float v50 = v34 + v44; + float v51 = (v34 - v44) * 0.6013448869350453f; + float v52 = v36 + v42; + float v53 = (v36 - v42) * 0.8999762231364156f; + float v54 = v38 + v40; + float v55 = (v38 - v40) * 2.5629154477415055f; + float v56 = v48 + v54; + float v57 = (v48 - v54) * 0.541196100146197f; + float v58 = v50 + v52; + float v59 = (v50 - v52) * 1.3065629648763764f; + float v60 = v56 + v58; + float v61 = v56 - v58; + float v62 = v57 + v59; + float v63 = v57 - v59; + float v64 = float.MultiplyAddEstimate(v62, JxlDctScales.Sqrt2, v63); + float v65 = v49 + v55; + float v66 = (v49 - v55) * 0.541196100146197f; + float v67 = v51 + v53; + float v68 = (v51 - v53) * 1.3065629648763764f; + float v69 = v65 + v67; + float v70 = v65 - v67; + float v71 = v66 + v68; + float v72 = v66 - v68; + float v73 = float.MultiplyAddEstimate(v71, JxlDctScales.Sqrt2, v72); + float v74 = float.MultiplyAddEstimate(v69, JxlDctScales.Sqrt2, v73); + float v75 = v73 + v70; + float v76 = v70 + v72; + float v77 = v33 + v47; + float v78 = (v33 - v47) * 0.5097955791041592f; + float v79 = v35 + v45; + float v80 = (v35 - v45) * 0.6013448869350453f; + float v81 = v37 + v43; + float v82 = (v37 - v43) * 0.8999762231364156f; + float v83 = v39 + v41; + float v84 = (v39 - v41) * 2.5629154477415055f; + float v85 = v77 + v83; + float v86 = (v77 - v83) * 0.541196100146197f; + float v87 = v79 + v81; + float v88 = (v79 - v81) * 1.3065629648763764f; + float v89 = v85 + v87; + float v90 = v85 - v87; + float v91 = v86 + v88; + float v92 = v86 - v88; + float v93 = float.MultiplyAddEstimate(v91, JxlDctScales.Sqrt2, v92); + float v94 = v78 + v84; + float v95 = (v78 - v84) * 0.541196100146197f; + float v96 = v80 + v82; + float v97 = (v80 - v82) * 1.3065629648763764f; + float v98 = v94 + v96; + float v99 = v94 - v96; + float v100 = v95 + v97; + float v101 = v95 - v97; + float v102 = float.MultiplyAddEstimate(v100, JxlDctScales.Sqrt2, v101); + float v103 = float.MultiplyAddEstimate(v98, JxlDctScales.Sqrt2, v102); + float v104 = v102 + v99; + float v105 = v99 + v101; + float v106 = float.MultiplyAddEstimate(v89, JxlDctScales.Sqrt2, v103); + float v107 = v103 + v93; + float v108 = v93 + v104; + float v109 = v104 + v90; + float v110 = v90 + v105; + float v111 = v105 + v92; + float v112 = v92 + v101; + float v113 = v1 + v31; + float v114 = (v1 - v31) * 0.5024192861881557f; + float v115 = v3 + v29; + float v116 = (v3 - v29) * 0.5224986149396889f; + float v117 = v5 + v27; + float v118 = (v5 - v27) * 0.5669440348163577f; + float v119 = v7 + v25; + float v120 = (v7 - v25) * 0.6468217833599901f; + float v121 = v9 + v23; + float v122 = (v9 - v23) * 0.7881546234512502f; + float v123 = v11 + v21; + float v124 = (v11 - v21) * 1.060677685990347f; + float v125 = v13 + v19; + float v126 = (v13 - v19) * 1.7224470982383342f; + float v127 = v15 + v17; + float v128 = (v15 - v17) * 5.101148618689155f; + float v129 = v113 + v127; + float v130 = (v113 - v127) * 0.5097955791041592f; + float v131 = v115 + v125; + float v132 = (v115 - v125) * 0.6013448869350453f; + float v133 = v117 + v123; + float v134 = (v117 - v123) * 0.8999762231364156f; + float v135 = v119 + v121; + float v136 = (v119 - v121) * 2.5629154477415055f; + float v137 = v129 + v135; + float v138 = (v129 - v135) * 0.541196100146197f; + float v139 = v131 + v133; + float v140 = (v131 - v133) * 1.3065629648763764f; + float v141 = v137 + v139; + float v142 = v137 - v139; + float v143 = v138 + v140; + float v144 = v138 - v140; + float v145 = float.MultiplyAddEstimate(v143, JxlDctScales.Sqrt2, v144); + float v146 = v130 + v136; + float v147 = (v130 - v136) * 0.541196100146197f; + float v148 = v132 + v134; + float v149 = (v132 - v134) * 1.3065629648763764f; + float v150 = v146 + v148; + float v151 = v146 - v148; + float v152 = v147 + v149; + float v153 = v147 - v149; + float v154 = float.MultiplyAddEstimate(v152, JxlDctScales.Sqrt2, v153); + float v155 = float.MultiplyAddEstimate(v150, JxlDctScales.Sqrt2, v154); + float v156 = v154 + v151; + float v157 = v151 + v153; + float v158 = v114 + v128; + float v159 = (v114 - v128) * 0.5097955791041592f; + float v160 = v116 + v126; + float v161 = (v116 - v126) * 0.6013448869350453f; + float v162 = v118 + v124; + float v163 = (v118 - v124) * 0.8999762231364156f; + float v164 = v120 + v122; + float v165 = (v120 - v122) * 2.5629154477415055f; + float v166 = v158 + v164; + float v167 = (v158 - v164) * 0.541196100146197f; + float v168 = v160 + v162; + float v169 = (v160 - v162) * 1.3065629648763764f; + float v170 = v166 + v168; + float v171 = v166 - v168; + float v172 = v167 + v169; + float v173 = v167 - v169; + float v174 = float.MultiplyAddEstimate(v172, JxlDctScales.Sqrt2, v173); + float v175 = v159 + v165; + float v176 = (v159 - v165) * 0.541196100146197f; + float v177 = v161 + v163; + float v178 = (v161 - v163) * 1.3065629648763764f; + float v179 = v175 + v177; + float v180 = v175 - v177; + float v181 = v176 + v178; + float v182 = v176 - v178; + float v183 = float.MultiplyAddEstimate(v181, JxlDctScales.Sqrt2, v182); + float v184 = float.MultiplyAddEstimate(v179, JxlDctScales.Sqrt2, v183); + float v185 = v183 + v180; + float v186 = v180 + v182; + float v187 = float.MultiplyAddEstimate(v170, JxlDctScales.Sqrt2, v184); + float v188 = v184 + v174; + float v189 = v174 + v185; + float v190 = v185 + v171; + float v191 = v171 + v186; + float v192 = v186 + v173; + float v193 = v173 + v182; + float v194 = float.MultiplyAddEstimate(v141, JxlDctScales.Sqrt2, v187); + float v195 = v187 + v155; + float v196 = v155 + v188; + float v197 = v188 + v145; + float v198 = v145 + v189; + float v199 = v189 + v156; + float v200 = v156 + v190; + float v201 = v190 + v142; + float v202 = v142 + v191; + float v203 = v191 + v157; + float v204 = v157 + v192; + float v205 = v192 + v144; + float v206 = v144 + v193; + float v207 = v193 + v153; + float v208 = v153 + v182; + + values[0] = v60; + values[1] = v194; + values[2] = v106; + values[3] = v195; + values[4] = v74; + values[5] = v196; + values[6] = v107; + values[7] = v197; + values[8] = v64; + values[9] = v198; + values[10] = v108; + values[11] = v199; + values[12] = v75; + values[13] = v200; + values[14] = v109; + values[15] = v201; + values[16] = v61; + values[17] = v202; + values[18] = v110; + values[19] = v203; + values[20] = v76; + values[21] = v204; + values[22] = v111; + values[23] = v205; + values[24] = v63; + values[25] = v206; + values[26] = v112; + values[27] = v207; + values[28] = v72; + values[29] = v208; + values[30] = v101; + values[31] = v182; + } + + /// + /// Processes complete 1-lane forward batches along input columns. + /// + /// The number of positions in each transform. + /// The number of independent transforms. + /// The borrowed source matrix. + /// The source row stride, in floats. + /// The borrowed output matrix. + /// The destination row stride, in floats. + /// The first independent transform not already processed by a wider batch. + /// Borrowed storage for two length-by-1 banks. Only the first bank needs initialized inputs. + /// The first independent transform left after the complete batches. + private static int ForwardColumnsScalar( + int length, + int count, + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int first, + Span workspace) + { + Span values = workspace[..length]; + Span scratch = workspace.Slice(length, length); + ref float input = ref MemoryMarshal.GetReference(source); + ref float output = ref MemoryMarshal.GetReference(destination); + float scale = 1F / length; + + for (; first <= count - 1; first += 1) + { + // One vector covers adjacent columns at the same transform position. Physical strides never enter the coefficient-bank layout. + for (int i = 0; i < length; i++) + { + values[i] = Unsafe.Add(ref input, (i * sourceStride) + first); + } + + Forward(values, scratch); + for (int i = 0; i < length; i++) + { + Unsafe.Add(ref output, (i * destinationStride) + first) = values[i] * scale; + } + } + + return first; + } + + /// + /// Processes complete 1-lane forward batches along input rows. + /// + /// The number of positions in each transform. + /// The number of independent transforms. + /// The borrowed source matrix. + /// The source row stride, in floats. + /// The borrowed output matrix. + /// The destination row stride, in floats. + /// Whether transform positions become output rows and input rows become output columns. + /// The first independent transform not already processed by a wider batch. + /// Borrowed storage for two length-by-1 banks. Only the first bank needs initialized inputs. + /// The first independent transform left after the complete batches. + private static int ForwardRowsScalar( + int length, + int count, + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + bool transposed, + int first, + Span workspace) + { + Span values = workspace[..length]; + Span scratch = workspace.Slice(length, length); + ref float input = ref MemoryMarshal.GetReference(source); + ref float output = ref MemoryMarshal.GetReference(destination); + float scale = 1F / length; + + for (; first <= count - 1; first += 1) + { + source.Slice(first * sourceStride, length).CopyTo(values); + + Forward(values, scratch); + if (transposed) + { + // A complete vector is one contiguous output row. Only logical samples are stored; destination padding remains untouched. + for (int i = 0; i < length; i++) + { + Unsafe.Add(ref output, (i * destinationStride) + first) = values[i] * scale; + } + } + else + { + for (int i = 0; i < length; i++) + { + Unsafe.Add(ref output, (first * destinationStride) + i) = values[i] * scale; + } + } + } + + return first; + } + + /// + /// Computes independent inverse transforms using two nonoverlapping borrowed vector banks. + /// + /// One vector per transform position. Its length is a power of two from one through 256. + /// At least values.Length vectors. Previous contents are not read. + private static void Inverse(Span values, Span scratch) + { + // Small transforms stay in registers. The one-point transform is an identity and must not copy any trailing samples. + switch (values.Length) + { + case 1: + return; + case 2: + Inverse2(values); + return; + case 4: + Inverse4(values); + return; + case 8: + Inverse8(values); + return; + case 16: + Inverse16(values); + return; + case 32: + Inverse32(values); + return; + } + + int half = values.Length / 2; + // Separate frequency parity before either recursive call. The original bank can then serve as recursive scratch. + for (int i = 0; i < half; i++) + { + scratch[i] = values[2 * i]; + scratch[half + i] = values[(2 * i) + 1]; + } + + Inverse(scratch[..half], values); + + // Descend so every odd-frequency predecessor is read before its own entry changes. + for (int i = half - 1; i > 0; i--) + { + scratch[half + i] += scratch[half + i - 1]; + } + + scratch[half] *= JxlDctScales.Sqrt2; + Inverse(scratch.Slice(half, half), values); + ReadOnlySpan weights = JxlDctScales.GetMultipliers(values.Length); + + for (int i = 0; i < half; i++) + { + float weight = weights[i]; + float even = scratch[i]; + float odd = scratch[half + i]; + values[i] = float.MultiplyAddEstimate(weight, odd, even); + values[values.Length - i - 1] = float.MultiplyAddEstimate(-weight, odd, even); + } + } + + /// + /// Computes independent 2-point inverse transforms without intermediate memory passes. + /// + /// Exactly 2 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Inverse2(Span values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + float input0 = values[0]; + float input1 = values[1]; + + float v0 = input0 + input1; + float v1 = input0 - input1; + + values[0] = v0; + values[1] = v1; + } + + /// + /// Computes independent 4-point inverse transforms without intermediate memory passes. + /// + /// Exactly 4 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Inverse4(Span values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + float input0 = values[0]; + float input1 = values[1]; + float input2 = values[2]; + float input3 = values[3]; + + float v0 = input0 + input2; + float v1 = input0 - input2; + float v2 = input3 + input1; + float v3 = input1 * JxlDctScales.Sqrt2; + float v4 = v3 + v2; + float v5 = v3 - v2; + float v6 = float.MultiplyAddEstimate(0.541196100146197f, v4, v0); + float v7 = float.MultiplyAddEstimate(-0.541196100146197f, v4, v0); + float v8 = float.MultiplyAddEstimate(1.3065629648763764f, v5, v1); + float v9 = float.MultiplyAddEstimate(-1.3065629648763764f, v5, v1); + + values[0] = v6; + values[1] = v8; + values[2] = v9; + values[3] = v7; + } + + /// + /// Computes independent 8-point inverse transforms without intermediate memory passes. + /// + /// Exactly 8 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Inverse8(Span values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + float input0 = values[0]; + float input1 = values[1]; + float input2 = values[2]; + float input3 = values[3]; + float input4 = values[4]; + float input5 = values[5]; + float input6 = values[6]; + float input7 = values[7]; + + float v0 = input0 + input4; + float v1 = input0 - input4; + float v2 = input6 + input2; + float v3 = input2 * JxlDctScales.Sqrt2; + float v4 = v3 + v2; + float v5 = v3 - v2; + float v6 = float.MultiplyAddEstimate(0.541196100146197f, v4, v0); + float v7 = float.MultiplyAddEstimate(-0.541196100146197f, v4, v0); + float v8 = float.MultiplyAddEstimate(1.3065629648763764f, v5, v1); + float v9 = float.MultiplyAddEstimate(-1.3065629648763764f, v5, v1); + float v10 = input7 + input5; + float v11 = input5 + input3; + float v12 = input3 + input1; + float v13 = input1 * JxlDctScales.Sqrt2; + float v14 = v13 + v11; + float v15 = v13 - v11; + float v16 = v10 + v12; + float v17 = v12 * JxlDctScales.Sqrt2; + float v18 = v17 + v16; + float v19 = v17 - v16; + float v20 = float.MultiplyAddEstimate(0.541196100146197f, v18, v14); + float v21 = float.MultiplyAddEstimate(-0.541196100146197f, v18, v14); + float v22 = float.MultiplyAddEstimate(1.3065629648763764f, v19, v15); + float v23 = float.MultiplyAddEstimate(-1.3065629648763764f, v19, v15); + float v24 = float.MultiplyAddEstimate(0.5097955791041592f, v20, v6); + float v25 = float.MultiplyAddEstimate(-0.5097955791041592f, v20, v6); + float v26 = float.MultiplyAddEstimate(0.6013448869350453f, v22, v8); + float v27 = float.MultiplyAddEstimate(-0.6013448869350453f, v22, v8); + float v28 = float.MultiplyAddEstimate(0.8999762231364156f, v23, v9); + float v29 = float.MultiplyAddEstimate(-0.8999762231364156f, v23, v9); + float v30 = float.MultiplyAddEstimate(2.5629154477415055f, v21, v7); + float v31 = float.MultiplyAddEstimate(-2.5629154477415055f, v21, v7); + + values[0] = v24; + values[1] = v26; + values[2] = v28; + values[3] = v30; + values[4] = v31; + values[5] = v29; + values[6] = v27; + values[7] = v25; + } + + /// + /// Computes independent 16-point inverse transforms without intermediate memory passes. + /// + /// Exactly 16 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Inverse16(Span values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + float input0 = values[0]; + float input1 = values[1]; + float input2 = values[2]; + float input3 = values[3]; + float input4 = values[4]; + float input5 = values[5]; + float input6 = values[6]; + float input7 = values[7]; + float input8 = values[8]; + float input9 = values[9]; + float input10 = values[10]; + float input11 = values[11]; + float input12 = values[12]; + float input13 = values[13]; + float input14 = values[14]; + float input15 = values[15]; + + float v0 = input0 + input8; + float v1 = input0 - input8; + float v2 = input12 + input4; + float v3 = input4 * JxlDctScales.Sqrt2; + float v4 = v3 + v2; + float v5 = v3 - v2; + float v6 = float.MultiplyAddEstimate(0.541196100146197f, v4, v0); + float v7 = float.MultiplyAddEstimate(-0.541196100146197f, v4, v0); + float v8 = float.MultiplyAddEstimate(1.3065629648763764f, v5, v1); + float v9 = float.MultiplyAddEstimate(-1.3065629648763764f, v5, v1); + float v10 = input14 + input10; + float v11 = input10 + input6; + float v12 = input6 + input2; + float v13 = input2 * JxlDctScales.Sqrt2; + float v14 = v13 + v11; + float v15 = v13 - v11; + float v16 = v10 + v12; + float v17 = v12 * JxlDctScales.Sqrt2; + float v18 = v17 + v16; + float v19 = v17 - v16; + float v20 = float.MultiplyAddEstimate(0.541196100146197f, v18, v14); + float v21 = float.MultiplyAddEstimate(-0.541196100146197f, v18, v14); + float v22 = float.MultiplyAddEstimate(1.3065629648763764f, v19, v15); + float v23 = float.MultiplyAddEstimate(-1.3065629648763764f, v19, v15); + float v24 = float.MultiplyAddEstimate(0.5097955791041592f, v20, v6); + float v25 = float.MultiplyAddEstimate(-0.5097955791041592f, v20, v6); + float v26 = float.MultiplyAddEstimate(0.6013448869350453f, v22, v8); + float v27 = float.MultiplyAddEstimate(-0.6013448869350453f, v22, v8); + float v28 = float.MultiplyAddEstimate(0.8999762231364156f, v23, v9); + float v29 = float.MultiplyAddEstimate(-0.8999762231364156f, v23, v9); + float v30 = float.MultiplyAddEstimate(2.5629154477415055f, v21, v7); + float v31 = float.MultiplyAddEstimate(-2.5629154477415055f, v21, v7); + float v32 = input15 + input13; + float v33 = input13 + input11; + float v34 = input11 + input9; + float v35 = input9 + input7; + float v36 = input7 + input5; + float v37 = input5 + input3; + float v38 = input3 + input1; + float v39 = input1 * JxlDctScales.Sqrt2; + float v40 = v39 + v35; + float v41 = v39 - v35; + float v42 = v33 + v37; + float v43 = v37 * JxlDctScales.Sqrt2; + float v44 = v43 + v42; + float v45 = v43 - v42; + float v46 = float.MultiplyAddEstimate(0.541196100146197f, v44, v40); + float v47 = float.MultiplyAddEstimate(-0.541196100146197f, v44, v40); + float v48 = float.MultiplyAddEstimate(1.3065629648763764f, v45, v41); + float v49 = float.MultiplyAddEstimate(-1.3065629648763764f, v45, v41); + float v50 = v32 + v34; + float v51 = v34 + v36; + float v52 = v36 + v38; + float v53 = v38 * JxlDctScales.Sqrt2; + float v54 = v53 + v51; + float v55 = v53 - v51; + float v56 = v50 + v52; + float v57 = v52 * JxlDctScales.Sqrt2; + float v58 = v57 + v56; + float v59 = v57 - v56; + float v60 = float.MultiplyAddEstimate(0.541196100146197f, v58, v54); + float v61 = float.MultiplyAddEstimate(-0.541196100146197f, v58, v54); + float v62 = float.MultiplyAddEstimate(1.3065629648763764f, v59, v55); + float v63 = float.MultiplyAddEstimate(-1.3065629648763764f, v59, v55); + float v64 = float.MultiplyAddEstimate(0.5097955791041592f, v60, v46); + float v65 = float.MultiplyAddEstimate(-0.5097955791041592f, v60, v46); + float v66 = float.MultiplyAddEstimate(0.6013448869350453f, v62, v48); + float v67 = float.MultiplyAddEstimate(-0.6013448869350453f, v62, v48); + float v68 = float.MultiplyAddEstimate(0.8999762231364156f, v63, v49); + float v69 = float.MultiplyAddEstimate(-0.8999762231364156f, v63, v49); + float v70 = float.MultiplyAddEstimate(2.5629154477415055f, v61, v47); + float v71 = float.MultiplyAddEstimate(-2.5629154477415055f, v61, v47); + float v72 = float.MultiplyAddEstimate(0.5024192861881557f, v64, v24); + float v73 = float.MultiplyAddEstimate(-0.5024192861881557f, v64, v24); + float v74 = float.MultiplyAddEstimate(0.5224986149396889f, v66, v26); + float v75 = float.MultiplyAddEstimate(-0.5224986149396889f, v66, v26); + float v76 = float.MultiplyAddEstimate(0.5669440348163577f, v68, v28); + float v77 = float.MultiplyAddEstimate(-0.5669440348163577f, v68, v28); + float v78 = float.MultiplyAddEstimate(0.6468217833599901f, v70, v30); + float v79 = float.MultiplyAddEstimate(-0.6468217833599901f, v70, v30); + float v80 = float.MultiplyAddEstimate(0.7881546234512502f, v71, v31); + float v81 = float.MultiplyAddEstimate(-0.7881546234512502f, v71, v31); + float v82 = float.MultiplyAddEstimate(1.060677685990347f, v69, v29); + float v83 = float.MultiplyAddEstimate(-1.060677685990347f, v69, v29); + float v84 = float.MultiplyAddEstimate(1.7224470982383342f, v67, v27); + float v85 = float.MultiplyAddEstimate(-1.7224470982383342f, v67, v27); + float v86 = float.MultiplyAddEstimate(5.101148618689155f, v65, v25); + float v87 = float.MultiplyAddEstimate(-5.101148618689155f, v65, v25); + + values[0] = v72; + values[1] = v74; + values[2] = v76; + values[3] = v78; + values[4] = v80; + values[5] = v82; + values[6] = v84; + values[7] = v86; + values[8] = v87; + values[9] = v85; + values[10] = v83; + values[11] = v81; + values[12] = v79; + values[13] = v77; + values[14] = v75; + values[15] = v73; + } + + /// + /// Computes independent 32-point inverse transforms without intermediate memory passes. + /// + /// Exactly 32 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Inverse32(Span values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + float input0 = values[0]; + float input1 = values[1]; + float input2 = values[2]; + float input3 = values[3]; + float input4 = values[4]; + float input5 = values[5]; + float input6 = values[6]; + float input7 = values[7]; + float input8 = values[8]; + float input9 = values[9]; + float input10 = values[10]; + float input11 = values[11]; + float input12 = values[12]; + float input13 = values[13]; + float input14 = values[14]; + float input15 = values[15]; + float input16 = values[16]; + float input17 = values[17]; + float input18 = values[18]; + float input19 = values[19]; + float input20 = values[20]; + float input21 = values[21]; + float input22 = values[22]; + float input23 = values[23]; + float input24 = values[24]; + float input25 = values[25]; + float input26 = values[26]; + float input27 = values[27]; + float input28 = values[28]; + float input29 = values[29]; + float input30 = values[30]; + float input31 = values[31]; + + float v0 = input0 + input16; + float v1 = input0 - input16; + float v2 = input24 + input8; + float v3 = input8 * JxlDctScales.Sqrt2; + float v4 = v3 + v2; + float v5 = v3 - v2; + float v6 = float.MultiplyAddEstimate(0.541196100146197f, v4, v0); + float v7 = float.MultiplyAddEstimate(-0.541196100146197f, v4, v0); + float v8 = float.MultiplyAddEstimate(1.3065629648763764f, v5, v1); + float v9 = float.MultiplyAddEstimate(-1.3065629648763764f, v5, v1); + float v10 = input28 + input20; + float v11 = input20 + input12; + float v12 = input12 + input4; + float v13 = input4 * JxlDctScales.Sqrt2; + float v14 = v13 + v11; + float v15 = v13 - v11; + float v16 = v10 + v12; + float v17 = v12 * JxlDctScales.Sqrt2; + float v18 = v17 + v16; + float v19 = v17 - v16; + float v20 = float.MultiplyAddEstimate(0.541196100146197f, v18, v14); + float v21 = float.MultiplyAddEstimate(-0.541196100146197f, v18, v14); + float v22 = float.MultiplyAddEstimate(1.3065629648763764f, v19, v15); + float v23 = float.MultiplyAddEstimate(-1.3065629648763764f, v19, v15); + float v24 = float.MultiplyAddEstimate(0.5097955791041592f, v20, v6); + float v25 = float.MultiplyAddEstimate(-0.5097955791041592f, v20, v6); + float v26 = float.MultiplyAddEstimate(0.6013448869350453f, v22, v8); + float v27 = float.MultiplyAddEstimate(-0.6013448869350453f, v22, v8); + float v28 = float.MultiplyAddEstimate(0.8999762231364156f, v23, v9); + float v29 = float.MultiplyAddEstimate(-0.8999762231364156f, v23, v9); + float v30 = float.MultiplyAddEstimate(2.5629154477415055f, v21, v7); + float v31 = float.MultiplyAddEstimate(-2.5629154477415055f, v21, v7); + float v32 = input30 + input26; + float v33 = input26 + input22; + float v34 = input22 + input18; + float v35 = input18 + input14; + float v36 = input14 + input10; + float v37 = input10 + input6; + float v38 = input6 + input2; + float v39 = input2 * JxlDctScales.Sqrt2; + float v40 = v39 + v35; + float v41 = v39 - v35; + float v42 = v33 + v37; + float v43 = v37 * JxlDctScales.Sqrt2; + float v44 = v43 + v42; + float v45 = v43 - v42; + float v46 = float.MultiplyAddEstimate(0.541196100146197f, v44, v40); + float v47 = float.MultiplyAddEstimate(-0.541196100146197f, v44, v40); + float v48 = float.MultiplyAddEstimate(1.3065629648763764f, v45, v41); + float v49 = float.MultiplyAddEstimate(-1.3065629648763764f, v45, v41); + float v50 = v32 + v34; + float v51 = v34 + v36; + float v52 = v36 + v38; + float v53 = v38 * JxlDctScales.Sqrt2; + float v54 = v53 + v51; + float v55 = v53 - v51; + float v56 = v50 + v52; + float v57 = v52 * JxlDctScales.Sqrt2; + float v58 = v57 + v56; + float v59 = v57 - v56; + float v60 = float.MultiplyAddEstimate(0.541196100146197f, v58, v54); + float v61 = float.MultiplyAddEstimate(-0.541196100146197f, v58, v54); + float v62 = float.MultiplyAddEstimate(1.3065629648763764f, v59, v55); + float v63 = float.MultiplyAddEstimate(-1.3065629648763764f, v59, v55); + float v64 = float.MultiplyAddEstimate(0.5097955791041592f, v60, v46); + float v65 = float.MultiplyAddEstimate(-0.5097955791041592f, v60, v46); + float v66 = float.MultiplyAddEstimate(0.6013448869350453f, v62, v48); + float v67 = float.MultiplyAddEstimate(-0.6013448869350453f, v62, v48); + float v68 = float.MultiplyAddEstimate(0.8999762231364156f, v63, v49); + float v69 = float.MultiplyAddEstimate(-0.8999762231364156f, v63, v49); + float v70 = float.MultiplyAddEstimate(2.5629154477415055f, v61, v47); + float v71 = float.MultiplyAddEstimate(-2.5629154477415055f, v61, v47); + float v72 = float.MultiplyAddEstimate(0.5024192861881557f, v64, v24); + float v73 = float.MultiplyAddEstimate(-0.5024192861881557f, v64, v24); + float v74 = float.MultiplyAddEstimate(0.5224986149396889f, v66, v26); + float v75 = float.MultiplyAddEstimate(-0.5224986149396889f, v66, v26); + float v76 = float.MultiplyAddEstimate(0.5669440348163577f, v68, v28); + float v77 = float.MultiplyAddEstimate(-0.5669440348163577f, v68, v28); + float v78 = float.MultiplyAddEstimate(0.6468217833599901f, v70, v30); + float v79 = float.MultiplyAddEstimate(-0.6468217833599901f, v70, v30); + float v80 = float.MultiplyAddEstimate(0.7881546234512502f, v71, v31); + float v81 = float.MultiplyAddEstimate(-0.7881546234512502f, v71, v31); + float v82 = float.MultiplyAddEstimate(1.060677685990347f, v69, v29); + float v83 = float.MultiplyAddEstimate(-1.060677685990347f, v69, v29); + float v84 = float.MultiplyAddEstimate(1.7224470982383342f, v67, v27); + float v85 = float.MultiplyAddEstimate(-1.7224470982383342f, v67, v27); + float v86 = float.MultiplyAddEstimate(5.101148618689155f, v65, v25); + float v87 = float.MultiplyAddEstimate(-5.101148618689155f, v65, v25); + float v88 = input31 + input29; + float v89 = input29 + input27; + float v90 = input27 + input25; + float v91 = input25 + input23; + float v92 = input23 + input21; + float v93 = input21 + input19; + float v94 = input19 + input17; + float v95 = input17 + input15; + float v96 = input15 + input13; + float v97 = input13 + input11; + float v98 = input11 + input9; + float v99 = input9 + input7; + float v100 = input7 + input5; + float v101 = input5 + input3; + float v102 = input3 + input1; + float v103 = input1 * JxlDctScales.Sqrt2; + float v104 = v103 + v95; + float v105 = v103 - v95; + float v106 = v91 + v99; + float v107 = v99 * JxlDctScales.Sqrt2; + float v108 = v107 + v106; + float v109 = v107 - v106; + float v110 = float.MultiplyAddEstimate(0.541196100146197f, v108, v104); + float v111 = float.MultiplyAddEstimate(-0.541196100146197f, v108, v104); + float v112 = float.MultiplyAddEstimate(1.3065629648763764f, v109, v105); + float v113 = float.MultiplyAddEstimate(-1.3065629648763764f, v109, v105); + float v114 = v89 + v93; + float v115 = v93 + v97; + float v116 = v97 + v101; + float v117 = v101 * JxlDctScales.Sqrt2; + float v118 = v117 + v115; + float v119 = v117 - v115; + float v120 = v114 + v116; + float v121 = v116 * JxlDctScales.Sqrt2; + float v122 = v121 + v120; + float v123 = v121 - v120; + float v124 = float.MultiplyAddEstimate(0.541196100146197f, v122, v118); + float v125 = float.MultiplyAddEstimate(-0.541196100146197f, v122, v118); + float v126 = float.MultiplyAddEstimate(1.3065629648763764f, v123, v119); + float v127 = float.MultiplyAddEstimate(-1.3065629648763764f, v123, v119); + float v128 = float.MultiplyAddEstimate(0.5097955791041592f, v124, v110); + float v129 = float.MultiplyAddEstimate(-0.5097955791041592f, v124, v110); + float v130 = float.MultiplyAddEstimate(0.6013448869350453f, v126, v112); + float v131 = float.MultiplyAddEstimate(-0.6013448869350453f, v126, v112); + float v132 = float.MultiplyAddEstimate(0.8999762231364156f, v127, v113); + float v133 = float.MultiplyAddEstimate(-0.8999762231364156f, v127, v113); + float v134 = float.MultiplyAddEstimate(2.5629154477415055f, v125, v111); + float v135 = float.MultiplyAddEstimate(-2.5629154477415055f, v125, v111); + float v136 = v88 + v90; + float v137 = v90 + v92; + float v138 = v92 + v94; + float v139 = v94 + v96; + float v140 = v96 + v98; + float v141 = v98 + v100; + float v142 = v100 + v102; + float v143 = v102 * JxlDctScales.Sqrt2; + float v144 = v143 + v139; + float v145 = v143 - v139; + float v146 = v137 + v141; + float v147 = v141 * JxlDctScales.Sqrt2; + float v148 = v147 + v146; + float v149 = v147 - v146; + float v150 = float.MultiplyAddEstimate(0.541196100146197f, v148, v144); + float v151 = float.MultiplyAddEstimate(-0.541196100146197f, v148, v144); + float v152 = float.MultiplyAddEstimate(1.3065629648763764f, v149, v145); + float v153 = float.MultiplyAddEstimate(-1.3065629648763764f, v149, v145); + float v154 = v136 + v138; + float v155 = v138 + v140; + float v156 = v140 + v142; + float v157 = v142 * JxlDctScales.Sqrt2; + float v158 = v157 + v155; + float v159 = v157 - v155; + float v160 = v154 + v156; + float v161 = v156 * JxlDctScales.Sqrt2; + float v162 = v161 + v160; + float v163 = v161 - v160; + float v164 = float.MultiplyAddEstimate(0.541196100146197f, v162, v158); + float v165 = float.MultiplyAddEstimate(-0.541196100146197f, v162, v158); + float v166 = float.MultiplyAddEstimate(1.3065629648763764f, v163, v159); + float v167 = float.MultiplyAddEstimate(-1.3065629648763764f, v163, v159); + float v168 = float.MultiplyAddEstimate(0.5097955791041592f, v164, v150); + float v169 = float.MultiplyAddEstimate(-0.5097955791041592f, v164, v150); + float v170 = float.MultiplyAddEstimate(0.6013448869350453f, v166, v152); + float v171 = float.MultiplyAddEstimate(-0.6013448869350453f, v166, v152); + float v172 = float.MultiplyAddEstimate(0.8999762231364156f, v167, v153); + float v173 = float.MultiplyAddEstimate(-0.8999762231364156f, v167, v153); + float v174 = float.MultiplyAddEstimate(2.5629154477415055f, v165, v151); + float v175 = float.MultiplyAddEstimate(-2.5629154477415055f, v165, v151); + float v176 = float.MultiplyAddEstimate(0.5024192861881557f, v168, v128); + float v177 = float.MultiplyAddEstimate(-0.5024192861881557f, v168, v128); + float v178 = float.MultiplyAddEstimate(0.5224986149396889f, v170, v130); + float v179 = float.MultiplyAddEstimate(-0.5224986149396889f, v170, v130); + float v180 = float.MultiplyAddEstimate(0.5669440348163577f, v172, v132); + float v181 = float.MultiplyAddEstimate(-0.5669440348163577f, v172, v132); + float v182 = float.MultiplyAddEstimate(0.6468217833599901f, v174, v134); + float v183 = float.MultiplyAddEstimate(-0.6468217833599901f, v174, v134); + float v184 = float.MultiplyAddEstimate(0.7881546234512502f, v175, v135); + float v185 = float.MultiplyAddEstimate(-0.7881546234512502f, v175, v135); + float v186 = float.MultiplyAddEstimate(1.060677685990347f, v173, v133); + float v187 = float.MultiplyAddEstimate(-1.060677685990347f, v173, v133); + float v188 = float.MultiplyAddEstimate(1.7224470982383342f, v171, v131); + float v189 = float.MultiplyAddEstimate(-1.7224470982383342f, v171, v131); + float v190 = float.MultiplyAddEstimate(5.101148618689155f, v169, v129); + float v191 = float.MultiplyAddEstimate(-5.101148618689155f, v169, v129); + float v192 = float.MultiplyAddEstimate(0.5006029982351963f, v176, v72); + float v193 = float.MultiplyAddEstimate(-0.5006029982351963f, v176, v72); + float v194 = float.MultiplyAddEstimate(0.5054709598975436f, v178, v74); + float v195 = float.MultiplyAddEstimate(-0.5054709598975436f, v178, v74); + float v196 = float.MultiplyAddEstimate(0.5154473099226246f, v180, v76); + float v197 = float.MultiplyAddEstimate(-0.5154473099226246f, v180, v76); + float v198 = float.MultiplyAddEstimate(0.5310425910897841f, v182, v78); + float v199 = float.MultiplyAddEstimate(-0.5310425910897841f, v182, v78); + float v200 = float.MultiplyAddEstimate(0.5531038960344445f, v184, v80); + float v201 = float.MultiplyAddEstimate(-0.5531038960344445f, v184, v80); + float v202 = float.MultiplyAddEstimate(0.5829349682061339f, v186, v82); + float v203 = float.MultiplyAddEstimate(-0.5829349682061339f, v186, v82); + float v204 = float.MultiplyAddEstimate(0.6225041230356648f, v188, v84); + float v205 = float.MultiplyAddEstimate(-0.6225041230356648f, v188, v84); + float v206 = float.MultiplyAddEstimate(0.6748083414550057f, v190, v86); + float v207 = float.MultiplyAddEstimate(-0.6748083414550057f, v190, v86); + float v208 = float.MultiplyAddEstimate(0.7445362710022986f, v191, v87); + float v209 = float.MultiplyAddEstimate(-0.7445362710022986f, v191, v87); + float v210 = float.MultiplyAddEstimate(0.8393496454155268f, v189, v85); + float v211 = float.MultiplyAddEstimate(-0.8393496454155268f, v189, v85); + float v212 = float.MultiplyAddEstimate(0.9725682378619608f, v187, v83); + float v213 = float.MultiplyAddEstimate(-0.9725682378619608f, v187, v83); + float v214 = float.MultiplyAddEstimate(1.1694399334328847f, v185, v81); + float v215 = float.MultiplyAddEstimate(-1.1694399334328847f, v185, v81); + float v216 = float.MultiplyAddEstimate(1.4841646163141662f, v183, v79); + float v217 = float.MultiplyAddEstimate(-1.4841646163141662f, v183, v79); + float v218 = float.MultiplyAddEstimate(2.057781009953411f, v181, v77); + float v219 = float.MultiplyAddEstimate(-2.057781009953411f, v181, v77); + float v220 = float.MultiplyAddEstimate(3.407608418468719f, v179, v75); + float v221 = float.MultiplyAddEstimate(-3.407608418468719f, v179, v75); + float v222 = float.MultiplyAddEstimate(10.190008123548033f, v177, v73); + float v223 = float.MultiplyAddEstimate(-10.190008123548033f, v177, v73); + + values[0] = v192; + values[1] = v194; + values[2] = v196; + values[3] = v198; + values[4] = v200; + values[5] = v202; + values[6] = v204; + values[7] = v206; + values[8] = v208; + values[9] = v210; + values[10] = v212; + values[11] = v214; + values[12] = v216; + values[13] = v218; + values[14] = v220; + values[15] = v222; + values[16] = v223; + values[17] = v221; + values[18] = v219; + values[19] = v217; + values[20] = v215; + values[21] = v213; + values[22] = v211; + values[23] = v209; + values[24] = v207; + values[25] = v205; + values[26] = v203; + values[27] = v201; + values[28] = v199; + values[29] = v197; + values[30] = v195; + values[31] = v193; + } + + /// + /// Processes complete 1-lane inverse batches along input columns. + /// + /// The number of positions in each transform. + /// The number of independent transforms. + /// The borrowed source matrix. + /// The source row stride, in floats. + /// The borrowed output matrix. + /// The destination row stride, in floats. + /// The first independent transform not already processed by a wider batch. + /// Borrowed storage for two length-by-1 banks. Only the first bank needs initialized inputs. + /// The first independent transform left after the complete batches. + private static int InverseColumnsScalar( + int length, + int count, + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int first, + Span workspace) + { + Span values = workspace[..length]; + Span scratch = workspace.Slice(length, length); + ref float input = ref MemoryMarshal.GetReference(source); + ref float output = ref MemoryMarshal.GetReference(destination); + + for (; first <= count - 1; first += 1) + { + // One vector covers adjacent columns at the same transform position. Physical strides never enter the coefficient-bank layout. + for (int i = 0; i < length; i++) + { + values[i] = Unsafe.Add(ref input, (i * sourceStride) + first); + } + + Inverse(values, scratch); + for (int i = 0; i < length; i++) + { + Unsafe.Add(ref output, (i * destinationStride) + first) = values[i]; + } + } + + return first; + } + + /// + /// Processes complete 1-lane inverse batches along input rows. + /// + /// The number of positions in each transform. + /// The number of independent transforms. + /// The borrowed source matrix. + /// The source row stride, in floats. + /// The borrowed output matrix. + /// The destination row stride, in floats. + /// Whether transform positions become output rows and input rows become output columns. + /// The first independent transform not already processed by a wider batch. + /// Borrowed storage for two length-by-1 banks. Only the first bank needs initialized inputs. + /// The first independent transform left after the complete batches. + private static int InverseRowsScalar( + int length, + int count, + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + bool transposed, + int first, + Span workspace) + { + Span values = workspace[..length]; + Span scratch = workspace.Slice(length, length); + ref float input = ref MemoryMarshal.GetReference(source); + ref float output = ref MemoryMarshal.GetReference(destination); + + for (; first <= count - 1; first += 1) + { + source.Slice(first * sourceStride, length).CopyTo(values); + + Inverse(values, scratch); + if (transposed) + { + // A complete vector is one contiguous output row. Only logical samples are stored; destination padding remains untouched. + for (int i = 0; i < length; i++) + { + Unsafe.Add(ref output, (i * destinationStride) + first) = values[i]; + } + } + else + { + for (int i = 0; i < length; i++) + { + Unsafe.Add(ref output, (first * destinationStride) + i) = values[i]; + } + } + } + + return first; + } + + /// + /// Computes independent forward transforms using two nonoverlapping borrowed vector banks. + /// + /// One vector per transform position. Its length is a power of two from one through 256. + /// At least values.Length vectors. Previous contents are not read. + private static void Forward(Span> values, Span> scratch) + { + // Small transforms stay in registers. The one-point transform is an identity and must not copy any trailing samples. + switch (values.Length) + { + case 1: + return; + case 2: + Forward2(values); + return; + case 4: + Forward4(values); + return; + case 8: + Forward8(values); + return; + case 16: + Forward16(values); + return; + case 32: + Forward32(values); + return; + } + + int half = values.Length / 2; + ReadOnlySpan weights = JxlDctScales.GetMultipliers(values.Length); + + // Consume both original halves before reusing their bank as recursive scratch. Neither recursive call can then destroy unread input. + for (int i = 0; i < half; i++) + { + Vector128 first = values[i]; + Vector128 last = values[values.Length - i - 1]; + scratch[i] = first + last; + scratch[half + i] = (first - last) * Vector128.Create(weights[i]); + } + + Forward(scratch[..half], values); + Forward(scratch.Slice(half, half), values); + scratch[half] = Vector128.MultiplyAddEstimate(scratch[half], Vector128.Create(JxlDctScales.Sqrt2), scratch[half + 1]); + + for (int i = 1; i + 1 < half; i++) + { + scratch[half + i] += scratch[half + i + 1]; + } + + for (int i = 0; i < half; i++) + { + values[2 * i] = scratch[i]; + values[(2 * i) + 1] = scratch[half + i]; + } + } + + /// + /// Computes independent 2-point forward transforms without intermediate memory passes. + /// + /// Exactly 2 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Forward2(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector128 input0 = values[0]; + Vector128 input1 = values[1]; + + Vector128 v0 = input0 + input1; + Vector128 v1 = input0 - input1; + + values[0] = v0; + values[1] = v1; + } + + /// + /// Computes independent 4-point forward transforms without intermediate memory passes. + /// + /// Exactly 4 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Forward4(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector128 input0 = values[0]; + Vector128 input1 = values[1]; + Vector128 input2 = values[2]; + Vector128 input3 = values[3]; + + Vector128 v0 = input0 + input3; + Vector128 v1 = (input0 - input3) * Vector128.Create(0.541196100146197f); + Vector128 v2 = input1 + input2; + Vector128 v3 = (input1 - input2) * Vector128.Create(1.3065629648763764f); + Vector128 v4 = v0 + v2; + Vector128 v5 = v0 - v2; + Vector128 v6 = v1 + v3; + Vector128 v7 = v1 - v3; + Vector128 v8 = Vector128.MultiplyAddEstimate(v6, Vector128.Create(JxlDctScales.Sqrt2), v7); + + values[0] = v4; + values[1] = v8; + values[2] = v5; + values[3] = v7; + } + + /// + /// Computes independent 8-point forward transforms without intermediate memory passes. + /// + /// Exactly 8 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Forward8(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector128 input0 = values[0]; + Vector128 input1 = values[1]; + Vector128 input2 = values[2]; + Vector128 input3 = values[3]; + Vector128 input4 = values[4]; + Vector128 input5 = values[5]; + Vector128 input6 = values[6]; + Vector128 input7 = values[7]; + + Vector128 v0 = input0 + input7; + Vector128 v1 = (input0 - input7) * Vector128.Create(0.5097955791041592f); + Vector128 v2 = input1 + input6; + Vector128 v3 = (input1 - input6) * Vector128.Create(0.6013448869350453f); + Vector128 v4 = input2 + input5; + Vector128 v5 = (input2 - input5) * Vector128.Create(0.8999762231364156f); + Vector128 v6 = input3 + input4; + Vector128 v7 = (input3 - input4) * Vector128.Create(2.5629154477415055f); + Vector128 v8 = v0 + v6; + Vector128 v9 = (v0 - v6) * Vector128.Create(0.541196100146197f); + Vector128 v10 = v2 + v4; + Vector128 v11 = (v2 - v4) * Vector128.Create(1.3065629648763764f); + Vector128 v12 = v8 + v10; + Vector128 v13 = v8 - v10; + Vector128 v14 = v9 + v11; + Vector128 v15 = v9 - v11; + Vector128 v16 = Vector128.MultiplyAddEstimate(v14, Vector128.Create(JxlDctScales.Sqrt2), v15); + Vector128 v17 = v1 + v7; + Vector128 v18 = (v1 - v7) * Vector128.Create(0.541196100146197f); + Vector128 v19 = v3 + v5; + Vector128 v20 = (v3 - v5) * Vector128.Create(1.3065629648763764f); + Vector128 v21 = v17 + v19; + Vector128 v22 = v17 - v19; + Vector128 v23 = v18 + v20; + Vector128 v24 = v18 - v20; + Vector128 v25 = Vector128.MultiplyAddEstimate(v23, Vector128.Create(JxlDctScales.Sqrt2), v24); + Vector128 v26 = Vector128.MultiplyAddEstimate(v21, Vector128.Create(JxlDctScales.Sqrt2), v25); + Vector128 v27 = v25 + v22; + Vector128 v28 = v22 + v24; + + values[0] = v12; + values[1] = v26; + values[2] = v16; + values[3] = v27; + values[4] = v13; + values[5] = v28; + values[6] = v15; + values[7] = v24; + } + + /// + /// Computes independent 16-point forward transforms without intermediate memory passes. + /// + /// Exactly 16 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Forward16(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector128 input0 = values[0]; + Vector128 input1 = values[1]; + Vector128 input2 = values[2]; + Vector128 input3 = values[3]; + Vector128 input4 = values[4]; + Vector128 input5 = values[5]; + Vector128 input6 = values[6]; + Vector128 input7 = values[7]; + Vector128 input8 = values[8]; + Vector128 input9 = values[9]; + Vector128 input10 = values[10]; + Vector128 input11 = values[11]; + Vector128 input12 = values[12]; + Vector128 input13 = values[13]; + Vector128 input14 = values[14]; + Vector128 input15 = values[15]; + + Vector128 v0 = input0 + input15; + Vector128 v1 = (input0 - input15) * Vector128.Create(0.5024192861881557f); + Vector128 v2 = input1 + input14; + Vector128 v3 = (input1 - input14) * Vector128.Create(0.5224986149396889f); + Vector128 v4 = input2 + input13; + Vector128 v5 = (input2 - input13) * Vector128.Create(0.5669440348163577f); + Vector128 v6 = input3 + input12; + Vector128 v7 = (input3 - input12) * Vector128.Create(0.6468217833599901f); + Vector128 v8 = input4 + input11; + Vector128 v9 = (input4 - input11) * Vector128.Create(0.7881546234512502f); + Vector128 v10 = input5 + input10; + Vector128 v11 = (input5 - input10) * Vector128.Create(1.060677685990347f); + Vector128 v12 = input6 + input9; + Vector128 v13 = (input6 - input9) * Vector128.Create(1.7224470982383342f); + Vector128 v14 = input7 + input8; + Vector128 v15 = (input7 - input8) * Vector128.Create(5.101148618689155f); + Vector128 v16 = v0 + v14; + Vector128 v17 = (v0 - v14) * Vector128.Create(0.5097955791041592f); + Vector128 v18 = v2 + v12; + Vector128 v19 = (v2 - v12) * Vector128.Create(0.6013448869350453f); + Vector128 v20 = v4 + v10; + Vector128 v21 = (v4 - v10) * Vector128.Create(0.8999762231364156f); + Vector128 v22 = v6 + v8; + Vector128 v23 = (v6 - v8) * Vector128.Create(2.5629154477415055f); + Vector128 v24 = v16 + v22; + Vector128 v25 = (v16 - v22) * Vector128.Create(0.541196100146197f); + Vector128 v26 = v18 + v20; + Vector128 v27 = (v18 - v20) * Vector128.Create(1.3065629648763764f); + Vector128 v28 = v24 + v26; + Vector128 v29 = v24 - v26; + Vector128 v30 = v25 + v27; + Vector128 v31 = v25 - v27; + Vector128 v32 = Vector128.MultiplyAddEstimate(v30, Vector128.Create(JxlDctScales.Sqrt2), v31); + Vector128 v33 = v17 + v23; + Vector128 v34 = (v17 - v23) * Vector128.Create(0.541196100146197f); + Vector128 v35 = v19 + v21; + Vector128 v36 = (v19 - v21) * Vector128.Create(1.3065629648763764f); + Vector128 v37 = v33 + v35; + Vector128 v38 = v33 - v35; + Vector128 v39 = v34 + v36; + Vector128 v40 = v34 - v36; + Vector128 v41 = Vector128.MultiplyAddEstimate(v39, Vector128.Create(JxlDctScales.Sqrt2), v40); + Vector128 v42 = Vector128.MultiplyAddEstimate(v37, Vector128.Create(JxlDctScales.Sqrt2), v41); + Vector128 v43 = v41 + v38; + Vector128 v44 = v38 + v40; + Vector128 v45 = v1 + v15; + Vector128 v46 = (v1 - v15) * Vector128.Create(0.5097955791041592f); + Vector128 v47 = v3 + v13; + Vector128 v48 = (v3 - v13) * Vector128.Create(0.6013448869350453f); + Vector128 v49 = v5 + v11; + Vector128 v50 = (v5 - v11) * Vector128.Create(0.8999762231364156f); + Vector128 v51 = v7 + v9; + Vector128 v52 = (v7 - v9) * Vector128.Create(2.5629154477415055f); + Vector128 v53 = v45 + v51; + Vector128 v54 = (v45 - v51) * Vector128.Create(0.541196100146197f); + Vector128 v55 = v47 + v49; + Vector128 v56 = (v47 - v49) * Vector128.Create(1.3065629648763764f); + Vector128 v57 = v53 + v55; + Vector128 v58 = v53 - v55; + Vector128 v59 = v54 + v56; + Vector128 v60 = v54 - v56; + Vector128 v61 = Vector128.MultiplyAddEstimate(v59, Vector128.Create(JxlDctScales.Sqrt2), v60); + Vector128 v62 = v46 + v52; + Vector128 v63 = (v46 - v52) * Vector128.Create(0.541196100146197f); + Vector128 v64 = v48 + v50; + Vector128 v65 = (v48 - v50) * Vector128.Create(1.3065629648763764f); + Vector128 v66 = v62 + v64; + Vector128 v67 = v62 - v64; + Vector128 v68 = v63 + v65; + Vector128 v69 = v63 - v65; + Vector128 v70 = Vector128.MultiplyAddEstimate(v68, Vector128.Create(JxlDctScales.Sqrt2), v69); + Vector128 v71 = Vector128.MultiplyAddEstimate(v66, Vector128.Create(JxlDctScales.Sqrt2), v70); + Vector128 v72 = v70 + v67; + Vector128 v73 = v67 + v69; + Vector128 v74 = Vector128.MultiplyAddEstimate(v57, Vector128.Create(JxlDctScales.Sqrt2), v71); + Vector128 v75 = v71 + v61; + Vector128 v76 = v61 + v72; + Vector128 v77 = v72 + v58; + Vector128 v78 = v58 + v73; + Vector128 v79 = v73 + v60; + Vector128 v80 = v60 + v69; + + values[0] = v28; + values[1] = v74; + values[2] = v42; + values[3] = v75; + values[4] = v32; + values[5] = v76; + values[6] = v43; + values[7] = v77; + values[8] = v29; + values[9] = v78; + values[10] = v44; + values[11] = v79; + values[12] = v31; + values[13] = v80; + values[14] = v40; + values[15] = v69; + } + + /// + /// Computes independent 32-point forward transforms without intermediate memory passes. + /// + /// Exactly 32 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Forward32(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector128 input0 = values[0]; + Vector128 input1 = values[1]; + Vector128 input2 = values[2]; + Vector128 input3 = values[3]; + Vector128 input4 = values[4]; + Vector128 input5 = values[5]; + Vector128 input6 = values[6]; + Vector128 input7 = values[7]; + Vector128 input8 = values[8]; + Vector128 input9 = values[9]; + Vector128 input10 = values[10]; + Vector128 input11 = values[11]; + Vector128 input12 = values[12]; + Vector128 input13 = values[13]; + Vector128 input14 = values[14]; + Vector128 input15 = values[15]; + Vector128 input16 = values[16]; + Vector128 input17 = values[17]; + Vector128 input18 = values[18]; + Vector128 input19 = values[19]; + Vector128 input20 = values[20]; + Vector128 input21 = values[21]; + Vector128 input22 = values[22]; + Vector128 input23 = values[23]; + Vector128 input24 = values[24]; + Vector128 input25 = values[25]; + Vector128 input26 = values[26]; + Vector128 input27 = values[27]; + Vector128 input28 = values[28]; + Vector128 input29 = values[29]; + Vector128 input30 = values[30]; + Vector128 input31 = values[31]; + + Vector128 v0 = input0 + input31; + Vector128 v1 = (input0 - input31) * Vector128.Create(0.5006029982351963f); + Vector128 v2 = input1 + input30; + Vector128 v3 = (input1 - input30) * Vector128.Create(0.5054709598975436f); + Vector128 v4 = input2 + input29; + Vector128 v5 = (input2 - input29) * Vector128.Create(0.5154473099226246f); + Vector128 v6 = input3 + input28; + Vector128 v7 = (input3 - input28) * Vector128.Create(0.5310425910897841f); + Vector128 v8 = input4 + input27; + Vector128 v9 = (input4 - input27) * Vector128.Create(0.5531038960344445f); + Vector128 v10 = input5 + input26; + Vector128 v11 = (input5 - input26) * Vector128.Create(0.5829349682061339f); + Vector128 v12 = input6 + input25; + Vector128 v13 = (input6 - input25) * Vector128.Create(0.6225041230356648f); + Vector128 v14 = input7 + input24; + Vector128 v15 = (input7 - input24) * Vector128.Create(0.6748083414550057f); + Vector128 v16 = input8 + input23; + Vector128 v17 = (input8 - input23) * Vector128.Create(0.7445362710022986f); + Vector128 v18 = input9 + input22; + Vector128 v19 = (input9 - input22) * Vector128.Create(0.8393496454155268f); + Vector128 v20 = input10 + input21; + Vector128 v21 = (input10 - input21) * Vector128.Create(0.9725682378619608f); + Vector128 v22 = input11 + input20; + Vector128 v23 = (input11 - input20) * Vector128.Create(1.1694399334328847f); + Vector128 v24 = input12 + input19; + Vector128 v25 = (input12 - input19) * Vector128.Create(1.4841646163141662f); + Vector128 v26 = input13 + input18; + Vector128 v27 = (input13 - input18) * Vector128.Create(2.057781009953411f); + Vector128 v28 = input14 + input17; + Vector128 v29 = (input14 - input17) * Vector128.Create(3.407608418468719f); + Vector128 v30 = input15 + input16; + Vector128 v31 = (input15 - input16) * Vector128.Create(10.190008123548033f); + Vector128 v32 = v0 + v30; + Vector128 v33 = (v0 - v30) * Vector128.Create(0.5024192861881557f); + Vector128 v34 = v2 + v28; + Vector128 v35 = (v2 - v28) * Vector128.Create(0.5224986149396889f); + Vector128 v36 = v4 + v26; + Vector128 v37 = (v4 - v26) * Vector128.Create(0.5669440348163577f); + Vector128 v38 = v6 + v24; + Vector128 v39 = (v6 - v24) * Vector128.Create(0.6468217833599901f); + Vector128 v40 = v8 + v22; + Vector128 v41 = (v8 - v22) * Vector128.Create(0.7881546234512502f); + Vector128 v42 = v10 + v20; + Vector128 v43 = (v10 - v20) * Vector128.Create(1.060677685990347f); + Vector128 v44 = v12 + v18; + Vector128 v45 = (v12 - v18) * Vector128.Create(1.7224470982383342f); + Vector128 v46 = v14 + v16; + Vector128 v47 = (v14 - v16) * Vector128.Create(5.101148618689155f); + Vector128 v48 = v32 + v46; + Vector128 v49 = (v32 - v46) * Vector128.Create(0.5097955791041592f); + Vector128 v50 = v34 + v44; + Vector128 v51 = (v34 - v44) * Vector128.Create(0.6013448869350453f); + Vector128 v52 = v36 + v42; + Vector128 v53 = (v36 - v42) * Vector128.Create(0.8999762231364156f); + Vector128 v54 = v38 + v40; + Vector128 v55 = (v38 - v40) * Vector128.Create(2.5629154477415055f); + Vector128 v56 = v48 + v54; + Vector128 v57 = (v48 - v54) * Vector128.Create(0.541196100146197f); + Vector128 v58 = v50 + v52; + Vector128 v59 = (v50 - v52) * Vector128.Create(1.3065629648763764f); + Vector128 v60 = v56 + v58; + Vector128 v61 = v56 - v58; + Vector128 v62 = v57 + v59; + Vector128 v63 = v57 - v59; + Vector128 v64 = Vector128.MultiplyAddEstimate(v62, Vector128.Create(JxlDctScales.Sqrt2), v63); + Vector128 v65 = v49 + v55; + Vector128 v66 = (v49 - v55) * Vector128.Create(0.541196100146197f); + Vector128 v67 = v51 + v53; + Vector128 v68 = (v51 - v53) * Vector128.Create(1.3065629648763764f); + Vector128 v69 = v65 + v67; + Vector128 v70 = v65 - v67; + Vector128 v71 = v66 + v68; + Vector128 v72 = v66 - v68; + Vector128 v73 = Vector128.MultiplyAddEstimate(v71, Vector128.Create(JxlDctScales.Sqrt2), v72); + Vector128 v74 = Vector128.MultiplyAddEstimate(v69, Vector128.Create(JxlDctScales.Sqrt2), v73); + Vector128 v75 = v73 + v70; + Vector128 v76 = v70 + v72; + Vector128 v77 = v33 + v47; + Vector128 v78 = (v33 - v47) * Vector128.Create(0.5097955791041592f); + Vector128 v79 = v35 + v45; + Vector128 v80 = (v35 - v45) * Vector128.Create(0.6013448869350453f); + Vector128 v81 = v37 + v43; + Vector128 v82 = (v37 - v43) * Vector128.Create(0.8999762231364156f); + Vector128 v83 = v39 + v41; + Vector128 v84 = (v39 - v41) * Vector128.Create(2.5629154477415055f); + Vector128 v85 = v77 + v83; + Vector128 v86 = (v77 - v83) * Vector128.Create(0.541196100146197f); + Vector128 v87 = v79 + v81; + Vector128 v88 = (v79 - v81) * Vector128.Create(1.3065629648763764f); + Vector128 v89 = v85 + v87; + Vector128 v90 = v85 - v87; + Vector128 v91 = v86 + v88; + Vector128 v92 = v86 - v88; + Vector128 v93 = Vector128.MultiplyAddEstimate(v91, Vector128.Create(JxlDctScales.Sqrt2), v92); + Vector128 v94 = v78 + v84; + Vector128 v95 = (v78 - v84) * Vector128.Create(0.541196100146197f); + Vector128 v96 = v80 + v82; + Vector128 v97 = (v80 - v82) * Vector128.Create(1.3065629648763764f); + Vector128 v98 = v94 + v96; + Vector128 v99 = v94 - v96; + Vector128 v100 = v95 + v97; + Vector128 v101 = v95 - v97; + Vector128 v102 = Vector128.MultiplyAddEstimate(v100, Vector128.Create(JxlDctScales.Sqrt2), v101); + Vector128 v103 = Vector128.MultiplyAddEstimate(v98, Vector128.Create(JxlDctScales.Sqrt2), v102); + Vector128 v104 = v102 + v99; + Vector128 v105 = v99 + v101; + Vector128 v106 = Vector128.MultiplyAddEstimate(v89, Vector128.Create(JxlDctScales.Sqrt2), v103); + Vector128 v107 = v103 + v93; + Vector128 v108 = v93 + v104; + Vector128 v109 = v104 + v90; + Vector128 v110 = v90 + v105; + Vector128 v111 = v105 + v92; + Vector128 v112 = v92 + v101; + Vector128 v113 = v1 + v31; + Vector128 v114 = (v1 - v31) * Vector128.Create(0.5024192861881557f); + Vector128 v115 = v3 + v29; + Vector128 v116 = (v3 - v29) * Vector128.Create(0.5224986149396889f); + Vector128 v117 = v5 + v27; + Vector128 v118 = (v5 - v27) * Vector128.Create(0.5669440348163577f); + Vector128 v119 = v7 + v25; + Vector128 v120 = (v7 - v25) * Vector128.Create(0.6468217833599901f); + Vector128 v121 = v9 + v23; + Vector128 v122 = (v9 - v23) * Vector128.Create(0.7881546234512502f); + Vector128 v123 = v11 + v21; + Vector128 v124 = (v11 - v21) * Vector128.Create(1.060677685990347f); + Vector128 v125 = v13 + v19; + Vector128 v126 = (v13 - v19) * Vector128.Create(1.7224470982383342f); + Vector128 v127 = v15 + v17; + Vector128 v128 = (v15 - v17) * Vector128.Create(5.101148618689155f); + Vector128 v129 = v113 + v127; + Vector128 v130 = (v113 - v127) * Vector128.Create(0.5097955791041592f); + Vector128 v131 = v115 + v125; + Vector128 v132 = (v115 - v125) * Vector128.Create(0.6013448869350453f); + Vector128 v133 = v117 + v123; + Vector128 v134 = (v117 - v123) * Vector128.Create(0.8999762231364156f); + Vector128 v135 = v119 + v121; + Vector128 v136 = (v119 - v121) * Vector128.Create(2.5629154477415055f); + Vector128 v137 = v129 + v135; + Vector128 v138 = (v129 - v135) * Vector128.Create(0.541196100146197f); + Vector128 v139 = v131 + v133; + Vector128 v140 = (v131 - v133) * Vector128.Create(1.3065629648763764f); + Vector128 v141 = v137 + v139; + Vector128 v142 = v137 - v139; + Vector128 v143 = v138 + v140; + Vector128 v144 = v138 - v140; + Vector128 v145 = Vector128.MultiplyAddEstimate(v143, Vector128.Create(JxlDctScales.Sqrt2), v144); + Vector128 v146 = v130 + v136; + Vector128 v147 = (v130 - v136) * Vector128.Create(0.541196100146197f); + Vector128 v148 = v132 + v134; + Vector128 v149 = (v132 - v134) * Vector128.Create(1.3065629648763764f); + Vector128 v150 = v146 + v148; + Vector128 v151 = v146 - v148; + Vector128 v152 = v147 + v149; + Vector128 v153 = v147 - v149; + Vector128 v154 = Vector128.MultiplyAddEstimate(v152, Vector128.Create(JxlDctScales.Sqrt2), v153); + Vector128 v155 = Vector128.MultiplyAddEstimate(v150, Vector128.Create(JxlDctScales.Sqrt2), v154); + Vector128 v156 = v154 + v151; + Vector128 v157 = v151 + v153; + Vector128 v158 = v114 + v128; + Vector128 v159 = (v114 - v128) * Vector128.Create(0.5097955791041592f); + Vector128 v160 = v116 + v126; + Vector128 v161 = (v116 - v126) * Vector128.Create(0.6013448869350453f); + Vector128 v162 = v118 + v124; + Vector128 v163 = (v118 - v124) * Vector128.Create(0.8999762231364156f); + Vector128 v164 = v120 + v122; + Vector128 v165 = (v120 - v122) * Vector128.Create(2.5629154477415055f); + Vector128 v166 = v158 + v164; + Vector128 v167 = (v158 - v164) * Vector128.Create(0.541196100146197f); + Vector128 v168 = v160 + v162; + Vector128 v169 = (v160 - v162) * Vector128.Create(1.3065629648763764f); + Vector128 v170 = v166 + v168; + Vector128 v171 = v166 - v168; + Vector128 v172 = v167 + v169; + Vector128 v173 = v167 - v169; + Vector128 v174 = Vector128.MultiplyAddEstimate(v172, Vector128.Create(JxlDctScales.Sqrt2), v173); + Vector128 v175 = v159 + v165; + Vector128 v176 = (v159 - v165) * Vector128.Create(0.541196100146197f); + Vector128 v177 = v161 + v163; + Vector128 v178 = (v161 - v163) * Vector128.Create(1.3065629648763764f); + Vector128 v179 = v175 + v177; + Vector128 v180 = v175 - v177; + Vector128 v181 = v176 + v178; + Vector128 v182 = v176 - v178; + Vector128 v183 = Vector128.MultiplyAddEstimate(v181, Vector128.Create(JxlDctScales.Sqrt2), v182); + Vector128 v184 = Vector128.MultiplyAddEstimate(v179, Vector128.Create(JxlDctScales.Sqrt2), v183); + Vector128 v185 = v183 + v180; + Vector128 v186 = v180 + v182; + Vector128 v187 = Vector128.MultiplyAddEstimate(v170, Vector128.Create(JxlDctScales.Sqrt2), v184); + Vector128 v188 = v184 + v174; + Vector128 v189 = v174 + v185; + Vector128 v190 = v185 + v171; + Vector128 v191 = v171 + v186; + Vector128 v192 = v186 + v173; + Vector128 v193 = v173 + v182; + Vector128 v194 = Vector128.MultiplyAddEstimate(v141, Vector128.Create(JxlDctScales.Sqrt2), v187); + Vector128 v195 = v187 + v155; + Vector128 v196 = v155 + v188; + Vector128 v197 = v188 + v145; + Vector128 v198 = v145 + v189; + Vector128 v199 = v189 + v156; + Vector128 v200 = v156 + v190; + Vector128 v201 = v190 + v142; + Vector128 v202 = v142 + v191; + Vector128 v203 = v191 + v157; + Vector128 v204 = v157 + v192; + Vector128 v205 = v192 + v144; + Vector128 v206 = v144 + v193; + Vector128 v207 = v193 + v153; + Vector128 v208 = v153 + v182; + + values[0] = v60; + values[1] = v194; + values[2] = v106; + values[3] = v195; + values[4] = v74; + values[5] = v196; + values[6] = v107; + values[7] = v197; + values[8] = v64; + values[9] = v198; + values[10] = v108; + values[11] = v199; + values[12] = v75; + values[13] = v200; + values[14] = v109; + values[15] = v201; + values[16] = v61; + values[17] = v202; + values[18] = v110; + values[19] = v203; + values[20] = v76; + values[21] = v204; + values[22] = v111; + values[23] = v205; + values[24] = v63; + values[25] = v206; + values[26] = v112; + values[27] = v207; + values[28] = v72; + values[29] = v208; + values[30] = v101; + values[31] = v182; + } + + /// + /// Processes complete 4-lane forward batches along input columns. + /// + /// The number of positions in each transform. + /// The number of independent transforms. + /// The borrowed source matrix. + /// The source row stride, in floats. + /// The borrowed output matrix. + /// The destination row stride, in floats. + /// The first independent transform not already processed by a wider batch. + /// Borrowed storage for two length-by-4 banks. Only the first bank needs initialized inputs. + /// The first independent transform left after the complete batches. + private static int ForwardColumns128( + int length, + int count, + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int first, + Span workspace) + { + Span> values = MemoryMarshal.Cast>(workspace[..(length * 4)]); + Span> scratch = MemoryMarshal.Cast>(workspace.Slice(length * 4, length * 4)); + ref float input = ref MemoryMarshal.GetReference(source); + ref float output = ref MemoryMarshal.GetReference(destination); + Vector128 scale = Vector128.Create(1F / length); + + for (; first <= count - 4; first += 4) + { + // One vector covers adjacent columns at the same transform position. Physical strides never enter the coefficient-bank layout. + for (int i = 0; i < length; i++) + { + values[i] = Vector128.LoadUnsafe(ref input, (nuint)((i * sourceStride) + first)); + } + + Forward(values, scratch); + for (int i = 0; i < length; i++) + { + (values[i] * scale).StoreUnsafe(ref output, (nuint)((i * destinationStride) + first)); + } + } + + return first; + } + + /// + /// Processes complete 4-lane forward batches along input rows. + /// + /// The number of positions in each transform. + /// The number of independent transforms. + /// The borrowed source matrix. + /// The source row stride, in floats. + /// The borrowed output matrix. + /// The destination row stride, in floats. + /// Whether transform positions become output rows and input rows become output columns. + /// The first independent transform not already processed by a wider batch. + /// Borrowed storage for two length-by-4 banks. Only the first bank needs initialized inputs. + /// The first independent transform left after the complete batches. + private static int ForwardRows128( + int length, + int count, + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + bool transposed, + int first, + Span workspace) + { + Span> values = MemoryMarshal.Cast>(workspace[..(length * 4)]); + Span> scratch = MemoryMarshal.Cast>(workspace.Slice(length * 4, length * 4)); + ref float input = ref MemoryMarshal.GetReference(source); + ref float output = ref MemoryMarshal.GetReference(destination); + Vector128 scale = Vector128.Create(1F / length); + + for (; first <= count - 4; first += 4) + { + // Each square load contains adjacent samples from independent rows. Transposition puts those rows into SIMD lanes. + // The complete row band is loaded before any same-orientation output is written, so source and destination can be the same matrix. + for (int column = 0; column < length; column += 4) + { + Vector128 row0 = Vector128.LoadUnsafe(ref input, (nuint)(((first + 0) * sourceStride) + column)).AsInt32(); + Vector128 row1 = Vector128.LoadUnsafe(ref input, (nuint)(((first + 1) * sourceStride) + column)).AsInt32(); + Vector128 row2 = Vector128.LoadUnsafe(ref input, (nuint)(((first + 2) * sourceStride) + column)).AsInt32(); + Vector128 row3 = Vector128.LoadUnsafe(ref input, (nuint)(((first + 3) * sourceStride) + column)).AsInt32(); + + SimdUtils.Transpose(ref row0, ref row1, ref row2, ref row3); + values[column + 0] = row0.AsSingle(); + values[column + 1] = row1.AsSingle(); + values[column + 2] = row2.AsSingle(); + values[column + 3] = row3.AsSingle(); + } + + Forward(values, scratch); + if (transposed) + { + // A complete vector is one contiguous output row. Only logical samples are stored; destination padding remains untouched. + for (int i = 0; i < length; i++) + { + (values[i] * scale).StoreUnsafe(ref output, (nuint)((i * destinationStride) + first)); + } + } + else + { + // Restore row-major order one register tile at a time. This replaces a separate full-block transpose and copy. + for (int column = 0; column < length; column += 4) + { + Vector128 row0 = (values[column + 0] * scale).AsInt32(); + Vector128 row1 = (values[column + 1] * scale).AsInt32(); + Vector128 row2 = (values[column + 2] * scale).AsInt32(); + Vector128 row3 = (values[column + 3] * scale).AsInt32(); + + SimdUtils.Transpose(ref row0, ref row1, ref row2, ref row3); + row0.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 0) * destinationStride) + column)); + row1.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 1) * destinationStride) + column)); + row2.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 2) * destinationStride) + column)); + row3.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 3) * destinationStride) + column)); + } + } + } + + return first; + } + + /// + /// Computes independent inverse transforms using two nonoverlapping borrowed vector banks. + /// + /// One vector per transform position. Its length is a power of two from one through 256. + /// At least values.Length vectors. Previous contents are not read. + private static void Inverse(Span> values, Span> scratch) + { + // Small transforms stay in registers. The one-point transform is an identity and must not copy any trailing samples. + switch (values.Length) + { + case 1: + return; + case 2: + Inverse2(values); + return; + case 4: + Inverse4(values); + return; + case 8: + Inverse8(values); + return; + case 16: + Inverse16(values); + return; + case 32: + Inverse32(values); + return; + } + + int half = values.Length / 2; + // Separate frequency parity before either recursive call. The original bank can then serve as recursive scratch. + for (int i = 0; i < half; i++) + { + scratch[i] = values[2 * i]; + scratch[half + i] = values[(2 * i) + 1]; + } + + Inverse(scratch[..half], values); + + // Descend so every odd-frequency predecessor is read before its own entry changes. + for (int i = half - 1; i > 0; i--) + { + scratch[half + i] += scratch[half + i - 1]; + } + + scratch[half] *= Vector128.Create(JxlDctScales.Sqrt2); + Inverse(scratch.Slice(half, half), values); + ReadOnlySpan weights = JxlDctScales.GetMultipliers(values.Length); + + for (int i = 0; i < half; i++) + { + Vector128 weight = Vector128.Create(weights[i]); + Vector128 even = scratch[i]; + Vector128 odd = scratch[half + i]; + values[i] = Vector128.MultiplyAddEstimate(weight, odd, even); + values[values.Length - i - 1] = Vector128.MultiplyAddEstimate(-weight, odd, even); + } + } + + /// + /// Computes independent 2-point inverse transforms without intermediate memory passes. + /// + /// Exactly 2 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Inverse2(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector128 input0 = values[0]; + Vector128 input1 = values[1]; + + Vector128 v0 = input0 + input1; + Vector128 v1 = input0 - input1; + + values[0] = v0; + values[1] = v1; + } + + /// + /// Computes independent 4-point inverse transforms without intermediate memory passes. + /// + /// Exactly 4 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Inverse4(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector128 input0 = values[0]; + Vector128 input1 = values[1]; + Vector128 input2 = values[2]; + Vector128 input3 = values[3]; + + Vector128 v0 = input0 + input2; + Vector128 v1 = input0 - input2; + Vector128 v2 = input3 + input1; + Vector128 v3 = input1 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v4 = v3 + v2; + Vector128 v5 = v3 - v2; + Vector128 v6 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v4, v0); + Vector128 v7 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v4, v0); + Vector128 v8 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v5, v1); + Vector128 v9 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v5, v1); + + values[0] = v6; + values[1] = v8; + values[2] = v9; + values[3] = v7; + } + + /// + /// Computes independent 8-point inverse transforms without intermediate memory passes. + /// + /// Exactly 8 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Inverse8(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector128 input0 = values[0]; + Vector128 input1 = values[1]; + Vector128 input2 = values[2]; + Vector128 input3 = values[3]; + Vector128 input4 = values[4]; + Vector128 input5 = values[5]; + Vector128 input6 = values[6]; + Vector128 input7 = values[7]; + + Vector128 v0 = input0 + input4; + Vector128 v1 = input0 - input4; + Vector128 v2 = input6 + input2; + Vector128 v3 = input2 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v4 = v3 + v2; + Vector128 v5 = v3 - v2; + Vector128 v6 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v4, v0); + Vector128 v7 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v4, v0); + Vector128 v8 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v5, v1); + Vector128 v9 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v5, v1); + Vector128 v10 = input7 + input5; + Vector128 v11 = input5 + input3; + Vector128 v12 = input3 + input1; + Vector128 v13 = input1 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v14 = v13 + v11; + Vector128 v15 = v13 - v11; + Vector128 v16 = v10 + v12; + Vector128 v17 = v12 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v18 = v17 + v16; + Vector128 v19 = v17 - v16; + Vector128 v20 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v18, v14); + Vector128 v21 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v18, v14); + Vector128 v22 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v19, v15); + Vector128 v23 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v19, v15); + Vector128 v24 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5097955791041592f), v20, v6); + Vector128 v25 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5097955791041592f), v20, v6); + Vector128 v26 = Vector128.MultiplyAddEstimate(Vector128.Create(0.6013448869350453f), v22, v8); + Vector128 v27 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.6013448869350453f), v22, v8); + Vector128 v28 = Vector128.MultiplyAddEstimate(Vector128.Create(0.8999762231364156f), v23, v9); + Vector128 v29 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.8999762231364156f), v23, v9); + Vector128 v30 = Vector128.MultiplyAddEstimate(Vector128.Create(2.5629154477415055f), v21, v7); + Vector128 v31 = Vector128.MultiplyAddEstimate(-Vector128.Create(2.5629154477415055f), v21, v7); + + values[0] = v24; + values[1] = v26; + values[2] = v28; + values[3] = v30; + values[4] = v31; + values[5] = v29; + values[6] = v27; + values[7] = v25; + } + + /// + /// Computes independent 16-point inverse transforms without intermediate memory passes. + /// + /// Exactly 16 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Inverse16(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector128 input0 = values[0]; + Vector128 input1 = values[1]; + Vector128 input2 = values[2]; + Vector128 input3 = values[3]; + Vector128 input4 = values[4]; + Vector128 input5 = values[5]; + Vector128 input6 = values[6]; + Vector128 input7 = values[7]; + Vector128 input8 = values[8]; + Vector128 input9 = values[9]; + Vector128 input10 = values[10]; + Vector128 input11 = values[11]; + Vector128 input12 = values[12]; + Vector128 input13 = values[13]; + Vector128 input14 = values[14]; + Vector128 input15 = values[15]; + + Vector128 v0 = input0 + input8; + Vector128 v1 = input0 - input8; + Vector128 v2 = input12 + input4; + Vector128 v3 = input4 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v4 = v3 + v2; + Vector128 v5 = v3 - v2; + Vector128 v6 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v4, v0); + Vector128 v7 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v4, v0); + Vector128 v8 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v5, v1); + Vector128 v9 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v5, v1); + Vector128 v10 = input14 + input10; + Vector128 v11 = input10 + input6; + Vector128 v12 = input6 + input2; + Vector128 v13 = input2 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v14 = v13 + v11; + Vector128 v15 = v13 - v11; + Vector128 v16 = v10 + v12; + Vector128 v17 = v12 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v18 = v17 + v16; + Vector128 v19 = v17 - v16; + Vector128 v20 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v18, v14); + Vector128 v21 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v18, v14); + Vector128 v22 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v19, v15); + Vector128 v23 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v19, v15); + Vector128 v24 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5097955791041592f), v20, v6); + Vector128 v25 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5097955791041592f), v20, v6); + Vector128 v26 = Vector128.MultiplyAddEstimate(Vector128.Create(0.6013448869350453f), v22, v8); + Vector128 v27 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.6013448869350453f), v22, v8); + Vector128 v28 = Vector128.MultiplyAddEstimate(Vector128.Create(0.8999762231364156f), v23, v9); + Vector128 v29 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.8999762231364156f), v23, v9); + Vector128 v30 = Vector128.MultiplyAddEstimate(Vector128.Create(2.5629154477415055f), v21, v7); + Vector128 v31 = Vector128.MultiplyAddEstimate(-Vector128.Create(2.5629154477415055f), v21, v7); + Vector128 v32 = input15 + input13; + Vector128 v33 = input13 + input11; + Vector128 v34 = input11 + input9; + Vector128 v35 = input9 + input7; + Vector128 v36 = input7 + input5; + Vector128 v37 = input5 + input3; + Vector128 v38 = input3 + input1; + Vector128 v39 = input1 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v40 = v39 + v35; + Vector128 v41 = v39 - v35; + Vector128 v42 = v33 + v37; + Vector128 v43 = v37 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v44 = v43 + v42; + Vector128 v45 = v43 - v42; + Vector128 v46 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v44, v40); + Vector128 v47 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v44, v40); + Vector128 v48 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v45, v41); + Vector128 v49 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v45, v41); + Vector128 v50 = v32 + v34; + Vector128 v51 = v34 + v36; + Vector128 v52 = v36 + v38; + Vector128 v53 = v38 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v54 = v53 + v51; + Vector128 v55 = v53 - v51; + Vector128 v56 = v50 + v52; + Vector128 v57 = v52 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v58 = v57 + v56; + Vector128 v59 = v57 - v56; + Vector128 v60 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v58, v54); + Vector128 v61 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v58, v54); + Vector128 v62 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v59, v55); + Vector128 v63 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v59, v55); + Vector128 v64 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5097955791041592f), v60, v46); + Vector128 v65 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5097955791041592f), v60, v46); + Vector128 v66 = Vector128.MultiplyAddEstimate(Vector128.Create(0.6013448869350453f), v62, v48); + Vector128 v67 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.6013448869350453f), v62, v48); + Vector128 v68 = Vector128.MultiplyAddEstimate(Vector128.Create(0.8999762231364156f), v63, v49); + Vector128 v69 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.8999762231364156f), v63, v49); + Vector128 v70 = Vector128.MultiplyAddEstimate(Vector128.Create(2.5629154477415055f), v61, v47); + Vector128 v71 = Vector128.MultiplyAddEstimate(-Vector128.Create(2.5629154477415055f), v61, v47); + Vector128 v72 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5024192861881557f), v64, v24); + Vector128 v73 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5024192861881557f), v64, v24); + Vector128 v74 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5224986149396889f), v66, v26); + Vector128 v75 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5224986149396889f), v66, v26); + Vector128 v76 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5669440348163577f), v68, v28); + Vector128 v77 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5669440348163577f), v68, v28); + Vector128 v78 = Vector128.MultiplyAddEstimate(Vector128.Create(0.6468217833599901f), v70, v30); + Vector128 v79 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.6468217833599901f), v70, v30); + Vector128 v80 = Vector128.MultiplyAddEstimate(Vector128.Create(0.7881546234512502f), v71, v31); + Vector128 v81 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.7881546234512502f), v71, v31); + Vector128 v82 = Vector128.MultiplyAddEstimate(Vector128.Create(1.060677685990347f), v69, v29); + Vector128 v83 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.060677685990347f), v69, v29); + Vector128 v84 = Vector128.MultiplyAddEstimate(Vector128.Create(1.7224470982383342f), v67, v27); + Vector128 v85 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.7224470982383342f), v67, v27); + Vector128 v86 = Vector128.MultiplyAddEstimate(Vector128.Create(5.101148618689155f), v65, v25); + Vector128 v87 = Vector128.MultiplyAddEstimate(-Vector128.Create(5.101148618689155f), v65, v25); + + values[0] = v72; + values[1] = v74; + values[2] = v76; + values[3] = v78; + values[4] = v80; + values[5] = v82; + values[6] = v84; + values[7] = v86; + values[8] = v87; + values[9] = v85; + values[10] = v83; + values[11] = v81; + values[12] = v79; + values[13] = v77; + values[14] = v75; + values[15] = v73; + } + + /// + /// Computes independent 32-point inverse transforms without intermediate memory passes. + /// + /// Exactly 32 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Inverse32(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector128 input0 = values[0]; + Vector128 input1 = values[1]; + Vector128 input2 = values[2]; + Vector128 input3 = values[3]; + Vector128 input4 = values[4]; + Vector128 input5 = values[5]; + Vector128 input6 = values[6]; + Vector128 input7 = values[7]; + Vector128 input8 = values[8]; + Vector128 input9 = values[9]; + Vector128 input10 = values[10]; + Vector128 input11 = values[11]; + Vector128 input12 = values[12]; + Vector128 input13 = values[13]; + Vector128 input14 = values[14]; + Vector128 input15 = values[15]; + Vector128 input16 = values[16]; + Vector128 input17 = values[17]; + Vector128 input18 = values[18]; + Vector128 input19 = values[19]; + Vector128 input20 = values[20]; + Vector128 input21 = values[21]; + Vector128 input22 = values[22]; + Vector128 input23 = values[23]; + Vector128 input24 = values[24]; + Vector128 input25 = values[25]; + Vector128 input26 = values[26]; + Vector128 input27 = values[27]; + Vector128 input28 = values[28]; + Vector128 input29 = values[29]; + Vector128 input30 = values[30]; + Vector128 input31 = values[31]; + + Vector128 v0 = input0 + input16; + Vector128 v1 = input0 - input16; + Vector128 v2 = input24 + input8; + Vector128 v3 = input8 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v4 = v3 + v2; + Vector128 v5 = v3 - v2; + Vector128 v6 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v4, v0); + Vector128 v7 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v4, v0); + Vector128 v8 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v5, v1); + Vector128 v9 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v5, v1); + Vector128 v10 = input28 + input20; + Vector128 v11 = input20 + input12; + Vector128 v12 = input12 + input4; + Vector128 v13 = input4 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v14 = v13 + v11; + Vector128 v15 = v13 - v11; + Vector128 v16 = v10 + v12; + Vector128 v17 = v12 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v18 = v17 + v16; + Vector128 v19 = v17 - v16; + Vector128 v20 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v18, v14); + Vector128 v21 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v18, v14); + Vector128 v22 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v19, v15); + Vector128 v23 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v19, v15); + Vector128 v24 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5097955791041592f), v20, v6); + Vector128 v25 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5097955791041592f), v20, v6); + Vector128 v26 = Vector128.MultiplyAddEstimate(Vector128.Create(0.6013448869350453f), v22, v8); + Vector128 v27 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.6013448869350453f), v22, v8); + Vector128 v28 = Vector128.MultiplyAddEstimate(Vector128.Create(0.8999762231364156f), v23, v9); + Vector128 v29 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.8999762231364156f), v23, v9); + Vector128 v30 = Vector128.MultiplyAddEstimate(Vector128.Create(2.5629154477415055f), v21, v7); + Vector128 v31 = Vector128.MultiplyAddEstimate(-Vector128.Create(2.5629154477415055f), v21, v7); + Vector128 v32 = input30 + input26; + Vector128 v33 = input26 + input22; + Vector128 v34 = input22 + input18; + Vector128 v35 = input18 + input14; + Vector128 v36 = input14 + input10; + Vector128 v37 = input10 + input6; + Vector128 v38 = input6 + input2; + Vector128 v39 = input2 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v40 = v39 + v35; + Vector128 v41 = v39 - v35; + Vector128 v42 = v33 + v37; + Vector128 v43 = v37 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v44 = v43 + v42; + Vector128 v45 = v43 - v42; + Vector128 v46 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v44, v40); + Vector128 v47 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v44, v40); + Vector128 v48 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v45, v41); + Vector128 v49 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v45, v41); + Vector128 v50 = v32 + v34; + Vector128 v51 = v34 + v36; + Vector128 v52 = v36 + v38; + Vector128 v53 = v38 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v54 = v53 + v51; + Vector128 v55 = v53 - v51; + Vector128 v56 = v50 + v52; + Vector128 v57 = v52 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v58 = v57 + v56; + Vector128 v59 = v57 - v56; + Vector128 v60 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v58, v54); + Vector128 v61 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v58, v54); + Vector128 v62 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v59, v55); + Vector128 v63 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v59, v55); + Vector128 v64 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5097955791041592f), v60, v46); + Vector128 v65 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5097955791041592f), v60, v46); + Vector128 v66 = Vector128.MultiplyAddEstimate(Vector128.Create(0.6013448869350453f), v62, v48); + Vector128 v67 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.6013448869350453f), v62, v48); + Vector128 v68 = Vector128.MultiplyAddEstimate(Vector128.Create(0.8999762231364156f), v63, v49); + Vector128 v69 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.8999762231364156f), v63, v49); + Vector128 v70 = Vector128.MultiplyAddEstimate(Vector128.Create(2.5629154477415055f), v61, v47); + Vector128 v71 = Vector128.MultiplyAddEstimate(-Vector128.Create(2.5629154477415055f), v61, v47); + Vector128 v72 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5024192861881557f), v64, v24); + Vector128 v73 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5024192861881557f), v64, v24); + Vector128 v74 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5224986149396889f), v66, v26); + Vector128 v75 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5224986149396889f), v66, v26); + Vector128 v76 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5669440348163577f), v68, v28); + Vector128 v77 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5669440348163577f), v68, v28); + Vector128 v78 = Vector128.MultiplyAddEstimate(Vector128.Create(0.6468217833599901f), v70, v30); + Vector128 v79 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.6468217833599901f), v70, v30); + Vector128 v80 = Vector128.MultiplyAddEstimate(Vector128.Create(0.7881546234512502f), v71, v31); + Vector128 v81 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.7881546234512502f), v71, v31); + Vector128 v82 = Vector128.MultiplyAddEstimate(Vector128.Create(1.060677685990347f), v69, v29); + Vector128 v83 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.060677685990347f), v69, v29); + Vector128 v84 = Vector128.MultiplyAddEstimate(Vector128.Create(1.7224470982383342f), v67, v27); + Vector128 v85 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.7224470982383342f), v67, v27); + Vector128 v86 = Vector128.MultiplyAddEstimate(Vector128.Create(5.101148618689155f), v65, v25); + Vector128 v87 = Vector128.MultiplyAddEstimate(-Vector128.Create(5.101148618689155f), v65, v25); + Vector128 v88 = input31 + input29; + Vector128 v89 = input29 + input27; + Vector128 v90 = input27 + input25; + Vector128 v91 = input25 + input23; + Vector128 v92 = input23 + input21; + Vector128 v93 = input21 + input19; + Vector128 v94 = input19 + input17; + Vector128 v95 = input17 + input15; + Vector128 v96 = input15 + input13; + Vector128 v97 = input13 + input11; + Vector128 v98 = input11 + input9; + Vector128 v99 = input9 + input7; + Vector128 v100 = input7 + input5; + Vector128 v101 = input5 + input3; + Vector128 v102 = input3 + input1; + Vector128 v103 = input1 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v104 = v103 + v95; + Vector128 v105 = v103 - v95; + Vector128 v106 = v91 + v99; + Vector128 v107 = v99 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v108 = v107 + v106; + Vector128 v109 = v107 - v106; + Vector128 v110 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v108, v104); + Vector128 v111 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v108, v104); + Vector128 v112 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v109, v105); + Vector128 v113 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v109, v105); + Vector128 v114 = v89 + v93; + Vector128 v115 = v93 + v97; + Vector128 v116 = v97 + v101; + Vector128 v117 = v101 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v118 = v117 + v115; + Vector128 v119 = v117 - v115; + Vector128 v120 = v114 + v116; + Vector128 v121 = v116 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v122 = v121 + v120; + Vector128 v123 = v121 - v120; + Vector128 v124 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v122, v118); + Vector128 v125 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v122, v118); + Vector128 v126 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v123, v119); + Vector128 v127 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v123, v119); + Vector128 v128 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5097955791041592f), v124, v110); + Vector128 v129 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5097955791041592f), v124, v110); + Vector128 v130 = Vector128.MultiplyAddEstimate(Vector128.Create(0.6013448869350453f), v126, v112); + Vector128 v131 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.6013448869350453f), v126, v112); + Vector128 v132 = Vector128.MultiplyAddEstimate(Vector128.Create(0.8999762231364156f), v127, v113); + Vector128 v133 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.8999762231364156f), v127, v113); + Vector128 v134 = Vector128.MultiplyAddEstimate(Vector128.Create(2.5629154477415055f), v125, v111); + Vector128 v135 = Vector128.MultiplyAddEstimate(-Vector128.Create(2.5629154477415055f), v125, v111); + Vector128 v136 = v88 + v90; + Vector128 v137 = v90 + v92; + Vector128 v138 = v92 + v94; + Vector128 v139 = v94 + v96; + Vector128 v140 = v96 + v98; + Vector128 v141 = v98 + v100; + Vector128 v142 = v100 + v102; + Vector128 v143 = v102 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v144 = v143 + v139; + Vector128 v145 = v143 - v139; + Vector128 v146 = v137 + v141; + Vector128 v147 = v141 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v148 = v147 + v146; + Vector128 v149 = v147 - v146; + Vector128 v150 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v148, v144); + Vector128 v151 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v148, v144); + Vector128 v152 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v149, v145); + Vector128 v153 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v149, v145); + Vector128 v154 = v136 + v138; + Vector128 v155 = v138 + v140; + Vector128 v156 = v140 + v142; + Vector128 v157 = v142 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v158 = v157 + v155; + Vector128 v159 = v157 - v155; + Vector128 v160 = v154 + v156; + Vector128 v161 = v156 * Vector128.Create(JxlDctScales.Sqrt2); + Vector128 v162 = v161 + v160; + Vector128 v163 = v161 - v160; + Vector128 v164 = Vector128.MultiplyAddEstimate(Vector128.Create(0.541196100146197f), v162, v158); + Vector128 v165 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.541196100146197f), v162, v158); + Vector128 v166 = Vector128.MultiplyAddEstimate(Vector128.Create(1.3065629648763764f), v163, v159); + Vector128 v167 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.3065629648763764f), v163, v159); + Vector128 v168 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5097955791041592f), v164, v150); + Vector128 v169 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5097955791041592f), v164, v150); + Vector128 v170 = Vector128.MultiplyAddEstimate(Vector128.Create(0.6013448869350453f), v166, v152); + Vector128 v171 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.6013448869350453f), v166, v152); + Vector128 v172 = Vector128.MultiplyAddEstimate(Vector128.Create(0.8999762231364156f), v167, v153); + Vector128 v173 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.8999762231364156f), v167, v153); + Vector128 v174 = Vector128.MultiplyAddEstimate(Vector128.Create(2.5629154477415055f), v165, v151); + Vector128 v175 = Vector128.MultiplyAddEstimate(-Vector128.Create(2.5629154477415055f), v165, v151); + Vector128 v176 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5024192861881557f), v168, v128); + Vector128 v177 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5024192861881557f), v168, v128); + Vector128 v178 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5224986149396889f), v170, v130); + Vector128 v179 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5224986149396889f), v170, v130); + Vector128 v180 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5669440348163577f), v172, v132); + Vector128 v181 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5669440348163577f), v172, v132); + Vector128 v182 = Vector128.MultiplyAddEstimate(Vector128.Create(0.6468217833599901f), v174, v134); + Vector128 v183 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.6468217833599901f), v174, v134); + Vector128 v184 = Vector128.MultiplyAddEstimate(Vector128.Create(0.7881546234512502f), v175, v135); + Vector128 v185 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.7881546234512502f), v175, v135); + Vector128 v186 = Vector128.MultiplyAddEstimate(Vector128.Create(1.060677685990347f), v173, v133); + Vector128 v187 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.060677685990347f), v173, v133); + Vector128 v188 = Vector128.MultiplyAddEstimate(Vector128.Create(1.7224470982383342f), v171, v131); + Vector128 v189 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.7224470982383342f), v171, v131); + Vector128 v190 = Vector128.MultiplyAddEstimate(Vector128.Create(5.101148618689155f), v169, v129); + Vector128 v191 = Vector128.MultiplyAddEstimate(-Vector128.Create(5.101148618689155f), v169, v129); + Vector128 v192 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5006029982351963f), v176, v72); + Vector128 v193 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5006029982351963f), v176, v72); + Vector128 v194 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5054709598975436f), v178, v74); + Vector128 v195 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5054709598975436f), v178, v74); + Vector128 v196 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5154473099226246f), v180, v76); + Vector128 v197 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5154473099226246f), v180, v76); + Vector128 v198 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5310425910897841f), v182, v78); + Vector128 v199 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5310425910897841f), v182, v78); + Vector128 v200 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5531038960344445f), v184, v80); + Vector128 v201 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5531038960344445f), v184, v80); + Vector128 v202 = Vector128.MultiplyAddEstimate(Vector128.Create(0.5829349682061339f), v186, v82); + Vector128 v203 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.5829349682061339f), v186, v82); + Vector128 v204 = Vector128.MultiplyAddEstimate(Vector128.Create(0.6225041230356648f), v188, v84); + Vector128 v205 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.6225041230356648f), v188, v84); + Vector128 v206 = Vector128.MultiplyAddEstimate(Vector128.Create(0.6748083414550057f), v190, v86); + Vector128 v207 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.6748083414550057f), v190, v86); + Vector128 v208 = Vector128.MultiplyAddEstimate(Vector128.Create(0.7445362710022986f), v191, v87); + Vector128 v209 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.7445362710022986f), v191, v87); + Vector128 v210 = Vector128.MultiplyAddEstimate(Vector128.Create(0.8393496454155268f), v189, v85); + Vector128 v211 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.8393496454155268f), v189, v85); + Vector128 v212 = Vector128.MultiplyAddEstimate(Vector128.Create(0.9725682378619608f), v187, v83); + Vector128 v213 = Vector128.MultiplyAddEstimate(-Vector128.Create(0.9725682378619608f), v187, v83); + Vector128 v214 = Vector128.MultiplyAddEstimate(Vector128.Create(1.1694399334328847f), v185, v81); + Vector128 v215 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.1694399334328847f), v185, v81); + Vector128 v216 = Vector128.MultiplyAddEstimate(Vector128.Create(1.4841646163141662f), v183, v79); + Vector128 v217 = Vector128.MultiplyAddEstimate(-Vector128.Create(1.4841646163141662f), v183, v79); + Vector128 v218 = Vector128.MultiplyAddEstimate(Vector128.Create(2.057781009953411f), v181, v77); + Vector128 v219 = Vector128.MultiplyAddEstimate(-Vector128.Create(2.057781009953411f), v181, v77); + Vector128 v220 = Vector128.MultiplyAddEstimate(Vector128.Create(3.407608418468719f), v179, v75); + Vector128 v221 = Vector128.MultiplyAddEstimate(-Vector128.Create(3.407608418468719f), v179, v75); + Vector128 v222 = Vector128.MultiplyAddEstimate(Vector128.Create(10.190008123548033f), v177, v73); + Vector128 v223 = Vector128.MultiplyAddEstimate(-Vector128.Create(10.190008123548033f), v177, v73); + + values[0] = v192; + values[1] = v194; + values[2] = v196; + values[3] = v198; + values[4] = v200; + values[5] = v202; + values[6] = v204; + values[7] = v206; + values[8] = v208; + values[9] = v210; + values[10] = v212; + values[11] = v214; + values[12] = v216; + values[13] = v218; + values[14] = v220; + values[15] = v222; + values[16] = v223; + values[17] = v221; + values[18] = v219; + values[19] = v217; + values[20] = v215; + values[21] = v213; + values[22] = v211; + values[23] = v209; + values[24] = v207; + values[25] = v205; + values[26] = v203; + values[27] = v201; + values[28] = v199; + values[29] = v197; + values[30] = v195; + values[31] = v193; + } + + /// + /// Processes complete 4-lane inverse batches along input columns. + /// + /// The number of positions in each transform. + /// The number of independent transforms. + /// The borrowed source matrix. + /// The source row stride, in floats. + /// The borrowed output matrix. + /// The destination row stride, in floats. + /// The first independent transform not already processed by a wider batch. + /// Borrowed storage for two length-by-4 banks. Only the first bank needs initialized inputs. + /// The first independent transform left after the complete batches. + private static int InverseColumns128( + int length, + int count, + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int first, + Span workspace) + { + Span> values = MemoryMarshal.Cast>(workspace[..(length * 4)]); + Span> scratch = MemoryMarshal.Cast>(workspace.Slice(length * 4, length * 4)); + ref float input = ref MemoryMarshal.GetReference(source); + ref float output = ref MemoryMarshal.GetReference(destination); + + for (; first <= count - 4; first += 4) + { + // One vector covers adjacent columns at the same transform position. Physical strides never enter the coefficient-bank layout. + for (int i = 0; i < length; i++) + { + values[i] = Vector128.LoadUnsafe(ref input, (nuint)((i * sourceStride) + first)); + } + + Inverse(values, scratch); + for (int i = 0; i < length; i++) + { + values[i].StoreUnsafe(ref output, (nuint)((i * destinationStride) + first)); + } + } + + return first; + } + + /// + /// Processes complete 4-lane inverse batches along input rows. + /// + /// The number of positions in each transform. + /// The number of independent transforms. + /// The borrowed source matrix. + /// The source row stride, in floats. + /// The borrowed output matrix. + /// The destination row stride, in floats. + /// Whether transform positions become output rows and input rows become output columns. + /// The first independent transform not already processed by a wider batch. + /// Borrowed storage for two length-by-4 banks. Only the first bank needs initialized inputs. + /// The first independent transform left after the complete batches. + private static int InverseRows128( + int length, + int count, + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + bool transposed, + int first, + Span workspace) + { + Span> values = MemoryMarshal.Cast>(workspace[..(length * 4)]); + Span> scratch = MemoryMarshal.Cast>(workspace.Slice(length * 4, length * 4)); + ref float input = ref MemoryMarshal.GetReference(source); + ref float output = ref MemoryMarshal.GetReference(destination); + + for (; first <= count - 4; first += 4) + { + // Each square load contains adjacent samples from independent rows. Transposition puts those rows into SIMD lanes. + // The complete row band is loaded before any same-orientation output is written, so source and destination can be the same matrix. + for (int column = 0; column < length; column += 4) + { + Vector128 row0 = Vector128.LoadUnsafe(ref input, (nuint)(((first + 0) * sourceStride) + column)).AsInt32(); + Vector128 row1 = Vector128.LoadUnsafe(ref input, (nuint)(((first + 1) * sourceStride) + column)).AsInt32(); + Vector128 row2 = Vector128.LoadUnsafe(ref input, (nuint)(((first + 2) * sourceStride) + column)).AsInt32(); + Vector128 row3 = Vector128.LoadUnsafe(ref input, (nuint)(((first + 3) * sourceStride) + column)).AsInt32(); + + SimdUtils.Transpose(ref row0, ref row1, ref row2, ref row3); + values[column + 0] = row0.AsSingle(); + values[column + 1] = row1.AsSingle(); + values[column + 2] = row2.AsSingle(); + values[column + 3] = row3.AsSingle(); + } + + Inverse(values, scratch); + if (transposed) + { + // A complete vector is one contiguous output row. Only logical samples are stored; destination padding remains untouched. + for (int i = 0; i < length; i++) + { + values[i].StoreUnsafe(ref output, (nuint)((i * destinationStride) + first)); + } + } + else + { + // Restore row-major order one register tile at a time. This replaces a separate full-block transpose and copy. + for (int column = 0; column < length; column += 4) + { + Vector128 row0 = values[column + 0].AsInt32(); + Vector128 row1 = values[column + 1].AsInt32(); + Vector128 row2 = values[column + 2].AsInt32(); + Vector128 row3 = values[column + 3].AsInt32(); + + SimdUtils.Transpose(ref row0, ref row1, ref row2, ref row3); + row0.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 0) * destinationStride) + column)); + row1.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 1) * destinationStride) + column)); + row2.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 2) * destinationStride) + column)); + row3.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 3) * destinationStride) + column)); + } + } + } + + return first; + } + + /// + /// Computes independent forward transforms using two nonoverlapping borrowed vector banks. + /// + /// One vector per transform position. Its length is a power of two from one through 256. + /// At least values.Length vectors. Previous contents are not read. + private static void Forward(Span> values, Span> scratch) + { + // Small transforms stay in registers. The one-point transform is an identity and must not copy any trailing samples. + switch (values.Length) + { + case 1: + return; + case 2: + Forward2(values); + return; + case 4: + Forward4(values); + return; + case 8: + Forward8(values); + return; + case 16: + Forward16(values); + return; + case 32: + Forward32(values); + return; + } + + int half = values.Length / 2; + ReadOnlySpan weights = JxlDctScales.GetMultipliers(values.Length); + + // Consume both original halves before reusing their bank as recursive scratch. Neither recursive call can then destroy unread input. + for (int i = 0; i < half; i++) + { + Vector256 first = values[i]; + Vector256 last = values[values.Length - i - 1]; + scratch[i] = first + last; + scratch[half + i] = (first - last) * Vector256.Create(weights[i]); + } + + Forward(scratch[..half], values); + Forward(scratch.Slice(half, half), values); + scratch[half] = Vector256.MultiplyAddEstimate(scratch[half], Vector256.Create(JxlDctScales.Sqrt2), scratch[half + 1]); + + for (int i = 1; i + 1 < half; i++) + { + scratch[half + i] += scratch[half + i + 1]; + } + + for (int i = 0; i < half; i++) + { + values[2 * i] = scratch[i]; + values[(2 * i) + 1] = scratch[half + i]; + } + } + + /// + /// Computes independent 2-point forward transforms without intermediate memory passes. + /// + /// Exactly 2 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Forward2(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector256 input0 = values[0]; + Vector256 input1 = values[1]; + + Vector256 v0 = input0 + input1; + Vector256 v1 = input0 - input1; + + values[0] = v0; + values[1] = v1; + } + + /// + /// Computes independent 4-point forward transforms without intermediate memory passes. + /// + /// Exactly 4 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Forward4(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector256 input0 = values[0]; + Vector256 input1 = values[1]; + Vector256 input2 = values[2]; + Vector256 input3 = values[3]; + + Vector256 v0 = input0 + input3; + Vector256 v1 = (input0 - input3) * Vector256.Create(0.541196100146197f); + Vector256 v2 = input1 + input2; + Vector256 v3 = (input1 - input2) * Vector256.Create(1.3065629648763764f); + Vector256 v4 = v0 + v2; + Vector256 v5 = v0 - v2; + Vector256 v6 = v1 + v3; + Vector256 v7 = v1 - v3; + Vector256 v8 = Vector256.MultiplyAddEstimate(v6, Vector256.Create(JxlDctScales.Sqrt2), v7); + + values[0] = v4; + values[1] = v8; + values[2] = v5; + values[3] = v7; + } + + /// + /// Computes independent 8-point forward transforms without intermediate memory passes. + /// + /// Exactly 8 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Forward8(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector256 input0 = values[0]; + Vector256 input1 = values[1]; + Vector256 input2 = values[2]; + Vector256 input3 = values[3]; + Vector256 input4 = values[4]; + Vector256 input5 = values[5]; + Vector256 input6 = values[6]; + Vector256 input7 = values[7]; + + Vector256 v0 = input0 + input7; + Vector256 v1 = (input0 - input7) * Vector256.Create(0.5097955791041592f); + Vector256 v2 = input1 + input6; + Vector256 v3 = (input1 - input6) * Vector256.Create(0.6013448869350453f); + Vector256 v4 = input2 + input5; + Vector256 v5 = (input2 - input5) * Vector256.Create(0.8999762231364156f); + Vector256 v6 = input3 + input4; + Vector256 v7 = (input3 - input4) * Vector256.Create(2.5629154477415055f); + Vector256 v8 = v0 + v6; + Vector256 v9 = (v0 - v6) * Vector256.Create(0.541196100146197f); + Vector256 v10 = v2 + v4; + Vector256 v11 = (v2 - v4) * Vector256.Create(1.3065629648763764f); + Vector256 v12 = v8 + v10; + Vector256 v13 = v8 - v10; + Vector256 v14 = v9 + v11; + Vector256 v15 = v9 - v11; + Vector256 v16 = Vector256.MultiplyAddEstimate(v14, Vector256.Create(JxlDctScales.Sqrt2), v15); + Vector256 v17 = v1 + v7; + Vector256 v18 = (v1 - v7) * Vector256.Create(0.541196100146197f); + Vector256 v19 = v3 + v5; + Vector256 v20 = (v3 - v5) * Vector256.Create(1.3065629648763764f); + Vector256 v21 = v17 + v19; + Vector256 v22 = v17 - v19; + Vector256 v23 = v18 + v20; + Vector256 v24 = v18 - v20; + Vector256 v25 = Vector256.MultiplyAddEstimate(v23, Vector256.Create(JxlDctScales.Sqrt2), v24); + Vector256 v26 = Vector256.MultiplyAddEstimate(v21, Vector256.Create(JxlDctScales.Sqrt2), v25); + Vector256 v27 = v25 + v22; + Vector256 v28 = v22 + v24; + + values[0] = v12; + values[1] = v26; + values[2] = v16; + values[3] = v27; + values[4] = v13; + values[5] = v28; + values[6] = v15; + values[7] = v24; + } + + /// + /// Computes independent 16-point forward transforms without intermediate memory passes. + /// + /// Exactly 16 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Forward16(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector256 input0 = values[0]; + Vector256 input1 = values[1]; + Vector256 input2 = values[2]; + Vector256 input3 = values[3]; + Vector256 input4 = values[4]; + Vector256 input5 = values[5]; + Vector256 input6 = values[6]; + Vector256 input7 = values[7]; + Vector256 input8 = values[8]; + Vector256 input9 = values[9]; + Vector256 input10 = values[10]; + Vector256 input11 = values[11]; + Vector256 input12 = values[12]; + Vector256 input13 = values[13]; + Vector256 input14 = values[14]; + Vector256 input15 = values[15]; + + Vector256 v0 = input0 + input15; + Vector256 v1 = (input0 - input15) * Vector256.Create(0.5024192861881557f); + Vector256 v2 = input1 + input14; + Vector256 v3 = (input1 - input14) * Vector256.Create(0.5224986149396889f); + Vector256 v4 = input2 + input13; + Vector256 v5 = (input2 - input13) * Vector256.Create(0.5669440348163577f); + Vector256 v6 = input3 + input12; + Vector256 v7 = (input3 - input12) * Vector256.Create(0.6468217833599901f); + Vector256 v8 = input4 + input11; + Vector256 v9 = (input4 - input11) * Vector256.Create(0.7881546234512502f); + Vector256 v10 = input5 + input10; + Vector256 v11 = (input5 - input10) * Vector256.Create(1.060677685990347f); + Vector256 v12 = input6 + input9; + Vector256 v13 = (input6 - input9) * Vector256.Create(1.7224470982383342f); + Vector256 v14 = input7 + input8; + Vector256 v15 = (input7 - input8) * Vector256.Create(5.101148618689155f); + Vector256 v16 = v0 + v14; + Vector256 v17 = (v0 - v14) * Vector256.Create(0.5097955791041592f); + Vector256 v18 = v2 + v12; + Vector256 v19 = (v2 - v12) * Vector256.Create(0.6013448869350453f); + Vector256 v20 = v4 + v10; + Vector256 v21 = (v4 - v10) * Vector256.Create(0.8999762231364156f); + Vector256 v22 = v6 + v8; + Vector256 v23 = (v6 - v8) * Vector256.Create(2.5629154477415055f); + Vector256 v24 = v16 + v22; + Vector256 v25 = (v16 - v22) * Vector256.Create(0.541196100146197f); + Vector256 v26 = v18 + v20; + Vector256 v27 = (v18 - v20) * Vector256.Create(1.3065629648763764f); + Vector256 v28 = v24 + v26; + Vector256 v29 = v24 - v26; + Vector256 v30 = v25 + v27; + Vector256 v31 = v25 - v27; + Vector256 v32 = Vector256.MultiplyAddEstimate(v30, Vector256.Create(JxlDctScales.Sqrt2), v31); + Vector256 v33 = v17 + v23; + Vector256 v34 = (v17 - v23) * Vector256.Create(0.541196100146197f); + Vector256 v35 = v19 + v21; + Vector256 v36 = (v19 - v21) * Vector256.Create(1.3065629648763764f); + Vector256 v37 = v33 + v35; + Vector256 v38 = v33 - v35; + Vector256 v39 = v34 + v36; + Vector256 v40 = v34 - v36; + Vector256 v41 = Vector256.MultiplyAddEstimate(v39, Vector256.Create(JxlDctScales.Sqrt2), v40); + Vector256 v42 = Vector256.MultiplyAddEstimate(v37, Vector256.Create(JxlDctScales.Sqrt2), v41); + Vector256 v43 = v41 + v38; + Vector256 v44 = v38 + v40; + Vector256 v45 = v1 + v15; + Vector256 v46 = (v1 - v15) * Vector256.Create(0.5097955791041592f); + Vector256 v47 = v3 + v13; + Vector256 v48 = (v3 - v13) * Vector256.Create(0.6013448869350453f); + Vector256 v49 = v5 + v11; + Vector256 v50 = (v5 - v11) * Vector256.Create(0.8999762231364156f); + Vector256 v51 = v7 + v9; + Vector256 v52 = (v7 - v9) * Vector256.Create(2.5629154477415055f); + Vector256 v53 = v45 + v51; + Vector256 v54 = (v45 - v51) * Vector256.Create(0.541196100146197f); + Vector256 v55 = v47 + v49; + Vector256 v56 = (v47 - v49) * Vector256.Create(1.3065629648763764f); + Vector256 v57 = v53 + v55; + Vector256 v58 = v53 - v55; + Vector256 v59 = v54 + v56; + Vector256 v60 = v54 - v56; + Vector256 v61 = Vector256.MultiplyAddEstimate(v59, Vector256.Create(JxlDctScales.Sqrt2), v60); + Vector256 v62 = v46 + v52; + Vector256 v63 = (v46 - v52) * Vector256.Create(0.541196100146197f); + Vector256 v64 = v48 + v50; + Vector256 v65 = (v48 - v50) * Vector256.Create(1.3065629648763764f); + Vector256 v66 = v62 + v64; + Vector256 v67 = v62 - v64; + Vector256 v68 = v63 + v65; + Vector256 v69 = v63 - v65; + Vector256 v70 = Vector256.MultiplyAddEstimate(v68, Vector256.Create(JxlDctScales.Sqrt2), v69); + Vector256 v71 = Vector256.MultiplyAddEstimate(v66, Vector256.Create(JxlDctScales.Sqrt2), v70); + Vector256 v72 = v70 + v67; + Vector256 v73 = v67 + v69; + Vector256 v74 = Vector256.MultiplyAddEstimate(v57, Vector256.Create(JxlDctScales.Sqrt2), v71); + Vector256 v75 = v71 + v61; + Vector256 v76 = v61 + v72; + Vector256 v77 = v72 + v58; + Vector256 v78 = v58 + v73; + Vector256 v79 = v73 + v60; + Vector256 v80 = v60 + v69; + + values[0] = v28; + values[1] = v74; + values[2] = v42; + values[3] = v75; + values[4] = v32; + values[5] = v76; + values[6] = v43; + values[7] = v77; + values[8] = v29; + values[9] = v78; + values[10] = v44; + values[11] = v79; + values[12] = v31; + values[13] = v80; + values[14] = v40; + values[15] = v69; + } + + /// + /// Computes independent 32-point forward transforms without intermediate memory passes. + /// + /// Exactly 32 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Forward32(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector256 input0 = values[0]; + Vector256 input1 = values[1]; + Vector256 input2 = values[2]; + Vector256 input3 = values[3]; + Vector256 input4 = values[4]; + Vector256 input5 = values[5]; + Vector256 input6 = values[6]; + Vector256 input7 = values[7]; + Vector256 input8 = values[8]; + Vector256 input9 = values[9]; + Vector256 input10 = values[10]; + Vector256 input11 = values[11]; + Vector256 input12 = values[12]; + Vector256 input13 = values[13]; + Vector256 input14 = values[14]; + Vector256 input15 = values[15]; + Vector256 input16 = values[16]; + Vector256 input17 = values[17]; + Vector256 input18 = values[18]; + Vector256 input19 = values[19]; + Vector256 input20 = values[20]; + Vector256 input21 = values[21]; + Vector256 input22 = values[22]; + Vector256 input23 = values[23]; + Vector256 input24 = values[24]; + Vector256 input25 = values[25]; + Vector256 input26 = values[26]; + Vector256 input27 = values[27]; + Vector256 input28 = values[28]; + Vector256 input29 = values[29]; + Vector256 input30 = values[30]; + Vector256 input31 = values[31]; + + Vector256 v0 = input0 + input31; + Vector256 v1 = (input0 - input31) * Vector256.Create(0.5006029982351963f); + Vector256 v2 = input1 + input30; + Vector256 v3 = (input1 - input30) * Vector256.Create(0.5054709598975436f); + Vector256 v4 = input2 + input29; + Vector256 v5 = (input2 - input29) * Vector256.Create(0.5154473099226246f); + Vector256 v6 = input3 + input28; + Vector256 v7 = (input3 - input28) * Vector256.Create(0.5310425910897841f); + Vector256 v8 = input4 + input27; + Vector256 v9 = (input4 - input27) * Vector256.Create(0.5531038960344445f); + Vector256 v10 = input5 + input26; + Vector256 v11 = (input5 - input26) * Vector256.Create(0.5829349682061339f); + Vector256 v12 = input6 + input25; + Vector256 v13 = (input6 - input25) * Vector256.Create(0.6225041230356648f); + Vector256 v14 = input7 + input24; + Vector256 v15 = (input7 - input24) * Vector256.Create(0.6748083414550057f); + Vector256 v16 = input8 + input23; + Vector256 v17 = (input8 - input23) * Vector256.Create(0.7445362710022986f); + Vector256 v18 = input9 + input22; + Vector256 v19 = (input9 - input22) * Vector256.Create(0.8393496454155268f); + Vector256 v20 = input10 + input21; + Vector256 v21 = (input10 - input21) * Vector256.Create(0.9725682378619608f); + Vector256 v22 = input11 + input20; + Vector256 v23 = (input11 - input20) * Vector256.Create(1.1694399334328847f); + Vector256 v24 = input12 + input19; + Vector256 v25 = (input12 - input19) * Vector256.Create(1.4841646163141662f); + Vector256 v26 = input13 + input18; + Vector256 v27 = (input13 - input18) * Vector256.Create(2.057781009953411f); + Vector256 v28 = input14 + input17; + Vector256 v29 = (input14 - input17) * Vector256.Create(3.407608418468719f); + Vector256 v30 = input15 + input16; + Vector256 v31 = (input15 - input16) * Vector256.Create(10.190008123548033f); + Vector256 v32 = v0 + v30; + Vector256 v33 = (v0 - v30) * Vector256.Create(0.5024192861881557f); + Vector256 v34 = v2 + v28; + Vector256 v35 = (v2 - v28) * Vector256.Create(0.5224986149396889f); + Vector256 v36 = v4 + v26; + Vector256 v37 = (v4 - v26) * Vector256.Create(0.5669440348163577f); + Vector256 v38 = v6 + v24; + Vector256 v39 = (v6 - v24) * Vector256.Create(0.6468217833599901f); + Vector256 v40 = v8 + v22; + Vector256 v41 = (v8 - v22) * Vector256.Create(0.7881546234512502f); + Vector256 v42 = v10 + v20; + Vector256 v43 = (v10 - v20) * Vector256.Create(1.060677685990347f); + Vector256 v44 = v12 + v18; + Vector256 v45 = (v12 - v18) * Vector256.Create(1.7224470982383342f); + Vector256 v46 = v14 + v16; + Vector256 v47 = (v14 - v16) * Vector256.Create(5.101148618689155f); + Vector256 v48 = v32 + v46; + Vector256 v49 = (v32 - v46) * Vector256.Create(0.5097955791041592f); + Vector256 v50 = v34 + v44; + Vector256 v51 = (v34 - v44) * Vector256.Create(0.6013448869350453f); + Vector256 v52 = v36 + v42; + Vector256 v53 = (v36 - v42) * Vector256.Create(0.8999762231364156f); + Vector256 v54 = v38 + v40; + Vector256 v55 = (v38 - v40) * Vector256.Create(2.5629154477415055f); + Vector256 v56 = v48 + v54; + Vector256 v57 = (v48 - v54) * Vector256.Create(0.541196100146197f); + Vector256 v58 = v50 + v52; + Vector256 v59 = (v50 - v52) * Vector256.Create(1.3065629648763764f); + Vector256 v60 = v56 + v58; + Vector256 v61 = v56 - v58; + Vector256 v62 = v57 + v59; + Vector256 v63 = v57 - v59; + Vector256 v64 = Vector256.MultiplyAddEstimate(v62, Vector256.Create(JxlDctScales.Sqrt2), v63); + Vector256 v65 = v49 + v55; + Vector256 v66 = (v49 - v55) * Vector256.Create(0.541196100146197f); + Vector256 v67 = v51 + v53; + Vector256 v68 = (v51 - v53) * Vector256.Create(1.3065629648763764f); + Vector256 v69 = v65 + v67; + Vector256 v70 = v65 - v67; + Vector256 v71 = v66 + v68; + Vector256 v72 = v66 - v68; + Vector256 v73 = Vector256.MultiplyAddEstimate(v71, Vector256.Create(JxlDctScales.Sqrt2), v72); + Vector256 v74 = Vector256.MultiplyAddEstimate(v69, Vector256.Create(JxlDctScales.Sqrt2), v73); + Vector256 v75 = v73 + v70; + Vector256 v76 = v70 + v72; + Vector256 v77 = v33 + v47; + Vector256 v78 = (v33 - v47) * Vector256.Create(0.5097955791041592f); + Vector256 v79 = v35 + v45; + Vector256 v80 = (v35 - v45) * Vector256.Create(0.6013448869350453f); + Vector256 v81 = v37 + v43; + Vector256 v82 = (v37 - v43) * Vector256.Create(0.8999762231364156f); + Vector256 v83 = v39 + v41; + Vector256 v84 = (v39 - v41) * Vector256.Create(2.5629154477415055f); + Vector256 v85 = v77 + v83; + Vector256 v86 = (v77 - v83) * Vector256.Create(0.541196100146197f); + Vector256 v87 = v79 + v81; + Vector256 v88 = (v79 - v81) * Vector256.Create(1.3065629648763764f); + Vector256 v89 = v85 + v87; + Vector256 v90 = v85 - v87; + Vector256 v91 = v86 + v88; + Vector256 v92 = v86 - v88; + Vector256 v93 = Vector256.MultiplyAddEstimate(v91, Vector256.Create(JxlDctScales.Sqrt2), v92); + Vector256 v94 = v78 + v84; + Vector256 v95 = (v78 - v84) * Vector256.Create(0.541196100146197f); + Vector256 v96 = v80 + v82; + Vector256 v97 = (v80 - v82) * Vector256.Create(1.3065629648763764f); + Vector256 v98 = v94 + v96; + Vector256 v99 = v94 - v96; + Vector256 v100 = v95 + v97; + Vector256 v101 = v95 - v97; + Vector256 v102 = Vector256.MultiplyAddEstimate(v100, Vector256.Create(JxlDctScales.Sqrt2), v101); + Vector256 v103 = Vector256.MultiplyAddEstimate(v98, Vector256.Create(JxlDctScales.Sqrt2), v102); + Vector256 v104 = v102 + v99; + Vector256 v105 = v99 + v101; + Vector256 v106 = Vector256.MultiplyAddEstimate(v89, Vector256.Create(JxlDctScales.Sqrt2), v103); + Vector256 v107 = v103 + v93; + Vector256 v108 = v93 + v104; + Vector256 v109 = v104 + v90; + Vector256 v110 = v90 + v105; + Vector256 v111 = v105 + v92; + Vector256 v112 = v92 + v101; + Vector256 v113 = v1 + v31; + Vector256 v114 = (v1 - v31) * Vector256.Create(0.5024192861881557f); + Vector256 v115 = v3 + v29; + Vector256 v116 = (v3 - v29) * Vector256.Create(0.5224986149396889f); + Vector256 v117 = v5 + v27; + Vector256 v118 = (v5 - v27) * Vector256.Create(0.5669440348163577f); + Vector256 v119 = v7 + v25; + Vector256 v120 = (v7 - v25) * Vector256.Create(0.6468217833599901f); + Vector256 v121 = v9 + v23; + Vector256 v122 = (v9 - v23) * Vector256.Create(0.7881546234512502f); + Vector256 v123 = v11 + v21; + Vector256 v124 = (v11 - v21) * Vector256.Create(1.060677685990347f); + Vector256 v125 = v13 + v19; + Vector256 v126 = (v13 - v19) * Vector256.Create(1.7224470982383342f); + Vector256 v127 = v15 + v17; + Vector256 v128 = (v15 - v17) * Vector256.Create(5.101148618689155f); + Vector256 v129 = v113 + v127; + Vector256 v130 = (v113 - v127) * Vector256.Create(0.5097955791041592f); + Vector256 v131 = v115 + v125; + Vector256 v132 = (v115 - v125) * Vector256.Create(0.6013448869350453f); + Vector256 v133 = v117 + v123; + Vector256 v134 = (v117 - v123) * Vector256.Create(0.8999762231364156f); + Vector256 v135 = v119 + v121; + Vector256 v136 = (v119 - v121) * Vector256.Create(2.5629154477415055f); + Vector256 v137 = v129 + v135; + Vector256 v138 = (v129 - v135) * Vector256.Create(0.541196100146197f); + Vector256 v139 = v131 + v133; + Vector256 v140 = (v131 - v133) * Vector256.Create(1.3065629648763764f); + Vector256 v141 = v137 + v139; + Vector256 v142 = v137 - v139; + Vector256 v143 = v138 + v140; + Vector256 v144 = v138 - v140; + Vector256 v145 = Vector256.MultiplyAddEstimate(v143, Vector256.Create(JxlDctScales.Sqrt2), v144); + Vector256 v146 = v130 + v136; + Vector256 v147 = (v130 - v136) * Vector256.Create(0.541196100146197f); + Vector256 v148 = v132 + v134; + Vector256 v149 = (v132 - v134) * Vector256.Create(1.3065629648763764f); + Vector256 v150 = v146 + v148; + Vector256 v151 = v146 - v148; + Vector256 v152 = v147 + v149; + Vector256 v153 = v147 - v149; + Vector256 v154 = Vector256.MultiplyAddEstimate(v152, Vector256.Create(JxlDctScales.Sqrt2), v153); + Vector256 v155 = Vector256.MultiplyAddEstimate(v150, Vector256.Create(JxlDctScales.Sqrt2), v154); + Vector256 v156 = v154 + v151; + Vector256 v157 = v151 + v153; + Vector256 v158 = v114 + v128; + Vector256 v159 = (v114 - v128) * Vector256.Create(0.5097955791041592f); + Vector256 v160 = v116 + v126; + Vector256 v161 = (v116 - v126) * Vector256.Create(0.6013448869350453f); + Vector256 v162 = v118 + v124; + Vector256 v163 = (v118 - v124) * Vector256.Create(0.8999762231364156f); + Vector256 v164 = v120 + v122; + Vector256 v165 = (v120 - v122) * Vector256.Create(2.5629154477415055f); + Vector256 v166 = v158 + v164; + Vector256 v167 = (v158 - v164) * Vector256.Create(0.541196100146197f); + Vector256 v168 = v160 + v162; + Vector256 v169 = (v160 - v162) * Vector256.Create(1.3065629648763764f); + Vector256 v170 = v166 + v168; + Vector256 v171 = v166 - v168; + Vector256 v172 = v167 + v169; + Vector256 v173 = v167 - v169; + Vector256 v174 = Vector256.MultiplyAddEstimate(v172, Vector256.Create(JxlDctScales.Sqrt2), v173); + Vector256 v175 = v159 + v165; + Vector256 v176 = (v159 - v165) * Vector256.Create(0.541196100146197f); + Vector256 v177 = v161 + v163; + Vector256 v178 = (v161 - v163) * Vector256.Create(1.3065629648763764f); + Vector256 v179 = v175 + v177; + Vector256 v180 = v175 - v177; + Vector256 v181 = v176 + v178; + Vector256 v182 = v176 - v178; + Vector256 v183 = Vector256.MultiplyAddEstimate(v181, Vector256.Create(JxlDctScales.Sqrt2), v182); + Vector256 v184 = Vector256.MultiplyAddEstimate(v179, Vector256.Create(JxlDctScales.Sqrt2), v183); + Vector256 v185 = v183 + v180; + Vector256 v186 = v180 + v182; + Vector256 v187 = Vector256.MultiplyAddEstimate(v170, Vector256.Create(JxlDctScales.Sqrt2), v184); + Vector256 v188 = v184 + v174; + Vector256 v189 = v174 + v185; + Vector256 v190 = v185 + v171; + Vector256 v191 = v171 + v186; + Vector256 v192 = v186 + v173; + Vector256 v193 = v173 + v182; + Vector256 v194 = Vector256.MultiplyAddEstimate(v141, Vector256.Create(JxlDctScales.Sqrt2), v187); + Vector256 v195 = v187 + v155; + Vector256 v196 = v155 + v188; + Vector256 v197 = v188 + v145; + Vector256 v198 = v145 + v189; + Vector256 v199 = v189 + v156; + Vector256 v200 = v156 + v190; + Vector256 v201 = v190 + v142; + Vector256 v202 = v142 + v191; + Vector256 v203 = v191 + v157; + Vector256 v204 = v157 + v192; + Vector256 v205 = v192 + v144; + Vector256 v206 = v144 + v193; + Vector256 v207 = v193 + v153; + Vector256 v208 = v153 + v182; + + values[0] = v60; + values[1] = v194; + values[2] = v106; + values[3] = v195; + values[4] = v74; + values[5] = v196; + values[6] = v107; + values[7] = v197; + values[8] = v64; + values[9] = v198; + values[10] = v108; + values[11] = v199; + values[12] = v75; + values[13] = v200; + values[14] = v109; + values[15] = v201; + values[16] = v61; + values[17] = v202; + values[18] = v110; + values[19] = v203; + values[20] = v76; + values[21] = v204; + values[22] = v111; + values[23] = v205; + values[24] = v63; + values[25] = v206; + values[26] = v112; + values[27] = v207; + values[28] = v72; + values[29] = v208; + values[30] = v101; + values[31] = v182; + } + + /// + /// Processes complete 8-lane forward batches along input columns. + /// + /// The number of positions in each transform. + /// The number of independent transforms. + /// The borrowed source matrix. + /// The source row stride, in floats. + /// The borrowed output matrix. + /// The destination row stride, in floats. + /// The first independent transform not already processed by a wider batch. + /// Borrowed storage for two length-by-8 banks. Only the first bank needs initialized inputs. + /// The first independent transform left after the complete batches. + private static int ForwardColumns256( + int length, + int count, + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int first, + Span workspace) + { + Span> values = MemoryMarshal.Cast>(workspace[..(length * 8)]); + Span> scratch = MemoryMarshal.Cast>(workspace.Slice(length * 8, length * 8)); + ref float input = ref MemoryMarshal.GetReference(source); + ref float output = ref MemoryMarshal.GetReference(destination); + Vector256 scale = Vector256.Create(1F / length); + + for (; first <= count - 8; first += 8) + { + // One vector covers adjacent columns at the same transform position. Physical strides never enter the coefficient-bank layout. + for (int i = 0; i < length; i++) + { + values[i] = Vector256.LoadUnsafe(ref input, (nuint)((i * sourceStride) + first)); + } + + Forward(values, scratch); + for (int i = 0; i < length; i++) + { + (values[i] * scale).StoreUnsafe(ref output, (nuint)((i * destinationStride) + first)); + } + } + + return first; + } + + /// + /// Processes complete 8-lane forward batches along input rows. + /// + /// The number of positions in each transform. + /// The number of independent transforms. + /// The borrowed source matrix. + /// The source row stride, in floats. + /// The borrowed output matrix. + /// The destination row stride, in floats. + /// Whether transform positions become output rows and input rows become output columns. + /// The first independent transform not already processed by a wider batch. + /// Borrowed storage for two length-by-8 banks. Only the first bank needs initialized inputs. + /// The first independent transform left after the complete batches. + private static int ForwardRows256( + int length, + int count, + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + bool transposed, + int first, + Span workspace) + { + Span> values = MemoryMarshal.Cast>(workspace[..(length * 8)]); + Span> scratch = MemoryMarshal.Cast>(workspace.Slice(length * 8, length * 8)); + ref float input = ref MemoryMarshal.GetReference(source); + ref float output = ref MemoryMarshal.GetReference(destination); + Vector256 scale = Vector256.Create(1F / length); + + for (; first <= count - 8; first += 8) + { + // Each square load contains adjacent samples from independent rows. Transposition puts those rows into SIMD lanes. + // The complete row band is loaded before any same-orientation output is written, so source and destination can be the same matrix. + for (int column = 0; column < length; column += 8) + { + Vector256 row0 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 0) * sourceStride) + column)).AsInt32(); + Vector256 row1 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 1) * sourceStride) + column)).AsInt32(); + Vector256 row2 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 2) * sourceStride) + column)).AsInt32(); + Vector256 row3 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 3) * sourceStride) + column)).AsInt32(); + Vector256 row4 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 4) * sourceStride) + column)).AsInt32(); + Vector256 row5 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 5) * sourceStride) + column)).AsInt32(); + Vector256 row6 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 6) * sourceStride) + column)).AsInt32(); + Vector256 row7 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 7) * sourceStride) + column)).AsInt32(); + + SimdUtils.Transpose(ref row0, ref row1, ref row2, ref row3, ref row4, ref row5, ref row6, ref row7); + values[column + 0] = row0.AsSingle(); + values[column + 1] = row1.AsSingle(); + values[column + 2] = row2.AsSingle(); + values[column + 3] = row3.AsSingle(); + values[column + 4] = row4.AsSingle(); + values[column + 5] = row5.AsSingle(); + values[column + 6] = row6.AsSingle(); + values[column + 7] = row7.AsSingle(); + } + + Forward(values, scratch); + if (transposed) + { + // A complete vector is one contiguous output row. Only logical samples are stored; destination padding remains untouched. + for (int i = 0; i < length; i++) + { + (values[i] * scale).StoreUnsafe(ref output, (nuint)((i * destinationStride) + first)); + } + } + else + { + // Restore row-major order one register tile at a time. This replaces a separate full-block transpose and copy. + for (int column = 0; column < length; column += 8) + { + Vector256 row0 = (values[column + 0] * scale).AsInt32(); + Vector256 row1 = (values[column + 1] * scale).AsInt32(); + Vector256 row2 = (values[column + 2] * scale).AsInt32(); + Vector256 row3 = (values[column + 3] * scale).AsInt32(); + Vector256 row4 = (values[column + 4] * scale).AsInt32(); + Vector256 row5 = (values[column + 5] * scale).AsInt32(); + Vector256 row6 = (values[column + 6] * scale).AsInt32(); + Vector256 row7 = (values[column + 7] * scale).AsInt32(); + + SimdUtils.Transpose(ref row0, ref row1, ref row2, ref row3, ref row4, ref row5, ref row6, ref row7); + row0.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 0) * destinationStride) + column)); + row1.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 1) * destinationStride) + column)); + row2.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 2) * destinationStride) + column)); + row3.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 3) * destinationStride) + column)); + row4.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 4) * destinationStride) + column)); + row5.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 5) * destinationStride) + column)); + row6.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 6) * destinationStride) + column)); + row7.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 7) * destinationStride) + column)); + } + } + } + + return first; + } + + /// + /// Computes independent inverse transforms using two nonoverlapping borrowed vector banks. + /// + /// One vector per transform position. Its length is a power of two from one through 256. + /// At least values.Length vectors. Previous contents are not read. + private static void Inverse(Span> values, Span> scratch) + { + // Small transforms stay in registers. The one-point transform is an identity and must not copy any trailing samples. + switch (values.Length) + { + case 1: + return; + case 2: + Inverse2(values); + return; + case 4: + Inverse4(values); + return; + case 8: + Inverse8(values); + return; + case 16: + Inverse16(values); + return; + case 32: + Inverse32(values); + return; + } + + int half = values.Length / 2; + // Separate frequency parity before either recursive call. The original bank can then serve as recursive scratch. + for (int i = 0; i < half; i++) + { + scratch[i] = values[2 * i]; + scratch[half + i] = values[(2 * i) + 1]; + } + + Inverse(scratch[..half], values); + + // Descend so every odd-frequency predecessor is read before its own entry changes. + for (int i = half - 1; i > 0; i--) + { + scratch[half + i] += scratch[half + i - 1]; + } + + scratch[half] *= Vector256.Create(JxlDctScales.Sqrt2); + Inverse(scratch.Slice(half, half), values); + ReadOnlySpan weights = JxlDctScales.GetMultipliers(values.Length); + + for (int i = 0; i < half; i++) + { + Vector256 weight = Vector256.Create(weights[i]); + Vector256 even = scratch[i]; + Vector256 odd = scratch[half + i]; + values[i] = Vector256.MultiplyAddEstimate(weight, odd, even); + values[values.Length - i - 1] = Vector256.MultiplyAddEstimate(-weight, odd, even); + } + } + + /// + /// Computes independent 2-point inverse transforms without intermediate memory passes. + /// + /// Exactly 2 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Inverse2(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector256 input0 = values[0]; + Vector256 input1 = values[1]; + + Vector256 v0 = input0 + input1; + Vector256 v1 = input0 - input1; + + values[0] = v0; + values[1] = v1; + } + + /// + /// Computes independent 4-point inverse transforms without intermediate memory passes. + /// + /// Exactly 4 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Inverse4(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector256 input0 = values[0]; + Vector256 input1 = values[1]; + Vector256 input2 = values[2]; + Vector256 input3 = values[3]; + + Vector256 v0 = input0 + input2; + Vector256 v1 = input0 - input2; + Vector256 v2 = input3 + input1; + Vector256 v3 = input1 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v4 = v3 + v2; + Vector256 v5 = v3 - v2; + Vector256 v6 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v4, v0); + Vector256 v7 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v4, v0); + Vector256 v8 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v5, v1); + Vector256 v9 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v5, v1); + + values[0] = v6; + values[1] = v8; + values[2] = v9; + values[3] = v7; + } + + /// + /// Computes independent 8-point inverse transforms without intermediate memory passes. + /// + /// Exactly 8 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Inverse8(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector256 input0 = values[0]; + Vector256 input1 = values[1]; + Vector256 input2 = values[2]; + Vector256 input3 = values[3]; + Vector256 input4 = values[4]; + Vector256 input5 = values[5]; + Vector256 input6 = values[6]; + Vector256 input7 = values[7]; + + Vector256 v0 = input0 + input4; + Vector256 v1 = input0 - input4; + Vector256 v2 = input6 + input2; + Vector256 v3 = input2 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v4 = v3 + v2; + Vector256 v5 = v3 - v2; + Vector256 v6 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v4, v0); + Vector256 v7 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v4, v0); + Vector256 v8 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v5, v1); + Vector256 v9 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v5, v1); + Vector256 v10 = input7 + input5; + Vector256 v11 = input5 + input3; + Vector256 v12 = input3 + input1; + Vector256 v13 = input1 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v14 = v13 + v11; + Vector256 v15 = v13 - v11; + Vector256 v16 = v10 + v12; + Vector256 v17 = v12 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v18 = v17 + v16; + Vector256 v19 = v17 - v16; + Vector256 v20 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v18, v14); + Vector256 v21 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v18, v14); + Vector256 v22 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v19, v15); + Vector256 v23 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v19, v15); + Vector256 v24 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5097955791041592f), v20, v6); + Vector256 v25 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5097955791041592f), v20, v6); + Vector256 v26 = Vector256.MultiplyAddEstimate(Vector256.Create(0.6013448869350453f), v22, v8); + Vector256 v27 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.6013448869350453f), v22, v8); + Vector256 v28 = Vector256.MultiplyAddEstimate(Vector256.Create(0.8999762231364156f), v23, v9); + Vector256 v29 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.8999762231364156f), v23, v9); + Vector256 v30 = Vector256.MultiplyAddEstimate(Vector256.Create(2.5629154477415055f), v21, v7); + Vector256 v31 = Vector256.MultiplyAddEstimate(-Vector256.Create(2.5629154477415055f), v21, v7); + + values[0] = v24; + values[1] = v26; + values[2] = v28; + values[3] = v30; + values[4] = v31; + values[5] = v29; + values[6] = v27; + values[7] = v25; + } + + /// + /// Computes independent 16-point inverse transforms without intermediate memory passes. + /// + /// Exactly 16 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Inverse16(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector256 input0 = values[0]; + Vector256 input1 = values[1]; + Vector256 input2 = values[2]; + Vector256 input3 = values[3]; + Vector256 input4 = values[4]; + Vector256 input5 = values[5]; + Vector256 input6 = values[6]; + Vector256 input7 = values[7]; + Vector256 input8 = values[8]; + Vector256 input9 = values[9]; + Vector256 input10 = values[10]; + Vector256 input11 = values[11]; + Vector256 input12 = values[12]; + Vector256 input13 = values[13]; + Vector256 input14 = values[14]; + Vector256 input15 = values[15]; + + Vector256 v0 = input0 + input8; + Vector256 v1 = input0 - input8; + Vector256 v2 = input12 + input4; + Vector256 v3 = input4 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v4 = v3 + v2; + Vector256 v5 = v3 - v2; + Vector256 v6 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v4, v0); + Vector256 v7 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v4, v0); + Vector256 v8 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v5, v1); + Vector256 v9 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v5, v1); + Vector256 v10 = input14 + input10; + Vector256 v11 = input10 + input6; + Vector256 v12 = input6 + input2; + Vector256 v13 = input2 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v14 = v13 + v11; + Vector256 v15 = v13 - v11; + Vector256 v16 = v10 + v12; + Vector256 v17 = v12 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v18 = v17 + v16; + Vector256 v19 = v17 - v16; + Vector256 v20 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v18, v14); + Vector256 v21 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v18, v14); + Vector256 v22 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v19, v15); + Vector256 v23 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v19, v15); + Vector256 v24 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5097955791041592f), v20, v6); + Vector256 v25 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5097955791041592f), v20, v6); + Vector256 v26 = Vector256.MultiplyAddEstimate(Vector256.Create(0.6013448869350453f), v22, v8); + Vector256 v27 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.6013448869350453f), v22, v8); + Vector256 v28 = Vector256.MultiplyAddEstimate(Vector256.Create(0.8999762231364156f), v23, v9); + Vector256 v29 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.8999762231364156f), v23, v9); + Vector256 v30 = Vector256.MultiplyAddEstimate(Vector256.Create(2.5629154477415055f), v21, v7); + Vector256 v31 = Vector256.MultiplyAddEstimate(-Vector256.Create(2.5629154477415055f), v21, v7); + Vector256 v32 = input15 + input13; + Vector256 v33 = input13 + input11; + Vector256 v34 = input11 + input9; + Vector256 v35 = input9 + input7; + Vector256 v36 = input7 + input5; + Vector256 v37 = input5 + input3; + Vector256 v38 = input3 + input1; + Vector256 v39 = input1 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v40 = v39 + v35; + Vector256 v41 = v39 - v35; + Vector256 v42 = v33 + v37; + Vector256 v43 = v37 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v44 = v43 + v42; + Vector256 v45 = v43 - v42; + Vector256 v46 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v44, v40); + Vector256 v47 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v44, v40); + Vector256 v48 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v45, v41); + Vector256 v49 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v45, v41); + Vector256 v50 = v32 + v34; + Vector256 v51 = v34 + v36; + Vector256 v52 = v36 + v38; + Vector256 v53 = v38 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v54 = v53 + v51; + Vector256 v55 = v53 - v51; + Vector256 v56 = v50 + v52; + Vector256 v57 = v52 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v58 = v57 + v56; + Vector256 v59 = v57 - v56; + Vector256 v60 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v58, v54); + Vector256 v61 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v58, v54); + Vector256 v62 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v59, v55); + Vector256 v63 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v59, v55); + Vector256 v64 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5097955791041592f), v60, v46); + Vector256 v65 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5097955791041592f), v60, v46); + Vector256 v66 = Vector256.MultiplyAddEstimate(Vector256.Create(0.6013448869350453f), v62, v48); + Vector256 v67 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.6013448869350453f), v62, v48); + Vector256 v68 = Vector256.MultiplyAddEstimate(Vector256.Create(0.8999762231364156f), v63, v49); + Vector256 v69 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.8999762231364156f), v63, v49); + Vector256 v70 = Vector256.MultiplyAddEstimate(Vector256.Create(2.5629154477415055f), v61, v47); + Vector256 v71 = Vector256.MultiplyAddEstimate(-Vector256.Create(2.5629154477415055f), v61, v47); + Vector256 v72 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5024192861881557f), v64, v24); + Vector256 v73 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5024192861881557f), v64, v24); + Vector256 v74 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5224986149396889f), v66, v26); + Vector256 v75 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5224986149396889f), v66, v26); + Vector256 v76 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5669440348163577f), v68, v28); + Vector256 v77 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5669440348163577f), v68, v28); + Vector256 v78 = Vector256.MultiplyAddEstimate(Vector256.Create(0.6468217833599901f), v70, v30); + Vector256 v79 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.6468217833599901f), v70, v30); + Vector256 v80 = Vector256.MultiplyAddEstimate(Vector256.Create(0.7881546234512502f), v71, v31); + Vector256 v81 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.7881546234512502f), v71, v31); + Vector256 v82 = Vector256.MultiplyAddEstimate(Vector256.Create(1.060677685990347f), v69, v29); + Vector256 v83 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.060677685990347f), v69, v29); + Vector256 v84 = Vector256.MultiplyAddEstimate(Vector256.Create(1.7224470982383342f), v67, v27); + Vector256 v85 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.7224470982383342f), v67, v27); + Vector256 v86 = Vector256.MultiplyAddEstimate(Vector256.Create(5.101148618689155f), v65, v25); + Vector256 v87 = Vector256.MultiplyAddEstimate(-Vector256.Create(5.101148618689155f), v65, v25); + + values[0] = v72; + values[1] = v74; + values[2] = v76; + values[3] = v78; + values[4] = v80; + values[5] = v82; + values[6] = v84; + values[7] = v86; + values[8] = v87; + values[9] = v85; + values[10] = v83; + values[11] = v81; + values[12] = v79; + values[13] = v77; + values[14] = v75; + values[15] = v73; + } + + /// + /// Computes independent 32-point inverse transforms without intermediate memory passes. + /// + /// Exactly 32 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Inverse32(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector256 input0 = values[0]; + Vector256 input1 = values[1]; + Vector256 input2 = values[2]; + Vector256 input3 = values[3]; + Vector256 input4 = values[4]; + Vector256 input5 = values[5]; + Vector256 input6 = values[6]; + Vector256 input7 = values[7]; + Vector256 input8 = values[8]; + Vector256 input9 = values[9]; + Vector256 input10 = values[10]; + Vector256 input11 = values[11]; + Vector256 input12 = values[12]; + Vector256 input13 = values[13]; + Vector256 input14 = values[14]; + Vector256 input15 = values[15]; + Vector256 input16 = values[16]; + Vector256 input17 = values[17]; + Vector256 input18 = values[18]; + Vector256 input19 = values[19]; + Vector256 input20 = values[20]; + Vector256 input21 = values[21]; + Vector256 input22 = values[22]; + Vector256 input23 = values[23]; + Vector256 input24 = values[24]; + Vector256 input25 = values[25]; + Vector256 input26 = values[26]; + Vector256 input27 = values[27]; + Vector256 input28 = values[28]; + Vector256 input29 = values[29]; + Vector256 input30 = values[30]; + Vector256 input31 = values[31]; + + Vector256 v0 = input0 + input16; + Vector256 v1 = input0 - input16; + Vector256 v2 = input24 + input8; + Vector256 v3 = input8 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v4 = v3 + v2; + Vector256 v5 = v3 - v2; + Vector256 v6 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v4, v0); + Vector256 v7 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v4, v0); + Vector256 v8 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v5, v1); + Vector256 v9 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v5, v1); + Vector256 v10 = input28 + input20; + Vector256 v11 = input20 + input12; + Vector256 v12 = input12 + input4; + Vector256 v13 = input4 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v14 = v13 + v11; + Vector256 v15 = v13 - v11; + Vector256 v16 = v10 + v12; + Vector256 v17 = v12 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v18 = v17 + v16; + Vector256 v19 = v17 - v16; + Vector256 v20 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v18, v14); + Vector256 v21 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v18, v14); + Vector256 v22 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v19, v15); + Vector256 v23 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v19, v15); + Vector256 v24 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5097955791041592f), v20, v6); + Vector256 v25 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5097955791041592f), v20, v6); + Vector256 v26 = Vector256.MultiplyAddEstimate(Vector256.Create(0.6013448869350453f), v22, v8); + Vector256 v27 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.6013448869350453f), v22, v8); + Vector256 v28 = Vector256.MultiplyAddEstimate(Vector256.Create(0.8999762231364156f), v23, v9); + Vector256 v29 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.8999762231364156f), v23, v9); + Vector256 v30 = Vector256.MultiplyAddEstimate(Vector256.Create(2.5629154477415055f), v21, v7); + Vector256 v31 = Vector256.MultiplyAddEstimate(-Vector256.Create(2.5629154477415055f), v21, v7); + Vector256 v32 = input30 + input26; + Vector256 v33 = input26 + input22; + Vector256 v34 = input22 + input18; + Vector256 v35 = input18 + input14; + Vector256 v36 = input14 + input10; + Vector256 v37 = input10 + input6; + Vector256 v38 = input6 + input2; + Vector256 v39 = input2 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v40 = v39 + v35; + Vector256 v41 = v39 - v35; + Vector256 v42 = v33 + v37; + Vector256 v43 = v37 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v44 = v43 + v42; + Vector256 v45 = v43 - v42; + Vector256 v46 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v44, v40); + Vector256 v47 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v44, v40); + Vector256 v48 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v45, v41); + Vector256 v49 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v45, v41); + Vector256 v50 = v32 + v34; + Vector256 v51 = v34 + v36; + Vector256 v52 = v36 + v38; + Vector256 v53 = v38 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v54 = v53 + v51; + Vector256 v55 = v53 - v51; + Vector256 v56 = v50 + v52; + Vector256 v57 = v52 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v58 = v57 + v56; + Vector256 v59 = v57 - v56; + Vector256 v60 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v58, v54); + Vector256 v61 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v58, v54); + Vector256 v62 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v59, v55); + Vector256 v63 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v59, v55); + Vector256 v64 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5097955791041592f), v60, v46); + Vector256 v65 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5097955791041592f), v60, v46); + Vector256 v66 = Vector256.MultiplyAddEstimate(Vector256.Create(0.6013448869350453f), v62, v48); + Vector256 v67 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.6013448869350453f), v62, v48); + Vector256 v68 = Vector256.MultiplyAddEstimate(Vector256.Create(0.8999762231364156f), v63, v49); + Vector256 v69 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.8999762231364156f), v63, v49); + Vector256 v70 = Vector256.MultiplyAddEstimate(Vector256.Create(2.5629154477415055f), v61, v47); + Vector256 v71 = Vector256.MultiplyAddEstimate(-Vector256.Create(2.5629154477415055f), v61, v47); + Vector256 v72 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5024192861881557f), v64, v24); + Vector256 v73 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5024192861881557f), v64, v24); + Vector256 v74 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5224986149396889f), v66, v26); + Vector256 v75 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5224986149396889f), v66, v26); + Vector256 v76 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5669440348163577f), v68, v28); + Vector256 v77 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5669440348163577f), v68, v28); + Vector256 v78 = Vector256.MultiplyAddEstimate(Vector256.Create(0.6468217833599901f), v70, v30); + Vector256 v79 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.6468217833599901f), v70, v30); + Vector256 v80 = Vector256.MultiplyAddEstimate(Vector256.Create(0.7881546234512502f), v71, v31); + Vector256 v81 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.7881546234512502f), v71, v31); + Vector256 v82 = Vector256.MultiplyAddEstimate(Vector256.Create(1.060677685990347f), v69, v29); + Vector256 v83 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.060677685990347f), v69, v29); + Vector256 v84 = Vector256.MultiplyAddEstimate(Vector256.Create(1.7224470982383342f), v67, v27); + Vector256 v85 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.7224470982383342f), v67, v27); + Vector256 v86 = Vector256.MultiplyAddEstimate(Vector256.Create(5.101148618689155f), v65, v25); + Vector256 v87 = Vector256.MultiplyAddEstimate(-Vector256.Create(5.101148618689155f), v65, v25); + Vector256 v88 = input31 + input29; + Vector256 v89 = input29 + input27; + Vector256 v90 = input27 + input25; + Vector256 v91 = input25 + input23; + Vector256 v92 = input23 + input21; + Vector256 v93 = input21 + input19; + Vector256 v94 = input19 + input17; + Vector256 v95 = input17 + input15; + Vector256 v96 = input15 + input13; + Vector256 v97 = input13 + input11; + Vector256 v98 = input11 + input9; + Vector256 v99 = input9 + input7; + Vector256 v100 = input7 + input5; + Vector256 v101 = input5 + input3; + Vector256 v102 = input3 + input1; + Vector256 v103 = input1 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v104 = v103 + v95; + Vector256 v105 = v103 - v95; + Vector256 v106 = v91 + v99; + Vector256 v107 = v99 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v108 = v107 + v106; + Vector256 v109 = v107 - v106; + Vector256 v110 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v108, v104); + Vector256 v111 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v108, v104); + Vector256 v112 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v109, v105); + Vector256 v113 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v109, v105); + Vector256 v114 = v89 + v93; + Vector256 v115 = v93 + v97; + Vector256 v116 = v97 + v101; + Vector256 v117 = v101 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v118 = v117 + v115; + Vector256 v119 = v117 - v115; + Vector256 v120 = v114 + v116; + Vector256 v121 = v116 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v122 = v121 + v120; + Vector256 v123 = v121 - v120; + Vector256 v124 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v122, v118); + Vector256 v125 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v122, v118); + Vector256 v126 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v123, v119); + Vector256 v127 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v123, v119); + Vector256 v128 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5097955791041592f), v124, v110); + Vector256 v129 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5097955791041592f), v124, v110); + Vector256 v130 = Vector256.MultiplyAddEstimate(Vector256.Create(0.6013448869350453f), v126, v112); + Vector256 v131 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.6013448869350453f), v126, v112); + Vector256 v132 = Vector256.MultiplyAddEstimate(Vector256.Create(0.8999762231364156f), v127, v113); + Vector256 v133 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.8999762231364156f), v127, v113); + Vector256 v134 = Vector256.MultiplyAddEstimate(Vector256.Create(2.5629154477415055f), v125, v111); + Vector256 v135 = Vector256.MultiplyAddEstimate(-Vector256.Create(2.5629154477415055f), v125, v111); + Vector256 v136 = v88 + v90; + Vector256 v137 = v90 + v92; + Vector256 v138 = v92 + v94; + Vector256 v139 = v94 + v96; + Vector256 v140 = v96 + v98; + Vector256 v141 = v98 + v100; + Vector256 v142 = v100 + v102; + Vector256 v143 = v102 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v144 = v143 + v139; + Vector256 v145 = v143 - v139; + Vector256 v146 = v137 + v141; + Vector256 v147 = v141 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v148 = v147 + v146; + Vector256 v149 = v147 - v146; + Vector256 v150 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v148, v144); + Vector256 v151 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v148, v144); + Vector256 v152 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v149, v145); + Vector256 v153 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v149, v145); + Vector256 v154 = v136 + v138; + Vector256 v155 = v138 + v140; + Vector256 v156 = v140 + v142; + Vector256 v157 = v142 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v158 = v157 + v155; + Vector256 v159 = v157 - v155; + Vector256 v160 = v154 + v156; + Vector256 v161 = v156 * Vector256.Create(JxlDctScales.Sqrt2); + Vector256 v162 = v161 + v160; + Vector256 v163 = v161 - v160; + Vector256 v164 = Vector256.MultiplyAddEstimate(Vector256.Create(0.541196100146197f), v162, v158); + Vector256 v165 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.541196100146197f), v162, v158); + Vector256 v166 = Vector256.MultiplyAddEstimate(Vector256.Create(1.3065629648763764f), v163, v159); + Vector256 v167 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.3065629648763764f), v163, v159); + Vector256 v168 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5097955791041592f), v164, v150); + Vector256 v169 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5097955791041592f), v164, v150); + Vector256 v170 = Vector256.MultiplyAddEstimate(Vector256.Create(0.6013448869350453f), v166, v152); + Vector256 v171 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.6013448869350453f), v166, v152); + Vector256 v172 = Vector256.MultiplyAddEstimate(Vector256.Create(0.8999762231364156f), v167, v153); + Vector256 v173 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.8999762231364156f), v167, v153); + Vector256 v174 = Vector256.MultiplyAddEstimate(Vector256.Create(2.5629154477415055f), v165, v151); + Vector256 v175 = Vector256.MultiplyAddEstimate(-Vector256.Create(2.5629154477415055f), v165, v151); + Vector256 v176 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5024192861881557f), v168, v128); + Vector256 v177 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5024192861881557f), v168, v128); + Vector256 v178 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5224986149396889f), v170, v130); + Vector256 v179 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5224986149396889f), v170, v130); + Vector256 v180 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5669440348163577f), v172, v132); + Vector256 v181 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5669440348163577f), v172, v132); + Vector256 v182 = Vector256.MultiplyAddEstimate(Vector256.Create(0.6468217833599901f), v174, v134); + Vector256 v183 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.6468217833599901f), v174, v134); + Vector256 v184 = Vector256.MultiplyAddEstimate(Vector256.Create(0.7881546234512502f), v175, v135); + Vector256 v185 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.7881546234512502f), v175, v135); + Vector256 v186 = Vector256.MultiplyAddEstimate(Vector256.Create(1.060677685990347f), v173, v133); + Vector256 v187 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.060677685990347f), v173, v133); + Vector256 v188 = Vector256.MultiplyAddEstimate(Vector256.Create(1.7224470982383342f), v171, v131); + Vector256 v189 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.7224470982383342f), v171, v131); + Vector256 v190 = Vector256.MultiplyAddEstimate(Vector256.Create(5.101148618689155f), v169, v129); + Vector256 v191 = Vector256.MultiplyAddEstimate(-Vector256.Create(5.101148618689155f), v169, v129); + Vector256 v192 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5006029982351963f), v176, v72); + Vector256 v193 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5006029982351963f), v176, v72); + Vector256 v194 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5054709598975436f), v178, v74); + Vector256 v195 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5054709598975436f), v178, v74); + Vector256 v196 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5154473099226246f), v180, v76); + Vector256 v197 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5154473099226246f), v180, v76); + Vector256 v198 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5310425910897841f), v182, v78); + Vector256 v199 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5310425910897841f), v182, v78); + Vector256 v200 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5531038960344445f), v184, v80); + Vector256 v201 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5531038960344445f), v184, v80); + Vector256 v202 = Vector256.MultiplyAddEstimate(Vector256.Create(0.5829349682061339f), v186, v82); + Vector256 v203 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.5829349682061339f), v186, v82); + Vector256 v204 = Vector256.MultiplyAddEstimate(Vector256.Create(0.6225041230356648f), v188, v84); + Vector256 v205 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.6225041230356648f), v188, v84); + Vector256 v206 = Vector256.MultiplyAddEstimate(Vector256.Create(0.6748083414550057f), v190, v86); + Vector256 v207 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.6748083414550057f), v190, v86); + Vector256 v208 = Vector256.MultiplyAddEstimate(Vector256.Create(0.7445362710022986f), v191, v87); + Vector256 v209 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.7445362710022986f), v191, v87); + Vector256 v210 = Vector256.MultiplyAddEstimate(Vector256.Create(0.8393496454155268f), v189, v85); + Vector256 v211 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.8393496454155268f), v189, v85); + Vector256 v212 = Vector256.MultiplyAddEstimate(Vector256.Create(0.9725682378619608f), v187, v83); + Vector256 v213 = Vector256.MultiplyAddEstimate(-Vector256.Create(0.9725682378619608f), v187, v83); + Vector256 v214 = Vector256.MultiplyAddEstimate(Vector256.Create(1.1694399334328847f), v185, v81); + Vector256 v215 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.1694399334328847f), v185, v81); + Vector256 v216 = Vector256.MultiplyAddEstimate(Vector256.Create(1.4841646163141662f), v183, v79); + Vector256 v217 = Vector256.MultiplyAddEstimate(-Vector256.Create(1.4841646163141662f), v183, v79); + Vector256 v218 = Vector256.MultiplyAddEstimate(Vector256.Create(2.057781009953411f), v181, v77); + Vector256 v219 = Vector256.MultiplyAddEstimate(-Vector256.Create(2.057781009953411f), v181, v77); + Vector256 v220 = Vector256.MultiplyAddEstimate(Vector256.Create(3.407608418468719f), v179, v75); + Vector256 v221 = Vector256.MultiplyAddEstimate(-Vector256.Create(3.407608418468719f), v179, v75); + Vector256 v222 = Vector256.MultiplyAddEstimate(Vector256.Create(10.190008123548033f), v177, v73); + Vector256 v223 = Vector256.MultiplyAddEstimate(-Vector256.Create(10.190008123548033f), v177, v73); + + values[0] = v192; + values[1] = v194; + values[2] = v196; + values[3] = v198; + values[4] = v200; + values[5] = v202; + values[6] = v204; + values[7] = v206; + values[8] = v208; + values[9] = v210; + values[10] = v212; + values[11] = v214; + values[12] = v216; + values[13] = v218; + values[14] = v220; + values[15] = v222; + values[16] = v223; + values[17] = v221; + values[18] = v219; + values[19] = v217; + values[20] = v215; + values[21] = v213; + values[22] = v211; + values[23] = v209; + values[24] = v207; + values[25] = v205; + values[26] = v203; + values[27] = v201; + values[28] = v199; + values[29] = v197; + values[30] = v195; + values[31] = v193; + } + + /// + /// Processes complete 8-lane inverse batches along input columns. + /// + /// The number of positions in each transform. + /// The number of independent transforms. + /// The borrowed source matrix. + /// The source row stride, in floats. + /// The borrowed output matrix. + /// The destination row stride, in floats. + /// The first independent transform not already processed by a wider batch. + /// Borrowed storage for two length-by-8 banks. Only the first bank needs initialized inputs. + /// The first independent transform left after the complete batches. + private static int InverseColumns256( + int length, + int count, + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int first, + Span workspace) + { + Span> values = MemoryMarshal.Cast>(workspace[..(length * 8)]); + Span> scratch = MemoryMarshal.Cast>(workspace.Slice(length * 8, length * 8)); + ref float input = ref MemoryMarshal.GetReference(source); + ref float output = ref MemoryMarshal.GetReference(destination); + + for (; first <= count - 8; first += 8) + { + // One vector covers adjacent columns at the same transform position. Physical strides never enter the coefficient-bank layout. + for (int i = 0; i < length; i++) + { + values[i] = Vector256.LoadUnsafe(ref input, (nuint)((i * sourceStride) + first)); + } + + Inverse(values, scratch); + for (int i = 0; i < length; i++) + { + values[i].StoreUnsafe(ref output, (nuint)((i * destinationStride) + first)); + } + } + + return first; + } + + /// + /// Processes complete 8-lane inverse batches along input rows. + /// + /// The number of positions in each transform. + /// The number of independent transforms. + /// The borrowed source matrix. + /// The source row stride, in floats. + /// The borrowed output matrix. + /// The destination row stride, in floats. + /// Whether transform positions become output rows and input rows become output columns. + /// The first independent transform not already processed by a wider batch. + /// Borrowed storage for two length-by-8 banks. Only the first bank needs initialized inputs. + /// The first independent transform left after the complete batches. + private static int InverseRows256( + int length, + int count, + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + bool transposed, + int first, + Span workspace) + { + Span> values = MemoryMarshal.Cast>(workspace[..(length * 8)]); + Span> scratch = MemoryMarshal.Cast>(workspace.Slice(length * 8, length * 8)); + ref float input = ref MemoryMarshal.GetReference(source); + ref float output = ref MemoryMarshal.GetReference(destination); + + for (; first <= count - 8; first += 8) + { + // Each square load contains adjacent samples from independent rows. Transposition puts those rows into SIMD lanes. + // The complete row band is loaded before any same-orientation output is written, so source and destination can be the same matrix. + for (int column = 0; column < length; column += 8) + { + Vector256 row0 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 0) * sourceStride) + column)).AsInt32(); + Vector256 row1 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 1) * sourceStride) + column)).AsInt32(); + Vector256 row2 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 2) * sourceStride) + column)).AsInt32(); + Vector256 row3 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 3) * sourceStride) + column)).AsInt32(); + Vector256 row4 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 4) * sourceStride) + column)).AsInt32(); + Vector256 row5 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 5) * sourceStride) + column)).AsInt32(); + Vector256 row6 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 6) * sourceStride) + column)).AsInt32(); + Vector256 row7 = Vector256.LoadUnsafe(ref input, (nuint)(((first + 7) * sourceStride) + column)).AsInt32(); + + SimdUtils.Transpose(ref row0, ref row1, ref row2, ref row3, ref row4, ref row5, ref row6, ref row7); + values[column + 0] = row0.AsSingle(); + values[column + 1] = row1.AsSingle(); + values[column + 2] = row2.AsSingle(); + values[column + 3] = row3.AsSingle(); + values[column + 4] = row4.AsSingle(); + values[column + 5] = row5.AsSingle(); + values[column + 6] = row6.AsSingle(); + values[column + 7] = row7.AsSingle(); + } + + Inverse(values, scratch); + if (transposed) + { + // A complete vector is one contiguous output row. Only logical samples are stored; destination padding remains untouched. + for (int i = 0; i < length; i++) + { + values[i].StoreUnsafe(ref output, (nuint)((i * destinationStride) + first)); + } + } + else + { + // Restore row-major order one register tile at a time. This replaces a separate full-block transpose and copy. + for (int column = 0; column < length; column += 8) + { + Vector256 row0 = values[column + 0].AsInt32(); + Vector256 row1 = values[column + 1].AsInt32(); + Vector256 row2 = values[column + 2].AsInt32(); + Vector256 row3 = values[column + 3].AsInt32(); + Vector256 row4 = values[column + 4].AsInt32(); + Vector256 row5 = values[column + 5].AsInt32(); + Vector256 row6 = values[column + 6].AsInt32(); + Vector256 row7 = values[column + 7].AsInt32(); + + SimdUtils.Transpose(ref row0, ref row1, ref row2, ref row3, ref row4, ref row5, ref row6, ref row7); + row0.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 0) * destinationStride) + column)); + row1.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 1) * destinationStride) + column)); + row2.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 2) * destinationStride) + column)); + row3.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 3) * destinationStride) + column)); + row4.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 4) * destinationStride) + column)); + row5.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 5) * destinationStride) + column)); + row6.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 6) * destinationStride) + column)); + row7.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 7) * destinationStride) + column)); + } + } + } + + return first; + } + + /// + /// Computes independent forward transforms using two nonoverlapping borrowed vector banks. + /// + /// One vector per transform position. Its length is a power of two from one through 256. + /// At least values.Length vectors. Previous contents are not read. + private static void Forward(Span> values, Span> scratch) + { + // Small transforms stay in registers. The one-point transform is an identity and must not copy any trailing samples. + switch (values.Length) + { + case 1: + return; + case 2: + Forward2(values); + return; + case 4: + Forward4(values); + return; + case 8: + Forward8(values); + return; + case 16: + Forward16(values); + return; + case 32: + Forward32(values); + return; + } + + int half = values.Length / 2; + ReadOnlySpan weights = JxlDctScales.GetMultipliers(values.Length); + + // Consume both original halves before reusing their bank as recursive scratch. Neither recursive call can then destroy unread input. + for (int i = 0; i < half; i++) + { + Vector512 first = values[i]; + Vector512 last = values[values.Length - i - 1]; + scratch[i] = first + last; + scratch[half + i] = (first - last) * Vector512.Create(weights[i]); + } + + Forward(scratch[..half], values); + Forward(scratch.Slice(half, half), values); + scratch[half] = Vector512.MultiplyAddEstimate(scratch[half], Vector512.Create(JxlDctScales.Sqrt2), scratch[half + 1]); + + for (int i = 1; i + 1 < half; i++) + { + scratch[half + i] += scratch[half + i + 1]; + } + + for (int i = 0; i < half; i++) + { + values[2 * i] = scratch[i]; + values[(2 * i) + 1] = scratch[half + i]; + } + } + + /// + /// Computes independent 2-point forward transforms without intermediate memory passes. + /// + /// Exactly 2 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Forward2(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector512 input0 = values[0]; + Vector512 input1 = values[1]; + + Vector512 v0 = input0 + input1; + Vector512 v1 = input0 - input1; + + values[0] = v0; + values[1] = v1; + } + + /// + /// Computes independent 4-point forward transforms without intermediate memory passes. + /// + /// Exactly 4 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Forward4(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector512 input0 = values[0]; + Vector512 input1 = values[1]; + Vector512 input2 = values[2]; + Vector512 input3 = values[3]; + + Vector512 v0 = input0 + input3; + Vector512 v1 = (input0 - input3) * Vector512.Create(0.541196100146197f); + Vector512 v2 = input1 + input2; + Vector512 v3 = (input1 - input2) * Vector512.Create(1.3065629648763764f); + Vector512 v4 = v0 + v2; + Vector512 v5 = v0 - v2; + Vector512 v6 = v1 + v3; + Vector512 v7 = v1 - v3; + Vector512 v8 = Vector512.MultiplyAddEstimate(v6, Vector512.Create(JxlDctScales.Sqrt2), v7); + + values[0] = v4; + values[1] = v8; + values[2] = v5; + values[3] = v7; + } + + /// + /// Computes independent 8-point forward transforms without intermediate memory passes. + /// + /// Exactly 8 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Forward8(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector512 input0 = values[0]; + Vector512 input1 = values[1]; + Vector512 input2 = values[2]; + Vector512 input3 = values[3]; + Vector512 input4 = values[4]; + Vector512 input5 = values[5]; + Vector512 input6 = values[6]; + Vector512 input7 = values[7]; + + Vector512 v0 = input0 + input7; + Vector512 v1 = (input0 - input7) * Vector512.Create(0.5097955791041592f); + Vector512 v2 = input1 + input6; + Vector512 v3 = (input1 - input6) * Vector512.Create(0.6013448869350453f); + Vector512 v4 = input2 + input5; + Vector512 v5 = (input2 - input5) * Vector512.Create(0.8999762231364156f); + Vector512 v6 = input3 + input4; + Vector512 v7 = (input3 - input4) * Vector512.Create(2.5629154477415055f); + Vector512 v8 = v0 + v6; + Vector512 v9 = (v0 - v6) * Vector512.Create(0.541196100146197f); + Vector512 v10 = v2 + v4; + Vector512 v11 = (v2 - v4) * Vector512.Create(1.3065629648763764f); + Vector512 v12 = v8 + v10; + Vector512 v13 = v8 - v10; + Vector512 v14 = v9 + v11; + Vector512 v15 = v9 - v11; + Vector512 v16 = Vector512.MultiplyAddEstimate(v14, Vector512.Create(JxlDctScales.Sqrt2), v15); + Vector512 v17 = v1 + v7; + Vector512 v18 = (v1 - v7) * Vector512.Create(0.541196100146197f); + Vector512 v19 = v3 + v5; + Vector512 v20 = (v3 - v5) * Vector512.Create(1.3065629648763764f); + Vector512 v21 = v17 + v19; + Vector512 v22 = v17 - v19; + Vector512 v23 = v18 + v20; + Vector512 v24 = v18 - v20; + Vector512 v25 = Vector512.MultiplyAddEstimate(v23, Vector512.Create(JxlDctScales.Sqrt2), v24); + Vector512 v26 = Vector512.MultiplyAddEstimate(v21, Vector512.Create(JxlDctScales.Sqrt2), v25); + Vector512 v27 = v25 + v22; + Vector512 v28 = v22 + v24; + + values[0] = v12; + values[1] = v26; + values[2] = v16; + values[3] = v27; + values[4] = v13; + values[5] = v28; + values[6] = v15; + values[7] = v24; + } + + /// + /// Computes independent 16-point forward transforms without intermediate memory passes. + /// + /// Exactly 16 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Forward16(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector512 input0 = values[0]; + Vector512 input1 = values[1]; + Vector512 input2 = values[2]; + Vector512 input3 = values[3]; + Vector512 input4 = values[4]; + Vector512 input5 = values[5]; + Vector512 input6 = values[6]; + Vector512 input7 = values[7]; + Vector512 input8 = values[8]; + Vector512 input9 = values[9]; + Vector512 input10 = values[10]; + Vector512 input11 = values[11]; + Vector512 input12 = values[12]; + Vector512 input13 = values[13]; + Vector512 input14 = values[14]; + Vector512 input15 = values[15]; + + Vector512 v0 = input0 + input15; + Vector512 v1 = (input0 - input15) * Vector512.Create(0.5024192861881557f); + Vector512 v2 = input1 + input14; + Vector512 v3 = (input1 - input14) * Vector512.Create(0.5224986149396889f); + Vector512 v4 = input2 + input13; + Vector512 v5 = (input2 - input13) * Vector512.Create(0.5669440348163577f); + Vector512 v6 = input3 + input12; + Vector512 v7 = (input3 - input12) * Vector512.Create(0.6468217833599901f); + Vector512 v8 = input4 + input11; + Vector512 v9 = (input4 - input11) * Vector512.Create(0.7881546234512502f); + Vector512 v10 = input5 + input10; + Vector512 v11 = (input5 - input10) * Vector512.Create(1.060677685990347f); + Vector512 v12 = input6 + input9; + Vector512 v13 = (input6 - input9) * Vector512.Create(1.7224470982383342f); + Vector512 v14 = input7 + input8; + Vector512 v15 = (input7 - input8) * Vector512.Create(5.101148618689155f); + Vector512 v16 = v0 + v14; + Vector512 v17 = (v0 - v14) * Vector512.Create(0.5097955791041592f); + Vector512 v18 = v2 + v12; + Vector512 v19 = (v2 - v12) * Vector512.Create(0.6013448869350453f); + Vector512 v20 = v4 + v10; + Vector512 v21 = (v4 - v10) * Vector512.Create(0.8999762231364156f); + Vector512 v22 = v6 + v8; + Vector512 v23 = (v6 - v8) * Vector512.Create(2.5629154477415055f); + Vector512 v24 = v16 + v22; + Vector512 v25 = (v16 - v22) * Vector512.Create(0.541196100146197f); + Vector512 v26 = v18 + v20; + Vector512 v27 = (v18 - v20) * Vector512.Create(1.3065629648763764f); + Vector512 v28 = v24 + v26; + Vector512 v29 = v24 - v26; + Vector512 v30 = v25 + v27; + Vector512 v31 = v25 - v27; + Vector512 v32 = Vector512.MultiplyAddEstimate(v30, Vector512.Create(JxlDctScales.Sqrt2), v31); + Vector512 v33 = v17 + v23; + Vector512 v34 = (v17 - v23) * Vector512.Create(0.541196100146197f); + Vector512 v35 = v19 + v21; + Vector512 v36 = (v19 - v21) * Vector512.Create(1.3065629648763764f); + Vector512 v37 = v33 + v35; + Vector512 v38 = v33 - v35; + Vector512 v39 = v34 + v36; + Vector512 v40 = v34 - v36; + Vector512 v41 = Vector512.MultiplyAddEstimate(v39, Vector512.Create(JxlDctScales.Sqrt2), v40); + Vector512 v42 = Vector512.MultiplyAddEstimate(v37, Vector512.Create(JxlDctScales.Sqrt2), v41); + Vector512 v43 = v41 + v38; + Vector512 v44 = v38 + v40; + Vector512 v45 = v1 + v15; + Vector512 v46 = (v1 - v15) * Vector512.Create(0.5097955791041592f); + Vector512 v47 = v3 + v13; + Vector512 v48 = (v3 - v13) * Vector512.Create(0.6013448869350453f); + Vector512 v49 = v5 + v11; + Vector512 v50 = (v5 - v11) * Vector512.Create(0.8999762231364156f); + Vector512 v51 = v7 + v9; + Vector512 v52 = (v7 - v9) * Vector512.Create(2.5629154477415055f); + Vector512 v53 = v45 + v51; + Vector512 v54 = (v45 - v51) * Vector512.Create(0.541196100146197f); + Vector512 v55 = v47 + v49; + Vector512 v56 = (v47 - v49) * Vector512.Create(1.3065629648763764f); + Vector512 v57 = v53 + v55; + Vector512 v58 = v53 - v55; + Vector512 v59 = v54 + v56; + Vector512 v60 = v54 - v56; + Vector512 v61 = Vector512.MultiplyAddEstimate(v59, Vector512.Create(JxlDctScales.Sqrt2), v60); + Vector512 v62 = v46 + v52; + Vector512 v63 = (v46 - v52) * Vector512.Create(0.541196100146197f); + Vector512 v64 = v48 + v50; + Vector512 v65 = (v48 - v50) * Vector512.Create(1.3065629648763764f); + Vector512 v66 = v62 + v64; + Vector512 v67 = v62 - v64; + Vector512 v68 = v63 + v65; + Vector512 v69 = v63 - v65; + Vector512 v70 = Vector512.MultiplyAddEstimate(v68, Vector512.Create(JxlDctScales.Sqrt2), v69); + Vector512 v71 = Vector512.MultiplyAddEstimate(v66, Vector512.Create(JxlDctScales.Sqrt2), v70); + Vector512 v72 = v70 + v67; + Vector512 v73 = v67 + v69; + Vector512 v74 = Vector512.MultiplyAddEstimate(v57, Vector512.Create(JxlDctScales.Sqrt2), v71); + Vector512 v75 = v71 + v61; + Vector512 v76 = v61 + v72; + Vector512 v77 = v72 + v58; + Vector512 v78 = v58 + v73; + Vector512 v79 = v73 + v60; + Vector512 v80 = v60 + v69; + + values[0] = v28; + values[1] = v74; + values[2] = v42; + values[3] = v75; + values[4] = v32; + values[5] = v76; + values[6] = v43; + values[7] = v77; + values[8] = v29; + values[9] = v78; + values[10] = v44; + values[11] = v79; + values[12] = v31; + values[13] = v80; + values[14] = v40; + values[15] = v69; + } + + /// + /// Computes independent 32-point forward transforms without intermediate memory passes. + /// + /// Exactly 32 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Forward32(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector512 input0 = values[0]; + Vector512 input1 = values[1]; + Vector512 input2 = values[2]; + Vector512 input3 = values[3]; + Vector512 input4 = values[4]; + Vector512 input5 = values[5]; + Vector512 input6 = values[6]; + Vector512 input7 = values[7]; + Vector512 input8 = values[8]; + Vector512 input9 = values[9]; + Vector512 input10 = values[10]; + Vector512 input11 = values[11]; + Vector512 input12 = values[12]; + Vector512 input13 = values[13]; + Vector512 input14 = values[14]; + Vector512 input15 = values[15]; + Vector512 input16 = values[16]; + Vector512 input17 = values[17]; + Vector512 input18 = values[18]; + Vector512 input19 = values[19]; + Vector512 input20 = values[20]; + Vector512 input21 = values[21]; + Vector512 input22 = values[22]; + Vector512 input23 = values[23]; + Vector512 input24 = values[24]; + Vector512 input25 = values[25]; + Vector512 input26 = values[26]; + Vector512 input27 = values[27]; + Vector512 input28 = values[28]; + Vector512 input29 = values[29]; + Vector512 input30 = values[30]; + Vector512 input31 = values[31]; + + Vector512 v0 = input0 + input31; + Vector512 v1 = (input0 - input31) * Vector512.Create(0.5006029982351963f); + Vector512 v2 = input1 + input30; + Vector512 v3 = (input1 - input30) * Vector512.Create(0.5054709598975436f); + Vector512 v4 = input2 + input29; + Vector512 v5 = (input2 - input29) * Vector512.Create(0.5154473099226246f); + Vector512 v6 = input3 + input28; + Vector512 v7 = (input3 - input28) * Vector512.Create(0.5310425910897841f); + Vector512 v8 = input4 + input27; + Vector512 v9 = (input4 - input27) * Vector512.Create(0.5531038960344445f); + Vector512 v10 = input5 + input26; + Vector512 v11 = (input5 - input26) * Vector512.Create(0.5829349682061339f); + Vector512 v12 = input6 + input25; + Vector512 v13 = (input6 - input25) * Vector512.Create(0.6225041230356648f); + Vector512 v14 = input7 + input24; + Vector512 v15 = (input7 - input24) * Vector512.Create(0.6748083414550057f); + Vector512 v16 = input8 + input23; + Vector512 v17 = (input8 - input23) * Vector512.Create(0.7445362710022986f); + Vector512 v18 = input9 + input22; + Vector512 v19 = (input9 - input22) * Vector512.Create(0.8393496454155268f); + Vector512 v20 = input10 + input21; + Vector512 v21 = (input10 - input21) * Vector512.Create(0.9725682378619608f); + Vector512 v22 = input11 + input20; + Vector512 v23 = (input11 - input20) * Vector512.Create(1.1694399334328847f); + Vector512 v24 = input12 + input19; + Vector512 v25 = (input12 - input19) * Vector512.Create(1.4841646163141662f); + Vector512 v26 = input13 + input18; + Vector512 v27 = (input13 - input18) * Vector512.Create(2.057781009953411f); + Vector512 v28 = input14 + input17; + Vector512 v29 = (input14 - input17) * Vector512.Create(3.407608418468719f); + Vector512 v30 = input15 + input16; + Vector512 v31 = (input15 - input16) * Vector512.Create(10.190008123548033f); + Vector512 v32 = v0 + v30; + Vector512 v33 = (v0 - v30) * Vector512.Create(0.5024192861881557f); + Vector512 v34 = v2 + v28; + Vector512 v35 = (v2 - v28) * Vector512.Create(0.5224986149396889f); + Vector512 v36 = v4 + v26; + Vector512 v37 = (v4 - v26) * Vector512.Create(0.5669440348163577f); + Vector512 v38 = v6 + v24; + Vector512 v39 = (v6 - v24) * Vector512.Create(0.6468217833599901f); + Vector512 v40 = v8 + v22; + Vector512 v41 = (v8 - v22) * Vector512.Create(0.7881546234512502f); + Vector512 v42 = v10 + v20; + Vector512 v43 = (v10 - v20) * Vector512.Create(1.060677685990347f); + Vector512 v44 = v12 + v18; + Vector512 v45 = (v12 - v18) * Vector512.Create(1.7224470982383342f); + Vector512 v46 = v14 + v16; + Vector512 v47 = (v14 - v16) * Vector512.Create(5.101148618689155f); + Vector512 v48 = v32 + v46; + Vector512 v49 = (v32 - v46) * Vector512.Create(0.5097955791041592f); + Vector512 v50 = v34 + v44; + Vector512 v51 = (v34 - v44) * Vector512.Create(0.6013448869350453f); + Vector512 v52 = v36 + v42; + Vector512 v53 = (v36 - v42) * Vector512.Create(0.8999762231364156f); + Vector512 v54 = v38 + v40; + Vector512 v55 = (v38 - v40) * Vector512.Create(2.5629154477415055f); + Vector512 v56 = v48 + v54; + Vector512 v57 = (v48 - v54) * Vector512.Create(0.541196100146197f); + Vector512 v58 = v50 + v52; + Vector512 v59 = (v50 - v52) * Vector512.Create(1.3065629648763764f); + Vector512 v60 = v56 + v58; + Vector512 v61 = v56 - v58; + Vector512 v62 = v57 + v59; + Vector512 v63 = v57 - v59; + Vector512 v64 = Vector512.MultiplyAddEstimate(v62, Vector512.Create(JxlDctScales.Sqrt2), v63); + Vector512 v65 = v49 + v55; + Vector512 v66 = (v49 - v55) * Vector512.Create(0.541196100146197f); + Vector512 v67 = v51 + v53; + Vector512 v68 = (v51 - v53) * Vector512.Create(1.3065629648763764f); + Vector512 v69 = v65 + v67; + Vector512 v70 = v65 - v67; + Vector512 v71 = v66 + v68; + Vector512 v72 = v66 - v68; + Vector512 v73 = Vector512.MultiplyAddEstimate(v71, Vector512.Create(JxlDctScales.Sqrt2), v72); + Vector512 v74 = Vector512.MultiplyAddEstimate(v69, Vector512.Create(JxlDctScales.Sqrt2), v73); + Vector512 v75 = v73 + v70; + Vector512 v76 = v70 + v72; + Vector512 v77 = v33 + v47; + Vector512 v78 = (v33 - v47) * Vector512.Create(0.5097955791041592f); + Vector512 v79 = v35 + v45; + Vector512 v80 = (v35 - v45) * Vector512.Create(0.6013448869350453f); + Vector512 v81 = v37 + v43; + Vector512 v82 = (v37 - v43) * Vector512.Create(0.8999762231364156f); + Vector512 v83 = v39 + v41; + Vector512 v84 = (v39 - v41) * Vector512.Create(2.5629154477415055f); + Vector512 v85 = v77 + v83; + Vector512 v86 = (v77 - v83) * Vector512.Create(0.541196100146197f); + Vector512 v87 = v79 + v81; + Vector512 v88 = (v79 - v81) * Vector512.Create(1.3065629648763764f); + Vector512 v89 = v85 + v87; + Vector512 v90 = v85 - v87; + Vector512 v91 = v86 + v88; + Vector512 v92 = v86 - v88; + Vector512 v93 = Vector512.MultiplyAddEstimate(v91, Vector512.Create(JxlDctScales.Sqrt2), v92); + Vector512 v94 = v78 + v84; + Vector512 v95 = (v78 - v84) * Vector512.Create(0.541196100146197f); + Vector512 v96 = v80 + v82; + Vector512 v97 = (v80 - v82) * Vector512.Create(1.3065629648763764f); + Vector512 v98 = v94 + v96; + Vector512 v99 = v94 - v96; + Vector512 v100 = v95 + v97; + Vector512 v101 = v95 - v97; + Vector512 v102 = Vector512.MultiplyAddEstimate(v100, Vector512.Create(JxlDctScales.Sqrt2), v101); + Vector512 v103 = Vector512.MultiplyAddEstimate(v98, Vector512.Create(JxlDctScales.Sqrt2), v102); + Vector512 v104 = v102 + v99; + Vector512 v105 = v99 + v101; + Vector512 v106 = Vector512.MultiplyAddEstimate(v89, Vector512.Create(JxlDctScales.Sqrt2), v103); + Vector512 v107 = v103 + v93; + Vector512 v108 = v93 + v104; + Vector512 v109 = v104 + v90; + Vector512 v110 = v90 + v105; + Vector512 v111 = v105 + v92; + Vector512 v112 = v92 + v101; + Vector512 v113 = v1 + v31; + Vector512 v114 = (v1 - v31) * Vector512.Create(0.5024192861881557f); + Vector512 v115 = v3 + v29; + Vector512 v116 = (v3 - v29) * Vector512.Create(0.5224986149396889f); + Vector512 v117 = v5 + v27; + Vector512 v118 = (v5 - v27) * Vector512.Create(0.5669440348163577f); + Vector512 v119 = v7 + v25; + Vector512 v120 = (v7 - v25) * Vector512.Create(0.6468217833599901f); + Vector512 v121 = v9 + v23; + Vector512 v122 = (v9 - v23) * Vector512.Create(0.7881546234512502f); + Vector512 v123 = v11 + v21; + Vector512 v124 = (v11 - v21) * Vector512.Create(1.060677685990347f); + Vector512 v125 = v13 + v19; + Vector512 v126 = (v13 - v19) * Vector512.Create(1.7224470982383342f); + Vector512 v127 = v15 + v17; + Vector512 v128 = (v15 - v17) * Vector512.Create(5.101148618689155f); + Vector512 v129 = v113 + v127; + Vector512 v130 = (v113 - v127) * Vector512.Create(0.5097955791041592f); + Vector512 v131 = v115 + v125; + Vector512 v132 = (v115 - v125) * Vector512.Create(0.6013448869350453f); + Vector512 v133 = v117 + v123; + Vector512 v134 = (v117 - v123) * Vector512.Create(0.8999762231364156f); + Vector512 v135 = v119 + v121; + Vector512 v136 = (v119 - v121) * Vector512.Create(2.5629154477415055f); + Vector512 v137 = v129 + v135; + Vector512 v138 = (v129 - v135) * Vector512.Create(0.541196100146197f); + Vector512 v139 = v131 + v133; + Vector512 v140 = (v131 - v133) * Vector512.Create(1.3065629648763764f); + Vector512 v141 = v137 + v139; + Vector512 v142 = v137 - v139; + Vector512 v143 = v138 + v140; + Vector512 v144 = v138 - v140; + Vector512 v145 = Vector512.MultiplyAddEstimate(v143, Vector512.Create(JxlDctScales.Sqrt2), v144); + Vector512 v146 = v130 + v136; + Vector512 v147 = (v130 - v136) * Vector512.Create(0.541196100146197f); + Vector512 v148 = v132 + v134; + Vector512 v149 = (v132 - v134) * Vector512.Create(1.3065629648763764f); + Vector512 v150 = v146 + v148; + Vector512 v151 = v146 - v148; + Vector512 v152 = v147 + v149; + Vector512 v153 = v147 - v149; + Vector512 v154 = Vector512.MultiplyAddEstimate(v152, Vector512.Create(JxlDctScales.Sqrt2), v153); + Vector512 v155 = Vector512.MultiplyAddEstimate(v150, Vector512.Create(JxlDctScales.Sqrt2), v154); + Vector512 v156 = v154 + v151; + Vector512 v157 = v151 + v153; + Vector512 v158 = v114 + v128; + Vector512 v159 = (v114 - v128) * Vector512.Create(0.5097955791041592f); + Vector512 v160 = v116 + v126; + Vector512 v161 = (v116 - v126) * Vector512.Create(0.6013448869350453f); + Vector512 v162 = v118 + v124; + Vector512 v163 = (v118 - v124) * Vector512.Create(0.8999762231364156f); + Vector512 v164 = v120 + v122; + Vector512 v165 = (v120 - v122) * Vector512.Create(2.5629154477415055f); + Vector512 v166 = v158 + v164; + Vector512 v167 = (v158 - v164) * Vector512.Create(0.541196100146197f); + Vector512 v168 = v160 + v162; + Vector512 v169 = (v160 - v162) * Vector512.Create(1.3065629648763764f); + Vector512 v170 = v166 + v168; + Vector512 v171 = v166 - v168; + Vector512 v172 = v167 + v169; + Vector512 v173 = v167 - v169; + Vector512 v174 = Vector512.MultiplyAddEstimate(v172, Vector512.Create(JxlDctScales.Sqrt2), v173); + Vector512 v175 = v159 + v165; + Vector512 v176 = (v159 - v165) * Vector512.Create(0.541196100146197f); + Vector512 v177 = v161 + v163; + Vector512 v178 = (v161 - v163) * Vector512.Create(1.3065629648763764f); + Vector512 v179 = v175 + v177; + Vector512 v180 = v175 - v177; + Vector512 v181 = v176 + v178; + Vector512 v182 = v176 - v178; + Vector512 v183 = Vector512.MultiplyAddEstimate(v181, Vector512.Create(JxlDctScales.Sqrt2), v182); + Vector512 v184 = Vector512.MultiplyAddEstimate(v179, Vector512.Create(JxlDctScales.Sqrt2), v183); + Vector512 v185 = v183 + v180; + Vector512 v186 = v180 + v182; + Vector512 v187 = Vector512.MultiplyAddEstimate(v170, Vector512.Create(JxlDctScales.Sqrt2), v184); + Vector512 v188 = v184 + v174; + Vector512 v189 = v174 + v185; + Vector512 v190 = v185 + v171; + Vector512 v191 = v171 + v186; + Vector512 v192 = v186 + v173; + Vector512 v193 = v173 + v182; + Vector512 v194 = Vector512.MultiplyAddEstimate(v141, Vector512.Create(JxlDctScales.Sqrt2), v187); + Vector512 v195 = v187 + v155; + Vector512 v196 = v155 + v188; + Vector512 v197 = v188 + v145; + Vector512 v198 = v145 + v189; + Vector512 v199 = v189 + v156; + Vector512 v200 = v156 + v190; + Vector512 v201 = v190 + v142; + Vector512 v202 = v142 + v191; + Vector512 v203 = v191 + v157; + Vector512 v204 = v157 + v192; + Vector512 v205 = v192 + v144; + Vector512 v206 = v144 + v193; + Vector512 v207 = v193 + v153; + Vector512 v208 = v153 + v182; + + values[0] = v60; + values[1] = v194; + values[2] = v106; + values[3] = v195; + values[4] = v74; + values[5] = v196; + values[6] = v107; + values[7] = v197; + values[8] = v64; + values[9] = v198; + values[10] = v108; + values[11] = v199; + values[12] = v75; + values[13] = v200; + values[14] = v109; + values[15] = v201; + values[16] = v61; + values[17] = v202; + values[18] = v110; + values[19] = v203; + values[20] = v76; + values[21] = v204; + values[22] = v111; + values[23] = v205; + values[24] = v63; + values[25] = v206; + values[26] = v112; + values[27] = v207; + values[28] = v72; + values[29] = v208; + values[30] = v101; + values[31] = v182; + } + + /// + /// Processes complete 16-lane forward batches along input columns. + /// + /// The number of positions in each transform. + /// The number of independent transforms. + /// The borrowed source matrix. + /// The source row stride, in floats. + /// The borrowed output matrix. + /// The destination row stride, in floats. + /// The first independent transform not already processed by a wider batch. + /// Borrowed storage for two length-by-16 banks. Only the first bank needs initialized inputs. + /// The first independent transform left after the complete batches. + private static int ForwardColumns512( + int length, + int count, + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int first, + Span workspace) + { + Span> values = MemoryMarshal.Cast>(workspace[..(length * 16)]); + Span> scratch = MemoryMarshal.Cast>(workspace.Slice(length * 16, length * 16)); + ref float input = ref MemoryMarshal.GetReference(source); + ref float output = ref MemoryMarshal.GetReference(destination); + Vector512 scale = Vector512.Create(1F / length); + + for (; first <= count - 16; first += 16) + { + // One vector covers adjacent columns at the same transform position. Physical strides never enter the coefficient-bank layout. + for (int i = 0; i < length; i++) + { + values[i] = Vector512.LoadUnsafe(ref input, (nuint)((i * sourceStride) + first)); + } + + Forward(values, scratch); + for (int i = 0; i < length; i++) + { + (values[i] * scale).StoreUnsafe(ref output, (nuint)((i * destinationStride) + first)); + } + } + + return first; + } + + /// + /// Processes complete 16-lane forward batches along input rows. + /// + /// The number of positions in each transform. + /// The number of independent transforms. + /// The borrowed source matrix. + /// The source row stride, in floats. + /// The borrowed output matrix. + /// The destination row stride, in floats. + /// Whether transform positions become output rows and input rows become output columns. + /// The first independent transform not already processed by a wider batch. + /// Borrowed storage for two length-by-16 banks. Only the first bank needs initialized inputs. + /// The first independent transform left after the complete batches. + private static int ForwardRows512( + int length, + int count, + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + bool transposed, + int first, + Span workspace) + { + Span> values = MemoryMarshal.Cast>(workspace[..(length * 16)]); + Span> scratch = MemoryMarshal.Cast>(workspace.Slice(length * 16, length * 16)); + ref float input = ref MemoryMarshal.GetReference(source); + ref float output = ref MemoryMarshal.GetReference(destination); + Vector512 scale = Vector512.Create(1F / length); + + for (; first <= count - 16; first += 16) + { + // Each square load contains adjacent samples from independent rows. Transposition puts those rows into SIMD lanes. + // The complete row band is loaded before any same-orientation output is written, so source and destination can be the same matrix. + for (int column = 0; column < length; column += 16) + { + Vector512 row0 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 0) * sourceStride) + column)).AsInt32(); + Vector512 row1 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 1) * sourceStride) + column)).AsInt32(); + Vector512 row2 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 2) * sourceStride) + column)).AsInt32(); + Vector512 row3 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 3) * sourceStride) + column)).AsInt32(); + Vector512 row4 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 4) * sourceStride) + column)).AsInt32(); + Vector512 row5 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 5) * sourceStride) + column)).AsInt32(); + Vector512 row6 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 6) * sourceStride) + column)).AsInt32(); + Vector512 row7 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 7) * sourceStride) + column)).AsInt32(); + Vector512 row8 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 8) * sourceStride) + column)).AsInt32(); + Vector512 row9 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 9) * sourceStride) + column)).AsInt32(); + Vector512 row10 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 10) * sourceStride) + column)).AsInt32(); + Vector512 row11 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 11) * sourceStride) + column)).AsInt32(); + Vector512 row12 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 12) * sourceStride) + column)).AsInt32(); + Vector512 row13 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 13) * sourceStride) + column)).AsInt32(); + Vector512 row14 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 14) * sourceStride) + column)).AsInt32(); + Vector512 row15 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 15) * sourceStride) + column)).AsInt32(); + + SimdUtils.Transpose( + ref row0, + ref row1, + ref row2, + ref row3, + ref row4, + ref row5, + ref row6, + ref row7, + ref row8, + ref row9, + ref row10, + ref row11, + ref row12, + ref row13, + ref row14, + ref row15); + + values[column + 0] = row0.AsSingle(); + values[column + 1] = row1.AsSingle(); + values[column + 2] = row2.AsSingle(); + values[column + 3] = row3.AsSingle(); + values[column + 4] = row4.AsSingle(); + values[column + 5] = row5.AsSingle(); + values[column + 6] = row6.AsSingle(); + values[column + 7] = row7.AsSingle(); + values[column + 8] = row8.AsSingle(); + values[column + 9] = row9.AsSingle(); + values[column + 10] = row10.AsSingle(); + values[column + 11] = row11.AsSingle(); + values[column + 12] = row12.AsSingle(); + values[column + 13] = row13.AsSingle(); + values[column + 14] = row14.AsSingle(); + values[column + 15] = row15.AsSingle(); + } + + Forward(values, scratch); + if (transposed) + { + // A complete vector is one contiguous output row. Only logical samples are stored; destination padding remains untouched. + for (int i = 0; i < length; i++) + { + (values[i] * scale).StoreUnsafe(ref output, (nuint)((i * destinationStride) + first)); + } + } + else + { + // Restore row-major order one register tile at a time. This replaces a separate full-block transpose and copy. + for (int column = 0; column < length; column += 16) + { + Vector512 row0 = (values[column + 0] * scale).AsInt32(); + Vector512 row1 = (values[column + 1] * scale).AsInt32(); + Vector512 row2 = (values[column + 2] * scale).AsInt32(); + Vector512 row3 = (values[column + 3] * scale).AsInt32(); + Vector512 row4 = (values[column + 4] * scale).AsInt32(); + Vector512 row5 = (values[column + 5] * scale).AsInt32(); + Vector512 row6 = (values[column + 6] * scale).AsInt32(); + Vector512 row7 = (values[column + 7] * scale).AsInt32(); + Vector512 row8 = (values[column + 8] * scale).AsInt32(); + Vector512 row9 = (values[column + 9] * scale).AsInt32(); + Vector512 row10 = (values[column + 10] * scale).AsInt32(); + Vector512 row11 = (values[column + 11] * scale).AsInt32(); + Vector512 row12 = (values[column + 12] * scale).AsInt32(); + Vector512 row13 = (values[column + 13] * scale).AsInt32(); + Vector512 row14 = (values[column + 14] * scale).AsInt32(); + Vector512 row15 = (values[column + 15] * scale).AsInt32(); + + SimdUtils.Transpose( + ref row0, + ref row1, + ref row2, + ref row3, + ref row4, + ref row5, + ref row6, + ref row7, + ref row8, + ref row9, + ref row10, + ref row11, + ref row12, + ref row13, + ref row14, + ref row15); + + row0.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 0) * destinationStride) + column)); + row1.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 1) * destinationStride) + column)); + row2.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 2) * destinationStride) + column)); + row3.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 3) * destinationStride) + column)); + row4.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 4) * destinationStride) + column)); + row5.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 5) * destinationStride) + column)); + row6.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 6) * destinationStride) + column)); + row7.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 7) * destinationStride) + column)); + row8.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 8) * destinationStride) + column)); + row9.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 9) * destinationStride) + column)); + row10.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 10) * destinationStride) + column)); + row11.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 11) * destinationStride) + column)); + row12.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 12) * destinationStride) + column)); + row13.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 13) * destinationStride) + column)); + row14.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 14) * destinationStride) + column)); + row15.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 15) * destinationStride) + column)); + } + } + } + + return first; + } + + /// + /// Computes independent inverse transforms using two nonoverlapping borrowed vector banks. + /// + /// One vector per transform position. Its length is a power of two from one through 256. + /// At least values.Length vectors. Previous contents are not read. + private static void Inverse(Span> values, Span> scratch) + { + // Small transforms stay in registers. The one-point transform is an identity and must not copy any trailing samples. + switch (values.Length) + { + case 1: + return; + case 2: + Inverse2(values); + return; + case 4: + Inverse4(values); + return; + case 8: + Inverse8(values); + return; + case 16: + Inverse16(values); + return; + case 32: + Inverse32(values); + return; + } + + int half = values.Length / 2; + // Separate frequency parity before either recursive call. The original bank can then serve as recursive scratch. + for (int i = 0; i < half; i++) + { + scratch[i] = values[2 * i]; + scratch[half + i] = values[(2 * i) + 1]; + } + + Inverse(scratch[..half], values); + + // Descend so every odd-frequency predecessor is read before its own entry changes. + for (int i = half - 1; i > 0; i--) + { + scratch[half + i] += scratch[half + i - 1]; + } + + scratch[half] *= Vector512.Create(JxlDctScales.Sqrt2); + Inverse(scratch.Slice(half, half), values); + ReadOnlySpan weights = JxlDctScales.GetMultipliers(values.Length); + + for (int i = 0; i < half; i++) + { + Vector512 weight = Vector512.Create(weights[i]); + Vector512 even = scratch[i]; + Vector512 odd = scratch[half + i]; + values[i] = Vector512.MultiplyAddEstimate(weight, odd, even); + values[values.Length - i - 1] = Vector512.MultiplyAddEstimate(-weight, odd, even); + } + } + + /// + /// Computes independent 2-point inverse transforms without intermediate memory passes. + /// + /// Exactly 2 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Inverse2(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector512 input0 = values[0]; + Vector512 input1 = values[1]; + + Vector512 v0 = input0 + input1; + Vector512 v1 = input0 - input1; + + values[0] = v0; + values[1] = v1; + } + + /// + /// Computes independent 4-point inverse transforms without intermediate memory passes. + /// + /// Exactly 4 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Inverse4(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector512 input0 = values[0]; + Vector512 input1 = values[1]; + Vector512 input2 = values[2]; + Vector512 input3 = values[3]; + + Vector512 v0 = input0 + input2; + Vector512 v1 = input0 - input2; + Vector512 v2 = input3 + input1; + Vector512 v3 = input1 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v4 = v3 + v2; + Vector512 v5 = v3 - v2; + Vector512 v6 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v4, v0); + Vector512 v7 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v4, v0); + Vector512 v8 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v5, v1); + Vector512 v9 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v5, v1); + + values[0] = v6; + values[1] = v8; + values[2] = v9; + values[3] = v7; + } + + /// + /// Computes independent 8-point inverse transforms without intermediate memory passes. + /// + /// Exactly 8 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Inverse8(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector512 input0 = values[0]; + Vector512 input1 = values[1]; + Vector512 input2 = values[2]; + Vector512 input3 = values[3]; + Vector512 input4 = values[4]; + Vector512 input5 = values[5]; + Vector512 input6 = values[6]; + Vector512 input7 = values[7]; + + Vector512 v0 = input0 + input4; + Vector512 v1 = input0 - input4; + Vector512 v2 = input6 + input2; + Vector512 v3 = input2 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v4 = v3 + v2; + Vector512 v5 = v3 - v2; + Vector512 v6 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v4, v0); + Vector512 v7 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v4, v0); + Vector512 v8 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v5, v1); + Vector512 v9 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v5, v1); + Vector512 v10 = input7 + input5; + Vector512 v11 = input5 + input3; + Vector512 v12 = input3 + input1; + Vector512 v13 = input1 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v14 = v13 + v11; + Vector512 v15 = v13 - v11; + Vector512 v16 = v10 + v12; + Vector512 v17 = v12 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v18 = v17 + v16; + Vector512 v19 = v17 - v16; + Vector512 v20 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v18, v14); + Vector512 v21 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v18, v14); + Vector512 v22 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v19, v15); + Vector512 v23 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v19, v15); + Vector512 v24 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5097955791041592f), v20, v6); + Vector512 v25 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5097955791041592f), v20, v6); + Vector512 v26 = Vector512.MultiplyAddEstimate(Vector512.Create(0.6013448869350453f), v22, v8); + Vector512 v27 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.6013448869350453f), v22, v8); + Vector512 v28 = Vector512.MultiplyAddEstimate(Vector512.Create(0.8999762231364156f), v23, v9); + Vector512 v29 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.8999762231364156f), v23, v9); + Vector512 v30 = Vector512.MultiplyAddEstimate(Vector512.Create(2.5629154477415055f), v21, v7); + Vector512 v31 = Vector512.MultiplyAddEstimate(-Vector512.Create(2.5629154477415055f), v21, v7); + + values[0] = v24; + values[1] = v26; + values[2] = v28; + values[3] = v30; + values[4] = v31; + values[5] = v29; + values[6] = v27; + values[7] = v25; + } + + /// + /// Computes independent 16-point inverse transforms without intermediate memory passes. + /// + /// Exactly 16 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Inverse16(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector512 input0 = values[0]; + Vector512 input1 = values[1]; + Vector512 input2 = values[2]; + Vector512 input3 = values[3]; + Vector512 input4 = values[4]; + Vector512 input5 = values[5]; + Vector512 input6 = values[6]; + Vector512 input7 = values[7]; + Vector512 input8 = values[8]; + Vector512 input9 = values[9]; + Vector512 input10 = values[10]; + Vector512 input11 = values[11]; + Vector512 input12 = values[12]; + Vector512 input13 = values[13]; + Vector512 input14 = values[14]; + Vector512 input15 = values[15]; + + Vector512 v0 = input0 + input8; + Vector512 v1 = input0 - input8; + Vector512 v2 = input12 + input4; + Vector512 v3 = input4 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v4 = v3 + v2; + Vector512 v5 = v3 - v2; + Vector512 v6 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v4, v0); + Vector512 v7 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v4, v0); + Vector512 v8 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v5, v1); + Vector512 v9 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v5, v1); + Vector512 v10 = input14 + input10; + Vector512 v11 = input10 + input6; + Vector512 v12 = input6 + input2; + Vector512 v13 = input2 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v14 = v13 + v11; + Vector512 v15 = v13 - v11; + Vector512 v16 = v10 + v12; + Vector512 v17 = v12 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v18 = v17 + v16; + Vector512 v19 = v17 - v16; + Vector512 v20 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v18, v14); + Vector512 v21 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v18, v14); + Vector512 v22 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v19, v15); + Vector512 v23 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v19, v15); + Vector512 v24 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5097955791041592f), v20, v6); + Vector512 v25 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5097955791041592f), v20, v6); + Vector512 v26 = Vector512.MultiplyAddEstimate(Vector512.Create(0.6013448869350453f), v22, v8); + Vector512 v27 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.6013448869350453f), v22, v8); + Vector512 v28 = Vector512.MultiplyAddEstimate(Vector512.Create(0.8999762231364156f), v23, v9); + Vector512 v29 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.8999762231364156f), v23, v9); + Vector512 v30 = Vector512.MultiplyAddEstimate(Vector512.Create(2.5629154477415055f), v21, v7); + Vector512 v31 = Vector512.MultiplyAddEstimate(-Vector512.Create(2.5629154477415055f), v21, v7); + Vector512 v32 = input15 + input13; + Vector512 v33 = input13 + input11; + Vector512 v34 = input11 + input9; + Vector512 v35 = input9 + input7; + Vector512 v36 = input7 + input5; + Vector512 v37 = input5 + input3; + Vector512 v38 = input3 + input1; + Vector512 v39 = input1 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v40 = v39 + v35; + Vector512 v41 = v39 - v35; + Vector512 v42 = v33 + v37; + Vector512 v43 = v37 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v44 = v43 + v42; + Vector512 v45 = v43 - v42; + Vector512 v46 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v44, v40); + Vector512 v47 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v44, v40); + Vector512 v48 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v45, v41); + Vector512 v49 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v45, v41); + Vector512 v50 = v32 + v34; + Vector512 v51 = v34 + v36; + Vector512 v52 = v36 + v38; + Vector512 v53 = v38 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v54 = v53 + v51; + Vector512 v55 = v53 - v51; + Vector512 v56 = v50 + v52; + Vector512 v57 = v52 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v58 = v57 + v56; + Vector512 v59 = v57 - v56; + Vector512 v60 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v58, v54); + Vector512 v61 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v58, v54); + Vector512 v62 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v59, v55); + Vector512 v63 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v59, v55); + Vector512 v64 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5097955791041592f), v60, v46); + Vector512 v65 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5097955791041592f), v60, v46); + Vector512 v66 = Vector512.MultiplyAddEstimate(Vector512.Create(0.6013448869350453f), v62, v48); + Vector512 v67 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.6013448869350453f), v62, v48); + Vector512 v68 = Vector512.MultiplyAddEstimate(Vector512.Create(0.8999762231364156f), v63, v49); + Vector512 v69 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.8999762231364156f), v63, v49); + Vector512 v70 = Vector512.MultiplyAddEstimate(Vector512.Create(2.5629154477415055f), v61, v47); + Vector512 v71 = Vector512.MultiplyAddEstimate(-Vector512.Create(2.5629154477415055f), v61, v47); + Vector512 v72 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5024192861881557f), v64, v24); + Vector512 v73 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5024192861881557f), v64, v24); + Vector512 v74 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5224986149396889f), v66, v26); + Vector512 v75 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5224986149396889f), v66, v26); + Vector512 v76 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5669440348163577f), v68, v28); + Vector512 v77 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5669440348163577f), v68, v28); + Vector512 v78 = Vector512.MultiplyAddEstimate(Vector512.Create(0.6468217833599901f), v70, v30); + Vector512 v79 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.6468217833599901f), v70, v30); + Vector512 v80 = Vector512.MultiplyAddEstimate(Vector512.Create(0.7881546234512502f), v71, v31); + Vector512 v81 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.7881546234512502f), v71, v31); + Vector512 v82 = Vector512.MultiplyAddEstimate(Vector512.Create(1.060677685990347f), v69, v29); + Vector512 v83 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.060677685990347f), v69, v29); + Vector512 v84 = Vector512.MultiplyAddEstimate(Vector512.Create(1.7224470982383342f), v67, v27); + Vector512 v85 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.7224470982383342f), v67, v27); + Vector512 v86 = Vector512.MultiplyAddEstimate(Vector512.Create(5.101148618689155f), v65, v25); + Vector512 v87 = Vector512.MultiplyAddEstimate(-Vector512.Create(5.101148618689155f), v65, v25); + + values[0] = v72; + values[1] = v74; + values[2] = v76; + values[3] = v78; + values[4] = v80; + values[5] = v82; + values[6] = v84; + values[7] = v86; + values[8] = v87; + values[9] = v85; + values[10] = v83; + values[11] = v81; + values[12] = v79; + values[13] = v77; + values[14] = v75; + values[15] = v73; + } + + /// + /// Computes independent 32-point inverse transforms without intermediate memory passes. + /// + /// Exactly 32 position vectors, replaced with the transform results. Each lane remains independent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Inverse32(Span> values) + { + // The Perera-Liu radix-two factors retain libjxl's coefficient bits and multiply-add sites. + // Load every input before publishing any result. This also permits the larger workers to transform a bank in place. + Vector512 input0 = values[0]; + Vector512 input1 = values[1]; + Vector512 input2 = values[2]; + Vector512 input3 = values[3]; + Vector512 input4 = values[4]; + Vector512 input5 = values[5]; + Vector512 input6 = values[6]; + Vector512 input7 = values[7]; + Vector512 input8 = values[8]; + Vector512 input9 = values[9]; + Vector512 input10 = values[10]; + Vector512 input11 = values[11]; + Vector512 input12 = values[12]; + Vector512 input13 = values[13]; + Vector512 input14 = values[14]; + Vector512 input15 = values[15]; + Vector512 input16 = values[16]; + Vector512 input17 = values[17]; + Vector512 input18 = values[18]; + Vector512 input19 = values[19]; + Vector512 input20 = values[20]; + Vector512 input21 = values[21]; + Vector512 input22 = values[22]; + Vector512 input23 = values[23]; + Vector512 input24 = values[24]; + Vector512 input25 = values[25]; + Vector512 input26 = values[26]; + Vector512 input27 = values[27]; + Vector512 input28 = values[28]; + Vector512 input29 = values[29]; + Vector512 input30 = values[30]; + Vector512 input31 = values[31]; + + Vector512 v0 = input0 + input16; + Vector512 v1 = input0 - input16; + Vector512 v2 = input24 + input8; + Vector512 v3 = input8 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v4 = v3 + v2; + Vector512 v5 = v3 - v2; + Vector512 v6 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v4, v0); + Vector512 v7 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v4, v0); + Vector512 v8 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v5, v1); + Vector512 v9 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v5, v1); + Vector512 v10 = input28 + input20; + Vector512 v11 = input20 + input12; + Vector512 v12 = input12 + input4; + Vector512 v13 = input4 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v14 = v13 + v11; + Vector512 v15 = v13 - v11; + Vector512 v16 = v10 + v12; + Vector512 v17 = v12 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v18 = v17 + v16; + Vector512 v19 = v17 - v16; + Vector512 v20 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v18, v14); + Vector512 v21 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v18, v14); + Vector512 v22 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v19, v15); + Vector512 v23 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v19, v15); + Vector512 v24 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5097955791041592f), v20, v6); + Vector512 v25 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5097955791041592f), v20, v6); + Vector512 v26 = Vector512.MultiplyAddEstimate(Vector512.Create(0.6013448869350453f), v22, v8); + Vector512 v27 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.6013448869350453f), v22, v8); + Vector512 v28 = Vector512.MultiplyAddEstimate(Vector512.Create(0.8999762231364156f), v23, v9); + Vector512 v29 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.8999762231364156f), v23, v9); + Vector512 v30 = Vector512.MultiplyAddEstimate(Vector512.Create(2.5629154477415055f), v21, v7); + Vector512 v31 = Vector512.MultiplyAddEstimate(-Vector512.Create(2.5629154477415055f), v21, v7); + Vector512 v32 = input30 + input26; + Vector512 v33 = input26 + input22; + Vector512 v34 = input22 + input18; + Vector512 v35 = input18 + input14; + Vector512 v36 = input14 + input10; + Vector512 v37 = input10 + input6; + Vector512 v38 = input6 + input2; + Vector512 v39 = input2 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v40 = v39 + v35; + Vector512 v41 = v39 - v35; + Vector512 v42 = v33 + v37; + Vector512 v43 = v37 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v44 = v43 + v42; + Vector512 v45 = v43 - v42; + Vector512 v46 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v44, v40); + Vector512 v47 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v44, v40); + Vector512 v48 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v45, v41); + Vector512 v49 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v45, v41); + Vector512 v50 = v32 + v34; + Vector512 v51 = v34 + v36; + Vector512 v52 = v36 + v38; + Vector512 v53 = v38 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v54 = v53 + v51; + Vector512 v55 = v53 - v51; + Vector512 v56 = v50 + v52; + Vector512 v57 = v52 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v58 = v57 + v56; + Vector512 v59 = v57 - v56; + Vector512 v60 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v58, v54); + Vector512 v61 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v58, v54); + Vector512 v62 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v59, v55); + Vector512 v63 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v59, v55); + Vector512 v64 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5097955791041592f), v60, v46); + Vector512 v65 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5097955791041592f), v60, v46); + Vector512 v66 = Vector512.MultiplyAddEstimate(Vector512.Create(0.6013448869350453f), v62, v48); + Vector512 v67 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.6013448869350453f), v62, v48); + Vector512 v68 = Vector512.MultiplyAddEstimate(Vector512.Create(0.8999762231364156f), v63, v49); + Vector512 v69 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.8999762231364156f), v63, v49); + Vector512 v70 = Vector512.MultiplyAddEstimate(Vector512.Create(2.5629154477415055f), v61, v47); + Vector512 v71 = Vector512.MultiplyAddEstimate(-Vector512.Create(2.5629154477415055f), v61, v47); + Vector512 v72 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5024192861881557f), v64, v24); + Vector512 v73 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5024192861881557f), v64, v24); + Vector512 v74 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5224986149396889f), v66, v26); + Vector512 v75 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5224986149396889f), v66, v26); + Vector512 v76 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5669440348163577f), v68, v28); + Vector512 v77 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5669440348163577f), v68, v28); + Vector512 v78 = Vector512.MultiplyAddEstimate(Vector512.Create(0.6468217833599901f), v70, v30); + Vector512 v79 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.6468217833599901f), v70, v30); + Vector512 v80 = Vector512.MultiplyAddEstimate(Vector512.Create(0.7881546234512502f), v71, v31); + Vector512 v81 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.7881546234512502f), v71, v31); + Vector512 v82 = Vector512.MultiplyAddEstimate(Vector512.Create(1.060677685990347f), v69, v29); + Vector512 v83 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.060677685990347f), v69, v29); + Vector512 v84 = Vector512.MultiplyAddEstimate(Vector512.Create(1.7224470982383342f), v67, v27); + Vector512 v85 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.7224470982383342f), v67, v27); + Vector512 v86 = Vector512.MultiplyAddEstimate(Vector512.Create(5.101148618689155f), v65, v25); + Vector512 v87 = Vector512.MultiplyAddEstimate(-Vector512.Create(5.101148618689155f), v65, v25); + Vector512 v88 = input31 + input29; + Vector512 v89 = input29 + input27; + Vector512 v90 = input27 + input25; + Vector512 v91 = input25 + input23; + Vector512 v92 = input23 + input21; + Vector512 v93 = input21 + input19; + Vector512 v94 = input19 + input17; + Vector512 v95 = input17 + input15; + Vector512 v96 = input15 + input13; + Vector512 v97 = input13 + input11; + Vector512 v98 = input11 + input9; + Vector512 v99 = input9 + input7; + Vector512 v100 = input7 + input5; + Vector512 v101 = input5 + input3; + Vector512 v102 = input3 + input1; + Vector512 v103 = input1 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v104 = v103 + v95; + Vector512 v105 = v103 - v95; + Vector512 v106 = v91 + v99; + Vector512 v107 = v99 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v108 = v107 + v106; + Vector512 v109 = v107 - v106; + Vector512 v110 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v108, v104); + Vector512 v111 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v108, v104); + Vector512 v112 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v109, v105); + Vector512 v113 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v109, v105); + Vector512 v114 = v89 + v93; + Vector512 v115 = v93 + v97; + Vector512 v116 = v97 + v101; + Vector512 v117 = v101 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v118 = v117 + v115; + Vector512 v119 = v117 - v115; + Vector512 v120 = v114 + v116; + Vector512 v121 = v116 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v122 = v121 + v120; + Vector512 v123 = v121 - v120; + Vector512 v124 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v122, v118); + Vector512 v125 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v122, v118); + Vector512 v126 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v123, v119); + Vector512 v127 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v123, v119); + Vector512 v128 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5097955791041592f), v124, v110); + Vector512 v129 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5097955791041592f), v124, v110); + Vector512 v130 = Vector512.MultiplyAddEstimate(Vector512.Create(0.6013448869350453f), v126, v112); + Vector512 v131 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.6013448869350453f), v126, v112); + Vector512 v132 = Vector512.MultiplyAddEstimate(Vector512.Create(0.8999762231364156f), v127, v113); + Vector512 v133 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.8999762231364156f), v127, v113); + Vector512 v134 = Vector512.MultiplyAddEstimate(Vector512.Create(2.5629154477415055f), v125, v111); + Vector512 v135 = Vector512.MultiplyAddEstimate(-Vector512.Create(2.5629154477415055f), v125, v111); + Vector512 v136 = v88 + v90; + Vector512 v137 = v90 + v92; + Vector512 v138 = v92 + v94; + Vector512 v139 = v94 + v96; + Vector512 v140 = v96 + v98; + Vector512 v141 = v98 + v100; + Vector512 v142 = v100 + v102; + Vector512 v143 = v102 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v144 = v143 + v139; + Vector512 v145 = v143 - v139; + Vector512 v146 = v137 + v141; + Vector512 v147 = v141 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v148 = v147 + v146; + Vector512 v149 = v147 - v146; + Vector512 v150 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v148, v144); + Vector512 v151 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v148, v144); + Vector512 v152 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v149, v145); + Vector512 v153 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v149, v145); + Vector512 v154 = v136 + v138; + Vector512 v155 = v138 + v140; + Vector512 v156 = v140 + v142; + Vector512 v157 = v142 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v158 = v157 + v155; + Vector512 v159 = v157 - v155; + Vector512 v160 = v154 + v156; + Vector512 v161 = v156 * Vector512.Create(JxlDctScales.Sqrt2); + Vector512 v162 = v161 + v160; + Vector512 v163 = v161 - v160; + Vector512 v164 = Vector512.MultiplyAddEstimate(Vector512.Create(0.541196100146197f), v162, v158); + Vector512 v165 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.541196100146197f), v162, v158); + Vector512 v166 = Vector512.MultiplyAddEstimate(Vector512.Create(1.3065629648763764f), v163, v159); + Vector512 v167 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.3065629648763764f), v163, v159); + Vector512 v168 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5097955791041592f), v164, v150); + Vector512 v169 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5097955791041592f), v164, v150); + Vector512 v170 = Vector512.MultiplyAddEstimate(Vector512.Create(0.6013448869350453f), v166, v152); + Vector512 v171 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.6013448869350453f), v166, v152); + Vector512 v172 = Vector512.MultiplyAddEstimate(Vector512.Create(0.8999762231364156f), v167, v153); + Vector512 v173 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.8999762231364156f), v167, v153); + Vector512 v174 = Vector512.MultiplyAddEstimate(Vector512.Create(2.5629154477415055f), v165, v151); + Vector512 v175 = Vector512.MultiplyAddEstimate(-Vector512.Create(2.5629154477415055f), v165, v151); + Vector512 v176 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5024192861881557f), v168, v128); + Vector512 v177 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5024192861881557f), v168, v128); + Vector512 v178 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5224986149396889f), v170, v130); + Vector512 v179 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5224986149396889f), v170, v130); + Vector512 v180 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5669440348163577f), v172, v132); + Vector512 v181 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5669440348163577f), v172, v132); + Vector512 v182 = Vector512.MultiplyAddEstimate(Vector512.Create(0.6468217833599901f), v174, v134); + Vector512 v183 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.6468217833599901f), v174, v134); + Vector512 v184 = Vector512.MultiplyAddEstimate(Vector512.Create(0.7881546234512502f), v175, v135); + Vector512 v185 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.7881546234512502f), v175, v135); + Vector512 v186 = Vector512.MultiplyAddEstimate(Vector512.Create(1.060677685990347f), v173, v133); + Vector512 v187 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.060677685990347f), v173, v133); + Vector512 v188 = Vector512.MultiplyAddEstimate(Vector512.Create(1.7224470982383342f), v171, v131); + Vector512 v189 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.7224470982383342f), v171, v131); + Vector512 v190 = Vector512.MultiplyAddEstimate(Vector512.Create(5.101148618689155f), v169, v129); + Vector512 v191 = Vector512.MultiplyAddEstimate(-Vector512.Create(5.101148618689155f), v169, v129); + Vector512 v192 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5006029982351963f), v176, v72); + Vector512 v193 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5006029982351963f), v176, v72); + Vector512 v194 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5054709598975436f), v178, v74); + Vector512 v195 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5054709598975436f), v178, v74); + Vector512 v196 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5154473099226246f), v180, v76); + Vector512 v197 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5154473099226246f), v180, v76); + Vector512 v198 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5310425910897841f), v182, v78); + Vector512 v199 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5310425910897841f), v182, v78); + Vector512 v200 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5531038960344445f), v184, v80); + Vector512 v201 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5531038960344445f), v184, v80); + Vector512 v202 = Vector512.MultiplyAddEstimate(Vector512.Create(0.5829349682061339f), v186, v82); + Vector512 v203 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.5829349682061339f), v186, v82); + Vector512 v204 = Vector512.MultiplyAddEstimate(Vector512.Create(0.6225041230356648f), v188, v84); + Vector512 v205 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.6225041230356648f), v188, v84); + Vector512 v206 = Vector512.MultiplyAddEstimate(Vector512.Create(0.6748083414550057f), v190, v86); + Vector512 v207 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.6748083414550057f), v190, v86); + Vector512 v208 = Vector512.MultiplyAddEstimate(Vector512.Create(0.7445362710022986f), v191, v87); + Vector512 v209 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.7445362710022986f), v191, v87); + Vector512 v210 = Vector512.MultiplyAddEstimate(Vector512.Create(0.8393496454155268f), v189, v85); + Vector512 v211 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.8393496454155268f), v189, v85); + Vector512 v212 = Vector512.MultiplyAddEstimate(Vector512.Create(0.9725682378619608f), v187, v83); + Vector512 v213 = Vector512.MultiplyAddEstimate(-Vector512.Create(0.9725682378619608f), v187, v83); + Vector512 v214 = Vector512.MultiplyAddEstimate(Vector512.Create(1.1694399334328847f), v185, v81); + Vector512 v215 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.1694399334328847f), v185, v81); + Vector512 v216 = Vector512.MultiplyAddEstimate(Vector512.Create(1.4841646163141662f), v183, v79); + Vector512 v217 = Vector512.MultiplyAddEstimate(-Vector512.Create(1.4841646163141662f), v183, v79); + Vector512 v218 = Vector512.MultiplyAddEstimate(Vector512.Create(2.057781009953411f), v181, v77); + Vector512 v219 = Vector512.MultiplyAddEstimate(-Vector512.Create(2.057781009953411f), v181, v77); + Vector512 v220 = Vector512.MultiplyAddEstimate(Vector512.Create(3.407608418468719f), v179, v75); + Vector512 v221 = Vector512.MultiplyAddEstimate(-Vector512.Create(3.407608418468719f), v179, v75); + Vector512 v222 = Vector512.MultiplyAddEstimate(Vector512.Create(10.190008123548033f), v177, v73); + Vector512 v223 = Vector512.MultiplyAddEstimate(-Vector512.Create(10.190008123548033f), v177, v73); + + values[0] = v192; + values[1] = v194; + values[2] = v196; + values[3] = v198; + values[4] = v200; + values[5] = v202; + values[6] = v204; + values[7] = v206; + values[8] = v208; + values[9] = v210; + values[10] = v212; + values[11] = v214; + values[12] = v216; + values[13] = v218; + values[14] = v220; + values[15] = v222; + values[16] = v223; + values[17] = v221; + values[18] = v219; + values[19] = v217; + values[20] = v215; + values[21] = v213; + values[22] = v211; + values[23] = v209; + values[24] = v207; + values[25] = v205; + values[26] = v203; + values[27] = v201; + values[28] = v199; + values[29] = v197; + values[30] = v195; + values[31] = v193; + } + + /// + /// Processes complete 16-lane inverse batches along input columns. + /// + /// The number of positions in each transform. + /// The number of independent transforms. + /// The borrowed source matrix. + /// The source row stride, in floats. + /// The borrowed output matrix. + /// The destination row stride, in floats. + /// The first independent transform not already processed by a wider batch. + /// Borrowed storage for two length-by-16 banks. Only the first bank needs initialized inputs. + /// The first independent transform left after the complete batches. + private static int InverseColumns512( + int length, + int count, + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int first, + Span workspace) + { + Span> values = MemoryMarshal.Cast>(workspace[..(length * 16)]); + Span> scratch = MemoryMarshal.Cast>(workspace.Slice(length * 16, length * 16)); + ref float input = ref MemoryMarshal.GetReference(source); + ref float output = ref MemoryMarshal.GetReference(destination); + + for (; first <= count - 16; first += 16) + { + // One vector covers adjacent columns at the same transform position. Physical strides never enter the coefficient-bank layout. + for (int i = 0; i < length; i++) + { + values[i] = Vector512.LoadUnsafe(ref input, (nuint)((i * sourceStride) + first)); + } + + Inverse(values, scratch); + for (int i = 0; i < length; i++) + { + values[i].StoreUnsafe(ref output, (nuint)((i * destinationStride) + first)); + } + } + + return first; + } + + /// + /// Processes complete 16-lane inverse batches along input rows. + /// + /// The number of positions in each transform. + /// The number of independent transforms. + /// The borrowed source matrix. + /// The source row stride, in floats. + /// The borrowed output matrix. + /// The destination row stride, in floats. + /// Whether transform positions become output rows and input rows become output columns. + /// The first independent transform not already processed by a wider batch. + /// Borrowed storage for two length-by-16 banks. Only the first bank needs initialized inputs. + /// The first independent transform left after the complete batches. + private static int InverseRows512( + int length, + int count, + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + bool transposed, + int first, + Span workspace) + { + Span> values = MemoryMarshal.Cast>(workspace[..(length * 16)]); + Span> scratch = MemoryMarshal.Cast>(workspace.Slice(length * 16, length * 16)); + ref float input = ref MemoryMarshal.GetReference(source); + ref float output = ref MemoryMarshal.GetReference(destination); + + for (; first <= count - 16; first += 16) + { + // Each square load contains adjacent samples from independent rows. Transposition puts those rows into SIMD lanes. + // The complete row band is loaded before any same-orientation output is written, so source and destination can be the same matrix. + for (int column = 0; column < length; column += 16) + { + Vector512 row0 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 0) * sourceStride) + column)).AsInt32(); + Vector512 row1 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 1) * sourceStride) + column)).AsInt32(); + Vector512 row2 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 2) * sourceStride) + column)).AsInt32(); + Vector512 row3 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 3) * sourceStride) + column)).AsInt32(); + Vector512 row4 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 4) * sourceStride) + column)).AsInt32(); + Vector512 row5 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 5) * sourceStride) + column)).AsInt32(); + Vector512 row6 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 6) * sourceStride) + column)).AsInt32(); + Vector512 row7 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 7) * sourceStride) + column)).AsInt32(); + Vector512 row8 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 8) * sourceStride) + column)).AsInt32(); + Vector512 row9 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 9) * sourceStride) + column)).AsInt32(); + Vector512 row10 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 10) * sourceStride) + column)).AsInt32(); + Vector512 row11 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 11) * sourceStride) + column)).AsInt32(); + Vector512 row12 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 12) * sourceStride) + column)).AsInt32(); + Vector512 row13 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 13) * sourceStride) + column)).AsInt32(); + Vector512 row14 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 14) * sourceStride) + column)).AsInt32(); + Vector512 row15 = Vector512.LoadUnsafe(ref input, (nuint)(((first + 15) * sourceStride) + column)).AsInt32(); + + SimdUtils.Transpose( + ref row0, + ref row1, + ref row2, + ref row3, + ref row4, + ref row5, + ref row6, + ref row7, + ref row8, + ref row9, + ref row10, + ref row11, + ref row12, + ref row13, + ref row14, + ref row15); + + values[column + 0] = row0.AsSingle(); + values[column + 1] = row1.AsSingle(); + values[column + 2] = row2.AsSingle(); + values[column + 3] = row3.AsSingle(); + values[column + 4] = row4.AsSingle(); + values[column + 5] = row5.AsSingle(); + values[column + 6] = row6.AsSingle(); + values[column + 7] = row7.AsSingle(); + values[column + 8] = row8.AsSingle(); + values[column + 9] = row9.AsSingle(); + values[column + 10] = row10.AsSingle(); + values[column + 11] = row11.AsSingle(); + values[column + 12] = row12.AsSingle(); + values[column + 13] = row13.AsSingle(); + values[column + 14] = row14.AsSingle(); + values[column + 15] = row15.AsSingle(); + } + + Inverse(values, scratch); + if (transposed) + { + // A complete vector is one contiguous output row. Only logical samples are stored; destination padding remains untouched. + for (int i = 0; i < length; i++) + { + values[i].StoreUnsafe(ref output, (nuint)((i * destinationStride) + first)); + } + } + else + { + // Restore row-major order one register tile at a time. This replaces a separate full-block transpose and copy. + for (int column = 0; column < length; column += 16) + { + Vector512 row0 = values[column + 0].AsInt32(); + Vector512 row1 = values[column + 1].AsInt32(); + Vector512 row2 = values[column + 2].AsInt32(); + Vector512 row3 = values[column + 3].AsInt32(); + Vector512 row4 = values[column + 4].AsInt32(); + Vector512 row5 = values[column + 5].AsInt32(); + Vector512 row6 = values[column + 6].AsInt32(); + Vector512 row7 = values[column + 7].AsInt32(); + Vector512 row8 = values[column + 8].AsInt32(); + Vector512 row9 = values[column + 9].AsInt32(); + Vector512 row10 = values[column + 10].AsInt32(); + Vector512 row11 = values[column + 11].AsInt32(); + Vector512 row12 = values[column + 12].AsInt32(); + Vector512 row13 = values[column + 13].AsInt32(); + Vector512 row14 = values[column + 14].AsInt32(); + Vector512 row15 = values[column + 15].AsInt32(); + + SimdUtils.Transpose( + ref row0, + ref row1, + ref row2, + ref row3, + ref row4, + ref row5, + ref row6, + ref row7, + ref row8, + ref row9, + ref row10, + ref row11, + ref row12, + ref row13, + ref row14, + ref row15); + + row0.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 0) * destinationStride) + column)); + row1.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 1) * destinationStride) + column)); + row2.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 2) * destinationStride) + column)); + row3.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 3) * destinationStride) + column)); + row4.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 4) * destinationStride) + column)); + row5.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 5) * destinationStride) + column)); + row6.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 6) * destinationStride) + column)); + row7.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 7) * destinationStride) + column)); + row8.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 8) * destinationStride) + column)); + row9.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 9) * destinationStride) + column)); + row10.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 10) * destinationStride) + column)); + row11.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 11) * destinationStride) + column)); + row12.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 12) * destinationStride) + column)); + row13.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 13) * destinationStride) + column)); + row14.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 14) * destinationStride) + column)); + row15.AsSingle().StoreUnsafe(ref output, (nuint)(((first + 15) * destinationStride) + column)); + } + } + } + + return first; + } + +} From baf6b6a3c71cfeef27de25e2b751e020cb5bda4d Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Oct 2026 09:49:26 +1000 Subject: [PATCH 326/328] Keep compensated logarithm arithmetic in Numerics --- src/ImageSharp/Common/Helpers/MathUtils.cs | 46 ------------------- src/ImageSharp/Common/Helpers/Numerics.cs | 25 ++++++++++ .../Pipeline/Lookahead/Av1FirstPass.Row.cs | 3 +- .../Av1/Pipeline/Lookahead/Av1FirstPass.cs | 5 +- .../Av1FirstPassStatisticsAccumulator.cs | 6 +-- .../Av1/Pipeline/Lookahead/Av1SecondPass.cs | 6 +-- .../Encoder/Heuristics/JxlHeuristics.cs | 3 +- 7 files changed, 33 insertions(+), 61 deletions(-) delete mode 100644 src/ImageSharp/Common/Helpers/MathUtils.cs diff --git a/src/ImageSharp/Common/Helpers/MathUtils.cs b/src/ImageSharp/Common/Helpers/MathUtils.cs deleted file mode 100644 index e7ff7e4ceb..0000000000 --- a/src/ImageSharp/Common/Helpers/MathUtils.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Common.Helpers; - -/// -/// Additional math functions not provided by the .NET Runtime. -/// -internal static class MathUtils -{ - /// - /// Computes the natural logarithm of one plus a value without losing the low bits of small values. - /// - /// - /// evaluates log(1 + x), which discards the bits of that the rounded sum - /// u = 1 + x cannot hold. This method restores them with the first-order term (x - (u - 1)) / u, as the C runtime does. The - /// value u - 1 is exact, so the numerator is the exact rounding error of the sum. Then log(u + e) = log(u) + e / u to within - /// far less than one unit in the last place. The form matches the Windows C runtime bit for bit over twenty million integer and fractional - /// inputs. The correction is zero for every integer input, where the sum is exact. - /// - /// The value to add to one, at least zero. - /// The natural logarithm of one plus . - public static double Log1P(double value) - { - double sum = 1.0 + value; - return Math.Log(sum) + ((value - (sum - 1.0)) / sum); - } - - /// - /// Computes the natural logarithm of one plus a value without losing the low bits of small values. - /// - /// - /// evaluates log(1 + x), which discards the bits of that the rounded sum - /// u = 1 + x cannot hold. This method restores them with the first-order term (x - (u - 1)) / u, as the C runtime does. The - /// value u - 1 is exact, so the numerator is the exact rounding error of the sum. Then log(u + e) = log(u) + e / u to within - /// far less than one unit in the last place. The form matches the Windows C runtime bit for bit over twenty million integer and fractional - /// inputs. The correction is zero for every integer input, where the sum is exact. - /// - /// The value to add to one, at least zero. - /// The natural logarithm of one plus . - public static float Log1P(float value) - { - float sum = 1.0f + value; - return MathF.Log(sum) + ((value - (sum - 1.0f)) / sum); - } -} diff --git a/src/ImageSharp/Common/Helpers/Numerics.cs b/src/ImageSharp/Common/Helpers/Numerics.cs index c27f6de9a3..f7acb7d629 100644 --- a/src/ImageSharp/Common/Helpers/Numerics.cs +++ b/src/ImageSharp/Common/Helpers/Numerics.cs @@ -151,6 +151,31 @@ public static int Abs(int x) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double Pow3(double x) => x * x * x; + /// + /// Computes the natural logarithm of one plus a finite nonnegative value. + /// + /// The value to add to one. + /// The natural logarithm of one plus . + public static double Log1P(double value) + { + // Adding one can discard low bits of value. Add their first-order contribution to the logarithm + // so a small positive value does not become zero when the sum rounds to one. + double sum = 1.0 + value; + return Math.Log(sum) + ((value - (sum - 1.0)) / sum); + } + + /// + /// Computes the natural logarithm of one plus a finite nonnegative value. + /// + /// The value to add to one. + /// The natural logarithm of one plus . + public static float Log1P(float value) + { + // Retain the same correction as the double overload in the caller's component precision. + float sum = 1.0f + value; + return MathF.Log(sum) + ((value - (sum - 1.0f)) / sum); + } + /// /// Implementation of 1D Gaussian G(x) function /// diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Lookahead/Av1FirstPass.Row.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Lookahead/Av1FirstPass.Row.cs index 4e33f20f79..7bba540c40 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Lookahead/Av1FirstPass.Row.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Lookahead/Av1FirstPass.Row.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers; using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; @@ -177,7 +176,7 @@ private int PredictIntra(ref FrameContext frame, int unitRow, int unitColumn, bo } // A unit with a low intra error adds more than one to the intra factor. - double logIntra = MathUtils.Log1P(intraError); + double logIntra = Numerics.Log1P((double)intraError); if (logIntra < 10.0) { record.IntraFactor += 1.0 + ((10.0 - logIntra) * 0.05); diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Lookahead/Av1FirstPass.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Lookahead/Av1FirstPass.cs index 25574f9525..c91043e9b2 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Lookahead/Av1FirstPass.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Lookahead/Av1FirstPass.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Buffers; -using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; @@ -590,8 +589,8 @@ private static void NormalizeFirstPassStatistics( statistics.LongTermCodedError /= macroblockCount; statistics.IntraError /= macroblockCount; statistics.FrameAverageWaveletEnergy /= macroblockCount; - statistics.LogCodedError = MathUtils.Log1P(statistics.CodedError); - statistics.LogIntraError = MathUtils.Log1P(statistics.IntraError); + statistics.LogCodedError = Numerics.Log1P(statistics.CodedError); + statistics.LogIntraError = Numerics.Log1P(statistics.IntraError); statistics.MotionVectorRow /= frameHeight; statistics.MotionVectorRowAbsolute /= frameHeight; statistics.MotionVectorColumn /= frameWidth; diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Lookahead/Av1FirstPassStatisticsAccumulator.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Lookahead/Av1FirstPassStatisticsAccumulator.cs index a20af43335..dbcf83c39d 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Lookahead/Av1FirstPassStatisticsAccumulator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Lookahead/Av1FirstPassStatisticsAccumulator.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Common.Helpers; - namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Lookahead; /// @@ -21,8 +19,8 @@ public static void Accumulate(ref Av1FirstPassStatistics section, Av1FirstPassSt section.Frame += frame.Frame; section.Weight += frame.Weight; section.IntraError += frame.IntraError; - section.LogIntraError += MathUtils.Log1P(frame.IntraError); - section.LogCodedError += MathUtils.Log1P(frame.CodedError); + section.LogIntraError += Numerics.Log1P(frame.IntraError); + section.LogCodedError += Numerics.Log1P(frame.CodedError); section.FrameAverageWaveletEnergy += frame.FrameAverageWaveletEnergy; section.CodedError += frame.CodedError; section.SecondReferenceCodedError += frame.SecondReferenceCodedError; diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Lookahead/Av1SecondPass.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Lookahead/Av1SecondPass.cs index edc35928f5..1b1ce31a64 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Lookahead/Av1SecondPass.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Lookahead/Av1SecondPass.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Common.Helpers; - namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Lookahead; /// @@ -959,10 +957,10 @@ private void SetParametersFromStatistics(int position) /// The statistics. private void SetParameters(Av1FirstPassStatistics frameStatistics) { - this.macroblockAverageEnergy = MathUtils.Log1P(frameStatistics.IntraError); + this.macroblockAverageEnergy = Numerics.Log1P(frameStatistics.IntraError); if (this.totalStatistics.FrameAverageWaveletEnergy >= 0) { - this.frameAverageHaarEnergy = MathUtils.Log1P(frameStatistics.FrameAverageWaveletEnergy); + this.frameAverageHaarEnergy = Numerics.Log1P(frameStatistics.FrameAverageWaveletEnergy); } // A frame with at least 15 percent of intra skip blocks counts as animation or graphics. diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs index 8bed6e81c9..503d9463fd 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs @@ -4,7 +4,6 @@ using System.Buffers; using System.Numerics; using System.Numerics.Tensors; -using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Jxl.Cms; using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; @@ -1135,7 +1134,7 @@ public static void ComputeARHeuristics(Configuration configuration, JxlFrameHead Span contextHisto = histo.DangerousGetRowSpan(context); int multiplierIndex = epfStepsLookupTable[val] + (3 * context); - mul[multiplierIndex] = 1.0f / (1.0f + (c5 * MathUtils.Log1P(contextHisto[val] / totals[context]) / clampedButteraugli)); + mul[multiplierIndex] = 1.0f / (1.0f + (c5 * Numerics.Log1P(contextHisto[val] / totals[context]) / clampedButteraugli)); if (val == 0) { From c3652375aff162b8c9981d9f11e8bef1289df0fe Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Oct 2026 10:40:48 +1000 Subject: [PATCH 327/328] Restore JPEG color conversion and remove duplicated BT.601 helpers --- .../Common/Helpers/ColorNumerics.cs | 81 ------------------- .../JpegColorConverter.YCbCrOperator.cs | 52 +++++++++--- 2 files changed, 40 insertions(+), 93 deletions(-) diff --git a/src/ImageSharp/Common/Helpers/ColorNumerics.cs b/src/ImageSharp/Common/Helpers/ColorNumerics.cs index 4922b08cf7..3db75dcbe4 100644 --- a/src/ImageSharp/Common/Helpers/ColorNumerics.cs +++ b/src/ImageSharp/Common/Helpers/ColorNumerics.cs @@ -29,93 +29,12 @@ internal static class ColorNumerics /// public const float MinColorValue = -MaxColorValue; - // BT.601 fixes the red and blue chroma amplitudes. Derive green weights before their single conversion to float. - // These are compile-time expressions; sample conversion uses float arithmetic. - /// The BT.601 red contribution from zero-centered Cr, in the same units as Y. - public const float Bt601RedCrMultiplier = 1.402F; - - /// The magnitude of the BT.601 green contribution from zero-centered Cb. - public const float Bt601GreenCbMultiplier = (float)(0.114 * 1.772 / 0.587); - - /// The magnitude of the BT.601 green contribution from zero-centered Cr. - public const float Bt601GreenCrMultiplier = (float)(0.299 * 1.402 / 0.587); - - /// The BT.601 blue contribution from zero-centered Cb, in the same units as Y. - public const float Bt601BlueCbMultiplier = 1.772F; - /// /// Vector for converting pixel to gray value as specified by /// ITU-R Recommendation BT.709. /// public static readonly Vector4 Bt709 = new(.2126f, .7152f, .0722f, 0.0f); - /// Converts full-range BT.601 components to RGB without rounding, normalization or clipping. - /// The luma component. - /// The zero-centered blue chroma component, in the same units as luma. - /// The zero-centered red chroma component, in the same units as luma. - /// The unrounded red component. - /// The unrounded green component. - /// The unrounded blue component. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ConvertBt601YCbCrToRgb(float y, float cb, float cr, out float r, out float g, out float b) - { - // Centering and component scale belong to the caller. Keep the established scalar subtraction order; - // the destination pixel conversion applies its own bounds and final storage rounding. - r = y + (Bt601RedCrMultiplier * cr); - g = y - (Bt601GreenCbMultiplier * cb) - (Bt601GreenCrMultiplier * cr); - b = y + (Bt601BlueCbMultiplier * cb); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ConvertBt601YCbCrToRgb( - Vector128 y, - Vector128 cb, - Vector128 cr, - out Vector128 r, - out Vector128 g, - out Vector128 b) - { - // Each lane contains one position from the three component planes. The nested green multiply-adds retain - // the existing SIMD order: blue chroma contributes first, then red chroma. - r = Vector128.MultiplyAddEstimate(cr, Vector128.Create(Bt601RedCrMultiplier), y); - Vector128 greenBase = Vector128.MultiplyAddEstimate(cb, Vector128.Create(-Bt601GreenCbMultiplier), y); - g = Vector128.MultiplyAddEstimate(cr, Vector128.Create(-Bt601GreenCrMultiplier), greenBase); - b = Vector128.MultiplyAddEstimate(cb, Vector128.Create(Bt601BlueCbMultiplier), y); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ConvertBt601YCbCrToRgb( - Vector256 y, - Vector256 cb, - Vector256 cr, - out Vector256 r, - out Vector256 g, - out Vector256 b) - { - r = Vector256.MultiplyAddEstimate(cr, Vector256.Create(Bt601RedCrMultiplier), y); - Vector256 greenBase = Vector256.MultiplyAddEstimate(cb, Vector256.Create(-Bt601GreenCbMultiplier), y); - g = Vector256.MultiplyAddEstimate(cr, Vector256.Create(-Bt601GreenCrMultiplier), greenBase); - b = Vector256.MultiplyAddEstimate(cb, Vector256.Create(Bt601BlueCbMultiplier), y); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ConvertBt601YCbCrToRgb( - Vector512 y, - Vector512 cb, - Vector512 cr, - out Vector512 r, - out Vector512 g, - out Vector512 b) - { - r = Vector512.MultiplyAddEstimate(cr, Vector512.Create(Bt601RedCrMultiplier), y); - Vector512 greenBase = Vector512.MultiplyAddEstimate(cb, Vector512.Create(-Bt601GreenCbMultiplier), y); - g = Vector512.MultiplyAddEstimate(cr, Vector512.Create(-Bt601GreenCrMultiplier), greenBase); - b = Vector512.MultiplyAddEstimate(cb, Vector512.Create(Bt601BlueCbMultiplier), y); - } - /// /// Gets unrounded, unsaturated luminance using ITU-R Recommendation BT.709. /// diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrOperator.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrOperator.cs index 8589593252..2b128df038 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrOperator.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrOperator.cs @@ -18,22 +18,22 @@ internal abstract partial class JpegColorConverterBase /// /// The BT.601 red contribution from centered Cr. /// - public const float RCrMult = ColorNumerics.Bt601RedCrMultiplier; + public const float RCrMult = 1.402F; /// /// The BT.601 green contribution from centered Cb. /// - public const float GCbMult = ColorNumerics.Bt601GreenCbMultiplier; + public const float GCbMult = (float)(0.114 * 1.772 / 0.587); /// /// The BT.601 green contribution from centered Cr. /// - public const float GCrMult = ColorNumerics.Bt601GreenCrMultiplier; + public const float GCrMult = (float)(0.299 * 1.402 / 0.587); /// /// The BT.601 blue contribution from centered Cb. /// - public const float BCbMult = ColorNumerics.Bt601BlueCbMultiplier; + public const float BCbMult = 1.772F; /// public static JpegColorSpace ColorSpace => JpegColorSpace.YCbCr; @@ -290,8 +290,15 @@ public static void ConvertToRgb(ref float c0, ref float c1, ref float c2, float [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void ConvertToRgb(ref float c0, ref float c1, ref float c2, float halfValue) { - // JPEG centers stored chroma here. Its outer conversion owns device normalization and storage rounding. - ColorNumerics.ConvertBt601YCbCrToRgb(c0, c1 - halfValue, c2 - halfValue, out c0, out c1, out c2); + float y = c0; + float cb = c1 - halfValue; + float cr = c2 - halfValue; + + // Each lane is an independent sample. ICC needs the fractional device values, while + // the ordinary JPEG wrapper above applies its existing integer rounding afterward. + c0 = y + (RCrMult * cr); + c1 = y - (GCbMult * cb) - (GCrMult * cr); + c2 = y + (BCbMult * cb); } /// @@ -323,8 +330,15 @@ public static void ConvertToRgb( [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void ConvertToRgb(ref Vector128 c0, ref Vector128 c1, ref Vector128 c2, Vector128 halfValue) { - // JPEG centers stored chroma here. Its outer conversion owns device normalization and storage rounding. - ColorNumerics.ConvertBt601YCbCrToRgb(c0, c1 - halfValue, c2 - halfValue, out c0, out c1, out c2); + Vector128 y = c0; + Vector128 cb = c1 - halfValue; + Vector128 cr = c2 - halfValue; + + // Each lane is an independent sample. ICC needs the fractional device values, while + // the ordinary JPEG wrapper above applies its existing integer rounding afterward. + c0 = Vector128.MultiplyAddEstimate(cr, Vector128.Create(RCrMult), y); + c1 = Vector128.MultiplyAddEstimate(cr, Vector128.Create(-GCrMult), Vector128.MultiplyAddEstimate(cb, Vector128.Create(-GCbMult), y)); + c2 = Vector128.MultiplyAddEstimate(cb, Vector128.Create(BCbMult), y); } /// @@ -356,8 +370,15 @@ public static void ConvertToRgb( [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void ConvertToRgb(ref Vector256 c0, ref Vector256 c1, ref Vector256 c2, Vector256 halfValue) { - // JPEG centers stored chroma here. Its outer conversion owns device normalization and storage rounding. - ColorNumerics.ConvertBt601YCbCrToRgb(c0, c1 - halfValue, c2 - halfValue, out c0, out c1, out c2); + Vector256 y = c0; + Vector256 cb = c1 - halfValue; + Vector256 cr = c2 - halfValue; + + // Each lane is an independent sample. ICC needs the fractional device values, while + // the ordinary JPEG wrapper above applies its existing integer rounding afterward. + c0 = Vector256.MultiplyAddEstimate(cr, Vector256.Create(RCrMult), y); + c1 = Vector256.MultiplyAddEstimate(cr, Vector256.Create(-GCrMult), Vector256.MultiplyAddEstimate(cb, Vector256.Create(-GCbMult), y)); + c2 = Vector256.MultiplyAddEstimate(cb, Vector256.Create(BCbMult), y); } /// @@ -389,8 +410,15 @@ public static void ConvertToRgb( [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void ConvertToRgb(ref Vector512 c0, ref Vector512 c1, ref Vector512 c2, Vector512 halfValue) { - // JPEG centers stored chroma here. Its outer conversion owns device normalization and storage rounding. - ColorNumerics.ConvertBt601YCbCrToRgb(c0, c1 - halfValue, c2 - halfValue, out c0, out c1, out c2); + Vector512 y = c0; + Vector512 cb = c1 - halfValue; + Vector512 cr = c2 - halfValue; + + // Each lane is an independent sample. ICC needs the fractional device values, while + // the ordinary JPEG wrapper above applies its existing integer rounding afterward. + c0 = Vector512.MultiplyAddEstimate(cr, Vector512.Create(RCrMult), y); + c1 = Vector512.MultiplyAddEstimate(cr, Vector512.Create(-GCrMult), Vector512.MultiplyAddEstimate(cb, Vector512.Create(-GCbMult), y)); + c2 = Vector512.MultiplyAddEstimate(cb, Vector512.Create(BCbMult), y); } /// From 17da5493a6fb18b0c9b2068ed00bb0b074fb74bc Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Oct 2026 10:48:38 +1000 Subject: [PATCH 328/328] Share XYB conversion through color profiles and codec operators --- .../ColorProfiles/ColorConversionOptions.cs | 20 + .../ColorProfiles/KnownXybTransforms.cs | 29 + src/ImageSharp/ColorProfiles/Xyb.Operators.cs | 602 ++++++++++++++++++ src/ImageSharp/ColorProfiles/Xyb.cs | 246 +++++++ src/ImageSharp/ColorProfiles/XybTransform.cs | 131 ++++ .../Formats/Jxl/Cms/JxlOpsinConstants.cs | 31 +- .../Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs | 28 +- .../Decoder/JxlColorConverter.Operators.cs | 179 ++++++ .../Processing/Decoder/JxlColorConverter.cs | 178 ++++++ .../Processing/Decoder/JxlOpsinParameters.cs | 55 -- .../Decoder/JxlOutputEncodingInfo.cs | 11 +- .../Jxl/Processing/Decoder/JxlXybDecoder.cs | 270 +------- .../Encoder/Heuristics/JxlHeuristics.cs | 6 +- .../Jxl/Processing/Encoder/JxlXybEncoder.cs | 494 +++----------- .../Encoder/Noise/JxlPhotonNoise.cs | 9 +- .../Jxl/Processing/RenderPipeline/XybStage.cs | 150 ----- .../Processing/RenderPipeline/YCbCrStage.cs | 110 ---- .../Formats/Jxl/Processing/OpsinImageTests.cs | 206 +++--- 18 files changed, 1612 insertions(+), 1143 deletions(-) create mode 100644 src/ImageSharp/ColorProfiles/KnownXybTransforms.cs create mode 100644 src/ImageSharp/ColorProfiles/Xyb.Operators.cs create mode 100644 src/ImageSharp/ColorProfiles/Xyb.cs create mode 100644 src/ImageSharp/ColorProfiles/XybTransform.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlColorConverter.Operators.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlColorConverter.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOpsinParameters.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/XybStage.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/YCbCrStage.cs diff --git a/src/ImageSharp/ColorProfiles/ColorConversionOptions.cs b/src/ImageSharp/ColorProfiles/ColorConversionOptions.cs index a0fcd92df3..5e392d4d61 100644 --- a/src/ImageSharp/ColorProfiles/ColorConversionOptions.cs +++ b/src/ImageSharp/ColorProfiles/ColorConversionOptions.cs @@ -15,6 +15,8 @@ public class ColorConversionOptions { private Matrix4x4 adaptationMatrix; private YCbCrTransform yCbCrTransform; + private readonly XybTransform? sourceXybTransform; + private readonly XybTransform? targetXybTransform; /// /// Initializes a new instance of the class. @@ -50,6 +52,24 @@ public ColorConversionOptions() /// public RgbWorkingSpace TargetRgbWorkingSpace { get; init; } = KnownRgbWorkingSpaces.SRgb; + /// + /// Gets the transform used to convert source XYB samples to RGB. The default is . + /// + public XybTransform SourceXybTransform + { + get => this.sourceXybTransform ?? KnownXybTransforms.Default; + init => this.sourceXybTransform = value; + } + + /// + /// Gets the transform used to convert RGB to target XYB samples. The default is . + /// + public XybTransform TargetXybTransform + { + get => this.targetXybTransform ?? KnownXybTransforms.Default; + init => this.targetXybTransform = value; + } + /// /// Gets the YCbCr matrix to used to perform conversions from/to RGB. /// diff --git a/src/ImageSharp/ColorProfiles/KnownXybTransforms.cs b/src/ImageSharp/ColorProfiles/KnownXybTransforms.cs new file mode 100644 index 0000000000..875ce7fb57 --- /dev/null +++ b/src/ImageSharp/ColorProfiles/KnownXybTransforms.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.ColorProfiles; + +/// +/// Provides standard XYB color transforms. +/// +public static class KnownXybTransforms +{ + /// + /// Gets the standard JPEG XL absorbance transform with a reference intensity of 255 nits. + /// + public static XybTransform Default { get; } = new( + new Matrix4x4( + 0.30F, 0.23F, 0.24342268924547819F, 0F, + 1F - 0.078F - 0.30F, 1F - 0.078F - 0.23F, 0.20476744424496821F, 0F, + 0.078F, 0.078F, 1F - 0.24342268924547819F - 0.20476744424496821F, 0F, + 0F, 0F, 0F, 1F), + new Matrix4x4( + 11.031566901960783F, -3.254147380392157F, -3.6588512862745097F, 0F, + -9.866943921568629F, 4.418770392156863F, 2.7129230470588235F, 0F, + -0.16462299647058826F, -0.16462299647058826F, 1.9459282392156863F, 0F, + 0F, 0F, 0F, 1F), + new Vector3(-0.0037930732552754493F), + XybTransform.ReferenceIntensity); +} diff --git a/src/ImageSharp/ColorProfiles/Xyb.Operators.cs b/src/ImageSharp/ColorProfiles/Xyb.Operators.cs new file mode 100644 index 0000000000..6550c57e92 --- /dev/null +++ b/src/ImageSharp/ColorProfiles/Xyb.Operators.cs @@ -0,0 +1,602 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.ColorProfiles; + +/// +/// Provides the shared forward and inverse XYB arithmetic. Each SIMD lane is one independent color. +/// Matrices and cube-root biases are prepared before traversal. Codec rows and profile conversions use these same equations. +/// +public readonly partial struct Xyb +{ + /// + /// The IEEE single-precision mask that removes the sign bit before root normalization. + /// + private const uint MagnitudeMask = 0x7FFFFFFFU; + + /// + /// The IEEE single-precision fraction bits retained when rebuilding the normalized mantissa. + /// + private const uint MantissaMask = 0x007FFFFFU; + + /// + /// The IEEE bit pattern of the smallest positive normal float; smaller magnitudes need exponent normalization. + /// + private const uint MinimumNormalBits = 0x00800000U; + + /// + /// The number of fraction bits below the IEEE single-precision exponent field. + /// + private const int MantissaBits = 23; + + /// + /// The bias added to an IEEE single-precision normal exponent. + /// + private const int ExponentBias = 127; + + /// + /// The exact power-of-two shift that makes every nonzero subnormal float normal. + /// + private const int SubnormalExponentShift = 24; + + /// + /// The exact multiplier used before extracting a subnormal input's exponent. + /// + private const float SubnormalScale = 1 << SubnormalExponentShift; + + /// + /// The exponent offset divisible by three that makes all finite input exponents positive. + /// The smallest subnormal exponent is -149, so the adjusted exponent range is [1,277]. + /// + private const int ExponentOffset = 150; + + /// + /// The cube-root exponent offset removed after dividing the adjusted exponent by three. + /// + private const int ExponentQuotientOffset = ExponentOffset / 3; + + /// + /// The fixed-point reciprocal shift used for exact division by three over the adjusted exponent range. + /// + private const int ThirdShift = 16; + + /// + /// Ceil(2^16/3), which gives floor(v/3) after shifting for every integer v in [1,277]. + /// + private const int ThirdMultiplier = 21846; + + /// + /// The native bit-pattern bias used to seed the reciprocal cube root of a normalized mantissa. + /// + private const int ReciprocalRootSeedBias = 0x54800000; + + /// + /// The native exponent multiplier used by the reciprocal-root seed. + /// + private const int ReciprocalRootExponentMultiplier = 0x002AAAAA; + + /// + /// The rounded single-precision one-third coefficient used by native root refinement. + /// + private const float OneThird = 1F / 3F; + + /// + /// The rounded single-precision four-thirds coefficient used by native root refinement. + /// + private const float FourThirds = 4F / 3F; + + /// + /// The number of native reciprocal-root iterations before its final correction. + /// + private const int ReciprocalRootIterations = 3; + + /// + /// Applies the biased absorbance transform and opponent mix to one linear RGB sample. + /// + /// The finite red component in the pixel range. + /// The corresponding green component. + /// The corresponding blue component. + /// The intensity-scaled forward matrix in Vector3.Transform order. + /// The signed inverse biases, subtracted before root evaluation. + /// The signed cube roots added after root evaluation. + /// The X result. + /// The Y result. + /// The B result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void ConvertFromRgb( + float r, + float g, + float b, + in Matrix4x4 matrix, + Vector3 biases, + Vector3 cubeRootBiases, + out float x, + out float y, + out float blue) + { + // Preserve native B+bias, then G, then R multiply-add order. Moving bias outside that chain changes rounding near black. + float mixed0 = float.MultiplyAddEstimate(matrix.M31, b, -biases.X); + mixed0 = float.MultiplyAddEstimate(matrix.M21, g, mixed0); + mixed0 = float.MultiplyAddEstimate(matrix.M11, r, mixed0); + float mixed1 = float.MultiplyAddEstimate(matrix.M32, b, -biases.Y); + mixed1 = float.MultiplyAddEstimate(matrix.M22, g, mixed1); + mixed1 = float.MultiplyAddEstimate(matrix.M12, r, mixed1); + float mixed2 = float.MultiplyAddEstimate(matrix.M33, b, -biases.Z); + mixed2 = float.MultiplyAddEstimate(matrix.M23, g, mixed2); + mixed2 = float.MultiplyAddEstimate(matrix.M13, r, mixed2); + mixed0 = CubeRootAndAdd(MathF.Max(0F, mixed0), cubeRootBiases.X); + mixed1 = CubeRootAndAdd(MathF.Max(0F, mixed1), cubeRootBiases.Y); + mixed2 = CubeRootAndAdd(MathF.Max(0F, mixed2), cubeRootBiases.Z); + x = 0.5F * (mixed0 - mixed1); + y = 0.5F * (mixed0 + mixed1); + blue = mixed2; + } + + /// + /// Transforms 4 independent RGB samples with the same scalar coefficients and native evaluation order. + /// + /// The linear red lanes. + /// The linear green lanes. + /// The linear blue lanes. + /// The intensity-scaled forward matrix in Vector3.Transform order. + /// The signed inverse biases, subtracted before root evaluation. + /// The signed cube roots added after root evaluation. + /// The X lanes. + /// The Y lanes. + /// The B lanes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void ConvertFromRgb( + Vector128 r, + Vector128 g, + Vector128 b, + in Matrix4x4 matrix, + Vector3 biases, + Vector3 cubeRootBiases, + out Vector128 x, + out Vector128 y, + out Vector128 blue) + { + Vector128 mixed0 = Vector128.MultiplyAddEstimate(Vector128.Create(matrix.M31), b, Vector128.Create(-biases.X)); + mixed0 = Vector128.MultiplyAddEstimate(Vector128.Create(matrix.M21), g, mixed0); + mixed0 = Vector128.MultiplyAddEstimate(Vector128.Create(matrix.M11), r, mixed0); + + Vector128 mixed1 = Vector128.MultiplyAddEstimate(Vector128.Create(matrix.M32), b, Vector128.Create(-biases.Y)); + mixed1 = Vector128.MultiplyAddEstimate(Vector128.Create(matrix.M22), g, mixed1); + mixed1 = Vector128.MultiplyAddEstimate(Vector128.Create(matrix.M12), r, mixed1); + + Vector128 mixed2 = Vector128.MultiplyAddEstimate(Vector128.Create(matrix.M33), b, Vector128.Create(-biases.Z)); + mixed2 = Vector128.MultiplyAddEstimate(Vector128.Create(matrix.M23), g, mixed2); + mixed2 = Vector128.MultiplyAddEstimate(Vector128.Create(matrix.M13), r, mixed2); + + mixed0 = CubeRootAndAdd(Vector128.Max(Vector128.Zero, mixed0), Vector128.Create(cubeRootBiases.X)); + mixed1 = CubeRootAndAdd(Vector128.Max(Vector128.Zero, mixed1), Vector128.Create(cubeRootBiases.Y)); + mixed2 = CubeRootAndAdd(Vector128.Max(Vector128.Zero, mixed2), Vector128.Create(cubeRootBiases.Z)); + Vector128 half = Vector128.Create(0.5F); + x = half * (mixed0 - mixed1); + y = half * (mixed0 + mixed1); + blue = mixed2; + } + + /// + /// Transforms 8 independent RGB samples with the same scalar coefficients and native evaluation order. + /// + /// The linear red lanes. + /// The linear green lanes. + /// The linear blue lanes. + /// The intensity-scaled forward matrix in Vector3.Transform order. + /// The signed inverse biases, subtracted before root evaluation. + /// The signed cube roots added after root evaluation. + /// The X lanes. + /// The Y lanes. + /// The B lanes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void ConvertFromRgb( + Vector256 r, + Vector256 g, + Vector256 b, + in Matrix4x4 matrix, + Vector3 biases, + Vector3 cubeRootBiases, + out Vector256 x, + out Vector256 y, + out Vector256 blue) + { + Vector256 mixed0 = Vector256.MultiplyAddEstimate(Vector256.Create(matrix.M31), b, Vector256.Create(-biases.X)); + mixed0 = Vector256.MultiplyAddEstimate(Vector256.Create(matrix.M21), g, mixed0); + mixed0 = Vector256.MultiplyAddEstimate(Vector256.Create(matrix.M11), r, mixed0); + + Vector256 mixed1 = Vector256.MultiplyAddEstimate(Vector256.Create(matrix.M32), b, Vector256.Create(-biases.Y)); + mixed1 = Vector256.MultiplyAddEstimate(Vector256.Create(matrix.M22), g, mixed1); + mixed1 = Vector256.MultiplyAddEstimate(Vector256.Create(matrix.M12), r, mixed1); + + Vector256 mixed2 = Vector256.MultiplyAddEstimate(Vector256.Create(matrix.M33), b, Vector256.Create(-biases.Z)); + mixed2 = Vector256.MultiplyAddEstimate(Vector256.Create(matrix.M23), g, mixed2); + mixed2 = Vector256.MultiplyAddEstimate(Vector256.Create(matrix.M13), r, mixed2); + + mixed0 = CubeRootAndAdd(Vector256.Max(Vector256.Zero, mixed0), Vector256.Create(cubeRootBiases.X)); + mixed1 = CubeRootAndAdd(Vector256.Max(Vector256.Zero, mixed1), Vector256.Create(cubeRootBiases.Y)); + mixed2 = CubeRootAndAdd(Vector256.Max(Vector256.Zero, mixed2), Vector256.Create(cubeRootBiases.Z)); + Vector256 half = Vector256.Create(0.5F); + x = half * (mixed0 - mixed1); + y = half * (mixed0 + mixed1); + blue = mixed2; + } + + /// + /// Transforms 16 independent RGB samples with the same scalar coefficients and native evaluation order. + /// + /// The linear red lanes. + /// The linear green lanes. + /// The linear blue lanes. + /// The intensity-scaled forward matrix in Vector3.Transform order. + /// The signed inverse biases, subtracted before root evaluation. + /// The signed cube roots added after root evaluation. + /// The X lanes. + /// The Y lanes. + /// The B lanes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void ConvertFromRgb( + Vector512 r, + Vector512 g, + Vector512 b, + in Matrix4x4 matrix, + Vector3 biases, + Vector3 cubeRootBiases, + out Vector512 x, + out Vector512 y, + out Vector512 blue) + { + Vector512 mixed0 = Vector512.MultiplyAddEstimate(Vector512.Create(matrix.M31), b, Vector512.Create(-biases.X)); + mixed0 = Vector512.MultiplyAddEstimate(Vector512.Create(matrix.M21), g, mixed0); + mixed0 = Vector512.MultiplyAddEstimate(Vector512.Create(matrix.M11), r, mixed0); + + Vector512 mixed1 = Vector512.MultiplyAddEstimate(Vector512.Create(matrix.M32), b, Vector512.Create(-biases.Y)); + mixed1 = Vector512.MultiplyAddEstimate(Vector512.Create(matrix.M22), g, mixed1); + mixed1 = Vector512.MultiplyAddEstimate(Vector512.Create(matrix.M12), r, mixed1); + + Vector512 mixed2 = Vector512.MultiplyAddEstimate(Vector512.Create(matrix.M33), b, Vector512.Create(-biases.Z)); + mixed2 = Vector512.MultiplyAddEstimate(Vector512.Create(matrix.M23), g, mixed2); + mixed2 = Vector512.MultiplyAddEstimate(Vector512.Create(matrix.M13), r, mixed2); + + mixed0 = CubeRootAndAdd(Vector512.Max(Vector512.Zero, mixed0), Vector512.Create(cubeRootBiases.X)); + mixed1 = CubeRootAndAdd(Vector512.Max(Vector512.Zero, mixed1), Vector512.Create(cubeRootBiases.Y)); + mixed2 = CubeRootAndAdd(Vector512.Max(Vector512.Zero, mixed2), Vector512.Create(cubeRootBiases.Z)); + Vector512 half = Vector512.Create(0.5F); + x = half * (mixed0 - mixed1); + y = half * (mixed0 + mixed1); + blue = mixed2; + } + + /// + /// Applies the inverse opsin equations to one pixel in the same order as each SIMD lane. + /// + /// The X sample. + /// The Y sample. + /// The B sample. + /// The transposed, intensity-scaled inverse matrix. + /// The signed absorbance biases. + /// The signed cube roots of the negative biases. + /// The reconstructed red sample. + /// The reconstructed green sample. + /// The reconstructed blue sample. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void ConvertToRgb( + float x, + float y, + float b, + in Matrix4x4 matrix, + Vector3 biases, + Vector3 cubeRootBiases, + out float r, + out float g, + out float blue) + { + // Undo the opponent mix and the forward transform's cube-root offset before cubing. + float gammaR = (y + x) - cubeRootBiases.X; + float gammaG = (y - x) - cubeRootBiases.Y; + float gammaB = b - cubeRootBiases.Z; + float mixedR = float.MultiplyAddEstimate(gammaR * gammaR, gammaR, biases.X); + float mixedG = float.MultiplyAddEstimate(gammaG * gammaG, gammaG, biases.Y); + float mixedB = float.MultiplyAddEstimate(gammaB * gammaB, gammaB, biases.Z); + + // The runtime uses the same X, then Y, then Z multiply-add order as shared Transform3. + Vector3 linear = Vector3.Transform(new Vector3(mixedR, mixedG, mixedB), matrix); + r = linear.X; + g = linear.Y; + blue = linear.Z; + } + + /// + /// Reconstructs 4 independent pixels; each register holds one component in pixel order. + /// + /// The X samples. + /// The Y samples. + /// The B samples. + /// The transposed, intensity-scaled inverse matrix. + /// The signed absorbance biases, broadcast uniformly to all lanes. + /// The signed cube roots subtracted before cubing. + /// The reconstructed red samples. + /// The reconstructed green samples. + /// The reconstructed blue samples. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void ConvertToRgb( + Vector128 x, + Vector128 y, + Vector128 b, + in Matrix4x4 matrix, + Vector3 biases, + Vector3 cubeRootBiases, + out Vector128 r, + out Vector128 g, + out Vector128 blue) + { + Vector128 gammaR = (y + x) - Vector128.Create(cubeRootBiases.X); + Vector128 gammaG = (y - x) - Vector128.Create(cubeRootBiases.Y); + Vector128 gammaB = b - Vector128.Create(cubeRootBiases.Z); + Vector128 mixedR = Vector128.MultiplyAddEstimate(gammaR * gammaR, gammaR, Vector128.Create(biases.X)); + Vector128 mixedG = Vector128.MultiplyAddEstimate(gammaG * gammaG, gammaG, Vector128.Create(biases.Y)); + Vector128 mixedB = Vector128.MultiplyAddEstimate(gammaB * gammaB, gammaB, Vector128.Create(biases.Z)); + SimdUtils.Transform3(in matrix, mixedR, mixedG, mixedB, out r, out g, out blue); + } + + /// + /// Reconstructs 8 independent pixels; each register holds one component in pixel order. + /// + /// The X samples. + /// The Y samples. + /// The B samples. + /// The transposed, intensity-scaled inverse matrix. + /// The signed absorbance biases, broadcast uniformly to all lanes. + /// The signed cube roots subtracted before cubing. + /// The reconstructed red samples. + /// The reconstructed green samples. + /// The reconstructed blue samples. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void ConvertToRgb( + Vector256 x, + Vector256 y, + Vector256 b, + in Matrix4x4 matrix, + Vector3 biases, + Vector3 cubeRootBiases, + out Vector256 r, + out Vector256 g, + out Vector256 blue) + { + Vector256 gammaR = (y + x) - Vector256.Create(cubeRootBiases.X); + Vector256 gammaG = (y - x) - Vector256.Create(cubeRootBiases.Y); + Vector256 gammaB = b - Vector256.Create(cubeRootBiases.Z); + Vector256 mixedR = Vector256.MultiplyAddEstimate(gammaR * gammaR, gammaR, Vector256.Create(biases.X)); + Vector256 mixedG = Vector256.MultiplyAddEstimate(gammaG * gammaG, gammaG, Vector256.Create(biases.Y)); + Vector256 mixedB = Vector256.MultiplyAddEstimate(gammaB * gammaB, gammaB, Vector256.Create(biases.Z)); + SimdUtils.Transform3(in matrix, mixedR, mixedG, mixedB, out r, out g, out blue); + } + + /// + /// Reconstructs 16 independent pixels; each register holds one component in pixel order. + /// + /// The X samples. + /// The Y samples. + /// The B samples. + /// The transposed, intensity-scaled inverse matrix. + /// The signed absorbance biases, broadcast uniformly to all lanes. + /// The signed cube roots subtracted before cubing. + /// The reconstructed red samples. + /// The reconstructed green samples. + /// The reconstructed blue samples. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void ConvertToRgb( + Vector512 x, + Vector512 y, + Vector512 b, + in Matrix4x4 matrix, + Vector3 biases, + Vector3 cubeRootBiases, + out Vector512 r, + out Vector512 g, + out Vector512 blue) + { + Vector512 gammaR = (y + x) - Vector512.Create(cubeRootBiases.X); + Vector512 gammaG = (y - x) - Vector512.Create(cubeRootBiases.Y); + Vector512 gammaB = b - Vector512.Create(cubeRootBiases.Z); + Vector512 mixedR = Vector512.MultiplyAddEstimate(gammaR * gammaR, gammaR, Vector512.Create(biases.X)); + Vector512 mixedG = Vector512.MultiplyAddEstimate(gammaG * gammaG, gammaG, Vector512.Create(biases.Y)); + Vector512 mixedB = Vector512.MultiplyAddEstimate(gammaB * gammaB, gammaB, Vector512.Create(biases.Z)); + SimdUtils.Transform3(in matrix, mixedR, mixedG, mixedB, out r, out g, out blue); + } + + /// + /// Computes a nonnegative finite cube root plus its bias without overflowing reciprocal-root intermediates. + /// + /// The nonnegative finite absorbance, including zero and subnormal values. + /// The negative cube-root bias. + /// The biased root with the final addition performed as a multiply-add estimate. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float CubeRootAndAdd(float value, float add) + { + uint bits = BitConverter.SingleToUInt32Bits(value) & MagnitudeMask; + bool subnormal = bits < MinimumNormalBits; + float normalized = BitConverter.UInt32BitsToSingle(bits) * (subnormal ? SubnormalScale : 1F); + uint normalizedBits = BitConverter.SingleToUInt32Bits(normalized); + + // Standard subnormal preparation multiplies by 2^24, then corrects the extracted exponent. + // Zero has no exponent: substitute exponent zero before division and use final scale zero. + int exponent = bits == 0 ? 0 : (int)(normalizedBits >> MantissaBits) - ExponentBias - (subnormal ? SubnormalExponentShift : 0); + int positiveExponent = exponent + ExponentOffset; + + // Adding 150 keeps the exponent in [1,277] and preserves its remainder modulo three. + // Multiplication by ceil(2^16/3), then shifting 16 bits, gives the exact integer quotient throughout that interval. + int quotient = (positiveExponent * ThirdMultiplier) >> ThirdShift; + int remainder = positiveExponent - (3 * quotient); + float z = BitConverter.Int32BitsToSingle((int)(normalizedBits & MantissaMask) | ((ExponentBias + remainder) << MantissaBits)); + float scale = bits == 0 ? 0F : BitConverter.Int32BitsToSingle((quotient + ExponentBias - ExponentQuotientOffset) << MantissaBits); + float reciprocalRoot = BitConverter.Int32BitsToSingle(ReciprocalRootSeedBias - ((ExponentBias + remainder) * ReciprocalRootExponentMultiplier)); + float thirdZ = OneThird * z; + + // x=z*2^(3k), where 1<=z<8 and k=quotient-ExponentQuotientOffset. Reciprocal roots and their fourth powers stay in a normal, bounded range. + // Native reciprocal-root refinements now see only that range. They never see tiny normals or subnormal absorbance directly. + for (int i = 0; i < ReciprocalRootIterations; i++) + { + float squared = reciprocalRoot * reciprocalRoot; + reciprocalRoot = float.MultiplyAddEstimate(-thirdZ, squared * squared, FourThirds * reciprocalRoot); + } + + float squaredRoot = reciprocalRoot * reciprocalRoot; + reciprocalRoot = float.MultiplyAddEstimate(OneThird, float.MultiplyAddEstimate(-z, squaredRoot * squaredRoot, reciprocalRoot), reciprocalRoot); + squaredRoot = reciprocalRoot * reciprocalRoot; + + // Scaling a normal coefficient by 2^k is exact. Keep it inside the last multiply-add so bias cancellation gets no extra rounded root. + return float.MultiplyAddEstimate(squaredRoot * scale, z, add); + } + + /// + /// Evaluates bounded reciprocal-root refinements independently in each nonnegative finite absorbance lane. + /// + /// The absorbance lanes, including zero and subnormal values. + /// The corresponding negative cube-root biases. + /// The biased roots, with no scalar lane calls or temporary sample storage. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 CubeRootAndAdd(Vector128 value, Vector128 add) + { + Vector128 bits = value.AsUInt32() & Vector128.Create(MagnitudeMask); + Vector128 zero = Vector128.Equals(bits, Vector128.Zero); + Vector128 subnormal = Vector128.LessThan(bits, Vector128.Create(MinimumNormalBits)); + Vector128 normalization = Vector128.ConditionalSelect(subnormal.AsSingle(), Vector128.Create(SubnormalScale), Vector128.One); + Vector128 normalizedBits = (bits.AsSingle() * normalization).AsUInt32(); + Vector128 exponent = Vector128.ShiftRightLogical(normalizedBits, MantissaBits).AsInt32() - Vector128.Create(ExponentBias); + exponent -= Vector128.ConditionalSelect(subnormal.AsInt32(), Vector128.Create(SubnormalExponentShift), Vector128.Zero); + exponent = Vector128.ConditionalSelect(zero.AsInt32(), Vector128.Zero, exponent); + Vector128 positiveExponent = exponent + Vector128.Create(ExponentOffset); + + // For positiveExponent in [1,277], floor(v/3)=(v*21846)>>16. 21846=ceil(2^16/3). + // The reciprocal error is below 1/3, so it cannot cross the next quotient. All products fit Int32 and remain SIMD. + Vector128 quotient = Vector128.ShiftRightArithmetic(positiveExponent * Vector128.Create(ThirdMultiplier), ThirdShift); + Vector128 remainder = positiveExponent - (quotient * Vector128.Create(3)); + Vector128 normalExponent = remainder + Vector128.Create(ExponentBias); + Vector128 mantissa = normalizedBits & Vector128.Create(MantissaMask); + Vector128 z = (mantissa | Vector128.ShiftLeft(normalExponent, MantissaBits).AsUInt32()).AsSingle(); + Vector128 scale = Vector128.ShiftLeft(quotient + Vector128.Create(ExponentBias - ExponentQuotientOffset), MantissaBits).AsSingle(); + scale = Vector128.ConditionalSelect(zero.AsSingle(), Vector128.Zero, scale); + Vector128 seed = Vector128.Create(ReciprocalRootSeedBias) - (normalExponent * Vector128.Create(ReciprocalRootExponentMultiplier)); + Vector128 reciprocalRoot = seed.AsSingle(); + Vector128 thirdZ = Vector128.Create(OneThird) * z; + + // Every z is in [1,8), including zero lanes whose final scale is zero. The iteration matches the scalar arithmetic order. + for (int i = 0; i < ReciprocalRootIterations; i++) + { + Vector128 squared = reciprocalRoot * reciprocalRoot; + reciprocalRoot = Vector128.MultiplyAddEstimate(-thirdZ, squared * squared, Vector128.Create(FourThirds) * reciprocalRoot); + } + + Vector128 squaredRoot = reciprocalRoot * reciprocalRoot; + reciprocalRoot = Vector128.MultiplyAddEstimate( + Vector128.Create(OneThird), + Vector128.MultiplyAddEstimate(-z, squaredRoot * squaredRoot, reciprocalRoot), + reciprocalRoot); + + squaredRoot = reciprocalRoot * reciprocalRoot; + return Vector128.MultiplyAddEstimate(squaredRoot * scale, z, add); + } + + /// + /// Evaluates bounded reciprocal-root refinements independently in each nonnegative finite absorbance lane. + /// + /// The absorbance lanes, including zero and subnormal values. + /// The corresponding negative cube-root biases. + /// The biased roots, with no scalar lane calls or temporary sample storage. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 CubeRootAndAdd(Vector256 value, Vector256 add) + { + Vector256 bits = value.AsUInt32() & Vector256.Create(MagnitudeMask); + Vector256 zero = Vector256.Equals(bits, Vector256.Zero); + Vector256 subnormal = Vector256.LessThan(bits, Vector256.Create(MinimumNormalBits)); + Vector256 normalization = Vector256.ConditionalSelect(subnormal.AsSingle(), Vector256.Create(SubnormalScale), Vector256.One); + Vector256 normalizedBits = (bits.AsSingle() * normalization).AsUInt32(); + Vector256 exponent = Vector256.ShiftRightLogical(normalizedBits, MantissaBits).AsInt32() - Vector256.Create(ExponentBias); + exponent -= Vector256.ConditionalSelect(subnormal.AsInt32(), Vector256.Create(SubnormalExponentShift), Vector256.Zero); + exponent = Vector256.ConditionalSelect(zero.AsInt32(), Vector256.Zero, exponent); + Vector256 positiveExponent = exponent + Vector256.Create(ExponentOffset); + + // For positiveExponent in [1,277], floor(v/3)=(v*21846)>>16. 21846=ceil(2^16/3). + // The reciprocal error is below 1/3, so it cannot cross the next quotient. All products fit Int32 and remain SIMD. + Vector256 quotient = Vector256.ShiftRightArithmetic(positiveExponent * Vector256.Create(ThirdMultiplier), ThirdShift); + Vector256 remainder = positiveExponent - (quotient * Vector256.Create(3)); + Vector256 normalExponent = remainder + Vector256.Create(ExponentBias); + Vector256 mantissa = normalizedBits & Vector256.Create(MantissaMask); + Vector256 z = (mantissa | Vector256.ShiftLeft(normalExponent, MantissaBits).AsUInt32()).AsSingle(); + Vector256 scale = Vector256.ShiftLeft(quotient + Vector256.Create(ExponentBias - ExponentQuotientOffset), MantissaBits).AsSingle(); + scale = Vector256.ConditionalSelect(zero.AsSingle(), Vector256.Zero, scale); + Vector256 seed = Vector256.Create(ReciprocalRootSeedBias) - (normalExponent * Vector256.Create(ReciprocalRootExponentMultiplier)); + Vector256 reciprocalRoot = seed.AsSingle(); + Vector256 thirdZ = Vector256.Create(OneThird) * z; + + // Every z is in [1,8), including zero lanes whose final scale is zero. The iteration matches the scalar arithmetic order. + for (int i = 0; i < ReciprocalRootIterations; i++) + { + Vector256 squared = reciprocalRoot * reciprocalRoot; + reciprocalRoot = Vector256.MultiplyAddEstimate(-thirdZ, squared * squared, Vector256.Create(FourThirds) * reciprocalRoot); + } + + Vector256 squaredRoot = reciprocalRoot * reciprocalRoot; + reciprocalRoot = Vector256.MultiplyAddEstimate( + Vector256.Create(OneThird), + Vector256.MultiplyAddEstimate(-z, squaredRoot * squaredRoot, reciprocalRoot), + reciprocalRoot); + + squaredRoot = reciprocalRoot * reciprocalRoot; + return Vector256.MultiplyAddEstimate(squaredRoot * scale, z, add); + } + + /// + /// Evaluates bounded reciprocal-root refinements independently in each nonnegative finite absorbance lane. + /// + /// The absorbance lanes, including zero and subnormal values. + /// The corresponding negative cube-root biases. + /// The biased roots, with no scalar lane calls or temporary sample storage. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 CubeRootAndAdd(Vector512 value, Vector512 add) + { + Vector512 bits = value.AsUInt32() & Vector512.Create(MagnitudeMask); + Vector512 zero = Vector512.Equals(bits, Vector512.Zero); + Vector512 subnormal = Vector512.LessThan(bits, Vector512.Create(MinimumNormalBits)); + Vector512 normalization = Vector512.ConditionalSelect(subnormal.AsSingle(), Vector512.Create(SubnormalScale), Vector512.One); + Vector512 normalizedBits = (bits.AsSingle() * normalization).AsUInt32(); + Vector512 exponent = Vector512.ShiftRightLogical(normalizedBits, MantissaBits).AsInt32() - Vector512.Create(ExponentBias); + exponent -= Vector512.ConditionalSelect(subnormal.AsInt32(), Vector512.Create(SubnormalExponentShift), Vector512.Zero); + exponent = Vector512.ConditionalSelect(zero.AsInt32(), Vector512.Zero, exponent); + Vector512 positiveExponent = exponent + Vector512.Create(ExponentOffset); + + // For positiveExponent in [1,277], floor(v/3)=(v*21846)>>16. 21846=ceil(2^16/3). + // The reciprocal error is below 1/3, so it cannot cross the next quotient. All products fit Int32 and remain SIMD. + Vector512 quotient = Vector512.ShiftRightArithmetic(positiveExponent * Vector512.Create(ThirdMultiplier), ThirdShift); + Vector512 remainder = positiveExponent - (quotient * Vector512.Create(3)); + Vector512 normalExponent = remainder + Vector512.Create(ExponentBias); + Vector512 mantissa = normalizedBits & Vector512.Create(MantissaMask); + Vector512 z = (mantissa | Vector512.ShiftLeft(normalExponent, MantissaBits).AsUInt32()).AsSingle(); + Vector512 scale = Vector512.ShiftLeft(quotient + Vector512.Create(ExponentBias - ExponentQuotientOffset), MantissaBits).AsSingle(); + scale = Vector512.ConditionalSelect(zero.AsSingle(), Vector512.Zero, scale); + Vector512 seed = Vector512.Create(ReciprocalRootSeedBias) - (normalExponent * Vector512.Create(ReciprocalRootExponentMultiplier)); + Vector512 reciprocalRoot = seed.AsSingle(); + Vector512 thirdZ = Vector512.Create(OneThird) * z; + + // Every z is in [1,8), including zero lanes whose final scale is zero. The iteration matches the scalar arithmetic order. + for (int i = 0; i < ReciprocalRootIterations; i++) + { + Vector512 squared = reciprocalRoot * reciprocalRoot; + reciprocalRoot = Vector512.MultiplyAddEstimate(-thirdZ, squared * squared, Vector512.Create(FourThirds) * reciprocalRoot); + } + + Vector512 squaredRoot = reciprocalRoot * reciprocalRoot; + reciprocalRoot = Vector512.MultiplyAddEstimate( + Vector512.Create(OneThird), + Vector512.MultiplyAddEstimate(-z, squaredRoot * squaredRoot, reciprocalRoot), + reciprocalRoot); + + squaredRoot = reciprocalRoot * reciprocalRoot; + return Vector512.MultiplyAddEstimate(squaredRoot * scale, z, add); + } +} diff --git a/src/ImageSharp/ColorProfiles/Xyb.cs b/src/ImageSharp/ColorProfiles/Xyb.cs new file mode 100644 index 0000000000..a4de526b4e --- /dev/null +++ b/src/ImageSharp/ColorProfiles/Xyb.cs @@ -0,0 +1,246 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.ColorProfiles; + +/// +/// Represents an XYB color with opponent X and Y components and a blue absorbance component. +/// Component values are not limited to the interval from zero to one. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly partial struct Xyb : IColorProfile +{ + /// + /// Initializes a new instance of the struct. + /// + /// The X opponent component. + /// The Y opponent component. + /// The blue absorbance component. + public Xyb(float x, float y, float b) + { + this.X = x; + this.Y = y; + this.B = b; + } + + /// + /// Initializes a new instance of the struct from three components. + /// + /// The X, Y and B components. + public Xyb(Vector3 source) + : this(source.X, source.Y, source.Z) + { + } + + /// + /// Gets the X opponent component. + /// + public float X { get; } + + /// + /// Gets the Y opponent component. + /// + public float Y { get; } + + /// + /// Gets the blue absorbance component. + /// + public float B { get; } + + /// + /// Compares two XYB colors for equality. + /// + /// The first color. + /// The second color. + /// Whether all three components are equal. + public static bool operator ==(Xyb left, Xyb right) => left.Equals(right); + + /// + /// Compares two XYB colors for inequality. + /// + /// The first color. + /// The second color. + /// Whether any component differs. + public static bool operator !=(Xyb left, Xyb right) => !left.Equals(right); + + /// + public Vector4 ToScaledVector4() => new(this.X, this.Y, this.B, 1F); + + /// + public static Xyb FromScaledVector4(Vector4 source) => new(source.AsVector3()); + + /// + public static void ToScaledVector4(ReadOnlySpan source, Span destination) + { + // XYB and RGB are both three unbounded floats. Reuse the existing bit-preserving layout conversion. + Rgb.ToScaledVector4(MemoryMarshal.Cast(source), destination); + } + + /// + public static void FromScaledVector4(ReadOnlySpan source, Span destination) + => Rgb.FromScaledVector4(source, MemoryMarshal.Cast(destination)); + + /// + public Rgb ToProfileConnectingSpace(ColorConversionOptions options) + { + XybTransform transform = options.SourceXybTransform; + Matrix4x4 matrix = transform.ScaledInverse; + ConvertToRgb(this.X, this.Y, this.B, in matrix, transform.Biases, transform.CubeRootBiases, out float r, out float g, out float b); + + // The RGB connection space uses the selected working space's transfer function, not raw linear samples. + return Rgb.FromScaledVector4(options.SourceRgbWorkingSpace.Compress(new Vector4(r, g, b, 1F))); + } + + /// + public static Xyb FromProfileConnectingSpace(ColorConversionOptions options, in Rgb source) + { + XybTransform transform = options.TargetXybTransform; + Matrix4x4 matrix = transform.ScaledForward; + Vector4 linear = options.TargetRgbWorkingSpace.Expand(source.ToScaledVector4()); + ConvertFromRgb(linear.X, linear.Y, linear.Z, in matrix, transform.Biases, transform.CubeRootBiases, out float x, out float y, out float b); + return new(x, y, b); + } + + /// + public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + XybTransform transform = options.SourceXybTransform; + Matrix4x4 matrix = transform.ScaledInverse; + Vector3 biases = transform.Biases; + Vector3 cubeRootBiases = transform.CubeRootBiases; + InlineArray16 storage = default; + Span buffer = storage; + + // One bounded stack block holds linear RGBA values for the working-space API. It is reused for every batch. + // Each batch reads its XYB input before publishing RGB, so source and destination can have the same start. + for (int offset = 0; offset < source.Length; offset += 16) + { + int count = Math.Min(16, source.Length - offset); + ReadOnlySpan packed = MemoryMarshal.Cast(source.Slice(offset, count)); + Span colors = buffer[..count]; + int i = 0; + + if (Vector512.IsHardwareAccelerated) + { + for (; i <= count - 16; i += 16) + { + SimdUtils.Deinterleave3(packed, i, out Vector512 x, out Vector512 y, out Vector512 b); + ConvertToRgb(x, y, b, in matrix, biases, cubeRootBiases, out Vector512 r, out Vector512 g, out Vector512 blue); + SimdUtils.Interleave4(r, g, blue, Vector512.One, ref Unsafe.As(ref colors[i])); + } + } + + if (Vector256.IsHardwareAccelerated) + { + for (; i <= count - 8; i += 8) + { + SimdUtils.Deinterleave3(packed, i, out Vector256 x, out Vector256 y, out Vector256 b); + ConvertToRgb(x, y, b, in matrix, biases, cubeRootBiases, out Vector256 r, out Vector256 g, out Vector256 blue); + SimdUtils.Interleave4(r, g, blue, Vector256.One, ref Unsafe.As(ref colors[i])); + } + } + + if (Vector128.IsHardwareAccelerated) + { + for (; i <= count - 4; i += 4) + { + SimdUtils.Deinterleave3(packed, i, out Vector128 x, out Vector128 y, out Vector128 b); + ConvertToRgb(x, y, b, in matrix, biases, cubeRootBiases, out Vector128 r, out Vector128 g, out Vector128 blue); + SimdUtils.Interleave4(r, g, blue, Vector128.One, ref Unsafe.As(ref colors[i])); + } + } + + for (; i < count; i++) + { + Vector3 value = packed[i]; + ConvertToRgb(value.X, value.Y, value.Z, in matrix, biases, cubeRootBiases, out float r, out float g, out float b); + colors[i] = new(r, g, b, 1F); + } + + options.SourceRgbWorkingSpace.Compress(colors); + Rgb.FromScaledVector4(colors, destination.Slice(offset, count)); + } + } + + /// + public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + XybTransform transform = options.TargetXybTransform; + Matrix4x4 matrix = transform.ScaledForward; + Vector3 biases = transform.Biases; + Vector3 cubeRootBiases = transform.CubeRootBiases; + InlineArray16 storage = default; + Span buffer = storage; + + // Expand one complete RGB batch before applying the nonlinear absorbance transform. + // Each register holds the same component from consecutive colors. Narrower widths finish the batch without padding reads. + for (int offset = 0; offset < source.Length; offset += 16) + { + int count = Math.Min(16, source.Length - offset); + Span colors = buffer[..count]; + Rgb.ToScaledVector4(source.Slice(offset, count), colors); + options.TargetRgbWorkingSpace.Expand(colors); + int i = 0; + + if (Vector512.IsHardwareAccelerated) + { + for (; i <= count - 16; i += 16) + { + SimdUtils.Deinterleave4(colors, i, out Vector512 r, out Vector512 g, out Vector512 b, out _); + ConvertFromRgb(r, g, b, in matrix, biases, cubeRootBiases, out Vector512 x, out Vector512 y, out Vector512 blue); + SimdUtils.Interleave3(x, y, blue, ref Unsafe.As(ref destination[offset + i])); + } + } + + if (Vector256.IsHardwareAccelerated) + { + for (; i <= count - 8; i += 8) + { + SimdUtils.Deinterleave4(colors, i, out Vector256 r, out Vector256 g, out Vector256 b, out _); + ConvertFromRgb(r, g, b, in matrix, biases, cubeRootBiases, out Vector256 x, out Vector256 y, out Vector256 blue); + SimdUtils.Interleave3(x, y, blue, ref Unsafe.As(ref destination[offset + i])); + } + } + + if (Vector128.IsHardwareAccelerated) + { + for (; i <= count - 4; i += 4) + { + SimdUtils.Deinterleave4(colors, i, out Vector128 r, out Vector128 g, out Vector128 b, out _); + ConvertFromRgb(r, g, b, in matrix, biases, cubeRootBiases, out Vector128 x, out Vector128 y, out Vector128 blue); + SimdUtils.Interleave3(x, y, blue, ref Unsafe.As(ref destination[offset + i])); + } + } + + for (; i < count; i++) + { + Vector4 linear = colors[i]; + ConvertFromRgb(linear.X, linear.Y, linear.Z, in matrix, biases, cubeRootBiases, out float x, out float y, out float b); + destination[offset + i] = new(x, y, b); + } + } + } + + /// + public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource() + => ChromaticAdaptionWhitePointSource.RgbWorkingSpace; + + /// + public bool Equals(Xyb other) => this.X == other.X && this.Y == other.Y && this.B == other.B; + + /// + public override bool Equals(object? obj) => obj is Xyb other && this.Equals(other); + + /// + public override int GetHashCode() => HashCode.Combine(this.X, this.Y, this.B); + + /// + public override string ToString() => FormattableString.Invariant($"Xyb({this.X}, {this.Y}, {this.B})"); +} diff --git a/src/ImageSharp/ColorProfiles/XybTransform.cs b/src/ImageSharp/ColorProfiles/XybTransform.cs new file mode 100644 index 0000000000..4e091099f1 --- /dev/null +++ b/src/ImageSharp/ColorProfiles/XybTransform.cs @@ -0,0 +1,131 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.ColorProfiles; + +/// +/// Defines the absorbance matrices, signed biases and reference intensity used by an XYB color transform. +/// +/// +/// Only the upper-left three by three coefficients of each supplied matrix are used. +/// Translation and the fourth component do not form part of the absorbance transform. +/// +public sealed class XybTransform +{ + /// + /// The reference intensity, in nits, at which the unscaled absorbance matrices operate. + /// + internal const float ReferenceIntensity = 255F; + + /// + /// The supplied forward matrix, when one is available. Decoder metadata needs only the inverse matrix. + /// + private readonly Matrix4x4? forward; + + /// + /// Initializes a new instance of the class from an inverse absorbance matrix. + /// + /// + /// The unscaled absorbance-to-linear-RGB coefficients, in order. + /// Only the upper-left three by three coefficients are used. + /// + /// The signed biases added after cubing during inverse conversion. + /// The positive finite reference intensity, in nits. + /// The reference intensity is not positive and finite. + public XybTransform(Matrix4x4 inverse, Vector3 biases, float intensityTarget) + { + if (!(intensityTarget > 0F && float.IsFinite(intensityTarget))) + { + throw new ArgumentOutOfRangeException(nameof(intensityTarget), "The reference intensity must be positive and finite."); + } + + // Absorbance has three components and no translation. Complete the unused fourth dimension with an identity axis, + // so a valid three by three matrix remains invertible even when the caller supplied zero for M44. + inverse.M14 = inverse.M24 = inverse.M34 = 0F; + inverse.M41 = inverse.M42 = inverse.M43 = 0F; + inverse.M44 = 1F; + this.forward = null; + this.Inverse = inverse; + this.Biases = biases; + this.IntensityTarget = intensityTarget; + this.CubeRootBiases = new(MathF.Cbrt(biases.X), MathF.Cbrt(biases.Y), MathF.Cbrt(biases.Z)); + } + + /// + /// Initializes a new instance of the class with both absorbance matrices. + /// + /// + /// The unscaled linear-RGB-to-absorbance coefficients, in order. + /// Only the upper-left three by three coefficients are used. + /// + /// The unscaled absorbance-to-linear-RGB coefficients in the same three by three matrix convention. + /// The signed biases added after cubing during inverse conversion. + /// The positive finite reference intensity, in nits. + /// The reference intensity is not positive and finite. + public XybTransform(Matrix4x4 forward, Matrix4x4 inverse, Vector3 biases, float intensityTarget) + : this(inverse, biases, intensityTarget) + { + // Frozen encoder coefficients need not equal a numerical inverse of rounded decoder coefficients. + // Give the supplied forward coefficients the same translation-free matrix layout as the inverse coefficients. + forward.M14 = forward.M24 = forward.M34 = 0F; + forward.M41 = forward.M42 = forward.M43 = 0F; + forward.M44 = 1F; + this.forward = forward; + } + + /// + /// Gets the unscaled linear-RGB-to-absorbance matrix. + /// + /// No forward matrix was supplied and the inverse matrix is singular. + public Matrix4x4 Forward + { + get + { + if (this.forward.HasValue) + { + return this.forward.Value; + } + + // An inverse-only source remains valid for decoding, even if its matrix is singular. + // Only an operation that requests forward conversion needs a mathematical inverse. + if (!Matrix4x4.Invert(this.Inverse, out Matrix4x4 result)) + { + throw new InvalidOperationException("The inverse XYB matrix is singular and has no forward transform."); + } + + return result; + } + } + + /// + /// Gets the unscaled absorbance-to-linear-RGB matrix. + /// + public Matrix4x4 Inverse { get; } + + /// + /// Gets the signed biases added after cubing during inverse conversion. + /// + public Vector3 Biases { get; } + + /// + /// Gets the reference intensity, in nits. + /// + public float IntensityTarget { get; } + + /// + /// Gets the signed cube roots removed before cubing and added after forward root evaluation. + /// + internal Vector3 CubeRootBiases { get; } + + /// + /// Gets the inverse matrix scaled to relative RGB, where one equals the reference intensity. + /// + internal Matrix4x4 ScaledInverse => this.Inverse * (ReferenceIntensity / this.IntensityTarget); + + /// + /// Gets the forward matrix scaled from relative RGB to the absolute XYB absorbance domain. + /// + internal Matrix4x4 ScaledForward => this.Forward * (this.IntensityTarget / ReferenceIntensity); +} diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlOpsinConstants.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlOpsinConstants.cs index 981c0a9456..76ba363ab9 100644 --- a/src/ImageSharp/Formats/Jxl/Cms/JxlOpsinConstants.cs +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlOpsinConstants.cs @@ -17,34 +17,13 @@ internal static class JxlOpsinConstants public const float YToBRatio = 1f; public const float BToYRatio = 1f / YToBRatio; - // Adjusting these constants influences the opsin absorbance. - public const float OpsinAbsorbanceBias0 = 0.0037930732552754493f; - public const float OpsinAbsorbanceBias1 = OpsinAbsorbanceBias0; - public const float OpsinAbsorbanceBias2 = OpsinAbsorbanceBias0; - - public static ReadOnlySpan OpsinAbsorbanceBias => [OpsinAbsorbanceBias0, OpsinAbsorbanceBias1, OpsinAbsorbanceBias2]; - - /// Gets the frozen forward absorbance matrix in row-major order. - public static ReadOnlySpan OpsinAbsorbanceMatrix => - [ - 0.30F, 1F - 0.078F - 0.30F, 0.078F, - 0.23F, 1F - 0.078F - 0.23F, 0.078F, - 0.24342268924547819F, 0.20476744424496821F, 1F - 0.24342268924547819F - 0.20476744424496821F - ]; - - /// Gets the output scales for X, Y and B minus Y in the native scaled-XYB representation. + /// + /// Gets the output scales for X, Y and B minus Y in the native scaled-XYB representation. + /// public static ReadOnlySpan ScaledXybScale => [22.995788804F, 1.183000077F, 1.502141333F]; - /// Gets the offsets added to X, Y and B minus Y before scaled-XYB multiplication. - public static ReadOnlySpan ScaledXybOffset => [0.015386134F, 0F, 0.27770459F]; - /// - /// Gets the frozen default inverse opsin matrix in row-major order, as defined by the JPEG XL bitstream. + /// Gets the offsets added to X, Y and B minus Y before scaled-XYB multiplication. /// - public static ReadOnlySpan DefaultInverseOpsinAbsorbanceMatrix => - [ - 11.031566901960783f, -9.866943921568629f, -0.16462299647058826f, - -3.254147380392157f, 4.418770392156863f, -0.16462299647058826f, - -3.6588512862745097f, 2.7129230470588235f, 1.9459282392156863f - ]; + public static ReadOnlySpan ScaledXybOffset => [0.015386134F, 0F, 0.27770459F]; } diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs index a8793106ac..c4ed94920c 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs @@ -1,7 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.Cms; +using System.Numerics; +using SixLabors.ImageSharp.ColorProfiles; using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; @@ -24,7 +25,7 @@ internal sealed class JxlOpsinInverseMatrix : IJxlFields public JxlOpsinInverseMatrix() => JxlBundle.Init(this); /// - /// Gets or sets the three negative opsin absorbance biases, in channel order. + /// Gets or sets the three signed opsin absorbance biases, in channel order. /// public InlineArray3 OpsinBiases { @@ -59,6 +60,23 @@ public JxlMatrix3x3F InverseMatrix set => this.inverseMatrix = value; } + /// + /// Creates the inverse color transform from the parsed matrix, biases and original reference intensity. + /// + /// The positive reference intensity established by the image header, in nits. + /// The shared transform with the wire matrix transposed to Vector3.Transform order. + public XybTransform CreateTransform(float intensityTarget) + { + ReadOnlySpan values = this.inverseMatrix.AsReadOnlySpan(); + Matrix4x4 matrix = new( + values[0], values[3], values[6], 0F, + values[1], values[4], values[7], 0F, + values[2], values[5], values[8], 0F, + 0F, 0F, 0F, 1F); + + return new(matrix, new Vector3(this.opsinBiases[0], this.opsinBiases[1], this.opsinBiases[2]), intensityTarget); + } + /// public bool Visit(JxlVisitor visitor) { @@ -69,12 +87,12 @@ public bool Visit(JxlVisitor visitor) } // The default matrix is frozen by the format. Recomputing an inverse can produce different rounded coefficients. - ReadOnlySpan defaultInverse = JxlOpsinConstants.DefaultInverseOpsinAbsorbanceMatrix; + Matrix4x4 defaultInverse = KnownXybTransforms.Default.Inverse; Span inverse = this.inverseMatrix.AsSpan(); for (int i = 0; i < inverse.Length; i++) { - if (!visitor.F16(defaultInverse[i], ref inverse[i])) + if (!visitor.F16(defaultInverse[i % 3, i / 3], ref inverse[i])) { return false; } @@ -82,7 +100,7 @@ public bool Visit(JxlVisitor visitor) for (int i = 0; i < 3; i++) { - if (!visitor.F16(-JxlOpsinConstants.OpsinAbsorbanceBias[i], ref this.opsinBiases[i])) + if (!visitor.F16(KnownXybTransforms.Default.Biases[i], ref this.opsinBiases[i])) { return false; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlColorConverter.Operators.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlColorConverter.Operators.cs new file mode 100644 index 0000000000..95f10068ac --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlColorConverter.Operators.cs @@ -0,0 +1,179 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.ColorProfiles; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +/// +/// Holds the component arithmetic and coefficients selected before the decoder processes rows. +/// +internal abstract partial class JxlColorConverter +{ + /// + /// Restores JPEG XL's Cb, Y and Cr layout and applies the shared BT.601 inverse matrix. + /// + private readonly struct YCbCrOperator : IColorOperator + { + /// + /// The luma bias removed by JPEG XL encoding, in units where RGB white equals one. + /// + private const float LumaOffset = 128F / 255F; + + /// + /// The shared inverse matrix transposed once to Vector3.Transform order. + /// + private readonly Matrix4x4 matrix; + + /// + /// Initializes a new instance of the struct. + /// + public YCbCrOperator() => this.matrix = Matrix4x4.Transpose(KnownYCbCrMatrices.BT601.Inverse); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Invoke(ref float component0, ref float component1, ref float component2) + { + Vector3 rgb = Vector3.Transform(new Vector3(component1 + LumaOffset, component0, component2), this.matrix); + component0 = rgb.X; + component1 = rgb.Y; + component2 = rgb.Z; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Invoke(ref Vector128 component0, ref Vector128 component1, ref Vector128 component2) + { + // Chroma is already centered. Only Y needs its wire-defined bias restored before the shared matrix. + SimdUtils.Transform3( + in this.matrix, + component1 + Vector128.Create(LumaOffset), + component0, + component2, + out component0, + out component1, + out component2); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Invoke(ref Vector256 component0, ref Vector256 component1, ref Vector256 component2) + { + // Chroma is already centered. Only Y needs its wire-defined bias restored before the shared matrix. + SimdUtils.Transform3( + in this.matrix, + component1 + Vector256.Create(LumaOffset), + component0, + component2, + out component0, + out component1, + out component2); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Invoke(ref Vector512 component0, ref Vector512 component1, ref Vector512 component2) + { + // Chroma is already centered. Only Y needs its wire-defined bias restored before the shared matrix. + SimdUtils.Transform3( + in this.matrix, + component1 + Vector512.Create(LumaOffset), + component0, + component2, + out component0, + out component1, + out component2); + } + } + + /// + /// Reconstructs linear RGB through the shared XYB arithmetic, without an intermediate pixel conversion. + /// + private readonly struct XybOperator : IColorOperator + { + /// + /// The inverse matrix scaled for the original reference intensity. + /// + private readonly Matrix4x4 matrix; + + /// + /// The signed biases added after cubing. + /// + private readonly Vector3 biases; + + /// + /// The signed cube roots removed before cubing. + /// + private readonly Vector3 cubeRootBiases; + + /// + /// Initializes a new instance of the struct. + /// + /// The parsed inverse transform and original reference intensity. + public XybOperator(XybTransform transform) + { + this.matrix = transform.ScaledInverse; + this.biases = transform.Biases; + this.cubeRootBiases = transform.CubeRootBiases; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Invoke(ref float component0, ref float component1, ref float component2) + => Xyb.ConvertToRgb( + component0, + component1, + component2, + in this.matrix, + this.biases, + this.cubeRootBiases, + out component0, + out component1, + out component2); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Invoke(ref Vector128 component0, ref Vector128 component1, ref Vector128 component2) + => Xyb.ConvertToRgb( + component0, + component1, + component2, + in this.matrix, + this.biases, + this.cubeRootBiases, + out component0, + out component1, + out component2); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Invoke(ref Vector256 component0, ref Vector256 component1, ref Vector256 component2) + => Xyb.ConvertToRgb( + component0, + component1, + component2, + in this.matrix, + this.biases, + this.cubeRootBiases, + out component0, + out component1, + out component2); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Invoke(ref Vector512 component0, ref Vector512 component1, ref Vector512 component2) + => Xyb.ConvertToRgb( + component0, + component1, + component2, + in this.matrix, + this.biases, + this.cubeRootBiases, + out component0, + out component1, + out component2); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlColorConverter.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlColorConverter.cs new file mode 100644 index 0000000000..b341e8448a --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlColorConverter.cs @@ -0,0 +1,178 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.ColorProfiles; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +/// +/// Converts reconstructed JPEG XL component rows to RGB with operation-wide prepared coefficients. +/// +internal abstract partial class JxlColorConverter +{ + /// + /// Creates the converter for centered Cb, biased Y and centered Cr components. + /// + /// The prepared BT.601 converter. + public static JxlColorConverter CreateYCbCr() => new Converter(new YCbCrOperator()); + + /// + /// Creates the converter from XYB to linear RGB. + /// + /// The selected inverse absorbance transform and original reference intensity. + /// The prepared XYB converter. + public static JxlColorConverter CreateXyb(XybTransform transform) => new Converter(new XybOperator(transform)); + + /// + /// Converts three equally sized logical rows without rounding or packing the RGB results. + /// + /// The first source component. + /// The corresponding second component. + /// The corresponding third component. + /// The equally sized red destination. + /// The equally sized green destination. + /// The equally sized blue destination. + /// Corresponding source and destination rows can have the same start. Offset overlap is not supported. + public abstract void Convert( + ReadOnlySpan source0, + ReadOnlySpan source1, + ReadOnlySpan source2, + Span red, + Span green, + Span blue); + + /// + /// Defines the same three-component arithmetic for one color and every supported SIMD width. + /// + private interface IColorOperator + { + /// + /// Replaces 1 independent source colors with RGB components. + /// + /// The first component, replaced by red. + /// The second component, replaced by green. + /// The third component, replaced by blue. + void Invoke(ref float component0, ref float component1, ref float component2); + + /// + /// Replaces 4 independent source colors with RGB components. + /// + /// The first component, replaced by red. + /// The second component, replaced by green. + /// The third component, replaced by blue. + void Invoke(ref Vector128 component0, ref Vector128 component1, ref Vector128 component2); + + /// + /// Replaces 8 independent source colors with RGB components. + /// + /// The first component, replaced by red. + /// The second component, replaced by green. + /// The third component, replaced by blue. + void Invoke(ref Vector256 component0, ref Vector256 component1, ref Vector256 component2); + + /// + /// Replaces 16 independent source colors with RGB components. + /// + /// The first component, replaced by red. + /// The second component, replaced by green. + /// The third component, replaced by blue. + void Invoke(ref Vector512 component0, ref Vector512 component1, ref Vector512 component2); + + } + + /// + /// Traverses the logical rows with the operator fixed by the closed generic type. + /// + /// The prepared, allocation-free component arithmetic. + private sealed class Converter : JxlColorConverter + where TOperator : struct, IColorOperator + { + /// + /// The immutable coefficients shared by every row and SIMD width. + /// + private readonly TOperator operation; + + /// + /// Initializes a new instance of the class. + /// + /// The operation prepared before processing the frame. + public Converter(TOperator operation) => this.operation = operation; + + /// + public override void Convert( + ReadOnlySpan source0, + ReadOnlySpan source1, + ReadOnlySpan source2, + Span red, + Span green, + Span blue) + { + ref float input0 = ref MemoryMarshal.GetReference(source0); + ref float input1 = ref MemoryMarshal.GetReference(source1); + ref float input2 = ref MemoryMarshal.GetReference(source2); + ref float output0 = ref MemoryMarshal.GetReference(red); + ref float output1 = ref MemoryMarshal.GetReference(green); + ref float output2 = ref MemoryMarshal.GetReference(blue); + int x = 0; + + // Each register contains one component from consecutive pixels. Read all three inputs before any store. + // Descending widths share one offset. Complete registers and the scalar tail never access row padding. + // The constrained call uses the prepared value type directly; no interface object or per-pixel dispatch is needed. + if (Vector512.IsHardwareAccelerated) + { + for (; x <= source0.Length - Vector512.Count; x += Vector512.Count) + { + Vector512 value0 = Vector512.LoadUnsafe(ref input0, (uint)x); + Vector512 value1 = Vector512.LoadUnsafe(ref input1, (uint)x); + Vector512 value2 = Vector512.LoadUnsafe(ref input2, (uint)x); + this.operation.Invoke(ref value0, ref value1, ref value2); + value0.StoreUnsafe(ref output0, (uint)x); + value1.StoreUnsafe(ref output1, (uint)x); + value2.StoreUnsafe(ref output2, (uint)x); + } + } + + if (Vector256.IsHardwareAccelerated) + { + for (; x <= source0.Length - Vector256.Count; x += Vector256.Count) + { + Vector256 value0 = Vector256.LoadUnsafe(ref input0, (uint)x); + Vector256 value1 = Vector256.LoadUnsafe(ref input1, (uint)x); + Vector256 value2 = Vector256.LoadUnsafe(ref input2, (uint)x); + this.operation.Invoke(ref value0, ref value1, ref value2); + value0.StoreUnsafe(ref output0, (uint)x); + value1.StoreUnsafe(ref output1, (uint)x); + value2.StoreUnsafe(ref output2, (uint)x); + } + } + + if (Vector128.IsHardwareAccelerated) + { + for (; x <= source0.Length - Vector128.Count; x += Vector128.Count) + { + Vector128 value0 = Vector128.LoadUnsafe(ref input0, (uint)x); + Vector128 value1 = Vector128.LoadUnsafe(ref input1, (uint)x); + Vector128 value2 = Vector128.LoadUnsafe(ref input2, (uint)x); + this.operation.Invoke(ref value0, ref value1, ref value2); + value0.StoreUnsafe(ref output0, (uint)x); + value1.StoreUnsafe(ref output1, (uint)x); + value2.StoreUnsafe(ref output2, (uint)x); + } + } + + for (; x < source0.Length; x++) + { + float value0 = Unsafe.Add(ref input0, x); + float value1 = Unsafe.Add(ref input1, x); + float value2 = Unsafe.Add(ref input2, x); + this.operation.Invoke(ref value0, ref value1, ref value2); + Unsafe.Add(ref output0, x) = value0; + Unsafe.Add(ref output1, x) = value1; + Unsafe.Add(ref output2, x) = value2; + } + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOpsinParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOpsinParameters.cs deleted file mode 100644 index 35a5fe6cc1..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOpsinParameters.cs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; - -/// Stores operation-wide coefficients for the inverse opsin transform. -internal sealed class JxlOpsinParameters -{ - /// Gets the intensity-scaled inverse matrix in the convention used by Vector3.Transform and SimdUtils.Transform3. - public Matrix4x4 InverseOpsinMatrix { get; private set; } - - /// Gets the three negative absorbance biases added after cubing. - public Vector3 OpsinBiases { get; private set; } - - /// Gets the signed cube roots subtracted before cubing. - public Vector3 OpsinBiasesCbrt { get; private set; } - - /// Gets or sets the three coefficient biases and shared numerator used by coefficient reconstruction. - public InlineArray4 QuantBiases { get; set; } - - /// Prepares the selected inverse matrix, negative biases and intensity scale before row processing. - /// The selected row-major inverse opsin matrix. - /// The three negative absorbance biases from the transform header. - /// The positive original intensity target, in nits, established by the image header. - public void Initialize(in JxlMatrix3x3F inverseMatrix, InlineArray3 biases, float intensityTarget) - { - // XYB uses absolute intensity. The matrix converts it to relative RGB, where one equals the original intensity target. - // Transpose the row-major 3 by 3 coefficients once. SIMD broadcasts each scalar; it needs no replicated coefficient bank. - ReadOnlySpan matrix = inverseMatrix.AsReadOnlySpan(); - float scale = 255F / intensityTarget; - this.InverseOpsinMatrix = new( - matrix[0] * scale, - matrix[3] * scale, - matrix[6] * scale, - 0F, - matrix[1] * scale, - matrix[4] * scale, - matrix[7] * scale, - 0F, - matrix[2] * scale, - matrix[5] * scale, - matrix[8] * scale, - 0F, - 0F, - 0F, - 0F, - 1F); - - this.OpsinBiases = new(biases[0], biases[1], biases[2]); - this.OpsinBiasesCbrt = new(MathF.Cbrt(biases[0]), MathF.Cbrt(biases[1]), MathF.Cbrt(biases[2])); - } -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOutputEncodingInfo.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOutputEncodingInfo.cs index da7d523559..df15e1d0a4 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOutputEncodingInfo.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOutputEncodingInfo.cs @@ -32,8 +32,6 @@ internal sealed class JxlOutputEncodingInfo public bool ColorEncodingIsOriginal { get; set; } - public JxlOpsinParameters OpsinParameters { get; set; } = new(); - public bool AllDefaultOpsin { get; set; } public float InverseGamma { get; set; } @@ -46,7 +44,9 @@ internal sealed class JxlOutputEncodingInfo public JxlCmsInterface Cms { get; set; } - /// Loads the original encoding metadata and prepares its inverse opsin coefficients. + /// + /// Loads the original encoding metadata for output selection. + /// /// The parsed image and transform headers. public void SetFromMetadata(JxlCodecMetadata metadata) { @@ -63,11 +63,6 @@ public void SetFromMetadata(JxlCodecMetadata metadata) this.DefaultTransform = inverseMatrix.AllDefault; this.XybEncoded = imageMetadata.XybEncoded; - JxlOpsinParameters parameters = this.OpsinParameters; - - parameters.Initialize(this.OriginalInverseMatrix, inverseMatrix.OpsinBiases, this.OriginalIntensityTarget); - parameters.QuantBiases = inverseMatrix.QuantBiases; - bool origOK = JxlXybDecoder.CanOutputToColorEncoding(imageMetadata.ColorEncoding); bool origGrey = this.OriginalColorEncoding.IsGray; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs index 70d025cab7..e28e94fd3a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs @@ -1,139 +1,27 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; -using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.ColorProfiles; using SixLabors.ImageSharp.Formats.Jxl.Cms; using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; -/// Reconstructs linear RGB from JPEG XL XYB samples. +/// +/// Reconstructs linear RGB from JPEG XL XYB samples. +/// internal static class JxlXybDecoder { - /// Converts three equally sized XYB rows to linear RGB without clamping reconstructed components. - /// The logical X samples. - /// The corresponding Y samples. - /// The corresponding B samples. - /// The equally sized red destination. - /// The equally sized green destination. - /// The equally sized blue destination. - /// The matrix and biases prepared before processing rows. - /// Corresponding source and destination rows can start at the same address. Offset overlap is not supported. - public static void ConvertXybToRgb( - ReadOnlySpan opsinX, - ReadOnlySpan opsinY, - ReadOnlySpan opsinB, - Span linearR, - Span linearG, - Span linearB, - JxlOpsinParameters parameters) - { - Matrix4x4 matrix = parameters.InverseOpsinMatrix; - Vector3 biases = parameters.OpsinBiases; - Vector3 cubeRootBiases = parameters.OpsinBiasesCbrt; - ref float inputX = ref MemoryMarshal.GetReference(opsinX); - ref float inputY = ref MemoryMarshal.GetReference(opsinY); - ref float inputB = ref MemoryMarshal.GetReference(opsinB); - ref float outputR = ref MemoryMarshal.GetReference(linearR); - ref float outputG = ref MemoryMarshal.GetReference(linearG); - ref float outputB = ref MemoryMarshal.GetReference(linearB); - int x = 0; - - // Each register contains one component from consecutive pixels. Load all three components before the first store. - // Only complete registers enter a SIMD loop. Narrower widths and the scalar tail start at the first unprocessed pixel. - if (Vector512.IsHardwareAccelerated) - { - for (; x <= opsinX.Length - Vector512.Count; x += Vector512.Count) - { - Vector512 input0 = Vector512.LoadUnsafe(ref inputX, (uint)x); - Vector512 input1 = Vector512.LoadUnsafe(ref inputY, (uint)x); - Vector512 input2 = Vector512.LoadUnsafe(ref inputB, (uint)x); - ConvertXybToRgb( - input0, - input1, - input2, - in matrix, - biases, - cubeRootBiases, - out Vector512 r, - out Vector512 g, - out Vector512 b); - - r.StoreUnsafe(ref outputR, (uint)x); - g.StoreUnsafe(ref outputG, (uint)x); - b.StoreUnsafe(ref outputB, (uint)x); - } - } - - if (Vector256.IsHardwareAccelerated) - { - for (; x <= opsinX.Length - Vector256.Count; x += Vector256.Count) - { - Vector256 input0 = Vector256.LoadUnsafe(ref inputX, (uint)x); - Vector256 input1 = Vector256.LoadUnsafe(ref inputY, (uint)x); - Vector256 input2 = Vector256.LoadUnsafe(ref inputB, (uint)x); - ConvertXybToRgb( - input0, - input1, - input2, - in matrix, - biases, - cubeRootBiases, - out Vector256 r, - out Vector256 g, - out Vector256 b); - - r.StoreUnsafe(ref outputR, (uint)x); - g.StoreUnsafe(ref outputG, (uint)x); - b.StoreUnsafe(ref outputB, (uint)x); - } - } - - if (Vector128.IsHardwareAccelerated) - { - for (; x <= opsinX.Length - Vector128.Count; x += Vector128.Count) - { - Vector128 input0 = Vector128.LoadUnsafe(ref inputX, (uint)x); - Vector128 input1 = Vector128.LoadUnsafe(ref inputY, (uint)x); - Vector128 input2 = Vector128.LoadUnsafe(ref inputB, (uint)x); - ConvertXybToRgb( - input0, - input1, - input2, - in matrix, - biases, - cubeRootBiases, - out Vector128 r, - out Vector128 g, - out Vector128 b); - - r.StoreUnsafe(ref outputR, (uint)x); - g.StoreUnsafe(ref outputG, (uint)x); - b.StoreUnsafe(ref outputB, (uint)x); - } - } - - for (; x < opsinX.Length; x++) - { - ConvertXybToRgb(opsinX[x], opsinY[x], opsinB[x], in matrix, biases, cubeRootBiases, out float r, out float g, out float b); - linearR[x] = r; - linearG[x] = g; - linearB[x] = b; - } - } - - /// Converts an XYB rectangle to an equally sized linear RGB image. + /// + /// Converts an XYB rectangle to an equally sized linear RGB image. + /// /// The source planes that contain the rectangle. /// The logical source rectangle. /// The destination, whose logical dimensions must equal the rectangle dimensions. - /// The prepared matrix and biases. + /// The inverse transform and original reference intensity. /// Whether the rectangle and destination have equal dimensions. - public static bool OpsinToLinear(JxlImage3 opsin, Rectangle rect, JxlImage3 linear, JxlOpsinParameters parameters) + public static bool OpsinToLinear(JxlImage3 opsin, Rectangle rect, JxlImage3 linear, XybTransform transform) { if (!JxlImageOperations.SameSize(rect, linear)) { @@ -148,6 +36,7 @@ public static bool OpsinToLinear(JxlImage3 opsin, Rectangle rect, JxlImag Span destination0 = linear.PlaneRow(0, 0); Span destination1 = linear.PlaneRow(1, 0); Span destination2 = linear.PlaneRow(2, 0); + JxlColorConverter converter = JxlColorConverter.CreateXyb(transform); int sourceStride = opsin.PixelsPerRow; int destinationStride = linear.PixelsPerRow; @@ -155,150 +44,21 @@ public static bool OpsinToLinear(JxlImage3 opsin, Rectangle rect, JxlImag { int sourceOffset = ((rect.Top + y) * sourceStride) + rect.Left; int destinationOffset = y * destinationStride; - ConvertXybToRgb( + converter.Convert( source0.Slice(sourceOffset, rect.Width), source1.Slice(sourceOffset, rect.Width), source2.Slice(sourceOffset, rect.Width), destination0.Slice(destinationOffset, rect.Width), destination1.Slice(destinationOffset, rect.Width), - destination2.Slice(destinationOffset, rect.Width), - parameters); + destination2.Slice(destinationOffset, rect.Width)); } return true; } - /// Applies the inverse opsin equations to one pixel in the same order as each SIMD lane. - /// The X sample. - /// The Y sample. - /// The B sample. - /// The transposed, intensity-scaled inverse matrix. - /// The negative absorbance biases. - /// The signed cube roots of the negative biases. - /// The reconstructed red sample. - /// The reconstructed green sample. - /// The reconstructed blue sample. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void ConvertXybToRgb( - float x, - float y, - float b, - in Matrix4x4 matrix, - Vector3 biases, - Vector3 cubeRootBiases, - out float r, - out float g, - out float blue) - { - // Undo the opponent mix and the forward transform's cube-root offset before cubing. - float gammaR = (y + x) - cubeRootBiases.X; - float gammaG = (y - x) - cubeRootBiases.Y; - float gammaB = b - cubeRootBiases.Z; - float mixedR = float.MultiplyAddEstimate(gammaR * gammaR, gammaR, biases.X); - float mixedG = float.MultiplyAddEstimate(gammaG * gammaG, gammaG, biases.Y); - float mixedB = float.MultiplyAddEstimate(gammaB * gammaB, gammaB, biases.Z); - - // The runtime uses the same X, then Y, then Z multiply-add order as shared Transform3. - Vector3 linear = Vector3.Transform(new Vector3(mixedR, mixedG, mixedB), matrix); - r = linear.X; - g = linear.Y; - blue = linear.Z; - } - - /// Reconstructs 4 independent pixels; each register holds one component in pixel order. - /// The X samples. - /// The Y samples. - /// The B samples. - /// The transposed, intensity-scaled inverse matrix. - /// The negative absorbance biases, broadcast uniformly to all lanes. - /// The signed cube roots subtracted before cubing. - /// The reconstructed red samples. - /// The reconstructed green samples. - /// The reconstructed blue samples. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void ConvertXybToRgb( - Vector128 x, - Vector128 y, - Vector128 b, - in Matrix4x4 matrix, - Vector3 biases, - Vector3 cubeRootBiases, - out Vector128 r, - out Vector128 g, - out Vector128 blue) - { - Vector128 gammaR = (y + x) - Vector128.Create(cubeRootBiases.X); - Vector128 gammaG = (y - x) - Vector128.Create(cubeRootBiases.Y); - Vector128 gammaB = b - Vector128.Create(cubeRootBiases.Z); - Vector128 mixedR = Vector128.MultiplyAddEstimate(gammaR * gammaR, gammaR, Vector128.Create(biases.X)); - Vector128 mixedG = Vector128.MultiplyAddEstimate(gammaG * gammaG, gammaG, Vector128.Create(biases.Y)); - Vector128 mixedB = Vector128.MultiplyAddEstimate(gammaB * gammaB, gammaB, Vector128.Create(biases.Z)); - SimdUtils.Transform3(in matrix, mixedR, mixedG, mixedB, out r, out g, out blue); - } - - /// Reconstructs 8 independent pixels; each register holds one component in pixel order. - /// The X samples. - /// The Y samples. - /// The B samples. - /// The transposed, intensity-scaled inverse matrix. - /// The negative absorbance biases, broadcast uniformly to all lanes. - /// The signed cube roots subtracted before cubing. - /// The reconstructed red samples. - /// The reconstructed green samples. - /// The reconstructed blue samples. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void ConvertXybToRgb( - Vector256 x, - Vector256 y, - Vector256 b, - in Matrix4x4 matrix, - Vector3 biases, - Vector3 cubeRootBiases, - out Vector256 r, - out Vector256 g, - out Vector256 blue) - { - Vector256 gammaR = (y + x) - Vector256.Create(cubeRootBiases.X); - Vector256 gammaG = (y - x) - Vector256.Create(cubeRootBiases.Y); - Vector256 gammaB = b - Vector256.Create(cubeRootBiases.Z); - Vector256 mixedR = Vector256.MultiplyAddEstimate(gammaR * gammaR, gammaR, Vector256.Create(biases.X)); - Vector256 mixedG = Vector256.MultiplyAddEstimate(gammaG * gammaG, gammaG, Vector256.Create(biases.Y)); - Vector256 mixedB = Vector256.MultiplyAddEstimate(gammaB * gammaB, gammaB, Vector256.Create(biases.Z)); - SimdUtils.Transform3(in matrix, mixedR, mixedG, mixedB, out r, out g, out blue); - } - - /// Reconstructs 16 independent pixels; each register holds one component in pixel order. - /// The X samples. - /// The Y samples. - /// The B samples. - /// The transposed, intensity-scaled inverse matrix. - /// The negative absorbance biases, broadcast uniformly to all lanes. - /// The signed cube roots subtracted before cubing. - /// The reconstructed red samples. - /// The reconstructed green samples. - /// The reconstructed blue samples. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void ConvertXybToRgb( - Vector512 x, - Vector512 y, - Vector512 b, - in Matrix4x4 matrix, - Vector3 biases, - Vector3 cubeRootBiases, - out Vector512 r, - out Vector512 g, - out Vector512 blue) - { - Vector512 gammaR = (y + x) - Vector512.Create(cubeRootBiases.X); - Vector512 gammaG = (y - x) - Vector512.Create(cubeRootBiases.Y); - Vector512 gammaB = b - Vector512.Create(cubeRootBiases.Z); - Vector512 mixedR = Vector512.MultiplyAddEstimate(gammaR * gammaR, gammaR, Vector512.Create(biases.X)); - Vector512 mixedG = Vector512.MultiplyAddEstimate(gammaG * gammaG, gammaG, Vector512.Create(biases.Y)); - Vector512 mixedB = Vector512.MultiplyAddEstimate(gammaB * gammaB, gammaB, Vector512.Create(biases.Z)); - SimdUtils.Transform3(in matrix, mixedR, mixedG, mixedB, out r, out g, out blue); - } - - /// Checks whether native reconstruction supports the encoding's direct fields and transfer function. + /// + /// Checks whether native reconstruction supports the encoding's direct fields and transfer function. + /// /// The requested encoding. /// Whether its direct color fields can describe supported reconstruction output. public static bool CanOutputToColorEncoding(JxlColorEncoding colorEncoding) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs index 503d9463fd..20e72d418b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Heuristics/JxlHeuristics.cs @@ -4,6 +4,7 @@ using System.Buffers; using System.Numerics; using System.Numerics.Tensors; +using SixLabors.ImageSharp.ColorProfiles; using SixLabors.ImageSharp.Formats.Jxl.Cms; using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; @@ -1194,11 +1195,8 @@ public static void DownsampleImage2Iterative(Configuration configuration, ref Jx // Temporary conversion data using JxlImage3 rgb = new(configuration, opsin.XSize, opsin.YSize); - JxlOpsinParameters opsinParameters = new(); - opsinParameters.Initialize(JxlMath.DefaultIntensityTarget); - // Convert Opsin XYB to linear - if (!JxlXybDecoder.OpsinToLinear(opsin, rgb.GetRectangle(), rgb, opsinParameters)) + if (!JxlXybDecoder.OpsinToLinear(opsin, rgb.GetRectangle(), rgb, KnownXybTransforms.Default)) { throw new InvalidOperationException("Failed to convert Opsin color-space to RGB"); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlXybEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlXybEncoder.cs index 865756fccf..0ca912469a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlXybEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlXybEncoder.cs @@ -1,9 +1,10 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Runtime.CompilerServices; +using System.Numerics; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.ColorProfiles; using SixLabors.ImageSharp.ColorProfiles.Companding; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Jxl.Cms; @@ -13,88 +14,26 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; -/// Converts linear or encoded sRGB planes to the encoder's XYB samples. +/// +/// Converts linear or encoded sRGB planes to the encoder's XYB samples. +/// internal static class JxlXybEncoder { - /// The IEEE single-precision mask that removes the sign bit before root normalization. - private const uint MagnitudeMask = 0x7FFFFFFFU; - - /// The IEEE single-precision fraction bits retained when rebuilding the normalized mantissa. - private const uint MantissaMask = 0x007FFFFFU; - - /// The IEEE bit pattern of the smallest positive normal float; smaller magnitudes need exponent normalization. - private const uint MinimumNormalBits = 0x00800000U; - - /// The number of fraction bits below the IEEE single-precision exponent field. - private const int MantissaBits = 23; - - /// The bias added to an IEEE single-precision normal exponent. - private const int ExponentBias = 127; - - /// The exact power-of-two shift that makes every nonzero subnormal float normal. - private const int SubnormalExponentShift = 24; - - /// The exact multiplier used before extracting a subnormal input's exponent. - private const float SubnormalScale = 1 << SubnormalExponentShift; - /// - /// The exponent offset divisible by three that makes all finite input exponents positive. - /// The smallest subnormal exponent is -149, so the adjusted exponent range is [1,277]. + /// Converts source planes to XYB and optionally retains the expanded linear samples. /// - private const int ExponentOffset = 150; - - /// The cube-root exponent offset removed after dividing the adjusted exponent by three. - private const int ExponentQuotientOffset = ExponentOffset / 3; - - /// The fixed-point reciprocal shift used for exact division by three over the adjusted exponent range. - private const int ThirdShift = 16; - - /// Ceil(2^16/3), which gives floor(v/3) after shifting for every integer v in [1,277]. - private const int ThirdMultiplier = 21846; - - /// The native bit-pattern bias used to seed the reciprocal cube root of a normalized mantissa. - private const int ReciprocalRootSeedBias = 0x54800000; - - /// The native exponent multiplier used by the reciprocal-root seed. - private const int ReciprocalRootExponentMultiplier = 0x002AAAAA; - - /// The rounded single-precision one-third coefficient used by native root refinement. - private const float OneThird = 1F / 3F; - - /// The rounded single-precision four-thirds coefficient used by native root refinement. - private const float FourThirds = 4F / 3F; - - /// The number of native reciprocal-root iterations before its final correction. - private const int ReciprocalRootIterations = 3; - - /// Prepares nine intensity-scaled matrix entries and three negative cube-root biases. - /// The positive image intensity target, in nits. - /// The twelve scalar coefficients reused by every row and SIMD width. - private static void ComputeOpsinAbsorbanceCoefficients(float intensityTarget, Span coefficients) - { - ReadOnlySpan matrix = JxlOpsinConstants.OpsinAbsorbanceMatrix; - ReadOnlySpan biases = JxlOpsinConstants.OpsinAbsorbanceBias; - float scale = intensityTarget / 255F; - for (int i = 0; i < matrix.Length; i++) - { - coefficients[i] = matrix[i] * scale; - } - - for (int c = 0; c < 3; c++) - { - coefficients[9 + c] = -MathF.Cbrt(biases[c]); - } - } - - /// Converts source planes to XYB and optionally retains the expanded linear samples. /// The shared identity or sRGB expansion operator selected before traversal. - /// The twelve prepared scalar coefficients. + /// The intensity-scaled forward absorbance matrix. + /// The signed inverse biases, subtracted before forward root evaluation. + /// The signed cube roots added after forward root evaluation. /// The source planes, which can also be the XYB destination. /// The equally sized XYB destination. /// The optional equally sized destination for expanded linear RGB. /// The input component operation. private static void RgbToXyb( - ReadOnlySpan coefficients, + in Matrix4x4 matrix, + Vector3 biases, + Vector3 cubeRootBiases, JxlImage3 source, JxlImage3 destination, JxlImage3? linear, @@ -131,12 +70,16 @@ private static void RgbToXyb( linear is null ? [] : linear0.Slice(linearOffset, width), linear is null ? [] : linear1.Slice(linearOffset, width), linear is null ? [] : linear2.Slice(linearOffset, width), - coefficients, + in matrix, + biases, + cubeRootBiases, operation); } } - /// Fuses shared input expansion, optional linear output and XYB reconstruction over one logical row. + /// + /// Fuses shared input expansion, optional linear output and XYB reconstruction over one logical row. + /// /// The shared input operation. /// The finite normalized red samples within the pixel range. /// The corresponding green samples. @@ -147,7 +90,9 @@ private static void RgbToXyb( /// The optional red destination, empty when no linear output is requested. /// The matching optional green destination. /// The matching optional blue destination. - /// The twelve scalar coefficients prepared before traversal. + /// The intensity-scaled forward absorbance matrix. + /// The signed inverse biases, subtracted before forward root evaluation. + /// The signed cube roots added after forward root evaluation. /// The shared input component operation. /// /// All logical rows have equal lengths. RGB and XYB rows can start at the same address; offset overlap is not supported. @@ -164,7 +109,9 @@ public static void RgbRowToXyb( Span linearR, Span linearG, Span linearB, - ReadOnlySpan coefficients, + in Matrix4x4 matrix, + Vector3 biases, + Vector3 cubeRootBiases, TOperator operation) where TOperator : struct, TensorPrimitives_.IUnaryOperator { @@ -181,7 +128,7 @@ public static void RgbRowToXyb( int x = 0; // Each register holds one component from consecutive pixels. The selected operation owns input expansion and its result safety. - // Load RGB before storing either output. Every complete SIMD block and scalar remainder uses the same coefficient layout. + // Load RGB before storing either output. Every complete SIMD block and scalar remainder uses the same prepared transform. if (Vector512.IsHardwareAccelerated) { for (; x <= inputR.Length - Vector512.Count; x += Vector512.Count) @@ -196,7 +143,17 @@ public static void RgbRowToXyb( b.StoreUnsafe(ref retainedB, (uint)x); } - LinearRgbToXyb(r, g, b, coefficients, out Vector512 valueX, out Vector512 valueY, out Vector512 valueB); + Xyb.ConvertFromRgb( + r, + g, + b, + in matrix, + biases, + cubeRootBiases, + out Vector512 valueX, + out Vector512 valueY, + out Vector512 valueB); + valueX.StoreUnsafe(ref destinationX, (uint)x); valueY.StoreUnsafe(ref destinationY, (uint)x); valueB.StoreUnsafe(ref destinationB, (uint)x); @@ -217,7 +174,17 @@ public static void RgbRowToXyb( b.StoreUnsafe(ref retainedB, (uint)x); } - LinearRgbToXyb(r, g, b, coefficients, out Vector256 valueX, out Vector256 valueY, out Vector256 valueB); + Xyb.ConvertFromRgb( + r, + g, + b, + in matrix, + biases, + cubeRootBiases, + out Vector256 valueX, + out Vector256 valueY, + out Vector256 valueB); + valueX.StoreUnsafe(ref destinationX, (uint)x); valueY.StoreUnsafe(ref destinationY, (uint)x); valueB.StoreUnsafe(ref destinationB, (uint)x); @@ -238,7 +205,17 @@ public static void RgbRowToXyb( b.StoreUnsafe(ref retainedB, (uint)x); } - LinearRgbToXyb(r, g, b, coefficients, out Vector128 valueX, out Vector128 valueY, out Vector128 valueB); + Xyb.ConvertFromRgb( + r, + g, + b, + in matrix, + biases, + cubeRootBiases, + out Vector128 valueX, + out Vector128 valueY, + out Vector128 valueB); + valueX.StoreUnsafe(ref destinationX, (uint)x); valueY.StoreUnsafe(ref destinationY, (uint)x); valueB.StoreUnsafe(ref destinationB, (uint)x); @@ -257,348 +234,16 @@ public static void RgbRowToXyb( linearB[x] = b; } - LinearRgbToXyb(r, g, b, coefficients, out float valueX, out float valueY, out float valueB); + Xyb.ConvertFromRgb(r, g, b, in matrix, biases, cubeRootBiases, out float valueX, out float valueY, out float valueB); outputX[x] = valueX; outputY[x] = valueY; outputB[x] = valueB; } } - /// Applies the biased absorbance transform and opponent mix to one linear RGB sample. - /// The finite red component in the pixel range. - /// The corresponding green component. - /// The corresponding blue component. - /// The twelve prepared scalar coefficients. - /// The X result. - /// The Y result. - /// The B result. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void LinearRgbToXyb(float r, float g, float b, ReadOnlySpan coefficients, out float x, out float y, out float blue) - { - ReadOnlySpan biases = JxlOpsinConstants.OpsinAbsorbanceBias; - - // Preserve native B+bias, then G, then R multiply-add order. Moving bias outside that chain changes rounding near black. - float mixed0 = float.MultiplyAddEstimate(coefficients[2], b, biases[0]); - mixed0 = float.MultiplyAddEstimate(coefficients[1], g, mixed0); - mixed0 = float.MultiplyAddEstimate(coefficients[0], r, mixed0); - float mixed1 = float.MultiplyAddEstimate(coefficients[5], b, biases[1]); - mixed1 = float.MultiplyAddEstimate(coefficients[4], g, mixed1); - mixed1 = float.MultiplyAddEstimate(coefficients[3], r, mixed1); - float mixed2 = float.MultiplyAddEstimate(coefficients[8], b, biases[2]); - mixed2 = float.MultiplyAddEstimate(coefficients[7], g, mixed2); - mixed2 = float.MultiplyAddEstimate(coefficients[6], r, mixed2); - mixed0 = CubeRootAndAdd(MathF.Max(0F, mixed0), coefficients[9]); - mixed1 = CubeRootAndAdd(MathF.Max(0F, mixed1), coefficients[10]); - mixed2 = CubeRootAndAdd(MathF.Max(0F, mixed2), coefficients[11]); - x = 0.5F * (mixed0 - mixed1); - y = 0.5F * (mixed0 + mixed1); - blue = mixed2; - } - - /// Transforms 4 independent RGB samples with the same scalar coefficients and native evaluation order. - /// The linear red lanes. - /// The linear green lanes. - /// The linear blue lanes. - /// The twelve prepared scalar coefficients. - /// The X lanes. - /// The Y lanes. - /// The B lanes. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void LinearRgbToXyb( - Vector128 r, - Vector128 g, - Vector128 b, - ReadOnlySpan coefficients, - out Vector128 x, - out Vector128 y, - out Vector128 blue) - { - ReadOnlySpan biases = JxlOpsinConstants.OpsinAbsorbanceBias; - Vector128 mixed0 = Vector128.MultiplyAddEstimate(Vector128.Create(coefficients[2]), b, Vector128.Create(biases[0])); - mixed0 = Vector128.MultiplyAddEstimate(Vector128.Create(coefficients[1]), g, mixed0); - mixed0 = Vector128.MultiplyAddEstimate(Vector128.Create(coefficients[0]), r, mixed0); - - Vector128 mixed1 = Vector128.MultiplyAddEstimate(Vector128.Create(coefficients[5]), b, Vector128.Create(biases[1])); - mixed1 = Vector128.MultiplyAddEstimate(Vector128.Create(coefficients[4]), g, mixed1); - mixed1 = Vector128.MultiplyAddEstimate(Vector128.Create(coefficients[3]), r, mixed1); - - Vector128 mixed2 = Vector128.MultiplyAddEstimate(Vector128.Create(coefficients[8]), b, Vector128.Create(biases[2])); - mixed2 = Vector128.MultiplyAddEstimate(Vector128.Create(coefficients[7]), g, mixed2); - mixed2 = Vector128.MultiplyAddEstimate(Vector128.Create(coefficients[6]), r, mixed2); - - mixed0 = CubeRootAndAdd(Vector128.Max(Vector128.Zero, mixed0), Vector128.Create(coefficients[9])); - mixed1 = CubeRootAndAdd(Vector128.Max(Vector128.Zero, mixed1), Vector128.Create(coefficients[10])); - mixed2 = CubeRootAndAdd(Vector128.Max(Vector128.Zero, mixed2), Vector128.Create(coefficients[11])); - Vector128 half = Vector128.Create(0.5F); - x = half * (mixed0 - mixed1); - y = half * (mixed0 + mixed1); - blue = mixed2; - } - - /// Transforms 8 independent RGB samples with the same scalar coefficients and native evaluation order. - /// The linear red lanes. - /// The linear green lanes. - /// The linear blue lanes. - /// The twelve prepared scalar coefficients. - /// The X lanes. - /// The Y lanes. - /// The B lanes. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void LinearRgbToXyb( - Vector256 r, - Vector256 g, - Vector256 b, - ReadOnlySpan coefficients, - out Vector256 x, - out Vector256 y, - out Vector256 blue) - { - ReadOnlySpan biases = JxlOpsinConstants.OpsinAbsorbanceBias; - Vector256 mixed0 = Vector256.MultiplyAddEstimate(Vector256.Create(coefficients[2]), b, Vector256.Create(biases[0])); - mixed0 = Vector256.MultiplyAddEstimate(Vector256.Create(coefficients[1]), g, mixed0); - mixed0 = Vector256.MultiplyAddEstimate(Vector256.Create(coefficients[0]), r, mixed0); - - Vector256 mixed1 = Vector256.MultiplyAddEstimate(Vector256.Create(coefficients[5]), b, Vector256.Create(biases[1])); - mixed1 = Vector256.MultiplyAddEstimate(Vector256.Create(coefficients[4]), g, mixed1); - mixed1 = Vector256.MultiplyAddEstimate(Vector256.Create(coefficients[3]), r, mixed1); - - Vector256 mixed2 = Vector256.MultiplyAddEstimate(Vector256.Create(coefficients[8]), b, Vector256.Create(biases[2])); - mixed2 = Vector256.MultiplyAddEstimate(Vector256.Create(coefficients[7]), g, mixed2); - mixed2 = Vector256.MultiplyAddEstimate(Vector256.Create(coefficients[6]), r, mixed2); - - mixed0 = CubeRootAndAdd(Vector256.Max(Vector256.Zero, mixed0), Vector256.Create(coefficients[9])); - mixed1 = CubeRootAndAdd(Vector256.Max(Vector256.Zero, mixed1), Vector256.Create(coefficients[10])); - mixed2 = CubeRootAndAdd(Vector256.Max(Vector256.Zero, mixed2), Vector256.Create(coefficients[11])); - Vector256 half = Vector256.Create(0.5F); - x = half * (mixed0 - mixed1); - y = half * (mixed0 + mixed1); - blue = mixed2; - } - - /// Transforms 16 independent RGB samples with the same scalar coefficients and native evaluation order. - /// The linear red lanes. - /// The linear green lanes. - /// The linear blue lanes. - /// The twelve prepared scalar coefficients. - /// The X lanes. - /// The Y lanes. - /// The B lanes. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void LinearRgbToXyb( - Vector512 r, - Vector512 g, - Vector512 b, - ReadOnlySpan coefficients, - out Vector512 x, - out Vector512 y, - out Vector512 blue) - { - ReadOnlySpan biases = JxlOpsinConstants.OpsinAbsorbanceBias; - Vector512 mixed0 = Vector512.MultiplyAddEstimate(Vector512.Create(coefficients[2]), b, Vector512.Create(biases[0])); - mixed0 = Vector512.MultiplyAddEstimate(Vector512.Create(coefficients[1]), g, mixed0); - mixed0 = Vector512.MultiplyAddEstimate(Vector512.Create(coefficients[0]), r, mixed0); - - Vector512 mixed1 = Vector512.MultiplyAddEstimate(Vector512.Create(coefficients[5]), b, Vector512.Create(biases[1])); - mixed1 = Vector512.MultiplyAddEstimate(Vector512.Create(coefficients[4]), g, mixed1); - mixed1 = Vector512.MultiplyAddEstimate(Vector512.Create(coefficients[3]), r, mixed1); - - Vector512 mixed2 = Vector512.MultiplyAddEstimate(Vector512.Create(coefficients[8]), b, Vector512.Create(biases[2])); - mixed2 = Vector512.MultiplyAddEstimate(Vector512.Create(coefficients[7]), g, mixed2); - mixed2 = Vector512.MultiplyAddEstimate(Vector512.Create(coefficients[6]), r, mixed2); - - mixed0 = CubeRootAndAdd(Vector512.Max(Vector512.Zero, mixed0), Vector512.Create(coefficients[9])); - mixed1 = CubeRootAndAdd(Vector512.Max(Vector512.Zero, mixed1), Vector512.Create(coefficients[10])); - mixed2 = CubeRootAndAdd(Vector512.Max(Vector512.Zero, mixed2), Vector512.Create(coefficients[11])); - Vector512 half = Vector512.Create(0.5F); - x = half * (mixed0 - mixed1); - y = half * (mixed0 + mixed1); - blue = mixed2; - } - - /// Computes a nonnegative finite cube root plus its bias without overflowing reciprocal-root intermediates. - /// The nonnegative finite absorbance, including zero and subnormal values. - /// The negative cube-root bias. - /// The biased root with the final addition performed as a multiply-add estimate. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static float CubeRootAndAdd(float value, float add) - { - uint bits = BitConverter.SingleToUInt32Bits(value) & MagnitudeMask; - bool subnormal = bits < MinimumNormalBits; - float normalized = BitConverter.UInt32BitsToSingle(bits) * (subnormal ? SubnormalScale : 1F); - uint normalizedBits = BitConverter.SingleToUInt32Bits(normalized); - - // Standard subnormal preparation multiplies by 2^24, then corrects the extracted exponent. - // Zero has no exponent: substitute exponent zero before division and use final scale zero. - int exponent = bits == 0 ? 0 : (int)(normalizedBits >> MantissaBits) - ExponentBias - (subnormal ? SubnormalExponentShift : 0); - int positiveExponent = exponent + ExponentOffset; - - // Adding 150 keeps the exponent in [1,277] and preserves its remainder modulo three. - // Multiplication by ceil(2^16/3), then shifting 16 bits, gives the exact integer quotient throughout that interval. - int quotient = (positiveExponent * ThirdMultiplier) >> ThirdShift; - int remainder = positiveExponent - (3 * quotient); - float z = BitConverter.Int32BitsToSingle((int)(normalizedBits & MantissaMask) | ((ExponentBias + remainder) << MantissaBits)); - float scale = bits == 0 ? 0F : BitConverter.Int32BitsToSingle((quotient + ExponentBias - ExponentQuotientOffset) << MantissaBits); - float reciprocalRoot = BitConverter.Int32BitsToSingle(ReciprocalRootSeedBias - ((ExponentBias + remainder) * ReciprocalRootExponentMultiplier)); - float thirdZ = OneThird * z; - - // x=z*2^(3k), where 1<=z<8 and k=quotient-ExponentQuotientOffset. Reciprocal roots and their fourth powers stay in a normal, bounded range. - // Native reciprocal-root refinements now see only that range. They never see tiny normals or subnormal absorbance directly. - for (int i = 0; i < ReciprocalRootIterations; i++) - { - float squared = reciprocalRoot * reciprocalRoot; - reciprocalRoot = float.MultiplyAddEstimate(-thirdZ, squared * squared, FourThirds * reciprocalRoot); - } - - float squaredRoot = reciprocalRoot * reciprocalRoot; - reciprocalRoot = float.MultiplyAddEstimate(OneThird, float.MultiplyAddEstimate(-z, squaredRoot * squaredRoot, reciprocalRoot), reciprocalRoot); - squaredRoot = reciprocalRoot * reciprocalRoot; - - // Scaling a normal coefficient by 2^k is exact. Keep it inside the last multiply-add so bias cancellation gets no extra rounded root. - return float.MultiplyAddEstimate(squaredRoot * scale, z, add); - } - - /// Evaluates bounded reciprocal-root refinements independently in each nonnegative finite absorbance lane. - /// The absorbance lanes, including zero and subnormal values. - /// The corresponding negative cube-root biases. - /// The biased roots, with no scalar lane calls or temporary sample storage. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector128 CubeRootAndAdd(Vector128 value, Vector128 add) - { - Vector128 bits = value.AsUInt32() & Vector128.Create(MagnitudeMask); - Vector128 zero = Vector128.Equals(bits, Vector128.Zero); - Vector128 subnormal = Vector128.LessThan(bits, Vector128.Create(MinimumNormalBits)); - Vector128 normalization = Vector128.ConditionalSelect(subnormal.AsSingle(), Vector128.Create(SubnormalScale), Vector128.One); - Vector128 normalizedBits = (bits.AsSingle() * normalization).AsUInt32(); - Vector128 exponent = Vector128.ShiftRightLogical(normalizedBits, MantissaBits).AsInt32() - Vector128.Create(ExponentBias); - exponent -= Vector128.ConditionalSelect(subnormal.AsInt32(), Vector128.Create(SubnormalExponentShift), Vector128.Zero); - exponent = Vector128.ConditionalSelect(zero.AsInt32(), Vector128.Zero, exponent); - Vector128 positiveExponent = exponent + Vector128.Create(ExponentOffset); - - // For positiveExponent in [1,277], floor(v/3)=(v*21846)>>16. 21846=ceil(2^16/3). - // The reciprocal error is below 1/3, so it cannot cross the next quotient. All products fit Int32 and remain SIMD. - Vector128 quotient = Vector128.ShiftRightArithmetic(positiveExponent * Vector128.Create(ThirdMultiplier), ThirdShift); - Vector128 remainder = positiveExponent - (quotient * Vector128.Create(3)); - Vector128 normalExponent = remainder + Vector128.Create(ExponentBias); - Vector128 mantissa = normalizedBits & Vector128.Create(MantissaMask); - Vector128 z = (mantissa | Vector128.ShiftLeft(normalExponent, MantissaBits).AsUInt32()).AsSingle(); - Vector128 scale = Vector128.ShiftLeft(quotient + Vector128.Create(ExponentBias - ExponentQuotientOffset), MantissaBits).AsSingle(); - scale = Vector128.ConditionalSelect(zero.AsSingle(), Vector128.Zero, scale); - Vector128 seed = Vector128.Create(ReciprocalRootSeedBias) - (normalExponent * Vector128.Create(ReciprocalRootExponentMultiplier)); - Vector128 reciprocalRoot = seed.AsSingle(); - Vector128 thirdZ = Vector128.Create(OneThird) * z; - - // Every z is in [1,8), including zero lanes whose final scale is zero. The iteration matches the scalar arithmetic order. - for (int i = 0; i < ReciprocalRootIterations; i++) - { - Vector128 squared = reciprocalRoot * reciprocalRoot; - reciprocalRoot = Vector128.MultiplyAddEstimate(-thirdZ, squared * squared, Vector128.Create(FourThirds) * reciprocalRoot); - } - - Vector128 squaredRoot = reciprocalRoot * reciprocalRoot; - reciprocalRoot = Vector128.MultiplyAddEstimate( - Vector128.Create(OneThird), - Vector128.MultiplyAddEstimate(-z, squaredRoot * squaredRoot, reciprocalRoot), - reciprocalRoot); - - squaredRoot = reciprocalRoot * reciprocalRoot; - return Vector128.MultiplyAddEstimate(squaredRoot * scale, z, add); - } - - /// Evaluates bounded reciprocal-root refinements independently in each nonnegative finite absorbance lane. - /// The absorbance lanes, including zero and subnormal values. - /// The corresponding negative cube-root biases. - /// The biased roots, with no scalar lane calls or temporary sample storage. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector256 CubeRootAndAdd(Vector256 value, Vector256 add) - { - Vector256 bits = value.AsUInt32() & Vector256.Create(MagnitudeMask); - Vector256 zero = Vector256.Equals(bits, Vector256.Zero); - Vector256 subnormal = Vector256.LessThan(bits, Vector256.Create(MinimumNormalBits)); - Vector256 normalization = Vector256.ConditionalSelect(subnormal.AsSingle(), Vector256.Create(SubnormalScale), Vector256.One); - Vector256 normalizedBits = (bits.AsSingle() * normalization).AsUInt32(); - Vector256 exponent = Vector256.ShiftRightLogical(normalizedBits, MantissaBits).AsInt32() - Vector256.Create(ExponentBias); - exponent -= Vector256.ConditionalSelect(subnormal.AsInt32(), Vector256.Create(SubnormalExponentShift), Vector256.Zero); - exponent = Vector256.ConditionalSelect(zero.AsInt32(), Vector256.Zero, exponent); - Vector256 positiveExponent = exponent + Vector256.Create(ExponentOffset); - - // For positiveExponent in [1,277], floor(v/3)=(v*21846)>>16. 21846=ceil(2^16/3). - // The reciprocal error is below 1/3, so it cannot cross the next quotient. All products fit Int32 and remain SIMD. - Vector256 quotient = Vector256.ShiftRightArithmetic(positiveExponent * Vector256.Create(ThirdMultiplier), ThirdShift); - Vector256 remainder = positiveExponent - (quotient * Vector256.Create(3)); - Vector256 normalExponent = remainder + Vector256.Create(ExponentBias); - Vector256 mantissa = normalizedBits & Vector256.Create(MantissaMask); - Vector256 z = (mantissa | Vector256.ShiftLeft(normalExponent, MantissaBits).AsUInt32()).AsSingle(); - Vector256 scale = Vector256.ShiftLeft(quotient + Vector256.Create(ExponentBias - ExponentQuotientOffset), MantissaBits).AsSingle(); - scale = Vector256.ConditionalSelect(zero.AsSingle(), Vector256.Zero, scale); - Vector256 seed = Vector256.Create(ReciprocalRootSeedBias) - (normalExponent * Vector256.Create(ReciprocalRootExponentMultiplier)); - Vector256 reciprocalRoot = seed.AsSingle(); - Vector256 thirdZ = Vector256.Create(OneThird) * z; - - // Every z is in [1,8), including zero lanes whose final scale is zero. The iteration matches the scalar arithmetic order. - for (int i = 0; i < ReciprocalRootIterations; i++) - { - Vector256 squared = reciprocalRoot * reciprocalRoot; - reciprocalRoot = Vector256.MultiplyAddEstimate(-thirdZ, squared * squared, Vector256.Create(FourThirds) * reciprocalRoot); - } - - Vector256 squaredRoot = reciprocalRoot * reciprocalRoot; - reciprocalRoot = Vector256.MultiplyAddEstimate( - Vector256.Create(OneThird), - Vector256.MultiplyAddEstimate(-z, squaredRoot * squaredRoot, reciprocalRoot), - reciprocalRoot); - - squaredRoot = reciprocalRoot * reciprocalRoot; - return Vector256.MultiplyAddEstimate(squaredRoot * scale, z, add); - } - - /// Evaluates bounded reciprocal-root refinements independently in each nonnegative finite absorbance lane. - /// The absorbance lanes, including zero and subnormal values. - /// The corresponding negative cube-root biases. - /// The biased roots, with no scalar lane calls or temporary sample storage. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector512 CubeRootAndAdd(Vector512 value, Vector512 add) - { - Vector512 bits = value.AsUInt32() & Vector512.Create(MagnitudeMask); - Vector512 zero = Vector512.Equals(bits, Vector512.Zero); - Vector512 subnormal = Vector512.LessThan(bits, Vector512.Create(MinimumNormalBits)); - Vector512 normalization = Vector512.ConditionalSelect(subnormal.AsSingle(), Vector512.Create(SubnormalScale), Vector512.One); - Vector512 normalizedBits = (bits.AsSingle() * normalization).AsUInt32(); - Vector512 exponent = Vector512.ShiftRightLogical(normalizedBits, MantissaBits).AsInt32() - Vector512.Create(ExponentBias); - exponent -= Vector512.ConditionalSelect(subnormal.AsInt32(), Vector512.Create(SubnormalExponentShift), Vector512.Zero); - exponent = Vector512.ConditionalSelect(zero.AsInt32(), Vector512.Zero, exponent); - Vector512 positiveExponent = exponent + Vector512.Create(ExponentOffset); - - // For positiveExponent in [1,277], floor(v/3)=(v*21846)>>16. 21846=ceil(2^16/3). - // The reciprocal error is below 1/3, so it cannot cross the next quotient. All products fit Int32 and remain SIMD. - Vector512 quotient = Vector512.ShiftRightArithmetic(positiveExponent * Vector512.Create(ThirdMultiplier), ThirdShift); - Vector512 remainder = positiveExponent - (quotient * Vector512.Create(3)); - Vector512 normalExponent = remainder + Vector512.Create(ExponentBias); - Vector512 mantissa = normalizedBits & Vector512.Create(MantissaMask); - Vector512 z = (mantissa | Vector512.ShiftLeft(normalExponent, MantissaBits).AsUInt32()).AsSingle(); - Vector512 scale = Vector512.ShiftLeft(quotient + Vector512.Create(ExponentBias - ExponentQuotientOffset), MantissaBits).AsSingle(); - scale = Vector512.ConditionalSelect(zero.AsSingle(), Vector512.Zero, scale); - Vector512 seed = Vector512.Create(ReciprocalRootSeedBias) - (normalExponent * Vector512.Create(ReciprocalRootExponentMultiplier)); - Vector512 reciprocalRoot = seed.AsSingle(); - Vector512 thirdZ = Vector512.Create(OneThird) * z; - - // Every z is in [1,8), including zero lanes whose final scale is zero. The iteration matches the scalar arithmetic order. - for (int i = 0; i < ReciprocalRootIterations; i++) - { - Vector512 squared = reciprocalRoot * reciprocalRoot; - reciprocalRoot = Vector512.MultiplyAddEstimate(-thirdZ, squared * squared, Vector512.Create(FourThirds) * reciprocalRoot); - } - - Vector512 squaredRoot = reciprocalRoot * reciprocalRoot; - reciprocalRoot = Vector512.MultiplyAddEstimate( - Vector512.Create(OneThird), - Vector512.MultiplyAddEstimate(-z, squaredRoot * squaredRoot, reciprocalRoot), - reciprocalRoot); - - squaredRoot = reciprocalRoot * reciprocalRoot; - return Vector512.MultiplyAddEstimate(squaredRoot * scale, z, add); - } - - /// Converts the image's direct sRGB samples to XYB, optionally retaining linear sRGB. + /// + /// Converts the image's direct sRGB samples to XYB, optionally retaining linear sRGB. + /// /// The configuration used by the general color conversion path. /// The source color encoding. /// The positive original intensity target, in nits. @@ -632,9 +277,12 @@ public static bool ToXyb( } } - // Twelve scalar coefficients serve every SIMD width. Source expansion, optional linear storage and XYB conversion share one pass. - Span coefficients = stackalloc float[12]; - ComputeOpsinAbsorbanceCoefficients(intensityTarget, coefficients); + // The shared default retains the exact native forward matrix. Scale it once for this image's original intensity. + // Source expansion, optional linear storage and shared XYB arithmetic then use one pass. + XybTransform transform = KnownXybTransforms.Default; + Matrix4x4 matrix = transform.Forward * (intensityTarget / XybTransform.ReferenceIntensity); + Vector3 biases = transform.Biases; + Vector3 cubeRootBiases = transform.CubeRootBiases; bool directSrgb = currentEncoding.HaveFields && currentEncoding.WhitePoint == JxlWhitePoint.D65 && (currentEncoding.ColorSpace == JxlColorSpace.Gray @@ -642,14 +290,14 @@ public static bool ToXyb( if (directSrgb && currentEncoding.TransferFunction.IsLinear) { - RgbToXyb(coefficients, image, image, linear, default(TensorPrimitives_.IdentityOperator)); + RgbToXyb(in matrix, biases, cubeRootBiases, image, image, linear, default(TensorPrimitives_.IdentityOperator)); return true; } if (directSrgb && currentEncoding.TransferFunction.IsSrgb) { CompandingUtilities.BoundedOperator expand = new(new(1.055F, 0.055F, 0.04045F), PixelColorRange.Extended); - RgbToXyb(coefficients, image, image, linear, expand); + RgbToXyb(in matrix, biases, cubeRootBiases, image, image, linear, expand); return true; } @@ -672,7 +320,7 @@ public static bool ToXyb( linear ?? image); // Read the actual transformed owner directly. When linear output is retained, no full-image copy back to image is needed. - RgbToXyb(coefficients, linear ?? image, image, null, default(TensorPrimitives_.IdentityOperator)); + RgbToXyb(in matrix, biases, cubeRootBiases, linear ?? image, image, null, default(TensorPrimitives_.IdentityOperator)); return true; } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlPhotonNoise.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlPhotonNoise.cs index ce48a39e32..93972e8f9a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlPhotonNoise.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlPhotonNoise.cs @@ -2,7 +2,7 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.Formats.Jxl.Cms; +using SixLabors.ImageSharp.ColorProfiles; using SixLabors.ImageSharp.Formats.Jxl.Processing.Noise; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Noise; @@ -34,7 +34,8 @@ internal static class JxlPhotonNoise public static JxlNoiseParameters SimulatePhotonNoise(int xSize, int ySize, float iso) { - float opsinAbsorbanceBiasCbrt = MathF.Cbrt(JxlOpsinConstants.OpsinAbsorbanceBias1); + float opsinAbsorbanceBias = -KnownXybTransforms.Default.Biases.Y; + float opsinAbsorbanceBiasCbrt = MathF.Cbrt(opsinAbsorbanceBias); float h18 = 10f / iso; float pixelAreaUm2 = SensorAreaUm2 / (xSize * ySize); float electronsPerPixel18 = EffectiveQuantumEfficiency * PhotonsPerLxSPerUm2 * h18 * pixelAreaUm2; @@ -45,11 +46,11 @@ public static JxlNoiseParameters SimulatePhotonNoise(int xSize, int ySize, float { float scaledIndex = i / (JxlNoiseParameters.NoisePoints - 2f); float y = 2 * scaledIndex; - float linear = MathF.Max(0f, Cube(y - opsinAbsorbanceBiasCbrt) + JxlOpsinConstants.OpsinAbsorbanceBias1); + float linear = MathF.Max(0f, Cube(y - opsinAbsorbanceBiasCbrt) + opsinAbsorbanceBias); float electronsPerPixel = electronsPerPixel18 * (linear / 0.18f); float noise = MathF.Sqrt(Square(InputReferredReadNoise) + electronsPerPixel + Square(PhotoResponseNonUniformity * electronsPerPixel)); float linearNoise = noise * (0.18f / electronsPerPixel18); - float opsinDerivative = (1f / 3) / Square(MathF.Sqrt(linear - JxlOpsinConstants.OpsinAbsorbanceBias1)); + float opsinDerivative = (1f / 3) / Square(MathF.Sqrt(linear - opsinAbsorbanceBias)); float opsinNoise = linearNoise * opsinDerivative; lookup[i] = Math.Clamp(opsinNoise / (0.22f * MathF.Sqrt(2f) * 1.13f), 0f, JxlNoiseConstants.NoiseLutMax); diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/XybStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/XybStage.cs deleted file mode 100644 index 0dda4385e7..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/XybStage.cs +++ /dev/null @@ -1,150 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; -using SixLabors.ImageSharp.Formats.Jxl.Cms; -using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; -using SixLabors.ImageSharp.Memory; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; - -/// Converts reconstructed XYB rows to linear RGB or the scaled-XYB output representation. -internal class XybStage : RenderPipelineStageBase -{ - /// The inverse transform prepared before processing rows. - private readonly JxlOpsinParameters opsinParameters; - - /// Whether the requested output retains XYB components instead of reconstructing RGB. - private readonly bool outputIsXYB; - - /// Creates the in-place row stage with the selected output transform. - /// The pipeline configuration. - /// The selected output encoding and prepared inverse transform. - public XybStage(Configuration configuration, JxlOutputEncodingInfo outputEncodingInfo) - : base(configuration) - { - this.opsinParameters = outputEncodingInfo.OpsinParameters; - this.outputIsXYB = outputEncodingInfo.ColorEncoding.ColorSpace == JxlColorSpace.Xyb; - } - - /// - public override string Name => "XYB"; - - /// - public override RenderPipelineChannelMode GetChannelMode(int channel) - => channel < 3 ? RenderPipelineChannelMode.InPlace : RenderPipelineChannelMode.Ignored; - - /// - public override void ProcessRow( - Buffer2D> inputRows, - Buffer2D> outputRows, - int xExtraLeft, - int xExtraRight, - int width, - int xPos, - int yPos) - { - if (xExtraLeft != 0 || xExtraRight != 0) - { - throw new InvalidOperationException("The XYB stage does not consume horizontal border samples."); - } - - // This point-wise transform needs no halo. Logical slices exclude the pipeline prefix and all right-border padding. - Span row0 = this.GetInputRow(inputRows, 0, 0)[..width]; - Span row1 = this.GetInputRow(inputRows, 1, 0)[..width]; - Span row2 = this.GetInputRow(inputRows, 2, 0)[..width]; - - if (this.outputIsXYB) - { - ScaleXyb(row0, row1, row2); - } - else - { - JxlXybDecoder.ConvertXybToRgb(row0, row1, row2, row0, row1, row2, this.opsinParameters); - } - } - - /// Applies native output offsets and scales to the three logical XYB rows in place. - /// The X row. - /// The corresponding Y row. - /// The corresponding B row, replaced by scaled B minus Y. - private static void ScaleXyb(Span rowX, Span rowY, Span rowB) - { - ReadOnlySpan scales = JxlOpsinConstants.ScaledXybScale; - ReadOnlySpan offsets = JxlOpsinConstants.ScaledXybOffset; - ref float firstX = ref MemoryMarshal.GetReference(rowX); - ref float firstY = ref MemoryMarshal.GetReference(rowY); - ref float firstB = ref MemoryMarshal.GetReference(rowB); - int x = 0; - - // Each lane is one pixel. B minus Y uses the original Y, so load all components before any in-place store. - // Full registers never cross the logical row end; narrower vectors and scalar pixels complete the remainder. - if (Vector512.IsHardwareAccelerated) - { - Vector512 scaleX = Vector512.Create(scales[0]); - Vector512 scaleY = Vector512.Create(scales[1]); - Vector512 scaleB = Vector512.Create(scales[2]); - Vector512 offsetX = Vector512.Create(offsets[0]); - Vector512 offsetY = Vector512.Create(offsets[1]); - Vector512 offsetB = Vector512.Create(offsets[2]); - for (; x <= rowX.Length - Vector512.Count; x += Vector512.Count) - { - Vector512 inputX = Vector512.LoadUnsafe(ref firstX, (uint)x); - Vector512 inputY = Vector512.LoadUnsafe(ref firstY, (uint)x); - Vector512 inputB = Vector512.LoadUnsafe(ref firstB, (uint)x); - ((inputX + offsetX) * scaleX).StoreUnsafe(ref firstX, (uint)x); - ((inputY + offsetY) * scaleY).StoreUnsafe(ref firstY, (uint)x); - (((inputB - inputY) + offsetB) * scaleB).StoreUnsafe(ref firstB, (uint)x); - } - } - - if (Vector256.IsHardwareAccelerated) - { - Vector256 scaleX = Vector256.Create(scales[0]); - Vector256 scaleY = Vector256.Create(scales[1]); - Vector256 scaleB = Vector256.Create(scales[2]); - Vector256 offsetX = Vector256.Create(offsets[0]); - Vector256 offsetY = Vector256.Create(offsets[1]); - Vector256 offsetB = Vector256.Create(offsets[2]); - for (; x <= rowX.Length - Vector256.Count; x += Vector256.Count) - { - Vector256 inputX = Vector256.LoadUnsafe(ref firstX, (uint)x); - Vector256 inputY = Vector256.LoadUnsafe(ref firstY, (uint)x); - Vector256 inputB = Vector256.LoadUnsafe(ref firstB, (uint)x); - ((inputX + offsetX) * scaleX).StoreUnsafe(ref firstX, (uint)x); - ((inputY + offsetY) * scaleY).StoreUnsafe(ref firstY, (uint)x); - (((inputB - inputY) + offsetB) * scaleB).StoreUnsafe(ref firstB, (uint)x); - } - } - - if (Vector128.IsHardwareAccelerated) - { - Vector128 scaleX = Vector128.Create(scales[0]); - Vector128 scaleY = Vector128.Create(scales[1]); - Vector128 scaleB = Vector128.Create(scales[2]); - Vector128 offsetX = Vector128.Create(offsets[0]); - Vector128 offsetY = Vector128.Create(offsets[1]); - Vector128 offsetB = Vector128.Create(offsets[2]); - for (; x <= rowX.Length - Vector128.Count; x += Vector128.Count) - { - Vector128 inputX = Vector128.LoadUnsafe(ref firstX, (uint)x); - Vector128 inputY = Vector128.LoadUnsafe(ref firstY, (uint)x); - Vector128 inputB = Vector128.LoadUnsafe(ref firstB, (uint)x); - ((inputX + offsetX) * scaleX).StoreUnsafe(ref firstX, (uint)x); - ((inputY + offsetY) * scaleY).StoreUnsafe(ref firstY, (uint)x); - (((inputB - inputY) + offsetB) * scaleB).StoreUnsafe(ref firstB, (uint)x); - } - } - - for (; x < rowX.Length; x++) - { - float inputX = rowX[x]; - float inputY = rowY[x]; - float inputB = rowB[x]; - rowX[x] = (inputX + offsets[0]) * scales[0]; - rowY[x] = (inputY + offsets[1]) * scales[1]; - rowB[x] = ((inputB - inputY) + offsets[2]) * scales[2]; - } - } -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/YCbCrStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/YCbCrStage.cs deleted file mode 100644 index 209facd23e..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/YCbCrStage.cs +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; -using SixLabors.ImageSharp.Memory; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; - -/// Converts JPEG XL's centered Cb, Y and Cr rows to RGB in place. -/// The configuration used by the render stage. -internal sealed class YCbCrStage(Configuration configuration) : RenderPipelineStageBase(configuration) -{ - /// The luma offset removed during JPEG XL encoding, with component units relative to RGB white at one. - private const float LumaOffset = 128F / 255F; - - /// - public override string Name => "YCbCr"; - - /// - public override void ProcessRow( - Buffer2D> inputRows, - Buffer2D> outputRows, - int xExtraLeft, - int xExtraRight, - int width, - int xPos, - int yPos) - { - // Native stage_ycbcr consumes point-wise samples. The renderer supplies no horizontal halo for this stage. - if (xExtraLeft != 0 || xExtraRight != 0) - { - throw new InvalidOperationException("The YCbCr stage does not consume horizontal border samples."); - } - - Span row0 = this.GetInputRow(inputRows, 0, 0)[..width]; - Span row1 = this.GetInputRow(inputRows, 1, 0)[..width]; - Span row2 = this.GetInputRow(inputRows, 2, 0)[..width]; - ref float first0 = ref MemoryMarshal.GetReference(row0); - ref float first1 = ref MemoryMarshal.GetReference(row1); - ref float first2 = ref MemoryMarshal.GetReference(row2); - int x = 0; - - // JPEG XL stores Cb, Y, Cr, with centered chroma and a biased Y. Restore Y before the shared BT.601 calculation. - // Read all three components before writing R, G, B over those rows. Only full logical blocks enter SIMD; - // narrower registers and the scalar tail complete the row without reading or changing its padded suffix. - if (Vector512.IsHardwareAccelerated) - { - Vector512 offset = Vector512.Create(LumaOffset); - - for (; x <= width - Vector512.Count; x += Vector512.Count) - { - Vector512 cb = Vector512.LoadUnsafe(ref first0, (uint)x); - Vector512 y = Vector512.LoadUnsafe(ref first1, (uint)x) + offset; - Vector512 cr = Vector512.LoadUnsafe(ref first2, (uint)x); - ColorNumerics.ConvertBt601YCbCrToRgb(y, cb, cr, out Vector512 r, out Vector512 g, out Vector512 b); - r.StoreUnsafe(ref first0, (uint)x); - g.StoreUnsafe(ref first1, (uint)x); - b.StoreUnsafe(ref first2, (uint)x); - } - } - - if (Vector256.IsHardwareAccelerated) - { - Vector256 offset = Vector256.Create(LumaOffset); - - for (; x <= width - Vector256.Count; x += Vector256.Count) - { - Vector256 cb = Vector256.LoadUnsafe(ref first0, (uint)x); - Vector256 y = Vector256.LoadUnsafe(ref first1, (uint)x) + offset; - Vector256 cr = Vector256.LoadUnsafe(ref first2, (uint)x); - ColorNumerics.ConvertBt601YCbCrToRgb(y, cb, cr, out Vector256 r, out Vector256 g, out Vector256 b); - r.StoreUnsafe(ref first0, (uint)x); - g.StoreUnsafe(ref first1, (uint)x); - b.StoreUnsafe(ref first2, (uint)x); - } - } - - if (Vector128.IsHardwareAccelerated) - { - Vector128 offset = Vector128.Create(LumaOffset); - - for (; x <= width - Vector128.Count; x += Vector128.Count) - { - Vector128 cb = Vector128.LoadUnsafe(ref first0, (uint)x); - Vector128 y = Vector128.LoadUnsafe(ref first1, (uint)x) + offset; - Vector128 cr = Vector128.LoadUnsafe(ref first2, (uint)x); - ColorNumerics.ConvertBt601YCbCrToRgb(y, cb, cr, out Vector128 r, out Vector128 g, out Vector128 b); - r.StoreUnsafe(ref first0, (uint)x); - g.StoreUnsafe(ref first1, (uint)x); - b.StoreUnsafe(ref first2, (uint)x); - } - } - - for (; x < width; x++) - { - float cb = row0[x]; - float y = row1[x] + LumaOffset; - float cr = row2[x]; - ColorNumerics.ConvertBt601YCbCrToRgb(y, cb, cr, out float r, out float g, out float b); - row0[x] = r; - row1[x] = g; - row2[x] = b; - } - } - - /// - public override RenderPipelineChannelMode GetChannelMode(int channel) - => channel < 3 ? RenderPipelineChannelMode.InPlace : RenderPipelineChannelMode.Ignored; -} diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/OpsinImageTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/OpsinImageTests.cs index a8dedfc1b9..8ffe5ef7af 100644 --- a/tests/ImageSharp.Tests/Formats/Jxl/Processing/OpsinImageTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/OpsinImageTests.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using SixLabors.ImageSharp.ColorProfiles; using SixLabors.ImageSharp.ColorProfiles.Companding; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Jxl.Cms; @@ -11,8 +12,6 @@ using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; -using SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; -using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.TestUtilities; @@ -38,14 +37,23 @@ private static void LinearSRgbToOpsin(float rgbR, float rgbG, float rgbB, out fl linear.PlaneRow(0, 0)[0] = rgbR; linear.PlaneRow(1, 0)[0] = rgbG; linear.PlaneRow(2, 0)[0] = rgbB; - Assert.True(JxlXybEncoder.ToXyb( - TestEnvironment.Configuration, - JxlColorEncoding.LinearSRgb, - JxlShared.DefaultIntensityTarget, - null, - new JxlImage3(), - JxlCmsInterface.CreateDefaultCms(), - null)); + XybTransform transform = KnownXybTransforms.Default; + Matrix4x4 matrix = transform.ScaledForward; + JxlXybEncoder.RgbRowToXyb( + linear.PlaneRow(0, 0)[..1], + linear.PlaneRow(1, 0)[..1], + linear.PlaneRow(2, 0)[..1], + linear.PlaneRow(0, 0)[..1], + linear.PlaneRow(1, 0)[..1], + linear.PlaneRow(2, 0)[..1], + [], + [], + [], + in matrix, + transform.Biases, + transform.CubeRootBiases, + default(TensorPrimitives_.IdentityOperator)); + xybX = linear.PlaneRow(0, 0)[0]; xybY = linear.PlaneRow(1, 0)[0]; xybB = linear.PlaneRow(2, 0)[0]; @@ -70,15 +78,11 @@ private static void OpsinToLinearSRgb(float xybX, float xybY, float xybB, out fl using JxlImage3 linear = new(TestEnvironment.Configuration, 1, 1); - JxlOpsinParameters opsinParameters = new(); - JxlOpsinInverseMatrix inverse = new(); - opsinParameters.Initialize(inverse.InverseMatrix, inverse.OpsinBiases, 255F); - Assert.True(JxlXybDecoder.OpsinToLinear( opsin, opsin.GetRectangle(), linear, - opsinParameters)); + KnownXybTransforms.Default)); rgbR = linear.PlaneRow(0, 0)[0]; rgbG = linear.PlaneRow(1, 0)[0]; @@ -103,17 +107,15 @@ public void VerifyOpsinAbsorbanceInverseMatrix() { const float tolerance = 1e-6f; - JxlMatrix3x3F matrix = JxlOpsinConstants.GetOpsinAbsorbanceInverseMatrix(); - - Assert.True(JxlMatrix3x3F.Invert(ref matrix)); + Assert.True(Matrix4x4.Invert(KnownXybTransforms.Default.Inverse, out Matrix4x4 matrix)); for (int j = 0; j < 3; j++) { for (int i = 0; i < 3; i++) { Assert.Equal( - matrix.DangerousGetReferenceTo(j, i), - JxlOpsinConstants.OpsinAbsorbanceMatrix[j][i], + matrix[j, i], + KnownXybTransforms.Default.Forward[j, i], tolerance); } } @@ -167,7 +169,9 @@ public void VerifyGray() } } - /// Checks bias subtraction, intensity scaling, rectangular origins and exact row guards at every SIMD capability. + /// + /// Checks bias subtraction, intensity scaling, rectangular origins and exact row guards at every SIMD capability. + /// [Fact] public void InverseOpsin_ReconstructsDyadicSamplesWithinRectangle() { @@ -183,7 +187,7 @@ static void RunTest() const float untouched = -777F; foreach (float intensity in intensities) { - JxlOpsinParameters parameters = CreateDyadicOpsinParameters(intensity); + XybTransform parameters = CreateDyadicOpsinTransform(intensity); foreach (int width in widths) { using JxlImage3 source = new(TestEnvironment.Configuration, width + 19, 6); @@ -241,73 +245,50 @@ static void RunTest() HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); } - /// Checks in-place RGB and scaled-XYB stage output with zero halos, short rows and unchanged extra-channel storage. + /// + /// Checks in-place XYB reconstruction, short rows and unchanged padding and extra-channel storage. + /// [Fact] - public void XybStage_ConvertsLogicalRowsAndPreservesPadding() + public void XybConverter_ConvertsLogicalRowsAndPreservesPadding() { static void RunTest() { ReadOnlySpan widths = [1, 3, 17, 33]; ReadOnlySpan xyb = [0F, 0F, 0F, 0.5F, 0.5F, 1F, -0.5F, 0.5F, -1F, 0.25F, 0.25F, -0.5F]; ReadOnlySpan rgb = [0F, 0F, 0F, 5.25F, -1.75F, 8.75F, 3.75F, 13.75F, -4.5F, 2.59375F, -1.40625F, -0.28125F]; - const int origin = RenderPipelineStageBase.RenderPipelineXOffset; + const int origin = 2; const float untouched = -777F; + JxlColorConverter converter = JxlColorConverter.CreateXyb(CreateDyadicOpsinTransform(510F)); + foreach (int width in widths) { - for (int scaled = 0; scaled < 2; scaled++) + using JxlPlane storage = new(TestEnvironment.Configuration, origin + width + 3, 4); + Span samples = storage.GetRow(0); + samples.Fill(untouched); + + for (int c = 0; c < 3; c++) { - int storageWidth = origin + width + 3; - using JxlPlane storage = new(TestEnvironment.Configuration, storageWidth, 4); - Span samples = storage.GetRow(0); - samples.Fill(untouched); - Memory[] table = new Memory[4]; - for (int c = 0; c < table.Length; c++) + for (int x = 0; x < width; x++) { - table[c] = storage.GetRowMemory(c)[..storageWidth]; - if (c < 3) - { - for (int x = 0; x < width; x++) - { - samples[(c * storage.PixelsPerRow) + origin + x] = xyb[((x % 4) * 3) + c]; - } - } + samples[(c * storage.PixelsPerRow) + origin + x] = xyb[((x % 4) * 3) + c]; } + } - // The row table borrows numeric storage. The stage reads no vertical or horizontal border samples and writes in place. - using Buffer2D> rows = new(MemoryGroup>.Wrap(table.AsMemory()), 4, 1); - JxlOutputEncodingInfo info = new() - { - OpsinParameters = CreateDyadicOpsinParameters(510F), - ColorEncoding = new() { ColorSpace = scaled == 0 ? JxlColorSpace.Rgb : JxlColorSpace.Xyb } - }; + // Each logical component row has a prefix and suffix. The fourth row is unrelated extra-channel storage. + Span row0 = samples.Slice(origin, width); + Span row1 = samples.Slice(storage.PixelsPerRow + origin, width); + Span row2 = samples.Slice((2 * storage.PixelsPerRow) + origin, width); + converter.Convert(row0, row1, row2, row0, row1, row2); - using XybStage stage = new(TestEnvironment.Configuration, info); - stage.ProcessRow(rows, rows, 0, 0, width, 0, 0); - for (int c = 0; c < 4; c++) - { - ReadOnlySpan actual = samples.Slice(c * storage.PixelsPerRow, storage.PixelsPerRow); - for (int i = 0; i < actual.Length; i++) - { - int x = i - origin; - float expected = untouched; - if (c < 3 && x >= 0 && x < width) - { - int sample = (x % 4) * 3; - if (scaled == 0) - { - expected = rgb[sample + c] * 0.5F; - } - else - { - // Native scaled output uses (X+offset)*scale, Y*scale, and (B-Y+offset)*scale in this order. - expected = c == 0 ? (xyb[sample] + 0.015386134F) * 22.995788804F - : c == 1 ? xyb[sample + 1] * 1.183000077F - : ((xyb[sample + 2] - xyb[sample + 1]) + 0.27770459F) * 1.502141333F; - } - } + for (int c = 0; c < 4; c++) + { + ReadOnlySpan actual = samples.Slice(c * storage.PixelsPerRow, storage.PixelsPerRow); - Assert.Equal(expected, actual[i]); - } + for (int i = 0; i < actual.Length; i++) + { + int x = i - origin; + float expected = c < 3 && x >= 0 && x < width ? rgb[((x % 4) * 3) + c] * 0.5F : untouched; + Assert.Equal(expected, actual[i]); } } } @@ -318,7 +299,9 @@ static void RunTest() HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); } - /// Checks the actual forward row path, native matrix equations, shared input expansion and exact storage bounds. + /// + /// Checks the actual forward row path, native matrix equations, shared input expansion and exact storage bounds. + /// [Fact] public void ForwardOpsin_ConvertsRowsAndRetainsRequestedLinearSamples() { @@ -346,8 +329,8 @@ static void RunTest() -0.25F, 0.125F, 0.5F ]; - Span coefficients = stackalloc float[12]; - coefficients[9] = coefficients[10] = coefficients[11] = -MathF.Cbrt(bias); + Vector3 biases = new(-bias); + Vector3 cubeRootBiases = new(-MathF.Cbrt(bias)); const float untouched = -777F; CompandingUtilities.BoundedOperator expand = new(new(1.055F, 0.055F, 0.04045F), PixelColorRange.Extended); foreach (int width in widths) @@ -366,10 +349,12 @@ static void RunTest() foreach (float intensity in intensities) { // These are native input parameters, independent of the production coefficient preparation method. - for (int i = 0; i < matrix.Length; i++) - { - coefficients[i] = matrix[i] * (intensity / 255F); - } + float scale = intensity / 255F; + Matrix4x4 scaledForward = new( + matrix[0] * scale, matrix[3] * scale, matrix[6] * scale, 0F, + matrix[1] * scale, matrix[4] * scale, matrix[7] * scale, 0F, + matrix[2] * scale, matrix[5] * scale, matrix[8] * scale, 0F, + 0F, 0F, 0F, 1F); for (int mode = 0; mode < 4; mode++) { @@ -394,7 +379,20 @@ static void RunTest() Span retained2 = retainLinear ? linear2.Slice(4, width) : []; if (encoded) { - JxlXybEncoder.RgbRowToXyb(row0, row1, row2, row0, row1, row2, retained0, retained1, retained2, coefficients, expand); + JxlXybEncoder.RgbRowToXyb( + row0, + row1, + row2, + row0, + row1, + row2, + retained0, + retained1, + retained2, + in scaledForward, + biases, + cubeRootBiases, + expand); } else { @@ -408,7 +406,9 @@ static void RunTest() retained0, retained1, retained2, - coefficients, + in scaledForward, + biases, + cubeRootBiases, default(TensorPrimitives_.IdentityOperator)); } @@ -491,30 +491,33 @@ static void RunTest() HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); } - /// Prepares a nonsymmetric dyadic inverse matrix and exact cube-root biases for independent numerical checks. + /// + /// Prepares a nonsymmetric dyadic inverse matrix and exact cube-root biases for independent numerical checks. + /// /// The relative-output intensity target: 255 or 510 nits. /// The prepared inverse transform. - private static JxlOpsinParameters CreateDyadicOpsinParameters(float intensityTarget) + private static XybTransform CreateDyadicOpsinTransform(float intensityTarget) { JxlMatrix3x3F matrix = default; ReadOnlySpan coefficients = [1F, 0.5F, -0.25F, -0.5F, 2F, 0.25F, 0.25F, -0.5F, 1F]; coefficients.CopyTo(matrix.AsSpan()); InlineArray3 biases = default; biases[0] = biases[1] = biases[2] = -1F; - JxlOpsinParameters parameters = new(); - parameters.Initialize(in matrix, biases, intensityTarget); - return parameters; + JxlOpsinInverseMatrix header = new() { InverseMatrix = matrix, OpsinBiases = biases }; + return header.CreateTransform(intensityTarget); } - /// Checks centered chroma, luma bias, HDR values and exact in-place YCbCr row bounds at every SIMD width. + /// + /// Checks centered chroma, luma bias, HDR values and exact in-place YCbCr row bounds at every SIMD width. + /// [Fact] - public void YCbCrStage_ConvertsCenteredChromaAndPreservesHdrAndPadding() + public void YCbCrConverter_ConvertsCenteredChromaAndPreservesHdrAndPadding() { static void RunTest() { ReadOnlySpan widths = [1, 3, 17, 33]; ReadOnlySpan chroma = [0F, 0F, 0.25F, 0F, 0F, 0.5F, 0.25F, 0.5F, -0.25F, -0.5F, 32768F, 0F, 0F, -32768F, 32768F, 32768F]; - const int origin = RenderPipelineStageBase.RenderPipelineXOffset; + const int origin = 2; const float untouched = -777F; const float storedY = -128F / 255F; const float greenCb = (float)(0.114 * 1.772 / 0.587); @@ -526,24 +529,21 @@ static void RunTest() using JxlPlane storage = new(TestEnvironment.Configuration, storageWidth, 4); Span samples = storage.GetRow(0); samples.Fill(untouched); - Memory[] table = new Memory[4]; - for (int c = 0; c < table.Length; c++) + for (int c = 0; c < 3; c++) { - table[c] = storage.GetRowMemory(c)[..storageWidth]; - if (c < 3) + for (int x = 0; x < width; x++) { - for (int x = 0; x < width; x++) - { - int sample = (x % 8) * 2; - samples[(c * storage.PixelsPerRow) + origin + x] = c == 0 ? chroma[sample] : c == 1 ? storedY : chroma[sample + 1]; - } + int sample = (x % 8) * 2; + samples[(c * storage.PixelsPerRow) + origin + x] = c == 0 ? chroma[sample] : c == 1 ? storedY : chroma[sample + 1]; } } - // The row table borrows the numeric allocation. JPEG XL stores Cb, biased Y and Cr in that order. - using Buffer2D> rows = new(MemoryGroup>.Wrap(table.AsMemory()), 4, 1); - using YCbCrStage stage = new(TestEnvironment.Configuration); - stage.ProcessRow(rows, rows, 0, 0, width, 0, 0); + // JPEG XL stores centered Cb, biased Y and centered Cr. Only the three logical rows can change. + Span row0 = samples.Slice(origin, width); + Span row1 = samples.Slice(storage.PixelsPerRow + origin, width); + Span row2 = samples.Slice((2 * storage.PixelsPerRow) + origin, width); + JxlColorConverter converter = JxlColorConverter.CreateYCbCr(); + converter.Convert(row0, row1, row2, row0, row1, row2); for (int c = 0; c < 4; c++) {