Browse Source

Normalize JPEG color conversion SIMD

pull/3161/head
James Jackson-South 3 weeks ago
parent
commit
e9db3675e9
  1. 245
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykOperator.cs
  2. 203
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleOperator.cs
  3. 594
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.Operator.cs
  4. 184
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbOperator.cs
  5. 159
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykOperator.cs
  6. 187
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKOperator.cs
  7. 263
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrOperator.cs
  8. 135
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKOperator.cs
  9. 155
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.cs
  10. 1
      tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/ColorConversionBenchmark.cs
  11. 239
      tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/JpegColorConverterOperatorComparison.cs
  12. 325
      tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/JpegColorConverterTraversalAssembly.cs
  13. 244
      tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/YCbCrOperatorAssembly.cs
  14. 127
      tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/YCbCrOperatorComparison.cs
  15. 354
      tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs

245
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
{
/// <summary>
/// Implements inverted JPEG CMYK conversion for scalar and SIMD lanes.
/// </summary>
internal readonly struct CmykOperator : IJpegColorConverterOperator
{
/// <inheritdoc/>
public static JpegColorSpace ColorSpace => JpegColorSpace.Cmyk;
/// <inheritdoc/>
public static int ComponentCount => 4;
/// <inheritdoc/>
[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;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(
ref Vector128<float> c0,
ref Vector128<float> c1,
ref Vector128<float> c2,
Vector128<float> c3,
Vector128<float> maximumValue,
Vector128<float> halfValue,
Vector128<float> scale)
{
// Each K lane supplies the common modulation factor for the corresponding C, M, and Y lanes.
Vector128<float> scaledK = c3 * scale * scale;
c0 *= scaledK;
c1 *= scaledK;
c2 *= scaledK;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(
ref Vector256<float> c0,
ref Vector256<float> c1,
ref Vector256<float> c2,
Vector256<float> c3,
Vector256<float> maximumValue,
Vector256<float> halfValue,
Vector256<float> scale)
{
// Eight independent CMYK samples remain lane-aligned throughout the modulation.
Vector256<float> scaledK = c3 * scale * scale;
c0 *= scaledK;
c1 *= scaledK;
c2 *= scaledK;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(
ref Vector512<float> c0,
ref Vector512<float> c1,
ref Vector512<float> c2,
Vector512<float> c3,
Vector512<float> maximumValue,
Vector512<float> halfValue,
Vector512<float> scale)
{
// Sixteen independent CMYK samples remain lane-aligned throughout the modulation.
Vector512<float> scaledK = c3 * scale * scale;
c0 *= scaledK;
c1 *= scaledK;
c2 *= scaledK;
}
/// <inheritdoc/>
[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;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(
Vector128<float> r,
Vector128<float> g,
Vector128<float> b,
Vector128<float> maximumValue,
Vector128<float> halfValue,
Vector128<float> scale,
out Vector128<float> c0,
out Vector128<float> c1,
out Vector128<float> c2,
out Vector128<float> c3)
{
Vector128<float> c = maximumValue - r;
Vector128<float> m = maximumValue - g;
Vector128<float> y = maximumValue - b;
Vector128<float> 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<float> nonBlack = ~Vector128.Equals(k, maximumValue);
Vector128<float> reciprocal = Vector128<float>.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;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(
Vector256<float> r,
Vector256<float> g,
Vector256<float> b,
Vector256<float> maximumValue,
Vector256<float> halfValue,
Vector256<float> scale,
out Vector256<float> c0,
out Vector256<float> c1,
out Vector256<float> c2,
out Vector256<float> c3)
{
Vector256<float> c = maximumValue - r;
Vector256<float> m = maximumValue - g;
Vector256<float> y = maximumValue - b;
Vector256<float> k = Vector256.Min(c, Vector256.Min(m, y));
// Masking preserves lane independence when a vector mixes pure black with chromatic pixels.
Vector256<float> nonBlack = ~Vector256.Equals(k, maximumValue);
Vector256<float> reciprocal = Vector256<float>.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;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(
Vector512<float> r,
Vector512<float> g,
Vector512<float> b,
Vector512<float> maximumValue,
Vector512<float> halfValue,
Vector512<float> scale,
out Vector512<float> c0,
out Vector512<float> c1,
out Vector512<float> c2,
out Vector512<float> c3)
{
Vector512<float> c = maximumValue - r;
Vector512<float> m = maximumValue - g;
Vector512<float> y = maximumValue - b;
Vector512<float> 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<float> nonBlack = ~Vector512.Equals(k, maximumValue);
Vector512<float> reciprocal = Vector512<float>.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;
}
/// <inheritdoc/>
public static void ConvertToRgbInPlaceWithIcc(
Configuration configuration,
IccProfile profile,
in ComponentValues values,
float maximumValue)
=> CmykScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, maximumValue);
}
}

203
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
{
/// <summary>
/// Implements grayscale expansion and RGB luminance reduction for scalar and SIMD lanes.
/// </summary>
internal readonly struct GrayScaleOperator : IJpegColorConverterOperator
{
/// <inheritdoc/>
public static JpegColorSpace ColorSpace => JpegColorSpace.Grayscale;
/// <inheritdoc/>
public static int ComponentCount => 1;
/// <inheritdoc/>
[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;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(
ref Vector128<float> c0,
ref Vector128<float> c1,
ref Vector128<float> c2,
Vector128<float> c3,
Vector128<float> maximumValue,
Vector128<float> halfValue,
Vector128<float> 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<float> luminance = c0 * scale;
c0 = luminance;
c1 = luminance;
c2 = luminance;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(
ref Vector256<float> c0,
ref Vector256<float> c1,
ref Vector256<float> c2,
Vector256<float> c3,
Vector256<float> maximumValue,
Vector256<float> halfValue,
Vector256<float> 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<float> luminance = c0 * scale;
c0 = luminance;
c1 = luminance;
c2 = luminance;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(
ref Vector512<float> c0,
ref Vector512<float> c1,
ref Vector512<float> c2,
Vector512<float> c3,
Vector512<float> maximumValue,
Vector512<float> halfValue,
Vector512<float> 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<float> luminance = c0 * scale;
c0 = luminance;
c1 = luminance;
c2 = luminance;
}
/// <inheritdoc/>
[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;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(
Vector128<float> r,
Vector128<float> g,
Vector128<float> b,
Vector128<float> maximumValue,
Vector128<float> halfValue,
Vector128<float> scale,
out Vector128<float> c0,
out Vector128<float> c1,
out Vector128<float> c2,
out Vector128<float> 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;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(
Vector256<float> r,
Vector256<float> g,
Vector256<float> b,
Vector256<float> maximumValue,
Vector256<float> halfValue,
Vector256<float> scale,
out Vector256<float> c0,
out Vector256<float> c1,
out Vector256<float> c2,
out Vector256<float> 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;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(
Vector512<float> r,
Vector512<float> g,
Vector512<float> b,
Vector512<float> maximumValue,
Vector512<float> halfValue,
Vector512<float> scale,
out Vector512<float> c0,
out Vector512<float> c1,
out Vector512<float> c2,
out Vector512<float> 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;
}
/// <inheritdoc/>
public static void ConvertToRgbInPlaceWithIcc(
Configuration configuration,
IccProfile profile,
in ComponentValues values,
float maximumValue)
=> GrayScaleScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, maximumValue);
}
}

594
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
{
/// <summary>
/// Defines the color-model-specific arithmetic used by <see cref="JpegColorConverter{TOperator}"/>.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
internal interface IJpegColorConverterOperator
{
/// <summary>
/// Gets the JPEG color space handled by the operator.
/// </summary>
static abstract JpegColorSpace ColorSpace { get; }
/// <summary>
/// Gets the number of component planes used by the color space.
/// </summary>
static abstract int ComponentCount { get; }
/// <summary>
/// Converts one JPEG sample to normalized RGB.
/// </summary>
/// <param name="c0">The first component, replaced by red.</param>
/// <param name="c1">The second component, replaced by green.</param>
/// <param name="c2">The third component, replaced by blue.</param>
/// <param name="c3">The fourth component, or zero for a three-component color space.</param>
/// <param name="maximumValue">The maximum component value for the configured precision.</param>
/// <param name="halfValue">The midpoint component value for the configured precision.</param>
/// <param name="scale">The reciprocal of <paramref name="maximumValue"/>.</param>
static abstract void ConvertToRgb(
ref float c0,
ref float c1,
ref float c2,
float c3,
float maximumValue,
float halfValue,
float scale);
/// <summary>
/// Converts four JPEG samples to normalized RGB.
/// </summary>
/// <param name="c0">The first component lanes, replaced by red.</param>
/// <param name="c1">The second component lanes, replaced by green.</param>
/// <param name="c2">The third component lanes, replaced by blue.</param>
/// <param name="c3">The fourth component lanes, or zero for a three-component color space.</param>
/// <param name="maximumValue">The maximum component value for the configured precision.</param>
/// <param name="halfValue">The midpoint component value for the configured precision.</param>
/// <param name="scale">The reciprocal of <paramref name="maximumValue"/> in every lane.</param>
static abstract void ConvertToRgb(
ref Vector128<float> c0,
ref Vector128<float> c1,
ref Vector128<float> c2,
Vector128<float> c3,
Vector128<float> maximumValue,
Vector128<float> halfValue,
Vector128<float> scale);
/// <summary>
/// Converts eight JPEG samples to normalized RGB.
/// </summary>
/// <param name="c0">The first component lanes, replaced by red.</param>
/// <param name="c1">The second component lanes, replaced by green.</param>
/// <param name="c2">The third component lanes, replaced by blue.</param>
/// <param name="c3">The fourth component lanes, or zero for a three-component color space.</param>
/// <param name="maximumValue">The maximum component value for the configured precision.</param>
/// <param name="halfValue">The midpoint component value for the configured precision.</param>
/// <param name="scale">The reciprocal of <paramref name="maximumValue"/> in every lane.</param>
static abstract void ConvertToRgb(
ref Vector256<float> c0,
ref Vector256<float> c1,
ref Vector256<float> c2,
Vector256<float> c3,
Vector256<float> maximumValue,
Vector256<float> halfValue,
Vector256<float> scale);
/// <summary>
/// Converts sixteen JPEG samples to normalized RGB.
/// </summary>
/// <param name="c0">The first component lanes, replaced by red.</param>
/// <param name="c1">The second component lanes, replaced by green.</param>
/// <param name="c2">The third component lanes, replaced by blue.</param>
/// <param name="c3">The fourth component lanes, or zero for a three-component color space.</param>
/// <param name="maximumValue">The maximum component value for the configured precision.</param>
/// <param name="halfValue">The midpoint component value for the configured precision.</param>
/// <param name="scale">The reciprocal of <paramref name="maximumValue"/> in every lane.</param>
static abstract void ConvertToRgb(
ref Vector512<float> c0,
ref Vector512<float> c1,
ref Vector512<float> c2,
Vector512<float> c3,
Vector512<float> maximumValue,
Vector512<float> halfValue,
Vector512<float> scale);
/// <summary>
/// Converts one RGB sample to JPEG components.
/// </summary>
/// <param name="r">The red value.</param>
/// <param name="g">The green value.</param>
/// <param name="b">The blue value.</param>
/// <param name="maximumValue">The maximum component value for the configured precision.</param>
/// <param name="halfValue">The midpoint component value for the configured precision.</param>
/// <param name="scale">The reciprocal of <paramref name="maximumValue"/>.</param>
/// <param name="c0">The first converted component.</param>
/// <param name="c1">The second converted component.</param>
/// <param name="c2">The third converted component.</param>
/// <param name="c3">The fourth converted component, if used.</param>
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);
/// <summary>
/// Converts four RGB samples to JPEG components.
/// </summary>
/// <param name="r">The red lanes.</param>
/// <param name="g">The green lanes.</param>
/// <param name="b">The blue lanes.</param>
/// <param name="maximumValue">The maximum component value for the configured precision.</param>
/// <param name="halfValue">The midpoint component value for the configured precision.</param>
/// <param name="scale">The reciprocal of <paramref name="maximumValue"/> in every lane.</param>
/// <param name="c0">The first converted component lanes.</param>
/// <param name="c1">The second converted component lanes.</param>
/// <param name="c2">The third converted component lanes.</param>
/// <param name="c3">The fourth converted component lanes, if used.</param>
static abstract void ConvertFromRgb(
Vector128<float> r,
Vector128<float> g,
Vector128<float> b,
Vector128<float> maximumValue,
Vector128<float> halfValue,
Vector128<float> scale,
out Vector128<float> c0,
out Vector128<float> c1,
out Vector128<float> c2,
out Vector128<float> c3);
/// <summary>
/// Converts eight RGB samples to JPEG components.
/// </summary>
/// <param name="r">The red lanes.</param>
/// <param name="g">The green lanes.</param>
/// <param name="b">The blue lanes.</param>
/// <param name="maximumValue">The maximum component value for the configured precision.</param>
/// <param name="halfValue">The midpoint component value for the configured precision.</param>
/// <param name="scale">The reciprocal of <paramref name="maximumValue"/> in every lane.</param>
/// <param name="c0">The first converted component lanes.</param>
/// <param name="c1">The second converted component lanes.</param>
/// <param name="c2">The third converted component lanes.</param>
/// <param name="c3">The fourth converted component lanes, if used.</param>
static abstract void ConvertFromRgb(
Vector256<float> r,
Vector256<float> g,
Vector256<float> b,
Vector256<float> maximumValue,
Vector256<float> halfValue,
Vector256<float> scale,
out Vector256<float> c0,
out Vector256<float> c1,
out Vector256<float> c2,
out Vector256<float> c3);
/// <summary>
/// Converts sixteen RGB samples to JPEG components.
/// </summary>
/// <param name="r">The red lanes.</param>
/// <param name="g">The green lanes.</param>
/// <param name="b">The blue lanes.</param>
/// <param name="maximumValue">The maximum component value for the configured precision.</param>
/// <param name="halfValue">The midpoint component value for the configured precision.</param>
/// <param name="scale">The reciprocal of <paramref name="maximumValue"/> in every lane.</param>
/// <param name="c0">The first converted component lanes.</param>
/// <param name="c1">The second converted component lanes.</param>
/// <param name="c2">The third converted component lanes.</param>
/// <param name="c3">The fourth converted component lanes, if used.</param>
static abstract void ConvertFromRgb(
Vector512<float> r,
Vector512<float> g,
Vector512<float> b,
Vector512<float> maximumValue,
Vector512<float> halfValue,
Vector512<float> scale,
out Vector512<float> c0,
out Vector512<float> c1,
out Vector512<float> c2,
out Vector512<float> c3);
/// <summary>
/// Converts JPEG component values to RGB using the supplied ICC profile.
/// </summary>
/// <param name="configuration">The configuration used to allocate temporary storage.</param>
/// <param name="profile">The source ICC profile.</param>
/// <param name="values">The component values to convert.</param>
/// <param name="maximumValue">The maximum component value for the configured precision.</param>
static abstract void ConvertToRgbInPlaceWithIcc(
Configuration configuration,
IccProfile profile,
in ComponentValues values,
float maximumValue);
}
/// <summary>
/// Converts a JPEG color model using a single operator-driven traversal for all SIMD widths.
/// </summary>
/// <typeparam name="TOperator">The color-model-specific arithmetic.</typeparam>
internal sealed class JpegColorConverter<TOperator> : JpegColorConverterBase
where TOperator : struct, IJpegColorConverterOperator
{
/// <summary>
/// Initializes a new instance of the <see cref="JpegColorConverter{TOperator}"/> class.
/// </summary>
/// <param name="precision">The precision in bits.</param>
public JpegColorConverter(int precision)
: base(TOperator.ColorSpace, precision)
{
}
/// <inheritdoc/>
public override bool IsAvailable => true;
/// <inheritdoc/>
public override int ElementsPerBatch
=> Vector512.IsHardwareAccelerated
? Vector512<float>.Count
: Vector256.IsHardwareAccelerated
? Vector256<float>.Count
: Vector128.IsHardwareAccelerated
? Vector128<float>.Count
: 1;
/// <inheritdoc/>
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<float>.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<float> maximumValue = Vector512.Create(this.MaximumValue);
Vector512<float> halfValue = Vector512.Create(this.HalfValue);
Vector512<float> scaleVector = Vector512.Create(scale);
for (; i <= oneVectorFromEnd; i += Vector512<float>.Count)
{
ref Vector512<float> c0 = ref Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref c0Base, i));
ref Vector512<float> c1 = ref Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref c1Base, i));
ref Vector512<float> c2 = ref Unsafe.As<float, Vector512<float>>(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<float> c3 = TOperator.ComponentCount == 4
? Unsafe.As<float, Vector512<float>>(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<float>.Count;
if (i <= oneVectorFromEnd)
{
// YMM precision state is materialized only for an eight-sample remainder or an AVX2-only loop.
Vector256<float> maximumValue = Vector256.Create(this.MaximumValue);
Vector256<float> halfValue = Vector256.Create(this.HalfValue);
Vector256<float> scaleVector = Vector256.Create(scale);
for (; i <= oneVectorFromEnd; i += Vector256<float>.Count)
{
ref Vector256<float> c0 = ref Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref c0Base, i));
ref Vector256<float> c1 = ref Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref c1Base, i));
ref Vector256<float> c2 = ref Unsafe.As<float, Vector256<float>>(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<float> c3 = TOperator.ComponentCount == 4
? Unsafe.As<float, Vector256<float>>(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<float>.Count;
if (i <= oneVectorFromEnd)
{
// XMM state is likewise created only when four samples remain for this stage.
Vector128<float> maximumValue = Vector128.Create(this.MaximumValue);
Vector128<float> halfValue = Vector128.Create(this.HalfValue);
Vector128<float> scaleVector = Vector128.Create(scale);
for (; i <= oneVectorFromEnd; i += Vector128<float>.Count)
{
ref Vector128<float> c0 = ref Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref c0Base, i));
ref Vector128<float> c1 = ref Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref c1Base, i));
ref Vector128<float> c2 = ref Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref c2Base, i));
// As at the wider stages, the fourth vector is loaded only for CMYK-shaped operators.
Vector128<float> c3 = TOperator.ComponentCount == 4
? Unsafe.As<float, Vector128<float>>(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);
}
}
/// <inheritdoc/>
public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile)
=> TOperator.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue);
/// <inheritdoc/>
public override void ConvertFromRgb(in ComponentValues values, Span<float> rLane, Span<float> gLane, Span<float> 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<float>.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<float> maximumValue = Vector512.Create(this.MaximumValue);
Vector512<float> halfValue = Vector512.Create(this.HalfValue);
Vector512<float> scaleVector = Vector512.Create(scale);
for (; i <= oneVectorFromEnd; i += Vector512<float>.Count)
{
Vector512<float> r = Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref rBase, i));
Vector512<float> g = Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref gBase, i));
Vector512<float> b = Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref bBase, i));
TOperator.ConvertFromRgb(
r,
g,
b,
maximumValue,
halfValue,
scaleVector,
out Vector512<float> c0,
out Vector512<float> c1,
out Vector512<float> c2,
out Vector512<float> 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<float, Vector512<float>>(ref Unsafe.Add(ref c0Base, i)) = c0;
if (TOperator.ComponentCount >= 2)
{
Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref c1Base, i)) = c1;
}
if (TOperator.ComponentCount >= 3)
{
Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref c2Base, i)) = c2;
}
if (TOperator.ComponentCount >= 4)
{
Unsafe.As<float, Vector512<float>>(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<float>.Count;
if (i <= oneVectorFromEnd)
{
// Materialize YMM state only for an eight-sample remainder or an AVX2-only loop.
Vector256<float> maximumValue = Vector256.Create(this.MaximumValue);
Vector256<float> halfValue = Vector256.Create(this.HalfValue);
Vector256<float> scaleVector = Vector256.Create(scale);
for (; i <= oneVectorFromEnd; i += Vector256<float>.Count)
{
Vector256<float> r = Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref rBase, i));
Vector256<float> g = Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref gBase, i));
Vector256<float> b = Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref bBase, i));
TOperator.ConvertFromRgb(
r,
g,
b,
maximumValue,
halfValue,
scaleVector,
out Vector256<float> c0,
out Vector256<float> c1,
out Vector256<float> c2,
out Vector256<float> c3);
// Static count checks write only planes owned by this color model.
Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref c0Base, i)) = c0;
if (TOperator.ComponentCount >= 2)
{
Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref c1Base, i)) = c1;
}
if (TOperator.ComponentCount >= 3)
{
Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref c2Base, i)) = c2;
}
if (TOperator.ComponentCount >= 4)
{
Unsafe.As<float, Vector256<float>>(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<float>.Count;
if (i <= oneVectorFromEnd)
{
// Materialize XMM state only when the final SIMD stage can consume four samples.
Vector128<float> maximumValue = Vector128.Create(this.MaximumValue);
Vector128<float> halfValue = Vector128.Create(this.HalfValue);
Vector128<float> scaleVector = Vector128.Create(scale);
for (; i <= oneVectorFromEnd; i += Vector128<float>.Count)
{
Vector128<float> r = Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref rBase, i));
Vector128<float> g = Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref gBase, i));
Vector128<float> b = Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref bBase, i));
TOperator.ConvertFromRgb(
r,
g,
b,
maximumValue,
halfValue,
scaleVector,
out Vector128<float> c0,
out Vector128<float> c1,
out Vector128<float> c2,
out Vector128<float> c3);
// Four results are stored only for the planes represented by the closed operator.
Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref c0Base, i)) = c0;
if (TOperator.ComponentCount >= 2)
{
Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref c1Base, i)) = c1;
}
if (TOperator.ComponentCount >= 3)
{
Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref c2Base, i)) = c2;
}
if (TOperator.ComponentCount >= 4)
{
Unsafe.As<float, Vector128<float>>(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;
}
}
}
}
}

184
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
{
/// <summary>
/// Implements direct JPEG RGB normalization and planar RGB copying for scalar and SIMD lanes.
/// </summary>
internal readonly struct RgbOperator : IJpegColorConverterOperator
{
/// <inheritdoc/>
public static JpegColorSpace ColorSpace => JpegColorSpace.RGB;
/// <inheritdoc/>
public static int ComponentCount => 3;
/// <inheritdoc/>
[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;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(
ref Vector128<float> c0,
ref Vector128<float> c1,
ref Vector128<float> c2,
Vector128<float> c3,
Vector128<float> maximumValue,
Vector128<float> halfValue,
Vector128<float> scale)
{
// Four samples from each planar channel remain in their lanes while sharing one normalization vector.
c0 *= scale;
c1 *= scale;
c2 *= scale;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(
ref Vector256<float> c0,
ref Vector256<float> c1,
ref Vector256<float> c2,
Vector256<float> c3,
Vector256<float> maximumValue,
Vector256<float> halfValue,
Vector256<float> scale)
{
// Eight samples per plane are normalized independently without channel shuffles.
c0 *= scale;
c1 *= scale;
c2 *= scale;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(
ref Vector512<float> c0,
ref Vector512<float> c1,
ref Vector512<float> c2,
Vector512<float> c3,
Vector512<float> maximumValue,
Vector512<float> halfValue,
Vector512<float> scale)
{
// Sixteen samples per plane are normalized independently without changing planar ordering.
c0 *= scale;
c1 *= scale;
c2 *= scale;
}
/// <inheritdoc/>
[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;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(
Vector128<float> r,
Vector128<float> g,
Vector128<float> b,
Vector128<float> maximumValue,
Vector128<float> halfValue,
Vector128<float> scale,
out Vector128<float> c0,
out Vector128<float> c1,
out Vector128<float> c2,
out Vector128<float> 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;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(
Vector256<float> r,
Vector256<float> g,
Vector256<float> b,
Vector256<float> maximumValue,
Vector256<float> halfValue,
Vector256<float> scale,
out Vector256<float> c0,
out Vector256<float> c1,
out Vector256<float> c2,
out Vector256<float> 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;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(
Vector512<float> r,
Vector512<float> g,
Vector512<float> b,
Vector512<float> maximumValue,
Vector512<float> halfValue,
Vector512<float> scale,
out Vector512<float> c0,
out Vector512<float> c1,
out Vector512<float> c2,
out Vector512<float> c3)
{
// The widest path is likewise a register-to-register planar copy for sixteen pixels.
c0 = r;
c1 = g;
c2 = b;
c3 = default;
}
/// <inheritdoc/>
public static void ConvertToRgbInPlaceWithIcc(
Configuration configuration,
IccProfile profile,
in ComponentValues values,
float maximumValue)
=> RgbScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, maximumValue);
}
}

159
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
{
/// <summary>
/// Implements non-inverted TIFF JPEG CMYK conversion for scalar and SIMD lanes.
/// </summary>
internal readonly struct TiffCmykOperator : IJpegColorConverterOperator
{
/// <inheritdoc/>
public static JpegColorSpace ColorSpace => JpegColorSpace.TiffCmyk;
/// <inheritdoc/>
public static int ComponentCount => 4;
/// <inheritdoc/>
[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;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(ref Vector128<float> c0, ref Vector128<float> c1, ref Vector128<float> c2, Vector128<float> c3, Vector128<float> maximumValue, Vector128<float> halfValue, Vector128<float> scale)
{
// K remains lane-aligned with its C/M/Y sample while one-minus performs the non-inverted CMYK mapping.
Vector128<float> k = Vector128<float>.One - (c3 * scale);
c0 = (Vector128<float>.One - (c0 * scale)) * k;
c1 = (Vector128<float>.One - (c1 * scale)) * k;
c2 = (Vector128<float>.One - (c2 * scale)) * k;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(ref Vector256<float> c0, ref Vector256<float> c1, ref Vector256<float> c2, Vector256<float> c3, Vector256<float> maximumValue, Vector256<float> halfValue, Vector256<float> scale)
{
// Eight conventional CMYK samples convert independently without channel rearrangement.
Vector256<float> k = Vector256<float>.One - (c3 * scale);
c0 = (Vector256<float>.One - (c0 * scale)) * k;
c1 = (Vector256<float>.One - (c1 * scale)) * k;
c2 = (Vector256<float>.One - (c2 * scale)) * k;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(ref Vector512<float> c0, ref Vector512<float> c1, ref Vector512<float> c2, Vector512<float> c3, Vector512<float> maximumValue, Vector512<float> halfValue, Vector512<float> scale)
{
// Sixteen conventional CMYK samples convert independently without channel rearrangement.
Vector512<float> k = Vector512<float>.One - (c3 * scale);
c0 = (Vector512<float>.One - (c0 * scale)) * k;
c1 = (Vector512<float>.One - (c1 * scale)) * k;
c2 = (Vector512<float>.One - (c2 * scale)) * k;
}
/// <inheritdoc/>
[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;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(Vector128<float> r, Vector128<float> g, Vector128<float> b, Vector128<float> maximumValue, Vector128<float> halfValue, Vector128<float> scale, out Vector128<float> c0, out Vector128<float> c1, out Vector128<float> c2, out Vector128<float> c3)
{
Vector128<float> c = maximumValue - r;
Vector128<float> m = maximumValue - g;
Vector128<float> y = maximumValue - b;
Vector128<float> k = Vector128.Min(c, Vector128.Min(m, y));
// The all-bits mask clears the undefined zero-divisor result only in pure-black lanes.
Vector128<float> nonBlack = ~Vector128.Equals(k, maximumValue);
Vector128<float> reciprocal = Vector128<float>.One / (maximumValue - k);
c0 = (((c - k) * reciprocal) & nonBlack) * maximumValue;
c1 = (((m - k) * reciprocal) & nonBlack) * maximumValue;
c2 = (((y - k) * reciprocal) & nonBlack) * maximumValue;
c3 = k;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(Vector256<float> r, Vector256<float> g, Vector256<float> b, Vector256<float> maximumValue, Vector256<float> halfValue, Vector256<float> scale, out Vector256<float> c0, out Vector256<float> c1, out Vector256<float> c2, out Vector256<float> c3)
{
Vector256<float> c = maximumValue - r;
Vector256<float> m = maximumValue - g;
Vector256<float> y = maximumValue - b;
Vector256<float> k = Vector256.Min(c, Vector256.Min(m, y));
// Eight lanes independently clear the pure-black singularity before returning conventional CMYK.
Vector256<float> nonBlack = ~Vector256.Equals(k, maximumValue);
Vector256<float> reciprocal = Vector256<float>.One / (maximumValue - k);
c0 = (((c - k) * reciprocal) & nonBlack) * maximumValue;
c1 = (((m - k) * reciprocal) & nonBlack) * maximumValue;
c2 = (((y - k) * reciprocal) & nonBlack) * maximumValue;
c3 = k;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(Vector512<float> r, Vector512<float> g, Vector512<float> b, Vector512<float> maximumValue, Vector512<float> halfValue, Vector512<float> scale, out Vector512<float> c0, out Vector512<float> c1, out Vector512<float> c2, out Vector512<float> c3)
{
Vector512<float> c = maximumValue - r;
Vector512<float> m = maximumValue - g;
Vector512<float> y = maximumValue - b;
Vector512<float> k = Vector512.Min(c, Vector512.Min(m, y));
// Sixteen lanes retain the same branchless singularity handling and component layout.
Vector512<float> nonBlack = ~Vector512.Equals(k, maximumValue);
Vector512<float> reciprocal = Vector512<float>.One / (maximumValue - k);
c0 = (((c - k) * reciprocal) & nonBlack) * maximumValue;
c1 = (((m - k) * reciprocal) & nonBlack) * maximumValue;
c2 = (((y - k) * reciprocal) & nonBlack) * maximumValue;
c3 = k;
}
/// <inheritdoc/>
public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maximumValue)
=> TiffCmykScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, maximumValue);
}
}

187
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
{
/// <summary>
/// Implements non-inverted TIFF JPEG YccK conversion for scalar and SIMD lanes.
/// </summary>
internal readonly struct TiffYccKOperator : IJpegColorConverterOperator
{
private const float SourceScale = 1F / 255F;
/// <inheritdoc/>
public static JpegColorSpace ColorSpace => JpegColorSpace.TiffYccK;
/// <inheritdoc/>
public static int ComponentCount => 4;
/// <inheritdoc/>
[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;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(ref Vector128<float> c0, ref Vector128<float> c1, ref Vector128<float> c2, Vector128<float> c3, Vector128<float> maximumValue, Vector128<float> halfValue, Vector128<float> scale)
{
Vector128<float> y = c0 * scale;
Vector128<float> cb = (c1 - halfValue) * scale;
Vector128<float> cr = (c2 - halfValue) * scale;
Vector128<float> k = Vector128<float>.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;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(ref Vector256<float> c0, ref Vector256<float> c1, ref Vector256<float> c2, Vector256<float> c3, Vector256<float> maximumValue, Vector256<float> halfValue, Vector256<float> scale)
{
Vector256<float> y = c0 * scale;
Vector256<float> cb = (c1 - halfValue) * scale;
Vector256<float> cr = (c2 - halfValue) * scale;
Vector256<float> k = Vector256<float>.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;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(ref Vector512<float> c0, ref Vector512<float> c1, ref Vector512<float> c2, Vector512<float> c3, Vector512<float> maximumValue, Vector512<float> halfValue, Vector512<float> scale)
{
Vector512<float> y = c0 * scale;
Vector512<float> cb = (c1 - halfValue) * scale;
Vector512<float> cr = (c2 - halfValue) * scale;
Vector512<float> k = Vector512<float>.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;
}
/// <inheritdoc/>
[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;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(Vector128<float> r, Vector128<float> g, Vector128<float> b, Vector128<float> maximumValue, Vector128<float> halfValue, Vector128<float> scale, out Vector128<float> c0, out Vector128<float> c1, out Vector128<float> c2, out Vector128<float> c3)
{
Vector128<float> sourceScale = Vector128.Create(SourceScale);
r *= sourceScale;
g *= sourceScale;
b *= sourceScale;
Vector128<float> k = Vector128<float>.One - Vector128.Max(r, Vector128.Max(g, b));
// The mask assigns no chromatic direction to pure-black lanes while preserving neighboring pixels.
Vector128<float> nonBlack = ~Vector128.Equals(k, Vector128<float>.One);
Vector128<float> divisor = Vector128<float>.One / (Vector128<float>.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;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(Vector256<float> r, Vector256<float> g, Vector256<float> b, Vector256<float> maximumValue, Vector256<float> halfValue, Vector256<float> scale, out Vector256<float> c0, out Vector256<float> c1, out Vector256<float> c2, out Vector256<float> c3)
{
Vector256<float> sourceScale = Vector256.Create(SourceScale);
r *= sourceScale;
g *= sourceScale;
b *= sourceScale;
Vector256<float> k = Vector256<float>.One - Vector256.Max(r, Vector256.Max(g, b));
// Eight lanes normalize chromatic direction independently and retain neutral chroma for black.
Vector256<float> nonBlack = ~Vector256.Equals(k, Vector256<float>.One);
Vector256<float> divisor = Vector256<float>.One / (Vector256<float>.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;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(Vector512<float> r, Vector512<float> g, Vector512<float> b, Vector512<float> maximumValue, Vector512<float> halfValue, Vector512<float> scale, out Vector512<float> c0, out Vector512<float> c1, out Vector512<float> c2, out Vector512<float> c3)
{
Vector512<float> sourceScale = Vector512.Create(SourceScale);
r *= sourceScale;
g *= sourceScale;
b *= sourceScale;
Vector512<float> k = Vector512<float>.One - Vector512.Max(r, Vector512.Max(g, b));
// Sixteen lanes normalize chromatic direction independently and retain neutral chroma for black.
Vector512<float> nonBlack = ~Vector512.Equals(k, Vector512<float>.One);
Vector512<float> divisor = Vector512<float>.One / (Vector512<float>.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;
}
/// <inheritdoc/>
public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maximumValue)
=> TiffYccKScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, maximumValue);
}
}

263
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
{
/// <summary>
/// Implements the JPEG YCbCr conversion formula for scalar and SIMD lanes.
/// </summary>
internal readonly struct YCbCrOperator : IJpegColorConverterOperator
{
/// <inheritdoc/>
public static JpegColorSpace ColorSpace => JpegColorSpace.YCbCr;
/// <inheritdoc/>
public static int ComponentCount => 3;
/// <inheritdoc/>
[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;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(
ref Vector128<float> c0,
ref Vector128<float> c1,
ref Vector128<float> c2,
Vector128<float> c3,
Vector128<float> maximumValue,
Vector128<float> halfValue,
Vector128<float> scale)
{
Vector128<float> y = c0;
Vector128<float> cb = c1 - halfValue;
Vector128<float> 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<float> r = Vector128_.MultiplyAddEstimate(cr, Vector128.Create(YCbCrScalar.RCrMult), y);
Vector128<float> g = Vector128_.MultiplyAddEstimate(
cr,
Vector128.Create(-YCbCrScalar.GCrMult),
Vector128_.MultiplyAddEstimate(cb, Vector128.Create(-YCbCrScalar.GCbMult), y));
Vector128<float> b = Vector128_.MultiplyAddEstimate(cb, Vector128.Create(YCbCrScalar.BCbMult), y);
c0 = Vector128_.RoundToNearestInteger(r) * scale;
c1 = Vector128_.RoundToNearestInteger(g) * scale;
c2 = Vector128_.RoundToNearestInteger(b) * scale;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(
ref Vector256<float> c0,
ref Vector256<float> c1,
ref Vector256<float> c2,
Vector256<float> c3,
Vector256<float> maximumValue,
Vector256<float> halfValue,
Vector256<float> scale)
{
Vector256<float> y = c0;
Vector256<float> cb = c1 - halfValue;
Vector256<float> 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<float> r = Vector256_.MultiplyAddEstimate(cr, Vector256.Create(YCbCrScalar.RCrMult), y);
Vector256<float> g = Vector256_.MultiplyAddEstimate(
cr,
Vector256.Create(-YCbCrScalar.GCrMult),
Vector256_.MultiplyAddEstimate(cb, Vector256.Create(-YCbCrScalar.GCbMult), y));
Vector256<float> b = Vector256_.MultiplyAddEstimate(cb, Vector256.Create(YCbCrScalar.BCbMult), y);
c0 = Vector256_.RoundToNearestInteger(r) * scale;
c1 = Vector256_.RoundToNearestInteger(g) * scale;
c2 = Vector256_.RoundToNearestInteger(b) * scale;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(
ref Vector512<float> c0,
ref Vector512<float> c1,
ref Vector512<float> c2,
Vector512<float> c3,
Vector512<float> maximumValue,
Vector512<float> halfValue,
Vector512<float> scale)
{
Vector512<float> y = c0;
Vector512<float> cb = c1 - halfValue;
Vector512<float> 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<float> r = Vector512_.MultiplyAddEstimate(cr, Vector512.Create(YCbCrScalar.RCrMult), y);
Vector512<float> g = Vector512_.MultiplyAddEstimate(
cr,
Vector512.Create(-YCbCrScalar.GCrMult),
Vector512_.MultiplyAddEstimate(cb, Vector512.Create(-YCbCrScalar.GCbMult), y));
Vector512<float> b = Vector512_.MultiplyAddEstimate(cb, Vector512.Create(YCbCrScalar.BCbMult), y);
c0 = Vector512_.RoundToNearestInteger(r) * scale;
c1 = Vector512_.RoundToNearestInteger(g) * scale;
c2 = Vector512_.RoundToNearestInteger(b) * scale;
}
/// <inheritdoc/>
[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;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(
Vector128<float> r,
Vector128<float> g,
Vector128<float> b,
Vector128<float> maximumValue,
Vector128<float> halfValue,
Vector128<float> scale,
out Vector128<float> c0,
out Vector128<float> c1,
out Vector128<float> c2,
out Vector128<float> 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;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(
Vector256<float> r,
Vector256<float> g,
Vector256<float> b,
Vector256<float> maximumValue,
Vector256<float> halfValue,
Vector256<float> scale,
out Vector256<float> c0,
out Vector256<float> c1,
out Vector256<float> c2,
out Vector256<float> 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;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(
Vector512<float> r,
Vector512<float> g,
Vector512<float> b,
Vector512<float> maximumValue,
Vector512<float> halfValue,
Vector512<float> scale,
out Vector512<float> c0,
out Vector512<float> c1,
out Vector512<float> c2,
out Vector512<float> 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;
}
/// <inheritdoc/>
public static void ConvertToRgbInPlaceWithIcc(
Configuration configuration,
IccProfile profile,
in ComponentValues values,
float maximumValue)
=> YCbCrScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, maximumValue);
}
}

135
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
{
/// <summary>
/// Implements inverted JPEG YccK conversion for scalar and SIMD lanes.
/// </summary>
internal readonly struct YccKOperator : IJpegColorConverterOperator
{
/// <inheritdoc/>
public static JpegColorSpace ColorSpace => JpegColorSpace.Ycck;
/// <inheritdoc/>
public static int ComponentCount => 4;
/// <inheritdoc/>
[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;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(ref Vector128<float> c0, ref Vector128<float> c1, ref Vector128<float> c2, Vector128<float> c3, Vector128<float> maximumValue, Vector128<float> halfValue, Vector128<float> scale)
{
Vector128<float> y = c0;
Vector128<float> cb = c1 - halfValue;
Vector128<float> cr = c2 - halfValue;
Vector128<float> scaledK = c3 * scale * scale;
// Four lanes reconstruct YCbCr concurrently; each rounded result is inverted and modulated by its K lane.
Vector128<float> r = Vector128_.MultiplyAddEstimate(cr, Vector128.Create(YCbCrScalar.RCrMult), y);
Vector128<float> g = Vector128_.MultiplyAddEstimate(cr, Vector128.Create(-YCbCrScalar.GCrMult), Vector128_.MultiplyAddEstimate(cb, Vector128.Create(-YCbCrScalar.GCbMult), y));
Vector128<float> 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;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(ref Vector256<float> c0, ref Vector256<float> c1, ref Vector256<float> c2, Vector256<float> c3, Vector256<float> maximumValue, Vector256<float> halfValue, Vector256<float> scale)
{
Vector256<float> y = c0;
Vector256<float> cb = c1 - halfValue;
Vector256<float> cr = c2 - halfValue;
Vector256<float> scaledK = c3 * scale * scale;
// Eight lanes retain planar alignment from Y/Cb/Cr/K through normalized RGB.
Vector256<float> r = Vector256_.MultiplyAddEstimate(cr, Vector256.Create(YCbCrScalar.RCrMult), y);
Vector256<float> g = Vector256_.MultiplyAddEstimate(cr, Vector256.Create(-YCbCrScalar.GCrMult), Vector256_.MultiplyAddEstimate(cb, Vector256.Create(-YCbCrScalar.GCbMult), y));
Vector256<float> 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;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(ref Vector512<float> c0, ref Vector512<float> c1, ref Vector512<float> c2, Vector512<float> c3, Vector512<float> maximumValue, Vector512<float> halfValue, Vector512<float> scale)
{
Vector512<float> y = c0;
Vector512<float> cb = c1 - halfValue;
Vector512<float> cr = c2 - halfValue;
Vector512<float> scaledK = c3 * scale * scale;
// Sixteen lanes use the same matrix, rounding, inversion, and K modulation order as scalar code.
Vector512<float> r = Vector512_.MultiplyAddEstimate(cr, Vector512.Create(YCbCrScalar.RCrMult), y);
Vector512<float> g = Vector512_.MultiplyAddEstimate(cr, Vector512.Create(-YCbCrScalar.GCrMult), Vector512_.MultiplyAddEstimate(cb, Vector512.Create(-YCbCrScalar.GCbMult), y));
Vector512<float> 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;
}
/// <inheritdoc/>
[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 _);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(Vector128<float> r, Vector128<float> g, Vector128<float> b, Vector128<float> maximumValue, Vector128<float> halfValue, Vector128<float> scale, out Vector128<float> c0, out Vector128<float> c1, out Vector128<float> c2, out Vector128<float> 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<float> c, out Vector128<float> m, out Vector128<float> y, out c3);
YCbCrOperator.ConvertFromRgb(maximumValue - c, maximumValue - m, maximumValue - y, maximumValue, halfValue, scale, out c0, out c1, out c2, out _);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(Vector256<float> r, Vector256<float> g, Vector256<float> b, Vector256<float> maximumValue, Vector256<float> halfValue, Vector256<float> scale, out Vector256<float> c0, out Vector256<float> c1, out Vector256<float> c2, out Vector256<float> c3)
{
// Eight pixels flow through CMYK extraction and YCbCr projection entirely in YMM registers.
CmykOperator.ConvertFromRgb(r, g, b, maximumValue, halfValue, scale, out Vector256<float> c, out Vector256<float> m, out Vector256<float> y, out c3);
YCbCrOperator.ConvertFromRgb(maximumValue - c, maximumValue - m, maximumValue - y, maximumValue, halfValue, scale, out c0, out c1, out c2, out _);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(Vector512<float> r, Vector512<float> g, Vector512<float> b, Vector512<float> maximumValue, Vector512<float> halfValue, Vector512<float> scale, out Vector512<float> c0, out Vector512<float> c1, out Vector512<float> c2, out Vector512<float> c3)
{
// Sixteen pixels flow through both mathematical stages in registers without materializing intermediate planes.
CmykOperator.ConvertFromRgb(r, g, b, maximumValue, halfValue, scale, out Vector512<float> c, out Vector512<float> m, out Vector512<float> y, out c3);
YCbCrOperator.ConvertFromRgb(maximumValue - c, maximumValue - m, maximumValue - y, maximumValue, halfValue, scale, out c0, out c1, out c2, out _);
}
/// <inheritdoc/>
public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maximumValue)
=> YccKScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, maximumValue);
}
}

155
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.cs

@ -248,161 +248,50 @@ internal abstract partial class JpegColorConverterBase
/// Returns the <see cref="JpegColorConverterBase"/>s for the YCbCr colorspace. /// Returns the <see cref="JpegColorConverterBase"/>s for the YCbCr colorspace.
/// </summary> /// </summary>
/// <param name="precision">The precision in bits.</param> /// <param name="precision">The precision in bits.</param>
private static JpegColorConverterBase GetYCbCrConverter(int precision) private static JpegColorConverter<YCbCrOperator> GetYCbCrConverter(int precision)
{ => new JpegColorConverter<YCbCrOperator>(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);
}
/// <summary> /// <summary>
/// Returns the <see cref="JpegColorConverterBase"/>s for the YccK colorspace. /// Returns the <see cref="JpegColorConverterBase"/>s for the YccK colorspace.
/// </summary> /// </summary>
/// <param name="precision">The precision in bits.</param> /// <param name="precision">The precision in bits.</param>
private static JpegColorConverterBase GetYccKConverter(int precision) private static JpegColorConverter<YccKOperator> GetYccKConverter(int precision)
{ => new JpegColorConverter<YccKOperator>(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);
}
/// <summary> /// <summary>
/// Returns the <see cref="JpegColorConverterBase"/>s for the CMYK colorspace. /// Returns the <see cref="JpegColorConverterBase"/>s for the CMYK colorspace.
/// </summary> /// </summary>
/// <param name="precision">The precision in bits.</param> /// <param name="precision">The precision in bits.</param>
private static JpegColorConverterBase GetCmykConverter(int precision) private static JpegColorConverter<CmykOperator> GetCmykConverter(int precision)
{ => new JpegColorConverter<CmykOperator>(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);
}
/// <summary> /// <summary>
/// Returns the <see cref="JpegColorConverterBase"/>s for the gray scale colorspace. /// Returns the <see cref="JpegColorConverterBase"/>s for the gray scale colorspace.
/// </summary> /// </summary>
/// <param name="precision">The precision in bits.</param> /// <param name="precision">The precision in bits.</param>
private static JpegColorConverterBase GetGrayScaleConverter(int precision) private static JpegColorConverter<GrayScaleOperator> GetGrayScaleConverter(int precision)
{ => new JpegColorConverter<GrayScaleOperator>(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);
}
/// <summary> /// <summary>
/// Returns the <see cref="JpegColorConverterBase"/>s for the RGB colorspace. /// Returns the <see cref="JpegColorConverterBase"/>s for the RGB colorspace.
/// </summary> /// </summary>
/// <param name="precision">The precision in bits.</param> /// <param name="precision">The precision in bits.</param>
private static JpegColorConverterBase GetRgbConverter(int precision) private static JpegColorConverter<RgbOperator> GetRgbConverter(int precision)
{ => new JpegColorConverter<RgbOperator>(precision);
if (JpegColorConverterVector512.IsSupported)
{
return new RgbVector512(precision);
}
if (JpegColorConverterVector256.IsSupported)
{
return new RgbVector256(precision);
}
if (JpegColorConverterVector128.IsSupported)
{
return new RgbVector128(precision);
}
return new RgbScalar(precision); /// <summary>
} /// Returns the <see cref="JpegColorConverterBase"/> for non-inverted TIFF CMYK.
/// </summary>
private static JpegColorConverterBase GetTiffCmykConverter(int precision) /// <param name="precision">The precision in bits.</param>
{ private static JpegColorConverter<TiffCmykOperator> GetTiffCmykConverter(int precision)
if (JpegColorConverterVector512.IsSupported) => new JpegColorConverter<TiffCmykOperator>(precision);
{
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);
}
return new TiffYccKScalar(precision); /// <summary>
} /// Returns the <see cref="JpegColorConverterBase"/> for non-inverted TIFF YccK.
/// </summary>
/// <param name="precision">The precision in bits.</param>
private static JpegColorConverter<TiffYccKOperator> GetTiffYccKConverter(int precision)
=> new JpegColorConverter<TiffYccKOperator>(precision);
/// <summary> /// <summary>
/// A stack-only struct to reference the input buffers using <see cref="ReadOnlySpan{T}"/>-s. /// A stack-only struct to reference the input buffers using <see cref="ReadOnlySpan{T}"/>-s.

1
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 // no need to dispose when buffer is not array owner
buffers[i] = Configuration.Default.MemoryAllocator.Allocate2D<float>(values.Length, 1); buffers[i] = Configuration.Default.MemoryAllocator.Allocate2D<float>(values.Length, 1);
values.CopyTo(buffers[i].DangerousGetRowSpan(0));
} }
return buffers; return buffers;

239
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;
/// <summary>
/// Compares each shared operator converter with the Vector512 converter it replaces.
/// </summary>
[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;
/// <summary>
/// Gets or sets the color model measured by the current benchmark case.
/// </summary>
[Params(
JpegColorModel.Grayscale,
JpegColorModel.Rgb,
JpegColorModel.Cmyk,
JpegColorModel.YCbCr,
JpegColorModel.YccK,
JpegColorModel.TiffCmyk,
JpegColorModel.TiffYccK)]
public JpegColorModel ColorModel { get; set; }
/// <summary>
/// Gets or sets the number of pixels converted by each invocation.
/// </summary>
[Params(128, 1024)]
public int Count { get; set; }
/// <summary>
/// Creates equivalent legacy and operator converters and their independent component buffers.
/// </summary>
[GlobalSetup]
public void Setup()
{
(JpegColorConverterBase Legacy, JpegColorConverterBase Operator, int ComponentCount) converters =
this.ColorModel switch
{
JpegColorModel.Grayscale => (
new JpegColorConverterBase.GrayScaleVector512(8),
new JpegColorConverterBase.JpegColorConverter<JpegColorConverterBase.GrayScaleOperator>(8),
1),
JpegColorModel.Rgb => (
new JpegColorConverterBase.RgbVector512(8),
new JpegColorConverterBase.JpegColorConverter<JpegColorConverterBase.RgbOperator>(8),
3),
JpegColorModel.Cmyk => (
new JpegColorConverterBase.CmykVector512(8),
new JpegColorConverterBase.JpegColorConverter<JpegColorConverterBase.CmykOperator>(8),
4),
JpegColorModel.YCbCr => (
new JpegColorConverterBase.YCbCrVector512(8),
new JpegColorConverterBase.JpegColorConverter<JpegColorConverterBase.YCbCrOperator>(8),
3),
JpegColorModel.YccK => (
new JpegColorConverterBase.YccKVector512(8),
new JpegColorConverterBase.JpegColorConverter<JpegColorConverterBase.YccKOperator>(8),
4),
JpegColorModel.TiffCmyk => (
new JpegColorConverterBase.TiffCmykVector512(8),
new JpegColorConverterBase.JpegColorConverter<JpegColorConverterBase.TiffCmykOperator>(8),
4),
JpegColorModel.TiffYccK => (
new JpegColorConverterBase.TiffYccKVector512(8),
new JpegColorConverterBase.JpegColorConverter<JpegColorConverterBase.TiffYccKOperator>(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);
}
/// <summary>
/// Converts JPEG components to RGB using the replaced Vector512 implementation.
/// </summary>
[Benchmark(Baseline = true)]
[BenchmarkCategory("ToRgb")]
public void LegacyToRgb()
{
JpegColorConverterBase.ComponentValues values = this.CreateLegacyValues();
this.legacy.ConvertToRgbInPlace(values);
}
/// <summary>
/// Converts JPEG components to RGB using the shared operator traversal.
/// </summary>
[Benchmark]
[BenchmarkCategory("ToRgb")]
public void OperatorToRgb()
{
JpegColorConverterBase.ComponentValues values = this.CreateOperatorValues();
this.operatorConverter.ConvertToRgbInPlace(values);
}
/// <summary>
/// Converts RGB to JPEG components using the replaced Vector512 implementation.
/// </summary>
[Benchmark(Baseline = true)]
[BenchmarkCategory("FromRgb")]
public void LegacyFromRgb()
{
JpegColorConverterBase.ComponentValues values = this.CreateLegacyValues();
this.legacy.ConvertFromRgb(values, this.r, this.g, this.b);
}
/// <summary>
/// Converts RGB to JPEG components using the shared operator traversal.
/// </summary>
[Benchmark]
[BenchmarkCategory("FromRgb")]
public void OperatorFromRgb()
{
JpegColorConverterBase.ComponentValues values = this.CreateOperatorValues();
this.operatorConverter.ConvertFromRgb(values, this.r, this.g, this.b);
}
/// <summary>
/// Creates a component view over the buffers owned by the legacy converter.
/// </summary>
/// <returns>The component view for the configured color model.</returns>
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 : []);
/// <summary>
/// Creates a component view over the buffers owned by the operator converter.
/// </summary>
/// <returns>The component view for the configured color model.</returns>
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 : []);
/// <summary>
/// Creates deterministic sample-domain values for one component plane.
/// </summary>
/// <param name="length">The number of samples to create.</param>
/// <param name="random">The deterministic random source shared by setup.</param>
/// <returns>The populated component plane.</returns>
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;
}
/// <summary>
/// Identifies the JPEG color model used by a benchmark case.
/// </summary>
public enum JpegColorModel
{
/// <summary>
/// One luminance component.
/// </summary>
Grayscale,
/// <summary>
/// Three direct RGB components.
/// </summary>
Rgb,
/// <summary>
/// Four inverted Adobe CMYK components.
/// </summary>
Cmyk,
/// <summary>
/// Three JPEG YCbCr components.
/// </summary>
YCbCr,
/// <summary>
/// Four inverted Adobe YCCK components.
/// </summary>
YccK,
/// <summary>
/// Four non-inverted TIFF CMYK components.
/// </summary>
TiffCmyk,
/// <summary>
/// Four non-inverted TIFF YCCK components.
/// </summary>
TiffYccK,
}
}

325
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;
/// <summary>
/// Exposes every closed JPEG operator traversal beside the Vector512 implementation it replaces.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[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<JpegColorConverterBase.GrayScaleOperator> grayscaleOperator = new(8);
private readonly JpegColorConverterBase.RgbVector512 rgbLegacy = new(8);
private readonly JpegColorConverterBase.JpegColorConverter<JpegColorConverterBase.RgbOperator> rgbOperator = new(8);
private readonly JpegColorConverterBase.CmykVector512 cmykLegacy = new(8);
private readonly JpegColorConverterBase.JpegColorConverter<JpegColorConverterBase.CmykOperator> cmykOperator = new(8);
private readonly JpegColorConverterBase.YCbCrVector512 yCbCrLegacy = new(8);
private readonly JpegColorConverterBase.JpegColorConverter<JpegColorConverterBase.YCbCrOperator> yCbCrOperator = new(8);
private readonly JpegColorConverterBase.YccKVector512 yccKLegacy = new(8);
private readonly JpegColorConverterBase.JpegColorConverter<JpegColorConverterBase.YccKOperator> yccKOperator = new(8);
private readonly JpegColorConverterBase.TiffCmykVector512 tiffCmykLegacy = new(8);
private readonly JpegColorConverterBase.JpegColorConverter<JpegColorConverterBase.TiffCmykOperator> tiffCmykOperator = new(8);
private readonly JpegColorConverterBase.TiffYccKVector512 tiffYccKLegacy = new(8);
private readonly JpegColorConverterBase.JpegColorConverter<JpegColorConverterBase.TiffYccKOperator> 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];
/// <summary>
/// Populates the component and RGB planes with deterministic sample-domain values.
/// </summary>
[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;
}
}
/// <summary>
/// Runs the replaced grayscale component-to-RGB traversal.
/// </summary>
[Benchmark(Baseline = true)]
[BenchmarkCategory("Grayscale.ToRgb")]
public void GrayscaleLegacyToRgb()
=> this.grayscaleLegacy.ConvertToRgbInPlace(this.CreateLegacyValues(1));
/// <summary>
/// Runs the shared grayscale component-to-RGB traversal.
/// </summary>
[Benchmark]
[BenchmarkCategory("Grayscale.ToRgb")]
public void GrayscaleOperatorToRgb()
=> this.grayscaleOperator.ConvertToRgbInPlace(this.CreateOperatorValues(1));
/// <summary>
/// Runs the replaced grayscale RGB-to-component traversal.
/// </summary>
[Benchmark(Baseline = true)]
[BenchmarkCategory("Grayscale.FromRgb")]
public void GrayscaleLegacyFromRgb()
=> this.grayscaleLegacy.ConvertFromRgb(this.CreateLegacyValues(1), this.r, this.g, this.b);
/// <summary>
/// Runs the shared grayscale RGB-to-component traversal.
/// </summary>
[Benchmark]
[BenchmarkCategory("Grayscale.FromRgb")]
public void GrayscaleOperatorFromRgb()
=> this.grayscaleOperator.ConvertFromRgb(this.CreateOperatorValues(1), this.r, this.g, this.b);
/// <summary>
/// Runs the replaced RGB component-to-RGB traversal.
/// </summary>
[Benchmark(Baseline = true)]
[BenchmarkCategory("Rgb.ToRgb")]
public void RgbLegacyToRgb()
=> this.rgbLegacy.ConvertToRgbInPlace(this.CreateLegacyValues(3));
/// <summary>
/// Runs the shared RGB component-to-RGB traversal.
/// </summary>
[Benchmark]
[BenchmarkCategory("Rgb.ToRgb")]
public void RgbOperatorToRgb()
=> this.rgbOperator.ConvertToRgbInPlace(this.CreateOperatorValues(3));
/// <summary>
/// Runs the replaced RGB RGB-to-component traversal.
/// </summary>
[Benchmark(Baseline = true)]
[BenchmarkCategory("Rgb.FromRgb")]
public void RgbLegacyFromRgb()
=> this.rgbLegacy.ConvertFromRgb(this.CreateLegacyValues(3), this.r, this.g, this.b);
/// <summary>
/// Runs the shared RGB RGB-to-component traversal.
/// </summary>
[Benchmark]
[BenchmarkCategory("Rgb.FromRgb")]
public void RgbOperatorFromRgb()
=> this.rgbOperator.ConvertFromRgb(this.CreateOperatorValues(3), this.r, this.g, this.b);
/// <summary>
/// Runs the replaced CMYK component-to-RGB traversal.
/// </summary>
[Benchmark(Baseline = true)]
[BenchmarkCategory("Cmyk.ToRgb")]
public void CmykLegacyToRgb()
=> this.cmykLegacy.ConvertToRgbInPlace(this.CreateLegacyValues(4));
/// <summary>
/// Runs the shared CMYK component-to-RGB traversal.
/// </summary>
[Benchmark]
[BenchmarkCategory("Cmyk.ToRgb")]
public void CmykOperatorToRgb()
=> this.cmykOperator.ConvertToRgbInPlace(this.CreateOperatorValues(4));
/// <summary>
/// Runs the replaced CMYK RGB-to-component traversal.
/// </summary>
[Benchmark(Baseline = true)]
[BenchmarkCategory("Cmyk.FromRgb")]
public void CmykLegacyFromRgb()
=> this.cmykLegacy.ConvertFromRgb(this.CreateLegacyValues(4), this.r, this.g, this.b);
/// <summary>
/// Runs the shared CMYK RGB-to-component traversal.
/// </summary>
[Benchmark]
[BenchmarkCategory("Cmyk.FromRgb")]
public void CmykOperatorFromRgb()
=> this.cmykOperator.ConvertFromRgb(this.CreateOperatorValues(4), this.r, this.g, this.b);
/// <summary>
/// Runs the replaced YCbCr component-to-RGB traversal.
/// </summary>
[Benchmark(Baseline = true)]
[BenchmarkCategory("YCbCr.ToRgb")]
public void YCbCrLegacyToRgb()
=> this.yCbCrLegacy.ConvertToRgbInPlace(this.CreateLegacyValues(3));
/// <summary>
/// Runs the shared YCbCr component-to-RGB traversal.
/// </summary>
[Benchmark]
[BenchmarkCategory("YCbCr.ToRgb")]
public void YCbCrOperatorToRgb()
=> this.yCbCrOperator.ConvertToRgbInPlace(this.CreateOperatorValues(3));
/// <summary>
/// Runs the replaced YCbCr RGB-to-component traversal.
/// </summary>
[Benchmark(Baseline = true)]
[BenchmarkCategory("YCbCr.FromRgb")]
public void YCbCrLegacyFromRgb()
=> this.yCbCrLegacy.ConvertFromRgb(this.CreateLegacyValues(3), this.r, this.g, this.b);
/// <summary>
/// Runs the shared YCbCr RGB-to-component traversal.
/// </summary>
[Benchmark]
[BenchmarkCategory("YCbCr.FromRgb")]
public void YCbCrOperatorFromRgb()
=> this.yCbCrOperator.ConvertFromRgb(this.CreateOperatorValues(3), this.r, this.g, this.b);
/// <summary>
/// Runs the replaced YCCK component-to-RGB traversal.
/// </summary>
[Benchmark(Baseline = true)]
[BenchmarkCategory("YccK.ToRgb")]
public void YccKLegacyToRgb()
=> this.yccKLegacy.ConvertToRgbInPlace(this.CreateLegacyValues(4));
/// <summary>
/// Runs the shared YCCK component-to-RGB traversal.
/// </summary>
[Benchmark]
[BenchmarkCategory("YccK.ToRgb")]
public void YccKOperatorToRgb()
=> this.yccKOperator.ConvertToRgbInPlace(this.CreateOperatorValues(4));
/// <summary>
/// Runs the replaced YCCK RGB-to-component traversal.
/// </summary>
[Benchmark(Baseline = true)]
[BenchmarkCategory("YccK.FromRgb")]
public void YccKLegacyFromRgb()
=> this.yccKLegacy.ConvertFromRgb(this.CreateLegacyValues(4), this.r, this.g, this.b);
/// <summary>
/// Runs the shared YCCK RGB-to-component traversal.
/// </summary>
[Benchmark]
[BenchmarkCategory("YccK.FromRgb")]
public void YccKOperatorFromRgb()
=> this.yccKOperator.ConvertFromRgb(this.CreateOperatorValues(4), this.r, this.g, this.b);
/// <summary>
/// Runs the replaced TIFF CMYK component-to-RGB traversal.
/// </summary>
[Benchmark(Baseline = true)]
[BenchmarkCategory("TiffCmyk.ToRgb")]
public void TiffCmykLegacyToRgb()
=> this.tiffCmykLegacy.ConvertToRgbInPlace(this.CreateLegacyValues(4));
/// <summary>
/// Runs the shared TIFF CMYK component-to-RGB traversal.
/// </summary>
[Benchmark]
[BenchmarkCategory("TiffCmyk.ToRgb")]
public void TiffCmykOperatorToRgb()
=> this.tiffCmykOperator.ConvertToRgbInPlace(this.CreateOperatorValues(4));
/// <summary>
/// Runs the replaced TIFF CMYK RGB-to-component traversal.
/// </summary>
[Benchmark(Baseline = true)]
[BenchmarkCategory("TiffCmyk.FromRgb")]
public void TiffCmykLegacyFromRgb()
=> this.tiffCmykLegacy.ConvertFromRgb(this.CreateLegacyValues(4), this.r, this.g, this.b);
/// <summary>
/// Runs the shared TIFF CMYK RGB-to-component traversal.
/// </summary>
[Benchmark]
[BenchmarkCategory("TiffCmyk.FromRgb")]
public void TiffCmykOperatorFromRgb()
=> this.tiffCmykOperator.ConvertFromRgb(this.CreateOperatorValues(4), this.r, this.g, this.b);
/// <summary>
/// Runs the replaced TIFF YCCK component-to-RGB traversal.
/// </summary>
[Benchmark(Baseline = true)]
[BenchmarkCategory("TiffYccK.ToRgb")]
public void TiffYccKLegacyToRgb()
=> this.tiffYccKLegacy.ConvertToRgbInPlace(this.CreateLegacyValues(4));
/// <summary>
/// Runs the shared TIFF YCCK component-to-RGB traversal.
/// </summary>
[Benchmark]
[BenchmarkCategory("TiffYccK.ToRgb")]
public void TiffYccKOperatorToRgb()
=> this.tiffYccKOperator.ConvertToRgbInPlace(this.CreateOperatorValues(4));
/// <summary>
/// Runs the replaced TIFF YCCK RGB-to-component traversal.
/// </summary>
[Benchmark(Baseline = true)]
[BenchmarkCategory("TiffYccK.FromRgb")]
public void TiffYccKLegacyFromRgb()
=> this.tiffYccKLegacy.ConvertFromRgb(this.CreateLegacyValues(4), this.r, this.g, this.b);
/// <summary>
/// Runs the shared TIFF YCCK RGB-to-component traversal.
/// </summary>
[Benchmark]
[BenchmarkCategory("TiffYccK.FromRgb")]
public void TiffYccKOperatorFromRgb()
=> this.tiffYccKOperator.ConvertFromRgb(this.CreateOperatorValues(4), this.r, this.g, this.b);
/// <summary>
/// Creates a correctly aliased component view over the legacy planes.
/// </summary>
/// <param name="componentCount">The number of component planes owned by the color model.</param>
/// <returns>The legacy component view.</returns>
[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 : []);
/// <summary>
/// Creates a correctly aliased component view over the operator planes.
/// </summary>
/// <param name="componentCount">The number of component planes owned by the color model.</param>
/// <returns>The operator component view.</returns>
[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 : []);
}

244
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;
/// <summary>
/// Exposes every YCbCr operator overload directly to the disassembly diagnoser.
/// </summary>
[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<float> vector128C0 = Vector128.Create(64F);
private readonly Vector128<float> vector128C1 = Vector128.Create(96F);
private readonly Vector128<float> vector128C2 = Vector128.Create(160F);
private readonly Vector128<float> vector128Maximum = Vector128.Create(MaximumValue);
private readonly Vector128<float> vector128Half = Vector128.Create(HalfValue);
private readonly Vector128<float> vector128Scale = Vector128.Create(Scale);
private readonly Vector256<float> vector256C0 = Vector256.Create(64F);
private readonly Vector256<float> vector256C1 = Vector256.Create(96F);
private readonly Vector256<float> vector256C2 = Vector256.Create(160F);
private readonly Vector256<float> vector256Maximum = Vector256.Create(MaximumValue);
private readonly Vector256<float> vector256Half = Vector256.Create(HalfValue);
private readonly Vector256<float> vector256Scale = Vector256.Create(Scale);
private readonly Vector512<float> vector512C0 = Vector512.Create(64F);
private readonly Vector512<float> vector512C1 = Vector512.Create(96F);
private readonly Vector512<float> vector512C2 = Vector512.Create(160F);
private readonly Vector512<float> vector512Maximum = Vector512.Create(MaximumValue);
private readonly Vector512<float> vector512Half = Vector512.Create(HalfValue);
private readonly Vector512<float> vector512Scale = Vector512.Create(Scale);
/// <summary>
/// Invokes the scalar JPEG-to-RGB operator.
/// </summary>
/// <returns>A checksum containing all three converted channels.</returns>
[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;
}
/// <summary>
/// Invokes the Vector128 JPEG-to-RGB operator.
/// </summary>
/// <returns>A checksum containing all three converted channel vectors.</returns>
[Benchmark]
[BenchmarkCategory("ToRgb")]
public Vector128<float> ToRgbVector128()
{
Vector128<float> c0 = this.vector128C0;
Vector128<float> c1 = this.vector128C1;
Vector128<float> 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;
}
/// <summary>
/// Invokes the Vector256 JPEG-to-RGB operator.
/// </summary>
/// <returns>A checksum containing all three converted channel vectors.</returns>
[Benchmark]
[BenchmarkCategory("ToRgb")]
public Vector256<float> ToRgbVector256()
{
Vector256<float> c0 = this.vector256C0;
Vector256<float> c1 = this.vector256C1;
Vector256<float> 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;
}
/// <summary>
/// Invokes the Vector512 JPEG-to-RGB operator.
/// </summary>
/// <returns>A checksum containing all three converted channel vectors.</returns>
[Benchmark]
[BenchmarkCategory("ToRgb")]
public Vector512<float> ToRgbVector512()
{
Vector512<float> c0 = this.vector512C0;
Vector512<float> c1 = this.vector512C1;
Vector512<float> 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;
}
/// <summary>
/// Invokes the scalar RGB-to-JPEG operator.
/// </summary>
/// <returns>A checksum containing all converted components.</returns>
[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;
}
/// <summary>
/// Invokes the Vector128 RGB-to-JPEG operator.
/// </summary>
/// <returns>A checksum containing all converted component vectors.</returns>
[Benchmark]
[BenchmarkCategory("FromRgb")]
public Vector128<float> FromRgbVector128()
{
JpegColorConverterBase.YCbCrOperator.ConvertFromRgb(
this.vector128C0,
this.vector128C1,
this.vector128C2,
this.vector128Maximum,
this.vector128Half,
this.vector128Scale,
out Vector128<float> c0,
out Vector128<float> c1,
out Vector128<float> c2,
out Vector128<float> c3);
// Include all planar results in the returned vector so the JIT retains every calculation.
return c0 + c1 + c2 + c3;
}
/// <summary>
/// Invokes the Vector256 RGB-to-JPEG operator.
/// </summary>
/// <returns>A checksum containing all converted component vectors.</returns>
[Benchmark]
[BenchmarkCategory("FromRgb")]
public Vector256<float> FromRgbVector256()
{
JpegColorConverterBase.YCbCrOperator.ConvertFromRgb(
this.vector256C0,
this.vector256C1,
this.vector256C2,
this.vector256Maximum,
this.vector256Half,
this.vector256Scale,
out Vector256<float> c0,
out Vector256<float> c1,
out Vector256<float> c2,
out Vector256<float> c3);
// Include all planar results in the returned vector so the JIT retains every calculation.
return c0 + c1 + c2 + c3;
}
/// <summary>
/// Invokes the Vector512 RGB-to-JPEG operator.
/// </summary>
/// <returns>A checksum containing all converted component vectors.</returns>
[Benchmark]
[BenchmarkCategory("FromRgb")]
public Vector512<float> FromRgbVector512()
{
JpegColorConverterBase.YCbCrOperator.ConvertFromRgb(
this.vector512C0,
this.vector512C1,
this.vector512C2,
this.vector512Maximum,
this.vector512Half,
this.vector512Scale,
out Vector512<float> c0,
out Vector512<float> c1,
out Vector512<float> c2,
out Vector512<float> c3);
// Include all planar results in the returned vector so the JIT retains every calculation.
return c0 + c1 + c2 + c3;
}
}

127
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;
/// <summary>
/// Compares the shared YCbCr operator traversal with the Vector512 implementation it replaces.
/// </summary>
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
[CategoriesColumn]
public class YCbCrOperatorComparison
{
private JpegColorConverterBase.YCbCrVector512 legacy;
private JpegColorConverterBase.JpegColorConverter<JpegColorConverterBase.YCbCrOperator> 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;
/// <summary>
/// Gets or sets the number of pixels converted by each invocation.
/// </summary>
[Params(8, 128, 1024)]
public int Count { get; set; }
/// <summary>
/// Creates equivalent converter inputs in independent component buffers.
/// </summary>
[GlobalSetup]
public void Setup()
{
this.legacy = new JpegColorConverterBase.YCbCrVector512(8);
this.operatorConverter =
new JpegColorConverterBase.JpegColorConverter<JpegColorConverterBase.YCbCrOperator>(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);
}
/// <summary>
/// Converts YCbCr components to RGB using the Vector512 implementation.
/// </summary>
[Benchmark(Baseline = true)]
[BenchmarkCategory("ToRgb")]
public void LegacyToRgb()
{
JpegColorConverterBase.ComponentValues values =
new(3, this.legacyC0, this.legacyC1, this.legacyC2, []);
this.legacy.ConvertToRgbInPlace(values);
}
/// <summary>
/// Converts YCbCr components to RGB using the shared operator traversal.
/// </summary>
[Benchmark]
[BenchmarkCategory("ToRgb")]
public void OperatorToRgb()
{
JpegColorConverterBase.ComponentValues values =
new(3, this.operatorC0, this.operatorC1, this.operatorC2, []);
this.operatorConverter.ConvertToRgbInPlace(values);
}
/// <summary>
/// Converts RGB to YCbCr components using the Vector512 implementation.
/// </summary>
[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);
}
/// <summary>
/// Converts RGB to YCbCr components using the shared operator traversal.
/// </summary>
[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);
}
/// <summary>
/// Creates deterministic sample-domain values for one component plane.
/// </summary>
/// <param name="length">The number of samples to create.</param>
/// <param name="random">The deterministic random source shared by setup.</param>
/// <returns>The populated component plane.</returns>
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;
}
}

354
tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.ColorProfiles; using SixLabors.ImageSharp.ColorProfiles;
using SixLabors.ImageSharp.ColorProfiles.Icc;
using SixLabors.ImageSharp.Formats.Jpeg.Components; using SixLabors.ImageSharp.Formats.Jpeg.Components;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Tests.ColorProfiles; using SixLabors.ImageSharp.Tests.ColorProfiles;
@ -43,6 +44,11 @@ public class JpegColorConverterTests
Assert.Throws<InvalidImageContentException>(() => JpegColorConverterBase.GetConverter(JpegColorSpace.YCbCr, invalidPrecision)); Assert.Throws<InvalidImageContentException>(() => JpegColorConverterBase.GetConverter(JpegColorSpace.YCbCr, invalidPrecision));
} }
/// <summary>
/// Verifies that each supported color space and precision resolves to an available converter.
/// </summary>
/// <param name="colorSpace">The JPEG color space.</param>
/// <param name="precision">The JPEG sample precision.</param>
[Theory] [Theory]
[InlineData(JpegColorSpace.Grayscale, 8)] [InlineData(JpegColorSpace.Grayscale, 8)]
[InlineData(JpegColorSpace.Grayscale, 12)] [InlineData(JpegColorSpace.Grayscale, 12)]
@ -54,6 +60,10 @@ public class JpegColorConverterTests
[InlineData(JpegColorSpace.RGB, 12)] [InlineData(JpegColorSpace.RGB, 12)]
[InlineData(JpegColorSpace.YCbCr, 8)] [InlineData(JpegColorSpace.YCbCr, 8)]
[InlineData(JpegColorSpace.YCbCr, 12)] [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) internal void GetConverterReturnsValidConverter(JpegColorSpace colorSpace, int precision)
{ {
JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(colorSpace, precision); JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(colorSpace, precision);
@ -74,19 +84,8 @@ public class JpegColorConverterTests
static void RunTest(string arg) static void RunTest(string arg)
{ {
// arrange // arrange
Type expectedType = typeof(JpegColorConverterBase.RgbScalar); Type expectedType =
if (JpegColorConverterBase.JpegColorConverterVector512.IsSupported) typeof(JpegColorConverterBase.JpegColorConverter<JpegColorConverterBase.RgbOperator>);
{
expectedType = typeof(JpegColorConverterBase.RgbVector512);
}
else if (JpegColorConverterBase.JpegColorConverterVector256.IsSupported)
{
expectedType = typeof(JpegColorConverterBase.RgbVector256);
}
else if (JpegColorConverterBase.JpegColorConverterVector128.IsSupported)
{
expectedType = typeof(JpegColorConverterBase.RgbVector128);
}
// act // act
JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(JpegColorSpace.RGB, 8); JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(JpegColorSpace.RGB, 8);
@ -107,19 +106,8 @@ public class JpegColorConverterTests
static void RunTest(string arg) static void RunTest(string arg)
{ {
// arrange // arrange
Type expectedType = typeof(JpegColorConverterBase.GrayScaleScalar); Type expectedType =
if (JpegColorConverterBase.JpegColorConverterVector512.IsSupported) typeof(JpegColorConverterBase.JpegColorConverter<JpegColorConverterBase.GrayScaleOperator>);
{
expectedType = typeof(JpegColorConverterBase.GrayScaleVector512);
}
else if (JpegColorConverterBase.JpegColorConverterVector256.IsSupported)
{
expectedType = typeof(JpegColorConverterBase.GrayScaleVector256);
}
else if (JpegColorConverterBase.JpegColorConverterVector128.IsSupported)
{
expectedType = typeof(JpegColorConverterBase.GrayScaleVector128);
}
// act // act
JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(JpegColorSpace.Grayscale, 8); JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(JpegColorSpace.Grayscale, 8);
@ -140,19 +128,8 @@ public class JpegColorConverterTests
static void RunTest(string arg) static void RunTest(string arg)
{ {
// arrange // arrange
Type expectedType = typeof(JpegColorConverterBase.CmykScalar); Type expectedType =
if (JpegColorConverterBase.JpegColorConverterVector512.IsSupported) typeof(JpegColorConverterBase.JpegColorConverter<JpegColorConverterBase.CmykOperator>);
{
expectedType = typeof(JpegColorConverterBase.CmykVector512);
}
else if (JpegColorConverterBase.JpegColorConverterVector256.IsSupported)
{
expectedType = typeof(JpegColorConverterBase.CmykVector256);
}
else if (JpegColorConverterBase.JpegColorConverterVector128.IsSupported)
{
expectedType = typeof(JpegColorConverterBase.CmykVector128);
}
// act // act
JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(JpegColorSpace.Cmyk, 8); JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(JpegColorSpace.Cmyk, 8);
@ -173,19 +150,8 @@ public class JpegColorConverterTests
static void RunTest(string arg) static void RunTest(string arg)
{ {
// arrange // arrange
Type expectedType = typeof(JpegColorConverterBase.YCbCrScalar); Type expectedType =
if (JpegColorConverterBase.JpegColorConverterVector512.IsSupported) typeof(JpegColorConverterBase.JpegColorConverter<JpegColorConverterBase.YCbCrOperator>);
{
expectedType = typeof(JpegColorConverterBase.YCbCrVector512);
}
else if (JpegColorConverterBase.JpegColorConverterVector256.IsSupported)
{
expectedType = typeof(JpegColorConverterBase.YCbCrVector256);
}
else if (JpegColorConverterBase.JpegColorConverterVector128.IsSupported)
{
expectedType = typeof(JpegColorConverterBase.YCbCrVector128);
}
// act // act
JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(JpegColorSpace.YCbCr, 8); JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(JpegColorSpace.YCbCr, 8);
@ -206,19 +172,8 @@ public class JpegColorConverterTests
static void RunTest(string arg) static void RunTest(string arg)
{ {
// arrange // arrange
Type expectedType = typeof(JpegColorConverterBase.YccKScalar); Type expectedType =
if (JpegColorConverterBase.JpegColorConverterVector512.IsSupported) typeof(JpegColorConverterBase.JpegColorConverter<JpegColorConverterBase.YccKOperator>);
{
expectedType = typeof(JpegColorConverterBase.YccKVector512);
}
else if (JpegColorConverterBase.JpegColorConverterVector256.IsSupported)
{
expectedType = typeof(JpegColorConverterBase.YccKVector256);
}
else if (JpegColorConverterBase.JpegColorConverterVector128.IsSupported)
{
expectedType = typeof(JpegColorConverterBase.YccKVector128);
}
// act // act
JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(JpegColorSpace.Ycck, 8); JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(JpegColorSpace.Ycck, 8);
@ -229,6 +184,25 @@ public class JpegColorConverterTests
} }
} }
/// <summary>
/// Verifies that TIFF color spaces resolve to their closed shared converter types.
/// </summary>
/// <param name="colorSpace">The TIFF JPEG color space.</param>
/// <param name="expectedType">The expected closed converter type.</param>
[Theory]
[InlineData(
JpegColorSpace.TiffCmyk,
typeof(JpegColorConverterBase.JpegColorConverter<JpegColorConverterBase.TiffCmykOperator>))]
[InlineData(
JpegColorSpace.TiffYccK,
typeof(JpegColorConverterBase.JpegColorConverter<JpegColorConverterBase.TiffYccKOperator>))]
internal void GetConverterReturnsCorrectConverterWithTiffColorSpace(JpegColorSpace colorSpace, Type expectedType)
{
JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(colorSpace, 8);
Assert.Equal(expectedType, converter.GetType());
}
[Theory] [Theory]
[InlineData(JpegColorSpace.Grayscale, 1)] [InlineData(JpegColorSpace.Grayscale, 1)]
[InlineData(JpegColorSpace.Ycck, 4)] [InlineData(JpegColorSpace.Ycck, 4)]
@ -306,6 +280,171 @@ public class JpegColorConverterTests
new JpegColorConverterBase.YCbCrScalar(8), new JpegColorConverterBase.YCbCrScalar(8),
precision: 2); precision: 2);
/// <summary>
/// Verifies YCbCr equivalence around every scalar and SIMD width boundary.
/// </summary>
/// <param name="length">The number of samples to convert.</param>
/// <param name="precision">The JPEG sample precision.</param>
[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<JpegColorConverterBase.YCbCrOperator>(precision);
JpegColorConverterBase baseline = new JpegColorConverterBase.YCbCrScalar(precision);
ValidateConversionToRgb(converter, baseline, length, 3, precision);
ValidateConversionFromRgb(converter, baseline, length, 3, precision);
}
/// <summary>
/// Verifies that the YCbCr operator retains scalar behavior when hardware intrinsics are disabled.
/// </summary>
[Fact]
public void YCbCrOperatorMatchesScalarWithoutHardwareIntrinsics()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest,
HwIntrinsics.DisableHWIntrinsic);
/// <summary>
/// Verifies converter equivalence around every scalar and SIMD width boundary.
/// </summary>
/// <param name="colorSpace">The color space under test.</param>
/// <param name="componentCount">The number of component planes written by the converter.</param>
[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);
}
}
/// <summary>
/// Verifies TIFF YCCK decoding around every scalar and SIMD width boundary.
/// </summary>
/// <param name="precision">The JPEG sample precision.</param>
[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);
}
}
/// <summary>
/// Verifies TIFF YCCK encoding against the canonical normalized color-profile conversion.
/// </summary>
[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, YccK>(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);
}
}
}
/// <summary>
/// Runs the YCbCr equivalence check in the feature-test process.
/// </summary>
/// <param name="arg">The unused feature-test argument.</param>
private static void RunTest(string arg)
{
const int length = 40;
const int precision = 8;
JpegColorConverterBase converter =
new JpegColorConverterBase.JpegColorConverter<JpegColorConverterBase.YCbCrOperator>(precision);
JpegColorConverterBase baseline = new JpegColorConverterBase.YCbCrScalar(precision);
ValidateConversionToRgb(converter, baseline, length, 3, precision);
ValidateConversionFromRgb(converter, baseline, length, 3, precision);
}
[Theory] [Theory]
[MemberData(nameof(Seeds))] [MemberData(nameof(Seeds))]
public void FromCmykBasic(int seed) => public void FromCmykBasic(int seed) =>
@ -736,6 +875,91 @@ public class JpegColorConverterTests
} }
} }
/// <summary>
/// Compares two component planes using an absolute floating-point tolerance.
/// </summary>
/// <param name="expected">The expected component values.</param>
/// <param name="actual">The actual component values.</param>
/// <param name="tolerance">The maximum permitted absolute difference.</param>
private static void CompareSequenceWithTolerance(Span<float> expected, Span<float> actual, float tolerance)
{
for (int i = 0; i < expected.Length; i++)
{
Assert.Equal(expected[i], actual[i], tolerance);
}
}
/// <summary>
/// Compares component-to-RGB conversion with a scalar reference implementation.
/// </summary>
/// <param name="converter">The shared converter under test.</param>
/// <param name="baseline">The scalar reference converter.</param>
/// <param name="length">The number of samples to convert.</param>
/// <param name="componentCount">The number of source component planes.</param>
/// <param name="precision">The JPEG sample precision.</param>
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);
}
/// <summary>
/// Compares RGB-to-component conversion with a scalar reference implementation.
/// </summary>
/// <param name="converter">The shared converter under test.</param>
/// <param name="baseline">The scalar reference converter.</param>
/// <param name="length">The number of samples to convert.</param>
/// <param name="componentCount">The number of destination component planes.</param>
/// <param name="precision">The JPEG sample precision.</param>
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( private static void Validate(
JpegColorSpace colorSpace, JpegColorSpace colorSpace,
in JpegColorConverterBase.ComponentValues original, in JpegColorConverterBase.ComponentValues original,

Loading…
Cancel
Save