From e9db3675e92222a99a52dd423f90997d29d8b780 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 25 Jul 2026 02:25:02 +1000 Subject: [PATCH] Normalize JPEG color conversion SIMD --- .../JpegColorConverter.CmykOperator.cs | 245 ++++++++ .../JpegColorConverter.GrayScaleOperator.cs | 203 ++++++ .../JpegColorConverter.Operator.cs | 594 ++++++++++++++++++ .../JpegColorConverter.RgbOperator.cs | 184 ++++++ .../JpegColorConverter.TiffCmykOperator.cs | 159 +++++ .../JpegColorConverter.TiffYccKOperator.cs | 187 ++++++ .../JpegColorConverter.YCbCrOperator.cs | 263 ++++++++ .../JpegColorConverter.YccKOperator.cs | 135 ++++ .../ColorConverters/JpegColorConverterBase.cs | 155 +---- .../ColorConversionBenchmark.cs | 1 + .../JpegColorConverterOperatorComparison.cs | 239 +++++++ .../JpegColorConverterTraversalAssembly.cs | 325 ++++++++++ .../ColorConversion/YCbCrOperatorAssembly.cs | 244 +++++++ .../YCbCrOperatorComparison.cs | 127 ++++ .../Formats/Jpg/JpegColorConverterTests.cs | 354 +++++++++-- 15 files changed, 3217 insertions(+), 198 deletions(-) create mode 100644 src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykOperator.cs create mode 100644 src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleOperator.cs create mode 100644 src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.Operator.cs create mode 100644 src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbOperator.cs create mode 100644 src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykOperator.cs create mode 100644 src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKOperator.cs create mode 100644 src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrOperator.cs create mode 100644 src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKOperator.cs create mode 100644 tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/JpegColorConverterOperatorComparison.cs create mode 100644 tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/JpegColorConverterTraversalAssembly.cs create mode 100644 tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/YCbCrOperatorAssembly.cs create mode 100644 tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/YCbCrOperatorComparison.cs diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykOperator.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykOperator.cs new file mode 100644 index 000000000..48281c34a --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykOperator.cs @@ -0,0 +1,245 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components; + +internal abstract partial class JpegColorConverterBase +{ + /// + /// Implements inverted JPEG CMYK conversion for scalar and SIMD lanes. + /// + internal readonly struct CmykOperator : IJpegColorConverterOperator + { + /// + public static JpegColorSpace ColorSpace => JpegColorSpace.Cmyk; + + /// + public static int ComponentCount => 4; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb( + ref float c0, + ref float c1, + ref float c2, + float c3, + float maximumValue, + float halfValue, + float scale) + { + // Adobe-style CMYK stores inverted component samples. Multiplying K by scale twice folds the + // two sample-domain divisions into one factor before it modulates the C, M, and Y planes. + float scaledK = c3 * scale * scale; + c0 *= scaledK; + c1 *= scaledK; + c2 *= scaledK; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb( + ref Vector128 c0, + ref Vector128 c1, + ref Vector128 c2, + Vector128 c3, + Vector128 maximumValue, + Vector128 halfValue, + Vector128 scale) + { + // Each K lane supplies the common modulation factor for the corresponding C, M, and Y lanes. + Vector128 scaledK = c3 * scale * scale; + c0 *= scaledK; + c1 *= scaledK; + c2 *= scaledK; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb( + ref Vector256 c0, + ref Vector256 c1, + ref Vector256 c2, + Vector256 c3, + Vector256 maximumValue, + Vector256 halfValue, + Vector256 scale) + { + // Eight independent CMYK samples remain lane-aligned throughout the modulation. + Vector256 scaledK = c3 * scale * scale; + c0 *= scaledK; + c1 *= scaledK; + c2 *= scaledK; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb( + ref Vector512 c0, + ref Vector512 c1, + ref Vector512 c2, + Vector512 c3, + Vector512 maximumValue, + Vector512 halfValue, + Vector512 scale) + { + // Sixteen independent CMYK samples remain lane-aligned throughout the modulation. + Vector512 scaledK = c3 * scale * scale; + c0 *= scaledK; + c1 *= scaledK; + c2 *= scaledK; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + float r, + float g, + float b, + float maximumValue, + float halfValue, + float scale, + out float c0, + out float c1, + out float c2, + out float c3) + { + float c = maximumValue - r; + float m = maximumValue - g; + float y = maximumValue - b; + float k = MathF.Min(c, MathF.Min(m, y)); + + // Pure black makes the chromatic divisor zero. In that case chromatic ink is defined as zero; + // otherwise remove K and normalize the remaining C, M, and Y contributions. + if (k >= maximumValue) + { + c = 0; + m = 0; + y = 0; + } + else + { + // The same remaining range normalizes every chromatic channel. Computing its reciprocal once + // replaces three divisions with one division and three multiplies. + float reciprocal = 1F / (maximumValue - k); + c = (c - k) * reciprocal; + m = (m - k) * reciprocal; + y = (y - k) * reciprocal; + } + + // JPEG CMYK is inverted, including K, so normalized chromatic values are reflected around max. + c0 = maximumValue - (c * maximumValue); + c1 = maximumValue - (m * maximumValue); + c2 = maximumValue - (y * maximumValue); + c3 = maximumValue - k; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector128 r, + Vector128 g, + Vector128 b, + Vector128 maximumValue, + Vector128 halfValue, + Vector128 scale, + out Vector128 c0, + out Vector128 c1, + out Vector128 c2, + out Vector128 c3) + { + Vector128 c = maximumValue - r; + Vector128 m = maximumValue - g; + Vector128 y = maximumValue - b; + Vector128 k = Vector128.Min(c, Vector128.Min(m, y)); + + // The all-bits mask clears the undefined zero-divisor result for pure-black lanes without a branch. + Vector128 nonBlack = ~Vector128.Equals(k, maximumValue); + Vector128 reciprocal = Vector128.One / (maximumValue - k); + c = ((c - k) * reciprocal) & nonBlack; + m = ((m - k) * reciprocal) & nonBlack; + y = ((y - k) * reciprocal) & nonBlack; + + c0 = maximumValue - (c * maximumValue); + c1 = maximumValue - (m * maximumValue); + c2 = maximumValue - (y * maximumValue); + c3 = maximumValue - k; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector256 r, + Vector256 g, + Vector256 b, + Vector256 maximumValue, + Vector256 halfValue, + Vector256 scale, + out Vector256 c0, + out Vector256 c1, + out Vector256 c2, + out Vector256 c3) + { + Vector256 c = maximumValue - r; + Vector256 m = maximumValue - g; + Vector256 y = maximumValue - b; + Vector256 k = Vector256.Min(c, Vector256.Min(m, y)); + + // Masking preserves lane independence when a vector mixes pure black with chromatic pixels. + Vector256 nonBlack = ~Vector256.Equals(k, maximumValue); + Vector256 reciprocal = Vector256.One / (maximumValue - k); + c = ((c - k) * reciprocal) & nonBlack; + m = ((m - k) * reciprocal) & nonBlack; + y = ((y - k) * reciprocal) & nonBlack; + + c0 = maximumValue - (c * maximumValue); + c1 = maximumValue - (m * maximumValue); + c2 = maximumValue - (y * maximumValue); + c3 = maximumValue - k; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector512 r, + Vector512 g, + Vector512 b, + Vector512 maximumValue, + Vector512 halfValue, + Vector512 scale, + out Vector512 c0, + out Vector512 c1, + out Vector512 c2, + out Vector512 c3) + { + Vector512 c = maximumValue - r; + Vector512 m = maximumValue - g; + Vector512 y = maximumValue - b; + Vector512 k = Vector512.Min(c, Vector512.Min(m, y)); + + // AVX-512 still uses a full floating-point mask value here because bitwise clearing exactly matches + // the narrower operator semantics and lets the JIT select the most suitable native instructions. + Vector512 nonBlack = ~Vector512.Equals(k, maximumValue); + Vector512 reciprocal = Vector512.One / (maximumValue - k); + c = ((c - k) * reciprocal) & nonBlack; + m = ((m - k) * reciprocal) & nonBlack; + y = ((y - k) * reciprocal) & nonBlack; + + c0 = maximumValue - (c * maximumValue); + c1 = maximumValue - (m * maximumValue); + c2 = maximumValue - (y * maximumValue); + c3 = maximumValue - k; + } + + /// + public static void ConvertToRgbInPlaceWithIcc( + Configuration configuration, + IccProfile profile, + in ComponentValues values, + float maximumValue) + => CmykScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, maximumValue); + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleOperator.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleOperator.cs new file mode 100644 index 000000000..2ac827da7 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleOperator.cs @@ -0,0 +1,203 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components; + +internal abstract partial class JpegColorConverterBase +{ + /// + /// Implements grayscale expansion and RGB luminance reduction for scalar and SIMD lanes. + /// + internal readonly struct GrayScaleOperator : IJpegColorConverterOperator + { + /// + public static JpegColorSpace ColorSpace => JpegColorSpace.Grayscale; + + /// + public static int ComponentCount => 1; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb( + ref float c0, + ref float c1, + ref float c2, + float c3, + float maximumValue, + float halfValue, + float scale) + { + // JPEG stores luminance in the integer sample domain. Normalize it once, then duplicate the + // same value into all three RGB planes. Keeping it local also prevents potentially aliasing + // byref stores from forcing the JIT to reload c0 between assignments. + float luminance = c0 * scale; + c0 = luminance; + c1 = luminance; + c2 = luminance; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb( + ref Vector128 c0, + ref Vector128 c1, + ref Vector128 c2, + Vector128 c3, + Vector128 maximumValue, + Vector128 halfValue, + Vector128 scale) + { + // Each XMM lane is one independent luminance sample. Reusing the normalized vector for R, G, + // and B avoids recomputing the scale and keeps it live across potentially aliasing byref stores. + Vector128 luminance = c0 * scale; + c0 = luminance; + c1 = luminance; + c2 = luminance; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb( + ref Vector256 c0, + ref Vector256 c1, + ref Vector256 c2, + Vector256 c3, + Vector256 maximumValue, + Vector256 halfValue, + Vector256 scale) + { + // Eight luminance samples occupy the YMM lanes. The local retains the normalized vector across + // all three output stores even when the destination planes alias. + Vector256 luminance = c0 * scale; + c0 = luminance; + c1 = luminance; + c2 = luminance; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb( + ref Vector512 c0, + ref Vector512 c1, + ref Vector512 c2, + Vector512 c3, + Vector512 maximumValue, + Vector512 halfValue, + Vector512 scale) + { + // Sixteen luminance samples occupy the ZMM lanes. The local retains the normalized vector across + // all three output stores without shuffles, interleaving, or source reloads. + Vector512 luminance = c0 * scale; + c0 = luminance; + c1 = luminance; + c2 = luminance; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + float r, + float g, + float b, + float maximumValue, + float halfValue, + float scale, + out float c0, + out float c1, + out float c2, + out float c3) + { + // Rec.601 luma weights operate directly in the encoder sample domain. Only c0 is stored for a + // one-component model; the remaining out values exist solely to satisfy the common operator shape. + c0 = (0.299F * r) + (0.587F * g) + (0.114F * b); + c1 = 0; + c2 = 0; + c3 = 0; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector128 r, + Vector128 g, + Vector128 b, + Vector128 maximumValue, + Vector128 halfValue, + Vector128 scale, + out Vector128 c0, + out Vector128 c1, + out Vector128 c2, + out Vector128 c3) + { + // The nested estimate gives each pixel the same multiply-add grouping as the scalar Rec.601 formula. + c0 = Vector128_.MultiplyAddEstimate( + Vector128.Create(0.299F), + r, + Vector128_.MultiplyAddEstimate(Vector128.Create(0.587F), g, Vector128.Create(0.114F) * b)); + c1 = default; + c2 = default; + c3 = default; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector256 r, + Vector256 g, + Vector256 b, + Vector256 maximumValue, + Vector256 halfValue, + Vector256 scale, + out Vector256 c0, + out Vector256 c1, + out Vector256 c2, + out Vector256 c3) + { + // YMM lanes evaluate the same Rec.601 equation independently, with no horizontal lane reduction. + c0 = Vector256_.MultiplyAddEstimate( + Vector256.Create(0.299F), + r, + Vector256_.MultiplyAddEstimate(Vector256.Create(0.587F), g, Vector256.Create(0.114F) * b)); + c1 = default; + c2 = default; + c3 = default; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector512 r, + Vector512 g, + Vector512 b, + Vector512 maximumValue, + Vector512 halfValue, + Vector512 scale, + out Vector512 c0, + out Vector512 c1, + out Vector512 c2, + out Vector512 c3) + { + // ZMM lanes retain the same arithmetic order as narrower paths so only SIMD width changes. + c0 = Vector512_.MultiplyAddEstimate( + Vector512.Create(0.299F), + r, + Vector512_.MultiplyAddEstimate(Vector512.Create(0.587F), g, Vector512.Create(0.114F) * b)); + c1 = default; + c2 = default; + c3 = default; + } + + /// + public static void ConvertToRgbInPlaceWithIcc( + Configuration configuration, + IccProfile profile, + in ComponentValues values, + float maximumValue) + => GrayScaleScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, maximumValue); + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.Operator.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.Operator.cs new file mode 100644 index 000000000..be9cedcb0 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.Operator.cs @@ -0,0 +1,594 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components; + +internal abstract partial class JpegColorConverterBase +{ + /// + /// Defines the color-model-specific arithmetic used by . + /// + /// + /// Each overload describes the same lane-wise transform. The generic traversal selects the widest + /// available overload and the JIT resolves these static interface calls for each closed converter type. + /// + internal interface IJpegColorConverterOperator + { + /// + /// Gets the JPEG color space handled by the operator. + /// + static abstract JpegColorSpace ColorSpace { get; } + + /// + /// Gets the number of component planes used by the color space. + /// + static abstract int ComponentCount { get; } + + /// + /// Converts one JPEG sample to normalized RGB. + /// + /// The first component, replaced by red. + /// The second component, replaced by green. + /// The third component, replaced by blue. + /// The fourth component, or zero for a three-component color space. + /// The maximum component value for the configured precision. + /// The midpoint component value for the configured precision. + /// The reciprocal of . + static abstract void ConvertToRgb( + ref float c0, + ref float c1, + ref float c2, + float c3, + float maximumValue, + float halfValue, + float scale); + + /// + /// Converts four JPEG samples to normalized RGB. + /// + /// The first component lanes, replaced by red. + /// The second component lanes, replaced by green. + /// The third component lanes, replaced by blue. + /// The fourth component lanes, or zero for a three-component color space. + /// The maximum component value for the configured precision. + /// The midpoint component value for the configured precision. + /// The reciprocal of in every lane. + static abstract void ConvertToRgb( + ref Vector128 c0, + ref Vector128 c1, + ref Vector128 c2, + Vector128 c3, + Vector128 maximumValue, + Vector128 halfValue, + Vector128 scale); + + /// + /// Converts eight JPEG samples to normalized RGB. + /// + /// The first component lanes, replaced by red. + /// The second component lanes, replaced by green. + /// The third component lanes, replaced by blue. + /// The fourth component lanes, or zero for a three-component color space. + /// The maximum component value for the configured precision. + /// The midpoint component value for the configured precision. + /// The reciprocal of in every lane. + static abstract void ConvertToRgb( + ref Vector256 c0, + ref Vector256 c1, + ref Vector256 c2, + Vector256 c3, + Vector256 maximumValue, + Vector256 halfValue, + Vector256 scale); + + /// + /// Converts sixteen JPEG samples to normalized RGB. + /// + /// The first component lanes, replaced by red. + /// The second component lanes, replaced by green. + /// The third component lanes, replaced by blue. + /// The fourth component lanes, or zero for a three-component color space. + /// The maximum component value for the configured precision. + /// The midpoint component value for the configured precision. + /// The reciprocal of in every lane. + static abstract void ConvertToRgb( + ref Vector512 c0, + ref Vector512 c1, + ref Vector512 c2, + Vector512 c3, + Vector512 maximumValue, + Vector512 halfValue, + Vector512 scale); + + /// + /// Converts one RGB sample to JPEG components. + /// + /// The red value. + /// The green value. + /// The blue value. + /// The maximum component value for the configured precision. + /// The midpoint component value for the configured precision. + /// The reciprocal of . + /// The first converted component. + /// The second converted component. + /// The third converted component. + /// The fourth converted component, if used. + static abstract void ConvertFromRgb( + float r, + float g, + float b, + float maximumValue, + float halfValue, + float scale, + out float c0, + out float c1, + out float c2, + out float c3); + + /// + /// Converts four RGB samples to JPEG components. + /// + /// The red lanes. + /// The green lanes. + /// The blue lanes. + /// The maximum component value for the configured precision. + /// The midpoint component value for the configured precision. + /// The reciprocal of in every lane. + /// The first converted component lanes. + /// The second converted component lanes. + /// The third converted component lanes. + /// The fourth converted component lanes, if used. + static abstract void ConvertFromRgb( + Vector128 r, + Vector128 g, + Vector128 b, + Vector128 maximumValue, + Vector128 halfValue, + Vector128 scale, + out Vector128 c0, + out Vector128 c1, + out Vector128 c2, + out Vector128 c3); + + /// + /// Converts eight RGB samples to JPEG components. + /// + /// The red lanes. + /// The green lanes. + /// The blue lanes. + /// The maximum component value for the configured precision. + /// The midpoint component value for the configured precision. + /// The reciprocal of in every lane. + /// The first converted component lanes. + /// The second converted component lanes. + /// The third converted component lanes. + /// The fourth converted component lanes, if used. + static abstract void ConvertFromRgb( + Vector256 r, + Vector256 g, + Vector256 b, + Vector256 maximumValue, + Vector256 halfValue, + Vector256 scale, + out Vector256 c0, + out Vector256 c1, + out Vector256 c2, + out Vector256 c3); + + /// + /// Converts sixteen RGB samples to JPEG components. + /// + /// The red lanes. + /// The green lanes. + /// The blue lanes. + /// The maximum component value for the configured precision. + /// The midpoint component value for the configured precision. + /// The reciprocal of in every lane. + /// The first converted component lanes. + /// The second converted component lanes. + /// The third converted component lanes. + /// The fourth converted component lanes, if used. + static abstract void ConvertFromRgb( + Vector512 r, + Vector512 g, + Vector512 b, + Vector512 maximumValue, + Vector512 halfValue, + Vector512 scale, + out Vector512 c0, + out Vector512 c1, + out Vector512 c2, + out Vector512 c3); + + /// + /// Converts JPEG component values to RGB using the supplied ICC profile. + /// + /// The configuration used to allocate temporary storage. + /// The source ICC profile. + /// The component values to convert. + /// The maximum component value for the configured precision. + static abstract void ConvertToRgbInPlaceWithIcc( + Configuration configuration, + IccProfile profile, + in ComponentValues values, + float maximumValue); + } + + /// + /// Converts a JPEG color model using a single operator-driven traversal for all SIMD widths. + /// + /// The color-model-specific arithmetic. + internal sealed class JpegColorConverter : JpegColorConverterBase + where TOperator : struct, IJpegColorConverterOperator + { + /// + /// Initializes a new instance of the class. + /// + /// The precision in bits. + public JpegColorConverter(int precision) + : base(TOperator.ColorSpace, precision) + { + } + + /// + public override bool IsAvailable => true; + + /// + public override int ElementsPerBatch + => Vector512.IsHardwareAccelerated + ? Vector512.Count + : Vector256.IsHardwareAccelerated + ? Vector256.Count + : Vector128.IsHardwareAccelerated + ? Vector128.Count + : 1; + + /// + public override void ConvertToRgbInPlace(in ComponentValues values) + { + // JPEG component processors own equally sized planar buffers. Capturing their first elements + // as byrefs lets every width share the same offset without introducing Span bounds checks in + // the hot loops. Component3 may be empty; its byref is only dereferenced for four-component operators. + ref float c0Base = ref MemoryMarshal.GetReference(values.Component0); + ref float c1Base = ref MemoryMarshal.GetReference(values.Component1); + ref float c2Base = ref MemoryMarshal.GetReference(values.Component2); + ref float c3Base = ref MemoryMarshal.GetReference(values.Component3); + + int length = values.Component0.Length; + int i = 0; + float scale = 1F / this.MaximumValue; + + // Descending widths keep one traversal while allowing an AVX-512 machine to process + // an eight-pixel JPEG block with AVX2 rather than sending the entire block to scalar code. + if (Vector512.IsHardwareAccelerated) + { + // Subtracting the lane count turns the loop condition into a single signed comparison. + // A negative value naturally skips this width, and i <= end proves every unaligned + // 64-byte reinterpretation remains entirely inside its component buffer. + int oneVectorFromEnd = length - Vector512.Count; + + if (i <= oneVectorFromEnd) + { + // Precision-derived values are broadcast only when this width has work. Keeping them outside + // the loop avoids repeated setup without penalizing rows handled entirely by narrower widths. + Vector512 maximumValue = Vector512.Create(this.MaximumValue); + Vector512 halfValue = Vector512.Create(this.HalfValue); + Vector512 scaleVector = Vector512.Create(scale); + + for (; i <= oneVectorFromEnd; i += Vector512.Count) + { + ref Vector512 c0 = ref Unsafe.As>(ref Unsafe.Add(ref c0Base, i)); + ref Vector512 c1 = ref Unsafe.As>(ref Unsafe.Add(ref c1Base, i)); + ref Vector512 c2 = ref Unsafe.As>(ref Unsafe.Add(ref c2Base, i)); + + // ComponentCount is a static property on the closed operator type, so the JIT removes + // this choice. Three-component models never dereference the empty Component3 byref. + Vector512 c3 = TOperator.ComponentCount == 4 + ? Unsafe.As>(ref Unsafe.Add(ref c3Base, i)) + : default; + + // c0-c2 alias the planar source vectors and are replaced in place with normalized RGB. + // c3 is passed by value because the fourth JPEG component must remain unchanged. + TOperator.ConvertToRgb(ref c0, ref c1, ref c2, c3, maximumValue, halfValue, scaleVector); + } + } + } + + if (Vector256.IsHardwareAccelerated) + { + // The shared offset continues where AVX-512 stopped. At this point fewer than sixteen + // samples remain, so this stage consumes the complete eight-sample remainder when present. + int oneVectorFromEnd = length - Vector256.Count; + + if (i <= oneVectorFromEnd) + { + // YMM precision state is materialized only for an eight-sample remainder or an AVX2-only loop. + Vector256 maximumValue = Vector256.Create(this.MaximumValue); + Vector256 halfValue = Vector256.Create(this.HalfValue); + Vector256 scaleVector = Vector256.Create(scale); + + for (; i <= oneVectorFromEnd; i += Vector256.Count) + { + ref Vector256 c0 = ref Unsafe.As>(ref Unsafe.Add(ref c0Base, i)); + ref Vector256 c1 = ref Unsafe.As>(ref Unsafe.Add(ref c1Base, i)); + ref Vector256 c2 = ref Unsafe.As>(ref Unsafe.Add(ref c2Base, i)); + + // The closed operator makes this a compile-time color-model choice, not a per-vector + // runtime abstraction or interface dispatch. + Vector256 c3 = TOperator.ComponentCount == 4 + ? Unsafe.As>(ref Unsafe.Add(ref c3Base, i)) + : default; + + TOperator.ConvertToRgb(ref c0, ref c1, ref c2, c3, maximumValue, halfValue, scaleVector); + } + } + } + + if (Vector128.IsHardwareAccelerated) + { + // SSE/AdvSimd handles the final four complete samples. This also gives non-AVX machines + // the same traversal without duplicating the control flow for another register width. + int oneVectorFromEnd = length - Vector128.Count; + + if (i <= oneVectorFromEnd) + { + // XMM state is likewise created only when four samples remain for this stage. + Vector128 maximumValue = Vector128.Create(this.MaximumValue); + Vector128 halfValue = Vector128.Create(this.HalfValue); + Vector128 scaleVector = Vector128.Create(scale); + + for (; i <= oneVectorFromEnd; i += Vector128.Count) + { + ref Vector128 c0 = ref Unsafe.As>(ref Unsafe.Add(ref c0Base, i)); + ref Vector128 c1 = ref Unsafe.As>(ref Unsafe.Add(ref c1Base, i)); + ref Vector128 c2 = ref Unsafe.As>(ref Unsafe.Add(ref c2Base, i)); + + // As at the wider stages, the fourth vector is loaded only for CMYK-shaped operators. + Vector128 c3 = TOperator.ComponentCount == 4 + ? Unsafe.As>(ref Unsafe.Add(ref c3Base, i)) + : default; + + TOperator.ConvertToRgb(ref c0, ref c1, ref c2, c3, maximumValue, halfValue, scaleVector); + } + } + } + + // Fewer than four samples remain after the SIMD cascade. Processing from the shared offset + // guarantees each sample is visited exactly once for arbitrary test lengths and JPEG block rows. + for (; i < length; i++) + { + float c3 = TOperator.ComponentCount == 4 ? Unsafe.Add(ref c3Base, i) : 0; + + TOperator.ConvertToRgb( + ref Unsafe.Add(ref c0Base, i), + ref Unsafe.Add(ref c1Base, i), + ref Unsafe.Add(ref c2Base, i), + c3, + this.MaximumValue, + this.HalfValue, + scale); + } + } + + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => TOperator.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + + /// + public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) + { + // The encoder supplies equally sized RGB planes and destination component planes. Byrefs preserve + // contiguous access and allow the same proven vector boundary to govern every participating lane. + // Component3 is empty for three-component formats and is only written by four-component operators. + ref float c0Base = ref MemoryMarshal.GetReference(values.Component0); + ref float c1Base = ref MemoryMarshal.GetReference(values.Component1); + ref float c2Base = ref MemoryMarshal.GetReference(values.Component2); + ref float c3Base = ref MemoryMarshal.GetReference(values.Component3); + ref float rBase = ref MemoryMarshal.GetReference(rLane); + ref float gBase = ref MemoryMarshal.GetReference(gLane); + ref float bBase = ref MemoryMarshal.GetReference(bLane); + + int length = values.Component0.Length; + int i = 0; + float scale = 1F / this.MaximumValue; + + // Each vector overload returns planar component vectors. Storing them here keeps the + // operator concerned only with color arithmetic and preserves contiguous lane access. + if (Vector512.IsHardwareAccelerated) + { + // The end offset proves all three 64-byte RGB reads and all component writes are in range. + // A short row yields a negative end and falls through to the next supported width. + int oneVectorFromEnd = length - Vector512.Count; + + if (i <= oneVectorFromEnd) + { + // Operators receive width-matched precision state only when this width has work, keeping + // invariant broadcasts outside the loop without charging narrower or scalar rows for them. + Vector512 maximumValue = Vector512.Create(this.MaximumValue); + Vector512 halfValue = Vector512.Create(this.HalfValue); + Vector512 scaleVector = Vector512.Create(scale); + + for (; i <= oneVectorFromEnd; i += Vector512.Count) + { + Vector512 r = Unsafe.As>(ref Unsafe.Add(ref rBase, i)); + Vector512 g = Unsafe.As>(ref Unsafe.Add(ref gBase, i)); + Vector512 b = Unsafe.As>(ref Unsafe.Add(ref bBase, i)); + + TOperator.ConvertFromRgb( + r, + g, + b, + maximumValue, + halfValue, + scaleVector, + out Vector512 c0, + out Vector512 c1, + out Vector512 c2, + out Vector512 c3); + + // Outputs remain planar: each vector contains sixteen consecutive samples from one + // JPEG component. Static count checks prevent grayscale from touching absent planes + // while disappearing completely from three- and four-component specializations. + Unsafe.As>(ref Unsafe.Add(ref c0Base, i)) = c0; + + if (TOperator.ComponentCount >= 2) + { + Unsafe.As>(ref Unsafe.Add(ref c1Base, i)) = c1; + } + + if (TOperator.ComponentCount >= 3) + { + Unsafe.As>(ref Unsafe.Add(ref c2Base, i)) = c2; + } + + if (TOperator.ComponentCount >= 4) + { + Unsafe.As>(ref Unsafe.Add(ref c3Base, i)) = c3; + } + } + } + } + + if (Vector256.IsHardwareAccelerated) + { + // Continue from the AVX-512 offset so an eight-sample tail stays vectorized on AVX-512 CPUs. + int oneVectorFromEnd = length - Vector256.Count; + + if (i <= oneVectorFromEnd) + { + // Materialize YMM state only for an eight-sample remainder or an AVX2-only loop. + Vector256 maximumValue = Vector256.Create(this.MaximumValue); + Vector256 halfValue = Vector256.Create(this.HalfValue); + Vector256 scaleVector = Vector256.Create(scale); + + for (; i <= oneVectorFromEnd; i += Vector256.Count) + { + Vector256 r = Unsafe.As>(ref Unsafe.Add(ref rBase, i)); + Vector256 g = Unsafe.As>(ref Unsafe.Add(ref gBase, i)); + Vector256 b = Unsafe.As>(ref Unsafe.Add(ref bBase, i)); + + TOperator.ConvertFromRgb( + r, + g, + b, + maximumValue, + halfValue, + scaleVector, + out Vector256 c0, + out Vector256 c1, + out Vector256 c2, + out Vector256 c3); + + // Static count checks write only planes owned by this color model. + Unsafe.As>(ref Unsafe.Add(ref c0Base, i)) = c0; + + if (TOperator.ComponentCount >= 2) + { + Unsafe.As>(ref Unsafe.Add(ref c1Base, i)) = c1; + } + + if (TOperator.ComponentCount >= 3) + { + Unsafe.As>(ref Unsafe.Add(ref c2Base, i)) = c2; + } + + if (TOperator.ComponentCount >= 4) + { + Unsafe.As>(ref Unsafe.Add(ref c3Base, i)) = c3; + } + } + } + } + + if (Vector128.IsHardwareAccelerated) + { + // The final SIMD stage consumes four complete RGB samples on SSE or AdvSimd hardware. + int oneVectorFromEnd = length - Vector128.Count; + + if (i <= oneVectorFromEnd) + { + // Materialize XMM state only when the final SIMD stage can consume four samples. + Vector128 maximumValue = Vector128.Create(this.MaximumValue); + Vector128 halfValue = Vector128.Create(this.HalfValue); + Vector128 scaleVector = Vector128.Create(scale); + + for (; i <= oneVectorFromEnd; i += Vector128.Count) + { + Vector128 r = Unsafe.As>(ref Unsafe.Add(ref rBase, i)); + Vector128 g = Unsafe.As>(ref Unsafe.Add(ref gBase, i)); + Vector128 b = Unsafe.As>(ref Unsafe.Add(ref bBase, i)); + + TOperator.ConvertFromRgb( + r, + g, + b, + maximumValue, + halfValue, + scaleVector, + out Vector128 c0, + out Vector128 c1, + out Vector128 c2, + out Vector128 c3); + + // Four results are stored only for the planes represented by the closed operator. + Unsafe.As>(ref Unsafe.Add(ref c0Base, i)) = c0; + + if (TOperator.ComponentCount >= 2) + { + Unsafe.As>(ref Unsafe.Add(ref c1Base, i)) = c1; + } + + if (TOperator.ComponentCount >= 3) + { + Unsafe.As>(ref Unsafe.Add(ref c2Base, i)) = c2; + } + + if (TOperator.ComponentCount >= 4) + { + Unsafe.As>(ref Unsafe.Add(ref c3Base, i)) = c3; + } + } + } + } + + // Scalar conversion is reserved for the zero-to-three samples that cannot fill Vector128. + for (; i < length; i++) + { + TOperator.ConvertFromRgb( + Unsafe.Add(ref rBase, i), + Unsafe.Add(ref gBase, i), + Unsafe.Add(ref bBase, i), + this.MaximumValue, + this.HalfValue, + scale, + out float c0, + out float c1, + out float c2, + out float c3); + + Unsafe.Add(ref c0Base, i) = c0; + + if (TOperator.ComponentCount >= 2) + { + Unsafe.Add(ref c1Base, i) = c1; + } + + if (TOperator.ComponentCount >= 3) + { + Unsafe.Add(ref c2Base, i) = c2; + } + + if (TOperator.ComponentCount >= 4) + { + Unsafe.Add(ref c3Base, i) = c3; + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbOperator.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbOperator.cs new file mode 100644 index 000000000..6d7281dc6 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbOperator.cs @@ -0,0 +1,184 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components; + +internal abstract partial class JpegColorConverterBase +{ + /// + /// Implements direct JPEG RGB normalization and planar RGB copying for scalar and SIMD lanes. + /// + internal readonly struct RgbOperator : IJpegColorConverterOperator + { + /// + public static JpegColorSpace ColorSpace => JpegColorSpace.RGB; + + /// + public static int ComponentCount => 3; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb( + ref float c0, + ref float c1, + ref float c2, + float c3, + float maximumValue, + float halfValue, + float scale) + { + // The JPEG planes already represent R, G, and B. Conversion therefore consists only of moving + // each integer-domain sample into the normalized floating-point domain consumed by pixel packing. + c0 *= scale; + c1 *= scale; + c2 *= scale; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb( + ref Vector128 c0, + ref Vector128 c1, + ref Vector128 c2, + Vector128 c3, + Vector128 maximumValue, + Vector128 halfValue, + Vector128 scale) + { + // Four samples from each planar channel remain in their lanes while sharing one normalization vector. + c0 *= scale; + c1 *= scale; + c2 *= scale; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb( + ref Vector256 c0, + ref Vector256 c1, + ref Vector256 c2, + Vector256 c3, + Vector256 maximumValue, + Vector256 halfValue, + Vector256 scale) + { + // Eight samples per plane are normalized independently without channel shuffles. + c0 *= scale; + c1 *= scale; + c2 *= scale; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb( + ref Vector512 c0, + ref Vector512 c1, + ref Vector512 c2, + Vector512 c3, + Vector512 maximumValue, + Vector512 halfValue, + Vector512 scale) + { + // Sixteen samples per plane are normalized independently without changing planar ordering. + c0 *= scale; + c1 *= scale; + c2 *= scale; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + float r, + float g, + float b, + float maximumValue, + float halfValue, + float scale, + out float c0, + out float c1, + out float c2, + out float c3) + { + // Encoder RGB lanes already use the JPEG sample domain, so the direct color model copies them. + c0 = r; + c1 = g; + c2 = b; + c3 = 0; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector128 r, + Vector128 g, + Vector128 b, + Vector128 maximumValue, + Vector128 halfValue, + Vector128 scale, + out Vector128 c0, + out Vector128 c1, + out Vector128 c2, + out Vector128 c3) + { + // The planar vectors map one-to-one to JPEG components; the fourth result is statically discarded. + c0 = r; + c1 = g; + c2 = b; + c3 = default; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector256 r, + Vector256 g, + Vector256 b, + Vector256 maximumValue, + Vector256 halfValue, + Vector256 scale, + out Vector256 c0, + out Vector256 c1, + out Vector256 c2, + out Vector256 c3) + { + // The planar vectors map one-to-one to JPEG components; no arithmetic or rearrangement is required. + c0 = r; + c1 = g; + c2 = b; + c3 = default; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector512 r, + Vector512 g, + Vector512 b, + Vector512 maximumValue, + Vector512 halfValue, + Vector512 scale, + out Vector512 c0, + out Vector512 c1, + out Vector512 c2, + out Vector512 c3) + { + // The widest path is likewise a register-to-register planar copy for sixteen pixels. + c0 = r; + c1 = g; + c2 = b; + c3 = default; + } + + /// + public static void ConvertToRgbInPlaceWithIcc( + Configuration configuration, + IccProfile profile, + in ComponentValues values, + float maximumValue) + => RgbScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, maximumValue); + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykOperator.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykOperator.cs new file mode 100644 index 000000000..88b6c2411 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykOperator.cs @@ -0,0 +1,159 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components; + +internal abstract partial class JpegColorConverterBase +{ + /// + /// Implements non-inverted TIFF JPEG CMYK conversion for scalar and SIMD lanes. + /// + internal readonly struct TiffCmykOperator : IJpegColorConverterOperator + { + /// + public static JpegColorSpace ColorSpace => JpegColorSpace.TiffCmyk; + + /// + public static int ComponentCount => 4; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref float c0, ref float c1, ref float c2, float c3, float maximumValue, float halfValue, float scale) + { + // TIFF stores conventional CMYK rather than Adobe's inverted representation. Normalize every + // component, invert C/M/Y, and let the remaining light after K modulate each RGB channel. + float k = 1F - (c3 * scale); + c0 = (1F - (c0 * scale)) * k; + c1 = (1F - (c1 * scale)) * k; + c2 = (1F - (c2 * scale)) * k; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector128 c0, ref Vector128 c1, ref Vector128 c2, Vector128 c3, Vector128 maximumValue, Vector128 halfValue, Vector128 scale) + { + // K remains lane-aligned with its C/M/Y sample while one-minus performs the non-inverted CMYK mapping. + Vector128 k = Vector128.One - (c3 * scale); + c0 = (Vector128.One - (c0 * scale)) * k; + c1 = (Vector128.One - (c1 * scale)) * k; + c2 = (Vector128.One - (c2 * scale)) * k; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector256 c0, ref Vector256 c1, ref Vector256 c2, Vector256 c3, Vector256 maximumValue, Vector256 halfValue, Vector256 scale) + { + // Eight conventional CMYK samples convert independently without channel rearrangement. + Vector256 k = Vector256.One - (c3 * scale); + c0 = (Vector256.One - (c0 * scale)) * k; + c1 = (Vector256.One - (c1 * scale)) * k; + c2 = (Vector256.One - (c2 * scale)) * k; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector512 c0, ref Vector512 c1, ref Vector512 c2, Vector512 c3, Vector512 maximumValue, Vector512 halfValue, Vector512 scale) + { + // Sixteen conventional CMYK samples convert independently without channel rearrangement. + Vector512 k = Vector512.One - (c3 * scale); + c0 = (Vector512.One - (c0 * scale)) * k; + c1 = (Vector512.One - (c1 * scale)) * k; + c2 = (Vector512.One - (c2 * scale)) * k; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb(float r, float g, float b, float maximumValue, float halfValue, float scale, out float c0, out float c1, out float c2, out float c3) + { + float c = maximumValue - r; + float m = maximumValue - g; + float y = maximumValue - b; + float k = MathF.Min(c, MathF.Min(m, y)); + + // Removing the shared black contribution requires division by the remaining range. Pure black + // consumes that range completely, so its chromatic components are defined as zero. + if (k >= maximumValue) + { + c = 0; + m = 0; + y = 0; + } + else + { + // One reciprocal normalizes C, M, and Y against their shared remaining range. + float reciprocal = 1F / (maximumValue - k); + c = (c - k) * reciprocal; + m = (m - k) * reciprocal; + y = (y - k) * reciprocal; + } + + // TIFF stores conventional CMYK: scale normalized C/M/Y back into the sample domain and retain K. + c0 = c * maximumValue; + c1 = m * maximumValue; + c2 = y * maximumValue; + c3 = k; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb(Vector128 r, Vector128 g, Vector128 b, Vector128 maximumValue, Vector128 halfValue, Vector128 scale, out Vector128 c0, out Vector128 c1, out Vector128 c2, out Vector128 c3) + { + Vector128 c = maximumValue - r; + Vector128 m = maximumValue - g; + Vector128 y = maximumValue - b; + Vector128 k = Vector128.Min(c, Vector128.Min(m, y)); + + // The all-bits mask clears the undefined zero-divisor result only in pure-black lanes. + Vector128 nonBlack = ~Vector128.Equals(k, maximumValue); + Vector128 reciprocal = Vector128.One / (maximumValue - k); + c0 = (((c - k) * reciprocal) & nonBlack) * maximumValue; + c1 = (((m - k) * reciprocal) & nonBlack) * maximumValue; + c2 = (((y - k) * reciprocal) & nonBlack) * maximumValue; + c3 = k; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb(Vector256 r, Vector256 g, Vector256 b, Vector256 maximumValue, Vector256 halfValue, Vector256 scale, out Vector256 c0, out Vector256 c1, out Vector256 c2, out Vector256 c3) + { + Vector256 c = maximumValue - r; + Vector256 m = maximumValue - g; + Vector256 y = maximumValue - b; + Vector256 k = Vector256.Min(c, Vector256.Min(m, y)); + + // Eight lanes independently clear the pure-black singularity before returning conventional CMYK. + Vector256 nonBlack = ~Vector256.Equals(k, maximumValue); + Vector256 reciprocal = Vector256.One / (maximumValue - k); + c0 = (((c - k) * reciprocal) & nonBlack) * maximumValue; + c1 = (((m - k) * reciprocal) & nonBlack) * maximumValue; + c2 = (((y - k) * reciprocal) & nonBlack) * maximumValue; + c3 = k; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb(Vector512 r, Vector512 g, Vector512 b, Vector512 maximumValue, Vector512 halfValue, Vector512 scale, out Vector512 c0, out Vector512 c1, out Vector512 c2, out Vector512 c3) + { + Vector512 c = maximumValue - r; + Vector512 m = maximumValue - g; + Vector512 y = maximumValue - b; + Vector512 k = Vector512.Min(c, Vector512.Min(m, y)); + + // Sixteen lanes retain the same branchless singularity handling and component layout. + Vector512 nonBlack = ~Vector512.Equals(k, maximumValue); + Vector512 reciprocal = Vector512.One / (maximumValue - k); + c0 = (((c - k) * reciprocal) & nonBlack) * maximumValue; + c1 = (((m - k) * reciprocal) & nonBlack) * maximumValue; + c2 = (((y - k) * reciprocal) & nonBlack) * maximumValue; + c3 = k; + } + + /// + public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maximumValue) + => TiffCmykScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, maximumValue); + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKOperator.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKOperator.cs new file mode 100644 index 000000000..c66d266a1 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKOperator.cs @@ -0,0 +1,187 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components; + +internal abstract partial class JpegColorConverterBase +{ + /// + /// Implements non-inverted TIFF JPEG YccK conversion for scalar and SIMD lanes. + /// + internal readonly struct TiffYccKOperator : IJpegColorConverterOperator + { + private const float SourceScale = 1F / 255F; + + /// + public static JpegColorSpace ColorSpace => JpegColorSpace.TiffYccK; + + /// + public static int ComponentCount => 4; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref float c0, ref float c1, ref float c2, float c3, float maximumValue, float halfValue, float scale) + { + float y = c0 * scale; + float cb = (c1 - halfValue) * scale; + float cr = (c2 - halfValue) * scale; + float k = 1F - (c3 * scale); + + // TIFF YccK is non-inverted: decode normalized YCbCr without integer rounding, then let the + // remaining light after K modulate all three channels. + c0 = (y + (YCbCrScalar.RCrMult * cr)) * k; + c1 = (y - (YCbCrScalar.GCbMult * cb) - (YCbCrScalar.GCrMult * cr)) * k; + c2 = (y + (YCbCrScalar.BCbMult * cb)) * k; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector128 c0, ref Vector128 c1, ref Vector128 c2, Vector128 c3, Vector128 maximumValue, Vector128 halfValue, Vector128 scale) + { + Vector128 y = c0 * scale; + Vector128 cb = (c1 - halfValue) * scale; + Vector128 cr = (c2 - halfValue) * scale; + Vector128 k = Vector128.One - (c3 * scale); + + // Four lanes apply the non-rounded YCbCr matrix before their lane-aligned K modulation. + c0 = Vector128_.MultiplyAddEstimate(cr, Vector128.Create(YCbCrScalar.RCrMult), y) * k; + c1 = Vector128_.MultiplyAddEstimate(cr, Vector128.Create(-YCbCrScalar.GCrMult), Vector128_.MultiplyAddEstimate(cb, Vector128.Create(-YCbCrScalar.GCbMult), y)) * k; + c2 = Vector128_.MultiplyAddEstimate(cb, Vector128.Create(YCbCrScalar.BCbMult), y) * k; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector256 c0, ref Vector256 c1, ref Vector256 c2, Vector256 c3, Vector256 maximumValue, Vector256 halfValue, Vector256 scale) + { + Vector256 y = c0 * scale; + Vector256 cb = (c1 - halfValue) * scale; + Vector256 cr = (c2 - halfValue) * scale; + Vector256 k = Vector256.One - (c3 * scale); + + // Eight lanes apply the non-rounded YCbCr matrix before their lane-aligned K modulation. + c0 = Vector256_.MultiplyAddEstimate(cr, Vector256.Create(YCbCrScalar.RCrMult), y) * k; + c1 = Vector256_.MultiplyAddEstimate(cr, Vector256.Create(-YCbCrScalar.GCrMult), Vector256_.MultiplyAddEstimate(cb, Vector256.Create(-YCbCrScalar.GCbMult), y)) * k; + c2 = Vector256_.MultiplyAddEstimate(cb, Vector256.Create(YCbCrScalar.BCbMult), y) * k; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector512 c0, ref Vector512 c1, ref Vector512 c2, Vector512 c3, Vector512 maximumValue, Vector512 halfValue, Vector512 scale) + { + Vector512 y = c0 * scale; + Vector512 cb = (c1 - halfValue) * scale; + Vector512 cr = (c2 - halfValue) * scale; + Vector512 k = Vector512.One - (c3 * scale); + + // Sixteen lanes apply the non-rounded YCbCr matrix before their lane-aligned K modulation. + c0 = Vector512_.MultiplyAddEstimate(cr, Vector512.Create(YCbCrScalar.RCrMult), y) * k; + c1 = Vector512_.MultiplyAddEstimate(cr, Vector512.Create(-YCbCrScalar.GCrMult), Vector512_.MultiplyAddEstimate(cb, Vector512.Create(-YCbCrScalar.GCbMult), y)) * k; + c2 = Vector512_.MultiplyAddEstimate(cb, Vector512.Create(YCbCrScalar.BCbMult), y) * k; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb(float r, float g, float b, float maximumValue, float halfValue, float scale, out float c0, out float c1, out float c2, out float c3) + { + r *= SourceScale; + g *= SourceScale; + b *= SourceScale; + float k = 1F - MathF.Max(r, MathF.Max(g, b)); + + // Dividing by the brightest channel removes K before YCbCr projection. Pure black has no + // chromatic direction, so it maps to zero luma and the neutral chroma midpoint. + if (k >= 1F) + { + c0 = 0; + c1 = halfValue; + c2 = halfValue; + c3 = maximumValue; + return; + } + + float divisor = 1F / (1F - k); + r *= divisor; + g *= divisor; + b *= divisor; + c0 = ((0.299F * r) + (0.587F * g) + (0.114F * b)) * maximumValue; + c1 = halfValue + (((-0.168736F * r) + (-0.331264F * g) + (0.5F * b)) * maximumValue); + c2 = halfValue + (((0.5F * r) + (-0.418688F * g) + (-0.081312F * b)) * maximumValue); + c3 = k * maximumValue; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb(Vector128 r, Vector128 g, Vector128 b, Vector128 maximumValue, Vector128 halfValue, Vector128 scale, out Vector128 c0, out Vector128 c1, out Vector128 c2, out Vector128 c3) + { + Vector128 sourceScale = Vector128.Create(SourceScale); + r *= sourceScale; + g *= sourceScale; + b *= sourceScale; + Vector128 k = Vector128.One - Vector128.Max(r, Vector128.Max(g, b)); + + // The mask assigns no chromatic direction to pure-black lanes while preserving neighboring pixels. + Vector128 nonBlack = ~Vector128.Equals(k, Vector128.One); + Vector128 divisor = Vector128.One / (Vector128.One - k); + r = (r * divisor) & nonBlack; + g = (g * divisor) & nonBlack; + b = (b * divisor) & nonBlack; + c0 = Vector128_.MultiplyAddEstimate(Vector128.Create(0.299F), r, Vector128_.MultiplyAddEstimate(Vector128.Create(0.587F), g, Vector128.Create(0.114F) * b)) * maximumValue; + c1 = halfValue + (Vector128_.MultiplyAddEstimate(Vector128.Create(-0.168736F), r, Vector128_.MultiplyAddEstimate(Vector128.Create(-0.331264F), g, Vector128.Create(0.5F) * b)) * maximumValue); + c2 = halfValue + (Vector128_.MultiplyAddEstimate(Vector128.Create(0.5F), r, Vector128_.MultiplyAddEstimate(Vector128.Create(-0.418688F), g, Vector128.Create(-0.081312F) * b)) * maximumValue); + c3 = k * maximumValue; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb(Vector256 r, Vector256 g, Vector256 b, Vector256 maximumValue, Vector256 halfValue, Vector256 scale, out Vector256 c0, out Vector256 c1, out Vector256 c2, out Vector256 c3) + { + Vector256 sourceScale = Vector256.Create(SourceScale); + r *= sourceScale; + g *= sourceScale; + b *= sourceScale; + Vector256 k = Vector256.One - Vector256.Max(r, Vector256.Max(g, b)); + + // Eight lanes normalize chromatic direction independently and retain neutral chroma for black. + Vector256 nonBlack = ~Vector256.Equals(k, Vector256.One); + Vector256 divisor = Vector256.One / (Vector256.One - k); + r = (r * divisor) & nonBlack; + g = (g * divisor) & nonBlack; + b = (b * divisor) & nonBlack; + c0 = Vector256_.MultiplyAddEstimate(Vector256.Create(0.299F), r, Vector256_.MultiplyAddEstimate(Vector256.Create(0.587F), g, Vector256.Create(0.114F) * b)) * maximumValue; + c1 = halfValue + (Vector256_.MultiplyAddEstimate(Vector256.Create(-0.168736F), r, Vector256_.MultiplyAddEstimate(Vector256.Create(-0.331264F), g, Vector256.Create(0.5F) * b)) * maximumValue); + c2 = halfValue + (Vector256_.MultiplyAddEstimate(Vector256.Create(0.5F), r, Vector256_.MultiplyAddEstimate(Vector256.Create(-0.418688F), g, Vector256.Create(-0.081312F) * b)) * maximumValue); + c3 = k * maximumValue; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb(Vector512 r, Vector512 g, Vector512 b, Vector512 maximumValue, Vector512 halfValue, Vector512 scale, out Vector512 c0, out Vector512 c1, out Vector512 c2, out Vector512 c3) + { + Vector512 sourceScale = Vector512.Create(SourceScale); + r *= sourceScale; + g *= sourceScale; + b *= sourceScale; + Vector512 k = Vector512.One - Vector512.Max(r, Vector512.Max(g, b)); + + // Sixteen lanes normalize chromatic direction independently and retain neutral chroma for black. + Vector512 nonBlack = ~Vector512.Equals(k, Vector512.One); + Vector512 divisor = Vector512.One / (Vector512.One - k); + r = (r * divisor) & nonBlack; + g = (g * divisor) & nonBlack; + b = (b * divisor) & nonBlack; + c0 = Vector512_.MultiplyAddEstimate(Vector512.Create(0.299F), r, Vector512_.MultiplyAddEstimate(Vector512.Create(0.587F), g, Vector512.Create(0.114F) * b)) * maximumValue; + c1 = halfValue + (Vector512_.MultiplyAddEstimate(Vector512.Create(-0.168736F), r, Vector512_.MultiplyAddEstimate(Vector512.Create(-0.331264F), g, Vector512.Create(0.5F) * b)) * maximumValue); + c2 = halfValue + (Vector512_.MultiplyAddEstimate(Vector512.Create(0.5F), r, Vector512_.MultiplyAddEstimate(Vector512.Create(-0.418688F), g, Vector512.Create(-0.081312F) * b)) * maximumValue); + c3 = k * maximumValue; + } + + /// + public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maximumValue) + => TiffYccKScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, maximumValue); + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrOperator.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrOperator.cs new file mode 100644 index 000000000..ca8671e9f --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrOperator.cs @@ -0,0 +1,263 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components; + +internal abstract partial class JpegColorConverterBase +{ + /// + /// Implements the JPEG YCbCr conversion formula for scalar and SIMD lanes. + /// + internal readonly struct YCbCrOperator : IJpegColorConverterOperator + { + /// + public static JpegColorSpace ColorSpace => JpegColorSpace.YCbCr; + + /// + public static int ComponentCount => 3; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb( + ref float c0, + ref float c1, + ref float c2, + float c3, + float maximumValue, + float halfValue, + float scale) + { + float y = c0; + float cb = c1 - halfValue; + float cr = c2 - halfValue; + + // c0/c1/c2 initially mean Y/Cb/Cr. Chroma is centered around zero before applying + // the BT.601 matrix, then integer-domain RGB is rounded away from zero and normalized + // to [nominally] 0..1. Values intentionally remain unclamped because quantizing RGB into the + // destination pixel format owns saturation; retaining overshoot avoids discarding color information. + c0 = MathF.Round(y + (YCbCrScalar.RCrMult * cr), MidpointRounding.AwayFromZero) * scale; + c1 = MathF.Round(y - (YCbCrScalar.GCbMult * cb) - (YCbCrScalar.GCrMult * cr), MidpointRounding.AwayFromZero) * scale; + c2 = MathF.Round(y + (YCbCrScalar.BCbMult * cb), MidpointRounding.AwayFromZero) * scale; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb( + ref Vector128 c0, + ref Vector128 c1, + ref Vector128 c2, + Vector128 c3, + Vector128 maximumValue, + Vector128 halfValue, + Vector128 scale) + { + Vector128 y = c0; + Vector128 cb = c1 - halfValue; + Vector128 cr = c2 - halfValue; + + // Lanes are four independent Y/Cb/Cr samples. MultiplyAddEstimate maps to FMA where available: + // R uses Cr, B uses Cb, and G subtracts both chroma contributions. Rounding occurs in the sample + // domain before the common normalization scale so all precisions use integer JPEG sample semantics. + Vector128 r = Vector128_.MultiplyAddEstimate(cr, Vector128.Create(YCbCrScalar.RCrMult), y); + Vector128 g = Vector128_.MultiplyAddEstimate( + cr, + Vector128.Create(-YCbCrScalar.GCrMult), + Vector128_.MultiplyAddEstimate(cb, Vector128.Create(-YCbCrScalar.GCbMult), y)); + Vector128 b = Vector128_.MultiplyAddEstimate(cb, Vector128.Create(YCbCrScalar.BCbMult), y); + + c0 = Vector128_.RoundToNearestInteger(r) * scale; + c1 = Vector128_.RoundToNearestInteger(g) * scale; + c2 = Vector128_.RoundToNearestInteger(b) * scale; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb( + ref Vector256 c0, + ref Vector256 c1, + ref Vector256 c2, + Vector256 c3, + Vector256 maximumValue, + Vector256 halfValue, + Vector256 scale) + { + Vector256 y = c0; + Vector256 cb = c1 - halfValue; + Vector256 cr = c2 - halfValue; + + // These eight lanes have the same layout and BT.601 arithmetic as the Vector128 overload. + // Keeping an explicit overload allows the JIT to emit native YMM operations without a width + // switch or decomposing the vector into smaller values. + Vector256 r = Vector256_.MultiplyAddEstimate(cr, Vector256.Create(YCbCrScalar.RCrMult), y); + Vector256 g = Vector256_.MultiplyAddEstimate( + cr, + Vector256.Create(-YCbCrScalar.GCrMult), + Vector256_.MultiplyAddEstimate(cb, Vector256.Create(-YCbCrScalar.GCbMult), y)); + Vector256 b = Vector256_.MultiplyAddEstimate(cb, Vector256.Create(YCbCrScalar.BCbMult), y); + + c0 = Vector256_.RoundToNearestInteger(r) * scale; + c1 = Vector256_.RoundToNearestInteger(g) * scale; + c2 = Vector256_.RoundToNearestInteger(b) * scale; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb( + ref Vector512 c0, + ref Vector512 c1, + ref Vector512 c2, + Vector512 c3, + Vector512 maximumValue, + Vector512 halfValue, + Vector512 scale) + { + Vector512 y = c0; + Vector512 cb = c1 - halfValue; + Vector512 cr = c2 - halfValue; + + // Sixteen independent samples occupy the ZMM lanes. The explicit constants are broadcasts; + // assembly inspection verifies the JIT hoists them from the loop and retains fused operations. + // The formula and rounding order remain identical to the narrower overloads. + Vector512 r = Vector512_.MultiplyAddEstimate(cr, Vector512.Create(YCbCrScalar.RCrMult), y); + Vector512 g = Vector512_.MultiplyAddEstimate( + cr, + Vector512.Create(-YCbCrScalar.GCrMult), + Vector512_.MultiplyAddEstimate(cb, Vector512.Create(-YCbCrScalar.GCbMult), y)); + Vector512 b = Vector512_.MultiplyAddEstimate(cb, Vector512.Create(YCbCrScalar.BCbMult), y); + + c0 = Vector512_.RoundToNearestInteger(r) * scale; + c1 = Vector512_.RoundToNearestInteger(g) * scale; + c2 = Vector512_.RoundToNearestInteger(b) * scale; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + float r, + float g, + float b, + float maximumValue, + float halfValue, + float scale, + out float c0, + out float c1, + out float c2, + out float c3) + { + // The RGB inputs are unnormalized 0..255 encoder lanes. The BT.601 luma weights form Y, + // while the signed chroma projections are biased by halfValue into the JPEG sample domain. + // YCbCr has no fourth component, so c3 is a compile-time-unused placeholder for the shared loop. + c0 = (0.299F * r) + (0.587F * g) + (0.114F * b); + c1 = halfValue - (0.168736F * r) - (0.331264F * g) + (0.5F * b); + c2 = halfValue + (0.5F * r) - (0.418688F * g) - (0.081312F * b); + c3 = 0; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector128 r, + Vector128 g, + Vector128 b, + Vector128 maximumValue, + Vector128 halfValue, + Vector128 scale, + out Vector128 c0, + out Vector128 c1, + out Vector128 c2, + out Vector128 c3) + { + // Each vector holds four consecutive values from one RGB plane. The nested multiply-add sequence + // produces four Y lanes, four Cb lanes, and four Cr lanes without transposition. The association + // exposes two FMA opportunities per output while preserving the scalar formula's term grouping. + c0 = Vector128_.MultiplyAddEstimate( + Vector128.Create(0.299F), + r, + Vector128_.MultiplyAddEstimate(Vector128.Create(0.587F), g, Vector128.Create(0.114F) * b)); + c1 = halfValue + Vector128_.MultiplyAddEstimate( + Vector128.Create(-0.168736F), + r, + Vector128_.MultiplyAddEstimate(Vector128.Create(-0.331264F), g, Vector128.Create(0.5F) * b)); + c2 = halfValue + Vector128_.MultiplyAddEstimate( + Vector128.Create(0.5F), + r, + Vector128_.MultiplyAddEstimate(Vector128.Create(-0.418688F), g, Vector128.Create(-0.081312F) * b)); + c3 = default; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector256 r, + Vector256 g, + Vector256 b, + Vector256 maximumValue, + Vector256 halfValue, + Vector256 scale, + out Vector256 c0, + out Vector256 c1, + out Vector256 c2, + out Vector256 c3) + { + // Eight planar RGB samples use the identical association as Vector128, allowing direct YMM FMA + // generation while preserving the component-per-vector output layout. + c0 = Vector256_.MultiplyAddEstimate( + Vector256.Create(0.299F), + r, + Vector256_.MultiplyAddEstimate(Vector256.Create(0.587F), g, Vector256.Create(0.114F) * b)); + c1 = halfValue + Vector256_.MultiplyAddEstimate( + Vector256.Create(-0.168736F), + r, + Vector256_.MultiplyAddEstimate(Vector256.Create(-0.331264F), g, Vector256.Create(0.5F) * b)); + c2 = halfValue + Vector256_.MultiplyAddEstimate( + Vector256.Create(0.5F), + r, + Vector256_.MultiplyAddEstimate(Vector256.Create(-0.418688F), g, Vector256.Create(-0.081312F) * b)); + c3 = default; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector512 r, + Vector512 g, + Vector512 b, + Vector512 maximumValue, + Vector512 halfValue, + Vector512 scale, + out Vector512 c0, + out Vector512 c1, + out Vector512 c2, + out Vector512 c3) + { + // Sixteen planar RGB samples use the same nested form. Constants are lane broadcasts and c3 is + // deliberately zero because the shared traversal removes the unused fourth store for this operator. + c0 = Vector512_.MultiplyAddEstimate( + Vector512.Create(0.299F), + r, + Vector512_.MultiplyAddEstimate(Vector512.Create(0.587F), g, Vector512.Create(0.114F) * b)); + c1 = halfValue + Vector512_.MultiplyAddEstimate( + Vector512.Create(-0.168736F), + r, + Vector512_.MultiplyAddEstimate(Vector512.Create(-0.331264F), g, Vector512.Create(0.5F) * b)); + c2 = halfValue + Vector512_.MultiplyAddEstimate( + Vector512.Create(0.5F), + r, + Vector512_.MultiplyAddEstimate(Vector512.Create(-0.418688F), g, Vector512.Create(-0.081312F) * b)); + c3 = default; + } + + /// + public static void ConvertToRgbInPlaceWithIcc( + Configuration configuration, + IccProfile profile, + in ComponentValues values, + float maximumValue) + => YCbCrScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, maximumValue); + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKOperator.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKOperator.cs new file mode 100644 index 000000000..ef7185dd3 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKOperator.cs @@ -0,0 +1,135 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components; + +internal abstract partial class JpegColorConverterBase +{ + /// + /// Implements inverted JPEG YccK conversion for scalar and SIMD lanes. + /// + internal readonly struct YccKOperator : IJpegColorConverterOperator + { + /// + public static JpegColorSpace ColorSpace => JpegColorSpace.Ycck; + + /// + public static int ComponentCount => 4; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref float c0, ref float c1, ref float c2, float c3, float maximumValue, float halfValue, float scale) + { + float y = c0; + float cb = c1 - halfValue; + float cr = c2 - halfValue; + float scaledK = c3 * scale * scale; + + // YccK first reconstructs inverted RGB in the integer sample domain. Rounding must occur before + // subtracting from max and applying K because changing that order changes encoded JPEG semantics. + c0 = (maximumValue - MathF.Round(y + (YCbCrScalar.RCrMult * cr), MidpointRounding.AwayFromZero)) * scaledK; + c1 = (maximumValue - MathF.Round(y - (YCbCrScalar.GCbMult * cb) - (YCbCrScalar.GCrMult * cr), MidpointRounding.AwayFromZero)) * scaledK; + c2 = (maximumValue - MathF.Round(y + (YCbCrScalar.BCbMult * cb), MidpointRounding.AwayFromZero)) * scaledK; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector128 c0, ref Vector128 c1, ref Vector128 c2, Vector128 c3, Vector128 maximumValue, Vector128 halfValue, Vector128 scale) + { + Vector128 y = c0; + Vector128 cb = c1 - halfValue; + Vector128 cr = c2 - halfValue; + Vector128 scaledK = c3 * scale * scale; + + // Four lanes reconstruct YCbCr concurrently; each rounded result is inverted and modulated by its K lane. + Vector128 r = Vector128_.MultiplyAddEstimate(cr, Vector128.Create(YCbCrScalar.RCrMult), y); + Vector128 g = Vector128_.MultiplyAddEstimate(cr, Vector128.Create(-YCbCrScalar.GCrMult), Vector128_.MultiplyAddEstimate(cb, Vector128.Create(-YCbCrScalar.GCbMult), y)); + Vector128 b = Vector128_.MultiplyAddEstimate(cb, Vector128.Create(YCbCrScalar.BCbMult), y); + c0 = (maximumValue - Vector128_.RoundToNearestInteger(r)) * scaledK; + c1 = (maximumValue - Vector128_.RoundToNearestInteger(g)) * scaledK; + c2 = (maximumValue - Vector128_.RoundToNearestInteger(b)) * scaledK; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector256 c0, ref Vector256 c1, ref Vector256 c2, Vector256 c3, Vector256 maximumValue, Vector256 halfValue, Vector256 scale) + { + Vector256 y = c0; + Vector256 cb = c1 - halfValue; + Vector256 cr = c2 - halfValue; + Vector256 scaledK = c3 * scale * scale; + + // Eight lanes retain planar alignment from Y/Cb/Cr/K through normalized RGB. + Vector256 r = Vector256_.MultiplyAddEstimate(cr, Vector256.Create(YCbCrScalar.RCrMult), y); + Vector256 g = Vector256_.MultiplyAddEstimate(cr, Vector256.Create(-YCbCrScalar.GCrMult), Vector256_.MultiplyAddEstimate(cb, Vector256.Create(-YCbCrScalar.GCbMult), y)); + Vector256 b = Vector256_.MultiplyAddEstimate(cb, Vector256.Create(YCbCrScalar.BCbMult), y); + c0 = (maximumValue - Vector256_.RoundToNearestInteger(r)) * scaledK; + c1 = (maximumValue - Vector256_.RoundToNearestInteger(g)) * scaledK; + c2 = (maximumValue - Vector256_.RoundToNearestInteger(b)) * scaledK; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector512 c0, ref Vector512 c1, ref Vector512 c2, Vector512 c3, Vector512 maximumValue, Vector512 halfValue, Vector512 scale) + { + Vector512 y = c0; + Vector512 cb = c1 - halfValue; + Vector512 cr = c2 - halfValue; + Vector512 scaledK = c3 * scale * scale; + + // Sixteen lanes use the same matrix, rounding, inversion, and K modulation order as scalar code. + Vector512 r = Vector512_.MultiplyAddEstimate(cr, Vector512.Create(YCbCrScalar.RCrMult), y); + Vector512 g = Vector512_.MultiplyAddEstimate(cr, Vector512.Create(-YCbCrScalar.GCrMult), Vector512_.MultiplyAddEstimate(cb, Vector512.Create(-YCbCrScalar.GCbMult), y)); + Vector512 b = Vector512_.MultiplyAddEstimate(cb, Vector512.Create(YCbCrScalar.BCbMult), y); + c0 = (maximumValue - Vector512_.RoundToNearestInteger(r)) * scaledK; + c1 = (maximumValue - Vector512_.RoundToNearestInteger(g)) * scaledK; + c2 = (maximumValue - Vector512_.RoundToNearestInteger(b)) * scaledK; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb(float r, float g, float b, float maximumValue, float halfValue, float scale, out float c0, out float c1, out float c2, out float c3) + { + // CMYK extraction supplies inverted chromatic samples and K. Reflecting the first three results + // reconstructs the chromatic RGB that YCbCr encodes, while K passes through untouched. + CmykOperator.ConvertFromRgb(r, g, b, maximumValue, halfValue, scale, out float c, out float m, out float y, out c3); + YCbCrOperator.ConvertFromRgb(maximumValue - c, maximumValue - m, maximumValue - y, maximumValue, halfValue, scale, out c0, out c1, out c2, out _); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb(Vector128 r, Vector128 g, Vector128 b, Vector128 maximumValue, Vector128 halfValue, Vector128 scale, out Vector128 c0, out Vector128 c1, out Vector128 c2, out Vector128 c3) + { + // Static constrained calls inline both stages, keeping four pixels in registers without materializing CMYK planes. + CmykOperator.ConvertFromRgb(r, g, b, maximumValue, halfValue, scale, out Vector128 c, out Vector128 m, out Vector128 y, out c3); + YCbCrOperator.ConvertFromRgb(maximumValue - c, maximumValue - m, maximumValue - y, maximumValue, halfValue, scale, out c0, out c1, out c2, out _); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb(Vector256 r, Vector256 g, Vector256 b, Vector256 maximumValue, Vector256 halfValue, Vector256 scale, out Vector256 c0, out Vector256 c1, out Vector256 c2, out Vector256 c3) + { + // Eight pixels flow through CMYK extraction and YCbCr projection entirely in YMM registers. + CmykOperator.ConvertFromRgb(r, g, b, maximumValue, halfValue, scale, out Vector256 c, out Vector256 m, out Vector256 y, out c3); + YCbCrOperator.ConvertFromRgb(maximumValue - c, maximumValue - m, maximumValue - y, maximumValue, halfValue, scale, out c0, out c1, out c2, out _); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb(Vector512 r, Vector512 g, Vector512 b, Vector512 maximumValue, Vector512 halfValue, Vector512 scale, out Vector512 c0, out Vector512 c1, out Vector512 c2, out Vector512 c3) + { + // Sixteen pixels flow through both mathematical stages in registers without materializing intermediate planes. + CmykOperator.ConvertFromRgb(r, g, b, maximumValue, halfValue, scale, out Vector512 c, out Vector512 m, out Vector512 y, out c3); + YCbCrOperator.ConvertFromRgb(maximumValue - c, maximumValue - m, maximumValue - y, maximumValue, halfValue, scale, out c0, out c1, out c2, out _); + } + + /// + public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maximumValue) + => YccKScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, maximumValue); + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.cs index 74227c7a6..26e4c3584 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.cs @@ -248,161 +248,50 @@ internal abstract partial class JpegColorConverterBase /// Returns the s for the YCbCr colorspace. /// /// The precision in bits. - private static JpegColorConverterBase GetYCbCrConverter(int precision) - { - if (JpegColorConverterVector512.IsSupported) - { - return new YCbCrVector512(precision); - } - - if (JpegColorConverterVector256.IsSupported) - { - return new YCbCrVector256(precision); - } - - if (JpegColorConverterVector128.IsSupported) - { - return new YCbCrVector128(precision); - } - - return new YCbCrScalar(precision); - } + private static JpegColorConverter GetYCbCrConverter(int precision) + => new JpegColorConverter(precision); /// /// Returns the s for the YccK colorspace. /// /// The precision in bits. - private static JpegColorConverterBase GetYccKConverter(int precision) - { - if (JpegColorConverterVector512.IsSupported) - { - return new YccKVector512(precision); - } - - if (JpegColorConverterVector256.IsSupported) - { - return new YccKVector256(precision); - } - - if (JpegColorConverterVector128.IsSupported) - { - return new YccKVector128(precision); - } - - return new YccKScalar(precision); - } + private static JpegColorConverter GetYccKConverter(int precision) + => new JpegColorConverter(precision); /// /// Returns the s for the CMYK colorspace. /// /// The precision in bits. - private static JpegColorConverterBase GetCmykConverter(int precision) - { - if (JpegColorConverterVector512.IsSupported) - { - return new CmykVector512(precision); - } - - if (JpegColorConverterVector256.IsSupported) - { - return new CmykVector256(precision); - } - - if (JpegColorConverterVector128.IsSupported) - { - return new CmykVector128(precision); - } - - return new CmykScalar(precision); - } + private static JpegColorConverter GetCmykConverter(int precision) + => new JpegColorConverter(precision); /// /// Returns the s for the gray scale colorspace. /// /// The precision in bits. - private static JpegColorConverterBase GetGrayScaleConverter(int precision) - { - if (JpegColorConverterVector512.IsSupported) - { - return new GrayScaleVector512(precision); - } - - if (JpegColorConverterVector256.IsSupported) - { - return new GrayScaleVector256(precision); - } - - if (JpegColorConverterVector128.IsSupported) - { - return new GrayScaleVector128(precision); - } - - return new GrayScaleScalar(precision); - } + private static JpegColorConverter GetGrayScaleConverter(int precision) + => new JpegColorConverter(precision); /// /// Returns the s for the RGB colorspace. /// /// The precision in bits. - private static JpegColorConverterBase GetRgbConverter(int precision) - { - if (JpegColorConverterVector512.IsSupported) - { - return new RgbVector512(precision); - } - - if (JpegColorConverterVector256.IsSupported) - { - return new RgbVector256(precision); - } - - if (JpegColorConverterVector128.IsSupported) - { - return new RgbVector128(precision); - } + private static JpegColorConverter GetRgbConverter(int precision) + => new JpegColorConverter(precision); - return new RgbScalar(precision); - } - - private static JpegColorConverterBase GetTiffCmykConverter(int precision) - { - if (JpegColorConverterVector512.IsSupported) - { - return new TiffCmykVector512(precision); - } - - if (JpegColorConverterVector256.IsSupported) - { - return new TiffCmykVector256(precision); - } - - if (JpegColorConverterVector128.IsSupported) - { - return new TiffCmykVector128(precision); - } - - return new TiffCmykScalar(precision); - } - - private static JpegColorConverterBase GetTiffYccKConverter(int precision) - { - if (JpegColorConverterVector512.IsSupported) - { - return new TiffYccKVector512(precision); - } - - if (JpegColorConverterVector256.IsSupported) - { - return new TiffYccKVector256(precision); - } - - if (JpegColorConverterVector128.IsSupported) - { - return new TiffYccKVector128(precision); - } + /// + /// Returns the for non-inverted TIFF CMYK. + /// + /// The precision in bits. + private static JpegColorConverter GetTiffCmykConverter(int precision) + => new JpegColorConverter(precision); - return new TiffYccKScalar(precision); - } + /// + /// Returns the for non-inverted TIFF YccK. + /// + /// The precision in bits. + private static JpegColorConverter GetTiffYccKConverter(int precision) + => new JpegColorConverter(precision); /// /// A stack-only struct to reference the input buffers using -s. diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/ColorConversionBenchmark.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/ColorConversionBenchmark.cs index 436aa9bcd..106bb5ff6 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/ColorConversionBenchmark.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/ColorConversionBenchmark.cs @@ -51,6 +51,7 @@ public abstract class ColorConversionBenchmark // no need to dispose when buffer is not array owner buffers[i] = Configuration.Default.MemoryAllocator.Allocate2D(values.Length, 1); + values.CopyTo(buffers[i].DangerousGetRowSpan(0)); } return buffers; diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/JpegColorConverterOperatorComparison.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/JpegColorConverterOperatorComparison.cs new file mode 100644 index 000000000..b614583ca --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/JpegColorConverterOperatorComparison.cs @@ -0,0 +1,239 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using SixLabors.ImageSharp.Formats.Jpeg.Components; + +namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg; + +/// +/// Compares each shared operator converter with the Vector512 converter it replaces. +/// +[Config(typeof(Config.Standard))] +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] +[CategoriesColumn] +public class JpegColorConverterOperatorComparison +{ + private JpegColorConverterBase legacy; + private JpegColorConverterBase operatorConverter; + private float[] legacyC0; + private float[] legacyC1; + private float[] legacyC2; + private float[] legacyC3; + private float[] operatorC0; + private float[] operatorC1; + private float[] operatorC2; + private float[] operatorC3; + private float[] r; + private float[] g; + private float[] b; + private int componentCount; + + /// + /// Gets or sets the color model measured by the current benchmark case. + /// + [Params( + JpegColorModel.Grayscale, + JpegColorModel.Rgb, + JpegColorModel.Cmyk, + JpegColorModel.YCbCr, + JpegColorModel.YccK, + JpegColorModel.TiffCmyk, + JpegColorModel.TiffYccK)] + public JpegColorModel ColorModel { get; set; } + + /// + /// Gets or sets the number of pixels converted by each invocation. + /// + [Params(128, 1024)] + public int Count { get; set; } + + /// + /// Creates equivalent legacy and operator converters and their independent component buffers. + /// + [GlobalSetup] + public void Setup() + { + (JpegColorConverterBase Legacy, JpegColorConverterBase Operator, int ComponentCount) converters = + this.ColorModel switch + { + JpegColorModel.Grayscale => ( + new JpegColorConverterBase.GrayScaleVector512(8), + new JpegColorConverterBase.JpegColorConverter(8), + 1), + JpegColorModel.Rgb => ( + new JpegColorConverterBase.RgbVector512(8), + new JpegColorConverterBase.JpegColorConverter(8), + 3), + JpegColorModel.Cmyk => ( + new JpegColorConverterBase.CmykVector512(8), + new JpegColorConverterBase.JpegColorConverter(8), + 4), + JpegColorModel.YCbCr => ( + new JpegColorConverterBase.YCbCrVector512(8), + new JpegColorConverterBase.JpegColorConverter(8), + 3), + JpegColorModel.YccK => ( + new JpegColorConverterBase.YccKVector512(8), + new JpegColorConverterBase.JpegColorConverter(8), + 4), + JpegColorModel.TiffCmyk => ( + new JpegColorConverterBase.TiffCmykVector512(8), + new JpegColorConverterBase.JpegColorConverter(8), + 4), + JpegColorModel.TiffYccK => ( + new JpegColorConverterBase.TiffYccKVector512(8), + new JpegColorConverterBase.JpegColorConverter(8), + 4), + _ => throw new InvalidOperationException(), + }; + + (this.legacy, this.operatorConverter, this.componentCount) = converters; + + Random random = new(42); + this.legacyC0 = CreateRandomValues(this.Count, random); + this.legacyC1 = CreateRandomValues(this.Count, random); + this.legacyC2 = CreateRandomValues(this.Count, random); + this.legacyC3 = CreateRandomValues(this.Count, random); + this.operatorC0 = this.legacyC0.ToArray(); + this.operatorC1 = this.legacyC1.ToArray(); + this.operatorC2 = this.legacyC2.ToArray(); + this.operatorC3 = this.legacyC3.ToArray(); + this.r = CreateRandomValues(this.Count, random); + this.g = CreateRandomValues(this.Count, random); + this.b = CreateRandomValues(this.Count, random); + } + + /// + /// Converts JPEG components to RGB using the replaced Vector512 implementation. + /// + [Benchmark(Baseline = true)] + [BenchmarkCategory("ToRgb")] + public void LegacyToRgb() + { + JpegColorConverterBase.ComponentValues values = this.CreateLegacyValues(); + + this.legacy.ConvertToRgbInPlace(values); + } + + /// + /// Converts JPEG components to RGB using the shared operator traversal. + /// + [Benchmark] + [BenchmarkCategory("ToRgb")] + public void OperatorToRgb() + { + JpegColorConverterBase.ComponentValues values = this.CreateOperatorValues(); + + this.operatorConverter.ConvertToRgbInPlace(values); + } + + /// + /// Converts RGB to JPEG components using the replaced Vector512 implementation. + /// + [Benchmark(Baseline = true)] + [BenchmarkCategory("FromRgb")] + public void LegacyFromRgb() + { + JpegColorConverterBase.ComponentValues values = this.CreateLegacyValues(); + + this.legacy.ConvertFromRgb(values, this.r, this.g, this.b); + } + + /// + /// Converts RGB to JPEG components using the shared operator traversal. + /// + [Benchmark] + [BenchmarkCategory("FromRgb")] + public void OperatorFromRgb() + { + JpegColorConverterBase.ComponentValues values = this.CreateOperatorValues(); + + this.operatorConverter.ConvertFromRgb(values, this.r, this.g, this.b); + } + + /// + /// Creates a component view over the buffers owned by the legacy converter. + /// + /// The component view for the configured color model. + private JpegColorConverterBase.ComponentValues CreateLegacyValues() + => new( + this.componentCount, + this.legacyC0, + this.componentCount > 1 ? this.legacyC1 : this.legacyC0, + this.componentCount > 2 ? this.legacyC2 : this.legacyC0, + this.componentCount > 3 ? this.legacyC3 : []); + + /// + /// Creates a component view over the buffers owned by the operator converter. + /// + /// The component view for the configured color model. + private JpegColorConverterBase.ComponentValues CreateOperatorValues() + => new( + this.componentCount, + this.operatorC0, + this.componentCount > 1 ? this.operatorC1 : this.operatorC0, + this.componentCount > 2 ? this.operatorC2 : this.operatorC0, + this.componentCount > 3 ? this.operatorC3 : []); + + /// + /// Creates deterministic sample-domain values for one component plane. + /// + /// The number of samples to create. + /// The deterministic random source shared by setup. + /// The populated component plane. + private static float[] CreateRandomValues(int length, Random random) + { + float[] values = new float[length]; + + for (int i = 0; i < values.Length; i++) + { + values[i] = (float)random.NextDouble() * 255F; + } + + return values; + } + + /// + /// Identifies the JPEG color model used by a benchmark case. + /// + public enum JpegColorModel + { + /// + /// One luminance component. + /// + Grayscale, + + /// + /// Three direct RGB components. + /// + Rgb, + + /// + /// Four inverted Adobe CMYK components. + /// + Cmyk, + + /// + /// Three JPEG YCbCr components. + /// + YCbCr, + + /// + /// Four inverted Adobe YCCK components. + /// + YccK, + + /// + /// Four non-inverted TIFF CMYK components. + /// + TiffCmyk, + + /// + /// Four non-inverted TIFF YCCK components. + /// + TiffYccK, + } +} diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/JpegColorConverterTraversalAssembly.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/JpegColorConverterTraversalAssembly.cs new file mode 100644 index 000000000..4ec7db8a1 --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/JpegColorConverterTraversalAssembly.cs @@ -0,0 +1,325 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using SixLabors.ImageSharp.Formats.Jpeg.Components; + +namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg; + +/// +/// Exposes every closed JPEG operator traversal beside the Vector512 implementation it replaces. +/// +/// +/// A 63-pixel buffer leaves 256-bit, 128-bit, and scalar remainders after the 512-bit loop, making +/// every operator overload visible in the generated traversal assembly on AVX-512 hardware. +/// +[Config(typeof(Config.Analysis))] +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] +[CategoriesColumn] +public class JpegColorConverterTraversalAssembly +{ + private const int Count = 63; + + private readonly JpegColorConverterBase.GrayScaleVector512 grayscaleLegacy = new(8); + private readonly JpegColorConverterBase.JpegColorConverter grayscaleOperator = new(8); + private readonly JpegColorConverterBase.RgbVector512 rgbLegacy = new(8); + private readonly JpegColorConverterBase.JpegColorConverter rgbOperator = new(8); + private readonly JpegColorConverterBase.CmykVector512 cmykLegacy = new(8); + private readonly JpegColorConverterBase.JpegColorConverter cmykOperator = new(8); + private readonly JpegColorConverterBase.YCbCrVector512 yCbCrLegacy = new(8); + private readonly JpegColorConverterBase.JpegColorConverter yCbCrOperator = new(8); + private readonly JpegColorConverterBase.YccKVector512 yccKLegacy = new(8); + private readonly JpegColorConverterBase.JpegColorConverter yccKOperator = new(8); + private readonly JpegColorConverterBase.TiffCmykVector512 tiffCmykLegacy = new(8); + private readonly JpegColorConverterBase.JpegColorConverter tiffCmykOperator = new(8); + private readonly JpegColorConverterBase.TiffYccKVector512 tiffYccKLegacy = new(8); + private readonly JpegColorConverterBase.JpegColorConverter tiffYccKOperator = new(8); + + private readonly float[] legacyC0 = new float[Count]; + private readonly float[] legacyC1 = new float[Count]; + private readonly float[] legacyC2 = new float[Count]; + private readonly float[] legacyC3 = new float[Count]; + private readonly float[] operatorC0 = new float[Count]; + private readonly float[] operatorC1 = new float[Count]; + private readonly float[] operatorC2 = new float[Count]; + private readonly float[] operatorC3 = new float[Count]; + private readonly float[] r = new float[Count]; + private readonly float[] g = new float[Count]; + private readonly float[] b = new float[Count]; + + /// + /// Populates the component and RGB planes with deterministic sample-domain values. + /// + [GlobalSetup] + public void Setup() + { + Random random = new(42); + + for (int i = 0; i < Count; i++) + { + // Independent non-constant lanes prevent the JIT from folding arithmetic or mask decisions. + this.legacyC0[i] = this.operatorC0[i] = (float)random.NextDouble() * 255F; + this.legacyC1[i] = this.operatorC1[i] = (float)random.NextDouble() * 255F; + this.legacyC2[i] = this.operatorC2[i] = (float)random.NextDouble() * 255F; + this.legacyC3[i] = this.operatorC3[i] = (float)random.NextDouble() * 255F; + this.r[i] = (float)random.NextDouble() * 255F; + this.g[i] = (float)random.NextDouble() * 255F; + this.b[i] = (float)random.NextDouble() * 255F; + } + } + + /// + /// Runs the replaced grayscale component-to-RGB traversal. + /// + [Benchmark(Baseline = true)] + [BenchmarkCategory("Grayscale.ToRgb")] + public void GrayscaleLegacyToRgb() + => this.grayscaleLegacy.ConvertToRgbInPlace(this.CreateLegacyValues(1)); + + /// + /// Runs the shared grayscale component-to-RGB traversal. + /// + [Benchmark] + [BenchmarkCategory("Grayscale.ToRgb")] + public void GrayscaleOperatorToRgb() + => this.grayscaleOperator.ConvertToRgbInPlace(this.CreateOperatorValues(1)); + + /// + /// Runs the replaced grayscale RGB-to-component traversal. + /// + [Benchmark(Baseline = true)] + [BenchmarkCategory("Grayscale.FromRgb")] + public void GrayscaleLegacyFromRgb() + => this.grayscaleLegacy.ConvertFromRgb(this.CreateLegacyValues(1), this.r, this.g, this.b); + + /// + /// Runs the shared grayscale RGB-to-component traversal. + /// + [Benchmark] + [BenchmarkCategory("Grayscale.FromRgb")] + public void GrayscaleOperatorFromRgb() + => this.grayscaleOperator.ConvertFromRgb(this.CreateOperatorValues(1), this.r, this.g, this.b); + + /// + /// Runs the replaced RGB component-to-RGB traversal. + /// + [Benchmark(Baseline = true)] + [BenchmarkCategory("Rgb.ToRgb")] + public void RgbLegacyToRgb() + => this.rgbLegacy.ConvertToRgbInPlace(this.CreateLegacyValues(3)); + + /// + /// Runs the shared RGB component-to-RGB traversal. + /// + [Benchmark] + [BenchmarkCategory("Rgb.ToRgb")] + public void RgbOperatorToRgb() + => this.rgbOperator.ConvertToRgbInPlace(this.CreateOperatorValues(3)); + + /// + /// Runs the replaced RGB RGB-to-component traversal. + /// + [Benchmark(Baseline = true)] + [BenchmarkCategory("Rgb.FromRgb")] + public void RgbLegacyFromRgb() + => this.rgbLegacy.ConvertFromRgb(this.CreateLegacyValues(3), this.r, this.g, this.b); + + /// + /// Runs the shared RGB RGB-to-component traversal. + /// + [Benchmark] + [BenchmarkCategory("Rgb.FromRgb")] + public void RgbOperatorFromRgb() + => this.rgbOperator.ConvertFromRgb(this.CreateOperatorValues(3), this.r, this.g, this.b); + + /// + /// Runs the replaced CMYK component-to-RGB traversal. + /// + [Benchmark(Baseline = true)] + [BenchmarkCategory("Cmyk.ToRgb")] + public void CmykLegacyToRgb() + => this.cmykLegacy.ConvertToRgbInPlace(this.CreateLegacyValues(4)); + + /// + /// Runs the shared CMYK component-to-RGB traversal. + /// + [Benchmark] + [BenchmarkCategory("Cmyk.ToRgb")] + public void CmykOperatorToRgb() + => this.cmykOperator.ConvertToRgbInPlace(this.CreateOperatorValues(4)); + + /// + /// Runs the replaced CMYK RGB-to-component traversal. + /// + [Benchmark(Baseline = true)] + [BenchmarkCategory("Cmyk.FromRgb")] + public void CmykLegacyFromRgb() + => this.cmykLegacy.ConvertFromRgb(this.CreateLegacyValues(4), this.r, this.g, this.b); + + /// + /// Runs the shared CMYK RGB-to-component traversal. + /// + [Benchmark] + [BenchmarkCategory("Cmyk.FromRgb")] + public void CmykOperatorFromRgb() + => this.cmykOperator.ConvertFromRgb(this.CreateOperatorValues(4), this.r, this.g, this.b); + + /// + /// Runs the replaced YCbCr component-to-RGB traversal. + /// + [Benchmark(Baseline = true)] + [BenchmarkCategory("YCbCr.ToRgb")] + public void YCbCrLegacyToRgb() + => this.yCbCrLegacy.ConvertToRgbInPlace(this.CreateLegacyValues(3)); + + /// + /// Runs the shared YCbCr component-to-RGB traversal. + /// + [Benchmark] + [BenchmarkCategory("YCbCr.ToRgb")] + public void YCbCrOperatorToRgb() + => this.yCbCrOperator.ConvertToRgbInPlace(this.CreateOperatorValues(3)); + + /// + /// Runs the replaced YCbCr RGB-to-component traversal. + /// + [Benchmark(Baseline = true)] + [BenchmarkCategory("YCbCr.FromRgb")] + public void YCbCrLegacyFromRgb() + => this.yCbCrLegacy.ConvertFromRgb(this.CreateLegacyValues(3), this.r, this.g, this.b); + + /// + /// Runs the shared YCbCr RGB-to-component traversal. + /// + [Benchmark] + [BenchmarkCategory("YCbCr.FromRgb")] + public void YCbCrOperatorFromRgb() + => this.yCbCrOperator.ConvertFromRgb(this.CreateOperatorValues(3), this.r, this.g, this.b); + + /// + /// Runs the replaced YCCK component-to-RGB traversal. + /// + [Benchmark(Baseline = true)] + [BenchmarkCategory("YccK.ToRgb")] + public void YccKLegacyToRgb() + => this.yccKLegacy.ConvertToRgbInPlace(this.CreateLegacyValues(4)); + + /// + /// Runs the shared YCCK component-to-RGB traversal. + /// + [Benchmark] + [BenchmarkCategory("YccK.ToRgb")] + public void YccKOperatorToRgb() + => this.yccKOperator.ConvertToRgbInPlace(this.CreateOperatorValues(4)); + + /// + /// Runs the replaced YCCK RGB-to-component traversal. + /// + [Benchmark(Baseline = true)] + [BenchmarkCategory("YccK.FromRgb")] + public void YccKLegacyFromRgb() + => this.yccKLegacy.ConvertFromRgb(this.CreateLegacyValues(4), this.r, this.g, this.b); + + /// + /// Runs the shared YCCK RGB-to-component traversal. + /// + [Benchmark] + [BenchmarkCategory("YccK.FromRgb")] + public void YccKOperatorFromRgb() + => this.yccKOperator.ConvertFromRgb(this.CreateOperatorValues(4), this.r, this.g, this.b); + + /// + /// Runs the replaced TIFF CMYK component-to-RGB traversal. + /// + [Benchmark(Baseline = true)] + [BenchmarkCategory("TiffCmyk.ToRgb")] + public void TiffCmykLegacyToRgb() + => this.tiffCmykLegacy.ConvertToRgbInPlace(this.CreateLegacyValues(4)); + + /// + /// Runs the shared TIFF CMYK component-to-RGB traversal. + /// + [Benchmark] + [BenchmarkCategory("TiffCmyk.ToRgb")] + public void TiffCmykOperatorToRgb() + => this.tiffCmykOperator.ConvertToRgbInPlace(this.CreateOperatorValues(4)); + + /// + /// Runs the replaced TIFF CMYK RGB-to-component traversal. + /// + [Benchmark(Baseline = true)] + [BenchmarkCategory("TiffCmyk.FromRgb")] + public void TiffCmykLegacyFromRgb() + => this.tiffCmykLegacy.ConvertFromRgb(this.CreateLegacyValues(4), this.r, this.g, this.b); + + /// + /// Runs the shared TIFF CMYK RGB-to-component traversal. + /// + [Benchmark] + [BenchmarkCategory("TiffCmyk.FromRgb")] + public void TiffCmykOperatorFromRgb() + => this.tiffCmykOperator.ConvertFromRgb(this.CreateOperatorValues(4), this.r, this.g, this.b); + + /// + /// Runs the replaced TIFF YCCK component-to-RGB traversal. + /// + [Benchmark(Baseline = true)] + [BenchmarkCategory("TiffYccK.ToRgb")] + public void TiffYccKLegacyToRgb() + => this.tiffYccKLegacy.ConvertToRgbInPlace(this.CreateLegacyValues(4)); + + /// + /// Runs the shared TIFF YCCK component-to-RGB traversal. + /// + [Benchmark] + [BenchmarkCategory("TiffYccK.ToRgb")] + public void TiffYccKOperatorToRgb() + => this.tiffYccKOperator.ConvertToRgbInPlace(this.CreateOperatorValues(4)); + + /// + /// Runs the replaced TIFF YCCK RGB-to-component traversal. + /// + [Benchmark(Baseline = true)] + [BenchmarkCategory("TiffYccK.FromRgb")] + public void TiffYccKLegacyFromRgb() + => this.tiffYccKLegacy.ConvertFromRgb(this.CreateLegacyValues(4), this.r, this.g, this.b); + + /// + /// Runs the shared TIFF YCCK RGB-to-component traversal. + /// + [Benchmark] + [BenchmarkCategory("TiffYccK.FromRgb")] + public void TiffYccKOperatorFromRgb() + => this.tiffYccKOperator.ConvertFromRgb(this.CreateOperatorValues(4), this.r, this.g, this.b); + + /// + /// Creates a correctly aliased component view over the legacy planes. + /// + /// The number of component planes owned by the color model. + /// The legacy component view. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private JpegColorConverterBase.ComponentValues CreateLegacyValues(int componentCount) + => new( + componentCount, + this.legacyC0, + componentCount > 1 ? this.legacyC1 : this.legacyC0, + componentCount > 2 ? this.legacyC2 : this.legacyC0, + componentCount > 3 ? this.legacyC3 : []); + + /// + /// Creates a correctly aliased component view over the operator planes. + /// + /// The number of component planes owned by the color model. + /// The operator component view. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private JpegColorConverterBase.ComponentValues CreateOperatorValues(int componentCount) + => new( + componentCount, + this.operatorC0, + componentCount > 1 ? this.operatorC1 : this.operatorC0, + componentCount > 2 ? this.operatorC2 : this.operatorC0, + componentCount > 3 ? this.operatorC3 : []); +} diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/YCbCrOperatorAssembly.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/YCbCrOperatorAssembly.cs new file mode 100644 index 000000000..b67ee76df --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/YCbCrOperatorAssembly.cs @@ -0,0 +1,244 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using SixLabors.ImageSharp.Formats.Jpeg.Components; + +namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg; + +/// +/// Exposes every YCbCr operator overload directly to the disassembly diagnoser. +/// +[Config(typeof(Config.Analysis))] +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] +[CategoriesColumn] +public class YCbCrOperatorAssembly +{ + private const float MaximumValue = 255F; + private const float HalfValue = 128F; + private const float Scale = 1F / MaximumValue; + + private float scalarC0 = 64F; + private float scalarC1 = 96F; + private float scalarC2 = 160F; + + private readonly Vector128 vector128C0 = Vector128.Create(64F); + private readonly Vector128 vector128C1 = Vector128.Create(96F); + private readonly Vector128 vector128C2 = Vector128.Create(160F); + private readonly Vector128 vector128Maximum = Vector128.Create(MaximumValue); + private readonly Vector128 vector128Half = Vector128.Create(HalfValue); + private readonly Vector128 vector128Scale = Vector128.Create(Scale); + + private readonly Vector256 vector256C0 = Vector256.Create(64F); + private readonly Vector256 vector256C1 = Vector256.Create(96F); + private readonly Vector256 vector256C2 = Vector256.Create(160F); + private readonly Vector256 vector256Maximum = Vector256.Create(MaximumValue); + private readonly Vector256 vector256Half = Vector256.Create(HalfValue); + private readonly Vector256 vector256Scale = Vector256.Create(Scale); + + private readonly Vector512 vector512C0 = Vector512.Create(64F); + private readonly Vector512 vector512C1 = Vector512.Create(96F); + private readonly Vector512 vector512C2 = Vector512.Create(160F); + private readonly Vector512 vector512Maximum = Vector512.Create(MaximumValue); + private readonly Vector512 vector512Half = Vector512.Create(HalfValue); + private readonly Vector512 vector512Scale = Vector512.Create(Scale); + + /// + /// Invokes the scalar JPEG-to-RGB operator. + /// + /// A checksum containing all three converted channels. + [Benchmark] + [BenchmarkCategory("ToRgb")] + public float ToRgbScalar() + { + float c0 = this.scalarC0; + float c1 = this.scalarC1; + float c2 = this.scalarC2; + + JpegColorConverterBase.YCbCrOperator.ConvertToRgb( + ref c0, + ref c1, + ref c2, + 0, + MaximumValue, + HalfValue, + Scale); + + // Returning the channel sum keeps every output live in the generated assembly. + return c0 + c1 + c2; + } + + /// + /// Invokes the Vector128 JPEG-to-RGB operator. + /// + /// A checksum containing all three converted channel vectors. + [Benchmark] + [BenchmarkCategory("ToRgb")] + public Vector128 ToRgbVector128() + { + Vector128 c0 = this.vector128C0; + Vector128 c1 = this.vector128C1; + Vector128 c2 = this.vector128C2; + + JpegColorConverterBase.YCbCrOperator.ConvertToRgb( + ref c0, + ref c1, + ref c2, + default, + this.vector128Maximum, + this.vector128Half, + this.vector128Scale); + + // The vector sum makes all RGB results observable without adding stores to the measured body. + return c0 + c1 + c2; + } + + /// + /// Invokes the Vector256 JPEG-to-RGB operator. + /// + /// A checksum containing all three converted channel vectors. + [Benchmark] + [BenchmarkCategory("ToRgb")] + public Vector256 ToRgbVector256() + { + Vector256 c0 = this.vector256C0; + Vector256 c1 = this.vector256C1; + Vector256 c2 = this.vector256C2; + + JpegColorConverterBase.YCbCrOperator.ConvertToRgb( + ref c0, + ref c1, + ref c2, + default, + this.vector256Maximum, + this.vector256Half, + this.vector256Scale); + + // The vector sum makes all RGB results observable without adding stores to the measured body. + return c0 + c1 + c2; + } + + /// + /// Invokes the Vector512 JPEG-to-RGB operator. + /// + /// A checksum containing all three converted channel vectors. + [Benchmark] + [BenchmarkCategory("ToRgb")] + public Vector512 ToRgbVector512() + { + Vector512 c0 = this.vector512C0; + Vector512 c1 = this.vector512C1; + Vector512 c2 = this.vector512C2; + + JpegColorConverterBase.YCbCrOperator.ConvertToRgb( + ref c0, + ref c1, + ref c2, + default, + this.vector512Maximum, + this.vector512Half, + this.vector512Scale); + + // The vector sum makes all RGB results observable without adding stores to the measured body. + return c0 + c1 + c2; + } + + /// + /// Invokes the scalar RGB-to-JPEG operator. + /// + /// A checksum containing all converted components. + [Benchmark] + [BenchmarkCategory("FromRgb")] + public float FromRgbScalar() + { + JpegColorConverterBase.YCbCrOperator.ConvertFromRgb( + this.scalarC0, + this.scalarC1, + this.scalarC2, + MaximumValue, + HalfValue, + Scale, + out float c0, + out float c1, + out float c2, + out float c3); + + // c3 is deliberately included so a future four-component implementation remains observable. + return c0 + c1 + c2 + c3; + } + + /// + /// Invokes the Vector128 RGB-to-JPEG operator. + /// + /// A checksum containing all converted component vectors. + [Benchmark] + [BenchmarkCategory("FromRgb")] + public Vector128 FromRgbVector128() + { + JpegColorConverterBase.YCbCrOperator.ConvertFromRgb( + this.vector128C0, + this.vector128C1, + this.vector128C2, + this.vector128Maximum, + this.vector128Half, + this.vector128Scale, + out Vector128 c0, + out Vector128 c1, + out Vector128 c2, + out Vector128 c3); + + // Include all planar results in the returned vector so the JIT retains every calculation. + return c0 + c1 + c2 + c3; + } + + /// + /// Invokes the Vector256 RGB-to-JPEG operator. + /// + /// A checksum containing all converted component vectors. + [Benchmark] + [BenchmarkCategory("FromRgb")] + public Vector256 FromRgbVector256() + { + JpegColorConverterBase.YCbCrOperator.ConvertFromRgb( + this.vector256C0, + this.vector256C1, + this.vector256C2, + this.vector256Maximum, + this.vector256Half, + this.vector256Scale, + out Vector256 c0, + out Vector256 c1, + out Vector256 c2, + out Vector256 c3); + + // Include all planar results in the returned vector so the JIT retains every calculation. + return c0 + c1 + c2 + c3; + } + + /// + /// Invokes the Vector512 RGB-to-JPEG operator. + /// + /// A checksum containing all converted component vectors. + [Benchmark] + [BenchmarkCategory("FromRgb")] + public Vector512 FromRgbVector512() + { + JpegColorConverterBase.YCbCrOperator.ConvertFromRgb( + this.vector512C0, + this.vector512C1, + this.vector512C2, + this.vector512Maximum, + this.vector512Half, + this.vector512Scale, + out Vector512 c0, + out Vector512 c1, + out Vector512 c2, + out Vector512 c3); + + // Include all planar results in the returned vector so the JIT retains every calculation. + return c0 + c1 + c2 + c3; + } +} diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/YCbCrOperatorComparison.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/YCbCrOperatorComparison.cs new file mode 100644 index 000000000..bba07b1e1 --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/YCbCrOperatorComparison.cs @@ -0,0 +1,127 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using SixLabors.ImageSharp.Formats.Jpeg.Components; + +namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg; + +/// +/// Compares the shared YCbCr operator traversal with the Vector512 implementation it replaces. +/// +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] +[CategoriesColumn] +public class YCbCrOperatorComparison +{ + private JpegColorConverterBase.YCbCrVector512 legacy; + private JpegColorConverterBase.JpegColorConverter operatorConverter; + private float[] legacyC0; + private float[] legacyC1; + private float[] legacyC2; + private float[] operatorC0; + private float[] operatorC1; + private float[] operatorC2; + private float[] r; + private float[] g; + private float[] b; + + /// + /// Gets or sets the number of pixels converted by each invocation. + /// + [Params(8, 128, 1024)] + public int Count { get; set; } + + /// + /// Creates equivalent converter inputs in independent component buffers. + /// + [GlobalSetup] + public void Setup() + { + this.legacy = new JpegColorConverterBase.YCbCrVector512(8); + this.operatorConverter = + new JpegColorConverterBase.JpegColorConverter(8); + + Random random = new(42); + this.legacyC0 = CreateRandomValues(this.Count, random); + this.legacyC1 = CreateRandomValues(this.Count, random); + this.legacyC2 = CreateRandomValues(this.Count, random); + this.operatorC0 = this.legacyC0.ToArray(); + this.operatorC1 = this.legacyC1.ToArray(); + this.operatorC2 = this.legacyC2.ToArray(); + this.r = CreateRandomValues(this.Count, random); + this.g = CreateRandomValues(this.Count, random); + this.b = CreateRandomValues(this.Count, random); + } + + /// + /// Converts YCbCr components to RGB using the Vector512 implementation. + /// + [Benchmark(Baseline = true)] + [BenchmarkCategory("ToRgb")] + public void LegacyToRgb() + { + JpegColorConverterBase.ComponentValues values = + new(3, this.legacyC0, this.legacyC1, this.legacyC2, []); + + this.legacy.ConvertToRgbInPlace(values); + } + + /// + /// Converts YCbCr components to RGB using the shared operator traversal. + /// + [Benchmark] + [BenchmarkCategory("ToRgb")] + public void OperatorToRgb() + { + JpegColorConverterBase.ComponentValues values = + new(3, this.operatorC0, this.operatorC1, this.operatorC2, []); + + this.operatorConverter.ConvertToRgbInPlace(values); + } + + /// + /// Converts RGB to YCbCr components using the Vector512 implementation. + /// + [Benchmark(Baseline = true)] + [BenchmarkCategory("FromRgb")] + public void LegacyFromRgb() + { + JpegColorConverterBase.ComponentValues values = + new(3, this.legacyC0, this.legacyC1, this.legacyC2, []); + + this.legacy.ConvertFromRgb(values, this.r, this.g, this.b); + } + + /// + /// Converts RGB to YCbCr components using the shared operator traversal. + /// + [Benchmark] + [BenchmarkCategory("FromRgb")] + public void OperatorFromRgb() + { + JpegColorConverterBase.ComponentValues values = + new(3, this.operatorC0, this.operatorC1, this.operatorC2, []); + + this.operatorConverter.ConvertFromRgb(values, this.r, this.g, this.b); + } + + /// + /// Creates deterministic sample-domain values for one component plane. + /// + /// The number of samples to create. + /// The deterministic random source shared by setup. + /// The populated component plane. + private static float[] CreateRandomValues(int length, Random random) + { + float[] values = new float[length]; + + for (int i = 0; i < values.Length; i++) + { + values[i] = (float)random.NextDouble() * 255F; + } + + return values; + } +} diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs index ba0dce4c5..c11456014 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.ColorProfiles.Icc; using SixLabors.ImageSharp.Formats.Jpeg.Components; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Tests.ColorProfiles; @@ -43,6 +44,11 @@ public class JpegColorConverterTests Assert.Throws(() => JpegColorConverterBase.GetConverter(JpegColorSpace.YCbCr, invalidPrecision)); } + /// + /// Verifies that each supported color space and precision resolves to an available converter. + /// + /// The JPEG color space. + /// The JPEG sample precision. [Theory] [InlineData(JpegColorSpace.Grayscale, 8)] [InlineData(JpegColorSpace.Grayscale, 12)] @@ -54,6 +60,10 @@ public class JpegColorConverterTests [InlineData(JpegColorSpace.RGB, 12)] [InlineData(JpegColorSpace.YCbCr, 8)] [InlineData(JpegColorSpace.YCbCr, 12)] + [InlineData(JpegColorSpace.TiffCmyk, 8)] + [InlineData(JpegColorSpace.TiffCmyk, 12)] + [InlineData(JpegColorSpace.TiffYccK, 8)] + [InlineData(JpegColorSpace.TiffYccK, 12)] internal void GetConverterReturnsValidConverter(JpegColorSpace colorSpace, int precision) { JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(colorSpace, precision); @@ -74,19 +84,8 @@ public class JpegColorConverterTests static void RunTest(string arg) { // arrange - Type expectedType = typeof(JpegColorConverterBase.RgbScalar); - if (JpegColorConverterBase.JpegColorConverterVector512.IsSupported) - { - expectedType = typeof(JpegColorConverterBase.RgbVector512); - } - else if (JpegColorConverterBase.JpegColorConverterVector256.IsSupported) - { - expectedType = typeof(JpegColorConverterBase.RgbVector256); - } - else if (JpegColorConverterBase.JpegColorConverterVector128.IsSupported) - { - expectedType = typeof(JpegColorConverterBase.RgbVector128); - } + Type expectedType = + typeof(JpegColorConverterBase.JpegColorConverter); // act JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(JpegColorSpace.RGB, 8); @@ -107,19 +106,8 @@ public class JpegColorConverterTests static void RunTest(string arg) { // arrange - Type expectedType = typeof(JpegColorConverterBase.GrayScaleScalar); - if (JpegColorConverterBase.JpegColorConverterVector512.IsSupported) - { - expectedType = typeof(JpegColorConverterBase.GrayScaleVector512); - } - else if (JpegColorConverterBase.JpegColorConverterVector256.IsSupported) - { - expectedType = typeof(JpegColorConverterBase.GrayScaleVector256); - } - else if (JpegColorConverterBase.JpegColorConverterVector128.IsSupported) - { - expectedType = typeof(JpegColorConverterBase.GrayScaleVector128); - } + Type expectedType = + typeof(JpegColorConverterBase.JpegColorConverter); // act JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(JpegColorSpace.Grayscale, 8); @@ -140,19 +128,8 @@ public class JpegColorConverterTests static void RunTest(string arg) { // arrange - Type expectedType = typeof(JpegColorConverterBase.CmykScalar); - if (JpegColorConverterBase.JpegColorConverterVector512.IsSupported) - { - expectedType = typeof(JpegColorConverterBase.CmykVector512); - } - else if (JpegColorConverterBase.JpegColorConverterVector256.IsSupported) - { - expectedType = typeof(JpegColorConverterBase.CmykVector256); - } - else if (JpegColorConverterBase.JpegColorConverterVector128.IsSupported) - { - expectedType = typeof(JpegColorConverterBase.CmykVector128); - } + Type expectedType = + typeof(JpegColorConverterBase.JpegColorConverter); // act JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(JpegColorSpace.Cmyk, 8); @@ -173,19 +150,8 @@ public class JpegColorConverterTests static void RunTest(string arg) { // arrange - Type expectedType = typeof(JpegColorConverterBase.YCbCrScalar); - if (JpegColorConverterBase.JpegColorConverterVector512.IsSupported) - { - expectedType = typeof(JpegColorConverterBase.YCbCrVector512); - } - else if (JpegColorConverterBase.JpegColorConverterVector256.IsSupported) - { - expectedType = typeof(JpegColorConverterBase.YCbCrVector256); - } - else if (JpegColorConverterBase.JpegColorConverterVector128.IsSupported) - { - expectedType = typeof(JpegColorConverterBase.YCbCrVector128); - } + Type expectedType = + typeof(JpegColorConverterBase.JpegColorConverter); // act JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(JpegColorSpace.YCbCr, 8); @@ -206,19 +172,8 @@ public class JpegColorConverterTests static void RunTest(string arg) { // arrange - Type expectedType = typeof(JpegColorConverterBase.YccKScalar); - if (JpegColorConverterBase.JpegColorConverterVector512.IsSupported) - { - expectedType = typeof(JpegColorConverterBase.YccKVector512); - } - else if (JpegColorConverterBase.JpegColorConverterVector256.IsSupported) - { - expectedType = typeof(JpegColorConverterBase.YccKVector256); - } - else if (JpegColorConverterBase.JpegColorConverterVector128.IsSupported) - { - expectedType = typeof(JpegColorConverterBase.YccKVector128); - } + Type expectedType = + typeof(JpegColorConverterBase.JpegColorConverter); // act JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(JpegColorSpace.Ycck, 8); @@ -229,6 +184,25 @@ public class JpegColorConverterTests } } + /// + /// Verifies that TIFF color spaces resolve to their closed shared converter types. + /// + /// The TIFF JPEG color space. + /// The expected closed converter type. + [Theory] + [InlineData( + JpegColorSpace.TiffCmyk, + typeof(JpegColorConverterBase.JpegColorConverter))] + [InlineData( + JpegColorSpace.TiffYccK, + typeof(JpegColorConverterBase.JpegColorConverter))] + internal void GetConverterReturnsCorrectConverterWithTiffColorSpace(JpegColorSpace colorSpace, Type expectedType) + { + JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(colorSpace, 8); + + Assert.Equal(expectedType, converter.GetType()); + } + [Theory] [InlineData(JpegColorSpace.Grayscale, 1)] [InlineData(JpegColorSpace.Ycck, 4)] @@ -306,6 +280,171 @@ public class JpegColorConverterTests new JpegColorConverterBase.YCbCrScalar(8), precision: 2); + /// + /// Verifies YCbCr equivalence around every scalar and SIMD width boundary. + /// + /// The number of samples to convert. + /// The JPEG sample precision. + [Theory] + [InlineData(1, 8)] + [InlineData(3, 12)] + [InlineData(4, 8)] + [InlineData(7, 12)] + [InlineData(8, 8)] + [InlineData(15, 12)] + [InlineData(16, 8)] + [InlineData(31, 12)] + [InlineData(32, 8)] + [InlineData(40, 12)] + [InlineData(64, 8)] + [InlineData(128, 12)] + public void YCbCrOperatorMatchesScalarForAllVectorBoundaries(int length, int precision) + { + JpegColorConverterBase converter = + new JpegColorConverterBase.JpegColorConverter(precision); + JpegColorConverterBase baseline = new JpegColorConverterBase.YCbCrScalar(precision); + + ValidateConversionToRgb(converter, baseline, length, 3, precision); + ValidateConversionFromRgb(converter, baseline, length, 3, precision); + } + + /// + /// Verifies that the YCbCr operator retains scalar behavior when hardware intrinsics are disabled. + /// + [Fact] + public void YCbCrOperatorMatchesScalarWithoutHardwareIntrinsics() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.DisableHWIntrinsic); + + /// + /// Verifies converter equivalence around every scalar and SIMD width boundary. + /// + /// The color space under test. + /// The number of component planes written by the converter. + [Theory] + [InlineData(JpegColorSpace.Grayscale, 1)] + [InlineData(JpegColorSpace.RGB, 3)] + [InlineData(JpegColorSpace.Cmyk, 4)] + [InlineData(JpegColorSpace.Ycck, 4)] + [InlineData(JpegColorSpace.TiffCmyk, 4)] + internal void OperatorsMatchScalarAcrossEveryWidthBoundary(JpegColorSpace colorSpace, int componentCount) + { + JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(colorSpace, 8); + JpegColorConverterBase baseline = colorSpace switch + { + JpegColorSpace.Grayscale => new JpegColorConverterBase.GrayScaleScalar(8), + JpegColorSpace.RGB => new JpegColorConverterBase.RgbScalar(8), + JpegColorSpace.Cmyk => new JpegColorConverterBase.CmykScalar(8), + JpegColorSpace.Ycck => new JpegColorConverterBase.YccKScalar(8), + JpegColorSpace.TiffCmyk => new JpegColorConverterBase.TiffCmykScalar(8), + _ => throw new InvalidOperationException(), + }; + + // These lengths exercise every point immediately below, at, and above the supported SIMD widths. + int[] lengths = [1, 3, 4, 7, 8, 15, 16, 31, 32, 40, 64, 128]; + + foreach (int length in lengths) + { + ValidateConversionToRgb(converter, baseline, length, componentCount, 8); + ValidateConversionFromRgb(converter, baseline, length, componentCount, 8); + } + } + + /// + /// Verifies TIFF YCCK decoding around every scalar and SIMD width boundary. + /// + /// The JPEG sample precision. + [Theory] + [InlineData(8)] + [InlineData(12)] + public void TiffYccKOperatorToRgbMatchesScalarAcrossEveryWidthBoundary(int precision) + { + JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(JpegColorSpace.TiffYccK, precision); + JpegColorConverterBase baseline = new JpegColorConverterBase.TiffYccKScalar(precision); + + // The combined lengths force scalar, 128-bit, 256-bit, and 512-bit work on capable hardware. + int[] lengths = [1, 3, 4, 7, 8, 15, 16, 31, 32, 40, 64, 128]; + + foreach (int length in lengths) + { + ValidateConversionToRgb(converter, baseline, length, 4, precision); + } + } + + /// + /// Verifies TIFF YCCK encoding against the canonical normalized color-profile conversion. + /// + [Fact] + public void TiffYccKOperatorFromRgbMatchesColorProfileDefinition() + { + const int maximumLength = 40; + const float maximumValue = 255F; + const float halfValue = 128F; + const float tolerance = 0.0001F; + float[] rSeed = [0, 255, 255, 0, 0, 127, 32, 240, 0, 255, 255, 0, 0, 127, 32, 240, 0, 255, 64, 192]; + float[] gSeed = [0, 255, 0, 255, 0, 127, 160, 16, 0, 255, 0, 255, 0, 127, 160, 16, 255, 0, 128, 96]; + float[] bSeed = [0, 255, 0, 0, 255, 127, 224, 80, 255, 0, 255, 0, 0, 127, 224, 80, 0, 255, 192, 32]; + float[] r = new float[maximumLength]; + float[] g = new float[maximumLength]; + float[] b = new float[maximumLength]; + JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(JpegColorSpace.TiffYccK, 8); + + // Repeating the color set provides enough lanes to exercise every SIMD width and each mixed-width tail. + rSeed.CopyTo(r, 0); + rSeed.CopyTo(r, rSeed.Length); + gSeed.CopyTo(g, 0); + gSeed.CopyTo(g, gSeed.Length); + bSeed.CopyTo(b, 0); + bSeed.CopyTo(b, bSeed.Length); + + // The normalized color-profile implementation is the canonical definition; JPEG stores each result + // in the configured integer sample domain. + ColorProfileConverter reference = new(); + int[] lengths = [1, 3, 4, 7, 8, 15, 16, 31, 32, 40]; + + foreach (int length in lengths) + { + float[] y = new float[length]; + float[] cb = new float[length]; + float[] cr = new float[length]; + float[] k = new float[length]; + JpegColorConverterBase.ComponentValues values = new(4, y, cb, cr, k); + + converter.ConvertFromRgb(values, r.AsSpan(0, length), g.AsSpan(0, length), b.AsSpan(0, length)); + + for (int i = 0; i < length; i++) + { + Rgb rgb = new(r[i] / maximumValue, g[i] / maximumValue, b[i] / maximumValue); + YccK expected = reference.Convert(rgb); + + Assert.Equal(expected.Y * maximumValue, y[i], tolerance); + + // JPEG centers chroma on the integer sample midpoint (128 at 8-bit precision), whereas + // the color-profile definition centers normalized chroma exactly on 0.5. + Assert.Equal(halfValue + ((expected.Cb - 0.5F) * maximumValue), cb[i], tolerance); + Assert.Equal(halfValue + ((expected.Cr - 0.5F) * maximumValue), cr[i], tolerance); + Assert.Equal(expected.K * maximumValue, k[i], tolerance); + } + } + } + + /// + /// Runs the YCbCr equivalence check in the feature-test process. + /// + /// The unused feature-test argument. + private static void RunTest(string arg) + { + const int length = 40; + const int precision = 8; + JpegColorConverterBase converter = + new JpegColorConverterBase.JpegColorConverter(precision); + JpegColorConverterBase baseline = new JpegColorConverterBase.YCbCrScalar(precision); + + ValidateConversionToRgb(converter, baseline, length, 3, precision); + ValidateConversionFromRgb(converter, baseline, length, 3, precision); + } + [Theory] [MemberData(nameof(Seeds))] public void FromCmykBasic(int seed) => @@ -736,6 +875,91 @@ public class JpegColorConverterTests } } + /// + /// Compares two component planes using an absolute floating-point tolerance. + /// + /// The expected component values. + /// The actual component values. + /// The maximum permitted absolute difference. + private static void CompareSequenceWithTolerance(Span expected, Span actual, float tolerance) + { + for (int i = 0; i < expected.Length; i++) + { + Assert.Equal(expected[i], actual[i], tolerance); + } + } + + /// + /// Compares component-to-RGB conversion with a scalar reference implementation. + /// + /// The shared converter under test. + /// The scalar reference converter. + /// The number of samples to convert. + /// The number of source component planes. + /// The JPEG sample precision. + private static void ValidateConversionToRgb( + JpegColorConverterBase converter, + JpegColorConverterBase baseline, + int length, + int componentCount, + int precision) + { + JpegColorConverterBase.ComponentValues expected = CreateRandomValues(length, componentCount, precision); + JpegColorConverterBase.ComponentValues actual = CreateRandomValues(length, componentCount, precision); + + baseline.ConvertToRgbInPlace(expected); + converter.ConvertToRgbInPlace(actual); + + // SIMD multiply-add instructions can differ from the scalar expression by the final rounding bit. + CompareSequenceWithTolerance(expected.Component0, actual.Component0, 0.0001F); + CompareSequenceWithTolerance(expected.Component1, actual.Component1, 0.0001F); + CompareSequenceWithTolerance(expected.Component2, actual.Component2, 0.0001F); + } + + /// + /// Compares RGB-to-component conversion with a scalar reference implementation. + /// + /// The shared converter under test. + /// The scalar reference converter. + /// The number of samples to convert. + /// The number of destination component planes. + /// The JPEG sample precision. + private static void ValidateConversionFromRgb( + JpegColorConverterBase converter, + JpegColorConverterBase baseline, + int length, + int componentCount, + int precision) + { + JpegColorConverterBase.ComponentValues expected = CreateRandomValues(length, componentCount, precision); + JpegColorConverterBase.ComponentValues actual = CreateRandomValues(length, componentCount, precision); + Random random = new(precision); + float[] rLane = CreateRandomValues(length, random); + float[] gLane = CreateRandomValues(length, random); + float[] bLane = CreateRandomValues(length, random); + + baseline.ConvertFromRgb(expected, rLane, gLane, bLane); + converter.ConvertFromRgb(actual, rLane, gLane, bLane); + + // The generic traversal must preserve every plane owned by the closed color model. + CompareSequenceWithTolerance(expected.Component0, actual.Component0, 2); + + if (componentCount >= 2) + { + CompareSequenceWithTolerance(expected.Component1, actual.Component1, 2); + } + + if (componentCount >= 3) + { + CompareSequenceWithTolerance(expected.Component2, actual.Component2, 2); + } + + if (componentCount >= 4) + { + CompareSequenceWithTolerance(expected.Component3, actual.Component3, 2); + } + } + private static void Validate( JpegColorSpace colorSpace, in JpegColorConverterBase.ComponentValues original,