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] 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 e5a6b4549..a5a557179 100644 --- a/src/ImageSharp/Common/Helpers/Numerics.cs +++ b/src/ImageSharp/Common/Helpers/Numerics.cs @@ -1033,4 +1033,13 @@ internal static class Numerics 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 6bb1f59ef..6b4c6ad63 100644 --- a/src/ImageSharp/Common/Helpers/Vector128Utilities.cs +++ b/src/ImageSharp/Common/Helpers/Vector128Utilities.cs @@ -859,4 +859,44 @@ internal static class 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 1dd712271..4bd78b88f 100644 --- a/src/ImageSharp/Common/Helpers/Vector256Utilities.cs +++ b/src/ImageSharp/Common/Helpers/Vector256Utilities.cs @@ -397,4 +397,170 @@ internal static class Vector256_ 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 700551a8f..d4cde5f25 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 d689b0469..998f8ae10 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 3b3c6bc20..7cc5b9fe5 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 ef74d8012..a6df675ac 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 f006d2fd0..acb9c3d41 100644 --- a/src/ImageSharp/Formats/Jxl/InlineArrays.cs +++ b/src/ImageSharp/Formats/Jxl/InlineArrays.cs @@ -7,30 +7,6 @@ using System.Runtime.CompilerServices; 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 d339e6908..8a37b7dba 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 000000000..316f1ed12 --- /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 000000000..9b8e2f9fa --- /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 000000000..2b05317ab --- /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 000000000..15b093541 --- /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 a7c9dbc1a..000000000 --- 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 825c81cbb..2edfeca0c 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 79eee3022..0157dba4e 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 @@ internal static class JxlMath 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 15aa58715..fadb8709f 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 2c50d92af..dfe12b8f3 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 9551b97e0..54a41f99a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs @@ -134,7 +134,7 @@ internal sealed class JxlQuantizer /// /// 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 @@ internal sealed class JxlQuantizer /// /// 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 @@ internal sealed class JxlQuantizer /// 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 @@ internal sealed class JxlQuantizer /// 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 000000000..daf28db69 --- /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 fead5354c..8a12e6dbf 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 @@ internal sealed class JxlQuantizerEncoding /// /// 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 @@ internal sealed class JxlQuantizerEncoding /// /// 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 @@ internal sealed class JxlQuantizerEncoding /// /// 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 @@ internal sealed class JxlQuantizerEncoding /// 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 @@ internal sealed class JxlQuantizerEncoding /// 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 @@ internal sealed class JxlQuantizerEncoding 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 000000000..b03c77ece --- /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 000000000..41a5b9c90 --- /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 000000000..dd8e7314e --- /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 b2a0c0c55..91258b852 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 98c1d2dc5..fcbef7afd 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 @@ internal static class JxlContextPrediction 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 000000000..f1822a2a0 --- /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 000000000..5144f14cb --- /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 d5fd89881..acf3b9532 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 @@ internal sealed class JxlModularChannel /// 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 @@ internal sealed class JxlModularChannel 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 488e198c4..ebcdcdcdc 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 4babb79f3..bfcd683c5 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 @@ internal static class JxlPalette 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 0f32e47ae..315fcf2b4 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 000000000..d21d0438c --- /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 7bb52fe5e..1d4c33277 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 @@ internal sealed class JxlSqueezeParameters : IJxlFields /// 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 860adb469..75ba98608 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 @@ internal sealed class JxlTransform : IJxlFields } } } + + 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 000000000..56f52fab8 --- /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 000000000..8a8536715 --- /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 000000000..52150ae27 --- /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 000000000..4d936af8f --- /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 000000000..b02b62db7 --- /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 000000000..acd7b8b76 --- /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 65f217792..fd9a068ed 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 971d73b84..b3925b7a7 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 000000000..1c294afa4 --- /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)}]"); + } + } +}