diff --git a/BCnEnc.Net/Encoder/Bptc/Bc7EncodingHelpers.cs b/BCnEnc.Net/Encoder/Bptc/Bc7EncodingHelpers.cs
index 5b39c06..e7063f4 100644
--- a/BCnEnc.Net/Encoder/Bptc/Bc7EncodingHelpers.cs
+++ b/BCnEnc.Net/Encoder/Bptc/Bc7EncodingHelpers.cs
@@ -1,5 +1,7 @@
using System;
using System.Linq;
+using System.Numerics;
+using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using BCnEncoder.Shared;
@@ -8,6 +10,10 @@ namespace BCnEncoder.Encoder.Bptc
internal static class Bc7EncodingHelpers
{
+ /// Luma is weighted more heavily than chroma when scoring endpoint candidates.
+ private const float YWeight = 4;
+ private const float AlphaWeight = 2;
+
private static readonly int[] varPatternRAlpha = new int[] { 1, -1, 1, 0, 0, -1, 0, 0, 0, 0 };
private static readonly int[] varPatternRNoAlpha = new int[] { 1, -1, 1, 0, 0, -1, 0, 0 };
@@ -301,15 +307,25 @@ public static ColorRgba32 ScaleDownEndpoint(ColorRgba32 endpoint, Bc7BlockType t
public static ColorRgba32 InterpolateColor(ColorRgba32 endPointStart, ColorRgba32 endPointEnd,
int colorIndex, int alphaIndex, int colorBitCount, int alphaBitCount)
{
+ return InterpolateColor(endPointStart, endPointEnd,
+ BptcEncodingHelpers.GetInterpolationWeights(colorBitCount)[colorIndex],
+ BptcEncodingHelpers.GetInterpolationWeights(alphaBitCount)[alphaIndex]);
+ }
- var result = new ColorRgba32(
- BptcEncodingHelpers.InterpolateByte(endPointStart.r, endPointEnd.r, colorIndex, colorBitCount),
- BptcEncodingHelpers.InterpolateByte(endPointStart.g, endPointEnd.g, colorIndex, colorBitCount),
- BptcEncodingHelpers.InterpolateByte(endPointStart.b, endPointEnd.b, colorIndex, colorBitCount),
- BptcEncodingHelpers.InterpolateByte(endPointStart.a, endPointEnd.a, alphaIndex, alphaBitCount)
+ ///
+ /// Interpolates with weights already looked up by the caller, so palette loops select the
+ /// weight table once instead of once per component.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ColorRgba32 InterpolateColor(ColorRgba32 endPointStart, ColorRgba32 endPointEnd,
+ int colorWeight, int alphaWeight)
+ {
+ return new ColorRgba32(
+ BptcEncodingHelpers.InterpolateByte(endPointStart.r, endPointEnd.r, colorWeight),
+ BptcEncodingHelpers.InterpolateByte(endPointStart.g, endPointEnd.g, colorWeight),
+ BptcEncodingHelpers.InterpolateByte(endPointStart.b, endPointEnd.b, colorWeight),
+ BptcEncodingHelpers.InterpolateByte(endPointStart.a, endPointEnd.a, alphaWeight)
);
-
- return result;
}
public static void ClampEndpoint(ref ColorRgba32 endpoint, byte colorMax, byte alphaMax)
@@ -320,39 +336,55 @@ public static void ClampEndpoint(ref ColorRgba32 endpoint, byte colorMax, byte a
if (endpoint.a > alphaMax) endpoint.a = alphaMax;
}
- private static int FindClosestColorIndex(ColorYCbCrAlpha color, ReadOnlySpan colors, out float bestError)
+ private static int FindClosestColorIndex(ColorYCbCrAlpha color, in YcbcrAlphaPalette palette, out float bestError)
{
- bestError = color.CalcDistWeighted(colors[0], 4, 2);
+ var y = new Vector4(color.y);
+ var cb = new Vector4(color.cb);
+ var cr = new Vector4(color.cr);
+ var alpha = new Vector4(color.alpha);
+
+ var bestSquared = float.MaxValue;
var bestIndex = 0;
- for (var i = 1; i < colors.Length; i++)
+
+ for (var g = 0; g < palette.Groups; g++)
{
- var error = color.CalcDistWeighted(colors[i], 4, 2);
- if (error < bestError)
- {
- bestIndex = i;
- bestError = error;
- }
+ var dy = y - palette.Y[g];
+ var dcb = cb - palette.Cb[g];
+ var dcr = cr - palette.Cr[g];
+ var da = alpha - palette.Alpha[g];
+
+ var d = dy * dy * YWeight + dcb * dcb + dcr * dcr + da * da * AlphaWeight;
+
+ // Lanes are checked in index order so ties keep the lowest index, like a scalar scan.
+ var first = g * 4;
+ if (d.X < bestSquared) { bestSquared = d.X; bestIndex = first; }
+ if (d.Y < bestSquared) { bestSquared = d.Y; bestIndex = first + 1; }
+ if (d.Z < bestSquared) { bestSquared = d.Z; bestIndex = first + 2; }
+ if (d.W < bestSquared) { bestSquared = d.W; bestIndex = first + 3; }
}
+
+ bestError = MathF.Sqrt(bestSquared);
return bestIndex;
}
private static int FindClosestColorIndex(ColorYCbCr color, ReadOnlySpan colors, out float bestError)
{
- bestError = color.CalcDistWeighted(colors[0], 4);
+ var bestSquared = color.CalcDistWeightedSquared(colors[0], 4);
var bestIndex = 0;
for (var i = 1; i < colors.Length; i++)
{
- var error = color.CalcDistWeighted(colors[i], 4);
- if (error < bestError)
+ var error = color.CalcDistWeightedSquared(colors[i], 4);
+ if (error < bestSquared)
{
bestIndex = i;
- bestError = error;
+ bestSquared = error;
}
- if (bestError == 0)
+ if (bestSquared == 0)
{
break;
}
}
+ bestError = MathF.Sqrt(bestSquared);
return bestIndex;
}
@@ -378,8 +410,103 @@ private static int FindClosestAlphaIndex(byte alpha, ReadOnlySpan alphas,
}
- private static float TrySubsetEndpoints(Bc7BlockType type, RawBlock4X4Rgba32 raw, ColorRgba32 ep0, ColorRgba32 ep1,
- ReadOnlySpan partitionTable, int subsetIndex, int type4IdxMode)
+ ///
+ /// Converts the block once so the endpoint search below does not redo it for every candidate.
+ /// The y/cb/cr components match built from the same pixel.
+ ///
+ private static void ConvertPixels(RawBlock4X4Rgba32 raw, Span pixelColors)
+ {
+ var pixels = raw.AsSpan;
+ for (var i = 0; i < 16; i++)
+ {
+ pixelColors[i] = new ColorYCbCrAlpha(pixels[i]);
+ }
+ }
+
+ ///
+ /// The interpolated endpoint palette in YCbCrAlpha, stored channel-wise in groups of four.
+ /// Scoring a pixel against four candidates is then a single set of SIMD operations, which is
+ /// what the endpoint search spends nearly all of its time on.
+ ///
+ private readonly ref struct YcbcrAlphaPalette
+ {
+ public readonly Span Y;
+ public readonly Span Cb;
+ public readonly Span Cr;
+ public readonly Span Alpha;
+
+ ///
+ /// Splits into the four channels. It needs one
+ /// per palette entry, since four entries per group times four channels is four vectors.
+ ///
+ public YcbcrAlphaPalette(Span storage)
+ {
+ var groups = storage.Length / 4;
+ Y = storage.Slice(0, groups);
+ Cb = storage.Slice(groups, groups);
+ Cr = storage.Slice(groups * 2, groups);
+ Alpha = storage.Slice(groups * 3, groups);
+ }
+
+ public int Groups => Y.Length;
+ }
+
+ ///
+ /// Builds the interpolated endpoint palette shared by color and alpha indices.
+ ///
+ private static YcbcrAlphaPalette BuildPalette(ColorRgba32 ep0, ColorRgba32 ep1, int colorIndexPrecision, int alphaIndexPrecision,
+ Span storage)
+ {
+ var colorWeights = BptcEncodingHelpers.GetInterpolationWeights(colorIndexPrecision);
+ var alphaWeights = BptcEncodingHelpers.GetInterpolationWeights(alphaIndexPrecision);
+ var normalized = ColorTables.Normalized;
+ var palette = new YcbcrAlphaPalette(storage);
+
+ for (var group = 0; group < palette.Groups; group++)
+ {
+ var first = group * 4;
+ var c0 = InterpolateColor(ep0, ep1, colorWeights[first], alphaWeights[first]);
+ var c1 = InterpolateColor(ep0, ep1, colorWeights[first + 1], alphaWeights[first + 1]);
+ var c2 = InterpolateColor(ep0, ep1, colorWeights[first + 2], alphaWeights[first + 2]);
+ var c3 = InterpolateColor(ep0, ep1, colorWeights[first + 3], alphaWeights[first + 3]);
+
+ var r = new Vector4(normalized[c0.r], normalized[c1.r], normalized[c2.r], normalized[c3.r]);
+ var g = new Vector4(normalized[c0.g], normalized[c1.g], normalized[c2.g], normalized[c3.g]);
+ var b = new Vector4(normalized[c0.b], normalized[c1.b], normalized[c2.b], normalized[c3.b]);
+
+ ColorYCbCrAlpha.FromNormalized(r, g, b, out var y, out var cb, out var cr);
+
+ palette.Y[group] = y;
+ palette.Cb[group] = cb;
+ palette.Cr[group] = cr;
+ palette.Alpha[group] = new Vector4(normalized[c0.a], normalized[c1.a], normalized[c2.a], normalized[c3.a]);
+ }
+
+ return palette;
+ }
+
+ ///
+ /// Builds the palettes for the modes that index color and alpha independently.
+ ///
+ private static void BuildSeparatePalettes(ColorRgba32 ep0, ColorRgba32 ep1, int colorIndexPrecision, int alphaIndexPrecision,
+ Span colors, Span alphas)
+ {
+ var colorWeights = BptcEncodingHelpers.GetInterpolationWeights(colorIndexPrecision);
+ var alphaWeights = BptcEncodingHelpers.GetInterpolationWeights(alphaIndexPrecision);
+
+ for (var i = 0; i < colors.Length; i++)
+ {
+ colors[i] = new ColorYCbCr(InterpolateColor(ep0, ep1, colorWeights[i], 0));
+ }
+
+ for (var i = 0; i < alphas.Length; i++)
+ {
+ alphas[i] = InterpolateColor(ep0, ep1, 0, alphaWeights[i]).a;
+ }
+ }
+
+ private static float TrySubsetEndpoints(Bc7BlockType type, RawBlock4X4Rgba32 raw, ReadOnlySpan pixelColors,
+ ColorRgba32 ep0, ColorRgba32 ep1, ReadOnlySpan partitionTable, int subsetIndex, int type4IdxMode)
{
var colorIndexPrecision = GetColorIndexBitCount(type, type4IdxMode);
var alphaIndexPrecision = GetAlphaIndexBitCount(type, type4IdxMode);
@@ -388,25 +515,15 @@ private static float TrySubsetEndpoints(Bc7BlockType type, RawBlock4X4Rgba32 raw
{ //separate indices for color and alpha
Span colors = stackalloc ColorYCbCr[1 << colorIndexPrecision];
Span alphas = stackalloc byte[1 << alphaIndexPrecision];
-
- for (var i = 0; i < colors.Length; i++)
- {
- colors[i] = new ColorYCbCr(InterpolateColor(ep0, ep1, i,
- 0, colorIndexPrecision, 0));
- }
-
- for (var i = 0; i < alphas.Length; i++)
- {
- alphas[i] = InterpolateColor(ep0, ep1, 0,
- i, 0, alphaIndexPrecision).a;
- }
+ BuildSeparatePalettes(ep0, ep1, colorIndexPrecision, alphaIndexPrecision, colors, alphas);
var pixels = raw.AsSpan;
float error = 0;
for (var i = 0; i < 16; i++)
{
- var pixelColor = new ColorYCbCr(pixels[i]);
+ var cached = pixelColors[i];
+ var pixelColor = new ColorYCbCr(cached.y, cached.cb, cached.cr);
FindClosestColorIndex(pixelColor, colors, out var ce);
FindClosestAlphaIndex(pixels[i].a, alphas, out var ae);
@@ -418,14 +535,9 @@ private static float TrySubsetEndpoints(Bc7BlockType type, RawBlock4X4Rgba32 raw
}
else
{
- Span colors = stackalloc ColorYCbCrAlpha[1 << colorIndexPrecision];
- for (var i = 0; i < colors.Length; i++)
- {
- colors[i] = new ColorYCbCrAlpha(InterpolateColor(ep0, ep1, i,
- i, colorIndexPrecision, alphaIndexPrecision));
- }
+ Span paletteStorage = stackalloc Vector4[1 << colorIndexPrecision];
+ var palette = BuildPalette(ep0, ep1, colorIndexPrecision, alphaIndexPrecision, paletteStorage);
- var pixels = raw.AsSpan;
float error = 0;
float count = 0;
@@ -433,9 +545,7 @@ private static float TrySubsetEndpoints(Bc7BlockType type, RawBlock4X4Rgba32 raw
{
if (partitionTable[i] == subsetIndex)
{
- var pixelColor = new ColorYCbCrAlpha(pixels[i]);
-
- FindClosestColorIndex(pixelColor, colors, out var e);
+ FindClosestColorIndex(pixelColors[i], palette, out var e);
error += e * e;
count++;
}
@@ -459,12 +569,8 @@ public static void FillSubsetIndices(Bc7BlockType type, RawBlock4X4Rgba32 raw, C
}
else
{
- Span colors = stackalloc ColorYCbCrAlpha[1 << colorIndexPrecision];
- for (var i = 0; i < colors.Length; i++)
- {
- colors[i] = new ColorYCbCrAlpha(InterpolateColor(ep0, ep1, i,
- i, colorIndexPrecision, alphaIndexPrecision));
- }
+ Span paletteStorage = stackalloc Vector4[1 << colorIndexPrecision];
+ var palette = BuildPalette(ep0, ep1, colorIndexPrecision, alphaIndexPrecision, paletteStorage);
var pixels = raw.AsSpan;
@@ -474,7 +580,7 @@ public static void FillSubsetIndices(Bc7BlockType type, RawBlock4X4Rgba32 raw, C
{
var pixelColor = new ColorYCbCrAlpha(pixels[i]);
- var index = FindClosestColorIndex(pixelColor, colors, out var e);
+ var index = FindClosestColorIndex(pixelColor, palette, out var e);
indicesToFill[i] = (byte)index;
}
}
@@ -494,18 +600,7 @@ public static void FillAlphaColorIndices(Bc7BlockType type, RawBlock4X4Rgba32 ra
{
Span colors = stackalloc ColorYCbCr[1 << colorIndexPrecision];
Span alphas = stackalloc byte[1 << alphaIndexPrecision];
-
- for (var i = 0; i < colors.Length; i++)
- {
- colors[i] = new ColorYCbCr(InterpolateColor(ep0, ep1, i,
- 0, colorIndexPrecision, 0));
- }
-
- for (var i = 0; i < alphas.Length; i++)
- {
- alphas[i] = InterpolateColor(ep0, ep1, 0,
- i, 0, alphaIndexPrecision).a;
- }
+ BuildSeparatePalettes(ep0, ep1, colorIndexPrecision, alphaIndexPrecision, colors, alphas);
var pixels = raw.AsSpan;
@@ -533,7 +628,10 @@ public static void OptimizeSubsetEndpointsWithPBit(Bc7BlockType type, RawBlock4X
var colorMax = (byte)((1 << GetColorComponentPrecision(type)) - 1);
var alphaMax = (byte)((1 << GetAlphaComponentPrecision(type)) - 1);
- var bestError = TrySubsetEndpoints(type, raw,
+ Span pixelColors = stackalloc ColorYCbCrAlpha[16];
+ ConvertPixels(raw, pixelColors);
+
+ var bestError = TrySubsetEndpoints(type, raw, pixelColors,
ExpandEndpoint(type, ep0, pBit0),
ExpandEndpoint(type, ep1, pBit1), partitionTable, subsetIndex, type4IdxMode
);
@@ -574,7 +672,7 @@ public static void OptimizeSubsetEndpointsWithPBit(Bc7BlockType type, RawBlock4X
ClampEndpoint(ref testEndPoint0, colorMax, alphaMax);
ClampEndpoint(ref testEndPoint1, colorMax, alphaMax);
- var error = TrySubsetEndpoints(type, raw,
+ var error = TrySubsetEndpoints(type, raw, pixelColors,
ExpandEndpoint(type, testEndPoint0, pBit0),
ExpandEndpoint(type, testEndPoint1, pBit1), partitionTable, subsetIndex, type4IdxMode
);
@@ -597,7 +695,7 @@ public static void OptimizeSubsetEndpointsWithPBit(Bc7BlockType type, RawBlock4X
);
ClampEndpoint(ref testEndPoint0, colorMax, alphaMax);
- var error = TrySubsetEndpoints(type, raw,
+ var error = TrySubsetEndpoints(type, raw, pixelColors,
ExpandEndpoint(type, testEndPoint0, pBit0),
ExpandEndpoint(type, ep1, pBit1), partitionTable, subsetIndex, type4IdxMode
);
@@ -619,7 +717,7 @@ public static void OptimizeSubsetEndpointsWithPBit(Bc7BlockType type, RawBlock4X
);
ClampEndpoint(ref testEndPoint1, colorMax, alphaMax);
- var error = TrySubsetEndpoints(type, raw,
+ var error = TrySubsetEndpoints(type, raw, pixelColors,
ExpandEndpoint(type, ep0, pBit0),
ExpandEndpoint(type, testEndPoint1, pBit1), partitionTable, subsetIndex, type4IdxMode
);
@@ -635,7 +733,7 @@ public static void OptimizeSubsetEndpointsWithPBit(Bc7BlockType type, RawBlock4X
{
{
var testPBit0 = pBit0 == 0 ? (byte)1 : (byte)0;
- var error = TrySubsetEndpoints(type, raw,
+ var error = TrySubsetEndpoints(type, raw, pixelColors,
ExpandEndpoint(type, ep0, testPBit0),
ExpandEndpoint(type, ep1, pBit1), partitionTable, subsetIndex, type4IdxMode
);
@@ -648,7 +746,7 @@ public static void OptimizeSubsetEndpointsWithPBit(Bc7BlockType type, RawBlock4X
}
{
var testPBit1 = pBit1 == 0 ? (byte)1 : (byte)0;
- var error = TrySubsetEndpoints(type, raw,
+ var error = TrySubsetEndpoints(type, raw, pixelColors,
ExpandEndpoint(type, ep0, pBit0),
ExpandEndpoint(type, ep1, testPBit1), partitionTable, subsetIndex, type4IdxMode
);
diff --git a/BCnEnc.Net/Encoder/Bptc/BptcEncodingHelpers.cs b/BCnEnc.Net/Encoder/Bptc/BptcEncodingHelpers.cs
index 0eb201d..96c6977 100644
--- a/BCnEnc.Net/Encoder/Bptc/BptcEncodingHelpers.cs
+++ b/BCnEnc.Net/Encoder/Bptc/BptcEncodingHelpers.cs
@@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
-using System.Linq;
+using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using BCnEncoder.Shared;
@@ -13,199 +13,136 @@ namespace BCnEncoder.Encoder.Bptc
{
internal static class BptcEncodingHelpers
{
+ private static readonly byte[] ColorInterpolationWeights0 = new byte[16];
private static readonly byte[] ColorInterpolationWeights2 = new byte[] { 0, 21, 43, 64 };
private static readonly byte[] ColorInterpolationWeights3 = new byte[] { 0, 9, 18, 27, 37, 46, 55, 64 };
private static readonly byte[] ColorInterpolationWeights4 = new byte[] { 0, 4, 9, 13, 17, 21, 26, 30, 34, 38, 43, 47, 51, 55, 60, 64 };
+ ///
+ /// Interpolation weights for an index precision. Precision 0 means "always endpoint 0",
+ /// which is a zero weight for every index, so callers never need to special-case it.
+ ///
+ public static ReadOnlySpan GetInterpolationWeights(int indexPrecision)
+ {
+ switch (indexPrecision)
+ {
+ case 2: return ColorInterpolationWeights2;
+ case 3: return ColorInterpolationWeights3;
+ case 4: return ColorInterpolationWeights4;
+ default: return ColorInterpolationWeights0;
+ }
+ }
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int InterpolateInt(int e0, int e1, int weight)
+ {
+ return ((64 - weight) * e0 + weight * e1 + 32) >> 6;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static byte InterpolateByte(byte e0, byte e1, int weight)
+ {
+ return (byte)InterpolateInt(e0, e1, weight);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int InterpolateInt(int e0, int e1, int index, int indexPrecision)
{
- if (indexPrecision == 0) return e0;
- var aWeights2 = ColorInterpolationWeights2;
- var aWeights3 = ColorInterpolationWeights3;
- var aWeights4 = ColorInterpolationWeights4;
-
- if (indexPrecision == 2)
- return (((64 - aWeights2[index]) * e0 + aWeights2[index] * e1 + 32) >> 6);
- if (indexPrecision == 3)
- return ((64 - aWeights3[index]) * e0 + aWeights3[index] * e1 + 32) >> 6;
- else // indexprecision == 4
- return ((64 - aWeights4[index]) * e0 + aWeights4[index] * e1 + 32) >> 6;
+ return InterpolateInt(e0, e1, GetInterpolationWeights(indexPrecision)[index]);
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte InterpolateByte(byte e0, byte e1, int index, int indexPrecision)
{
- if (indexPrecision == 0) return e0;
- var aWeights2 = ColorInterpolationWeights2;
- var aWeights3 = ColorInterpolationWeights3;
- var aWeights4 = ColorInterpolationWeights4;
-
- if (indexPrecision == 2)
- return (byte)(((64 - aWeights2[index]) * e0 + aWeights2[index] * e1 + 32) >> 6);
- if (indexPrecision == 3)
- return (byte)(((64 - aWeights3[index]) * e0 + aWeights3[index] * e1 + 32) >> 6);
- else // indexprecision == 4
- return (byte)(((64 - aWeights4[index]) * e0 + aWeights4[index] * e1 + 32) >> 6);
+ return InterpolateByte(e0, e1, GetInterpolationWeights(indexPrecision)[index]);
}
public static int[] Rank2SubsetPartitions(ClusterIndices4X4 reducedIndicesBlock, int numDistinctClusters, bool smallIndex = false)
{
- var output = Enumerable.Range(0, smallIndex ? 32 : 64).ToArray();
-
- // Copy struct to array before the closure so that reducedIndicesBlock is not
- // heap-captured. On .NET Framework, MemoryMarshalPolyfills.CreateSpan uses
- // Unsafe.AsPointer, which is only GC-safe for stack-allocated structs.
- #if NETSTANDARD2_0
- var indices = new int[16];
- reducedIndicesBlock.AsSpan.CopyTo(indices);
- #endif
+ var partitionCount = smallIndex ? 32 : 64;
+ var indices = reducedIndicesBlock.AsSpan;
- int CalculatePartitionError(int partitionIndex)
+ Span errors = stackalloc int[partitionCount];
+ for (var p = 0; p < partitionCount; p++)
{
- #if NETSTANDARD2_1
- var indices = reducedIndicesBlock.AsSpan;
- #endif
-
- var error = 0;
- ReadOnlySpan partitionTable = Bc7Block.Subsets2PartitionTable[partitionIndex];
- Span subset0 = stackalloc int[numDistinctClusters];
- Span subset1 = stackalloc int[numDistinctClusters];
- var max0Idx = 0;
- var max1Idx = 0;
-
- //Calculate largest cluster index for each subset
- for (var i = 0; i < 16; i++)
- {
- if (partitionTable[i] == 0)
- {
- var r = indices[i];
- subset0[r]++;
- var count = subset0[r];
- if (count > subset0[max0Idx])
- {
- max0Idx = r;
- }
- }
- else
- {
- var r = indices[i];
- subset1[r]++;
- var count = subset1[r];
- if (count > subset1[max1Idx])
- {
- max1Idx = r;
- }
- }
- }
-
- // Calculate error by counting as error everything that does not match the largest cluster
- for (var i = 0; i < 16; i++)
- {
- if (partitionTable[i] == 0)
- {
- if (indices[i] != max0Idx) error++;
- }
- else
- {
- if (indices[i] != max1Idx) error++;
- }
- }
-
- return error;
+ errors[p] = CalculatePartitionError(indices, Bc7Block.Subsets2PartitionTable[p], 2, numDistinctClusters);
}
- output = output.OrderBy(CalculatePartitionError).ToArray();
-
- return output;
+ return RankByError(errors);
}
public static int[] Rank3SubsetPartitions(ClusterIndices4X4 reducedIndicesBlock, int numDistinctClusters)
{
- var output = Enumerable.Range(0, 64).ToArray();
-
- // Copy struct to array before the closure so that reducedIndicesBlock is not
- // heap-captured. On .NET Framework, MemoryMarshalPolyfills.CreateSpan uses
- // Unsafe.AsPointer, which is only GC-safe for stack-allocated structs.
- #if NETSTANDARD2_0
- var indices = new int[16];
- reducedIndicesBlock.AsSpan.CopyTo(indices);
- #endif
+ const int partitionCount = 64;
+ var indices = reducedIndicesBlock.AsSpan;
- int CalculatePartitionError(int partitionIndex)
+ Span errors = stackalloc int[partitionCount];
+ for (var p = 0; p < partitionCount; p++)
{
- #if NETSTANDARD2_1
- var indices = reducedIndicesBlock.AsSpan;
- #endif
+ errors[p] = CalculatePartitionError(indices, Bc7Block.Subsets3PartitionTable[p], 3, numDistinctClusters);
+ }
- var error = 0;
- ReadOnlySpan partitionTable = Bc7Block.Subsets3PartitionTable[partitionIndex];
+ return RankByError(errors);
+ }
- Span subset0 = stackalloc int[numDistinctClusters];
- Span subset1 = stackalloc int[numDistinctClusters];
- Span subset2 = stackalloc int[numDistinctClusters];
- var max0Idx = 0;
- var max1Idx = 0;
- var max2Idx = 0;
+ ///
+ /// Counts, per subset, how many pixels do not belong to that subset's most common cluster.
+ ///
+ private static int CalculatePartitionError(ReadOnlySpan indices, ReadOnlySpan partitionTable,
+ int numSubsets, int numDistinctClusters)
+ {
+ Span histogram = stackalloc int[numSubsets * numDistinctClusters];
+ Span largestCluster = stackalloc int[numSubsets];
+ histogram.Clear();
+ largestCluster.Clear();
- //Calculate largest cluster index for each subset
- for (var i = 0; i < 16; i++)
- {
- if (partitionTable[i] == 0)
- {
- var r = indices[i];
- subset0[r]++;
- var count = subset0[r];
- if (count > subset0[max0Idx])
- {
- max0Idx = r;
- }
- }
- else if (partitionTable[i] == 1)
- {
- var r = indices[i];
- subset1[r]++;
- var count = subset1[r];
- if (count > subset1[max1Idx])
- {
- max1Idx = r;
- }
- }
- else
- {
- var r = indices[i];
- subset2[r]++;
- var count = subset2[r];
- if (count > subset2[max2Idx])
- {
- max2Idx = r;
- }
- }
- }
+ for (var i = 0; i < 16; i++)
+ {
+ var subset = partitionTable[i];
+ var cluster = indices[i];
+ var bucket = subset * numDistinctClusters;
- // Calculate error by counting as error everything that does not match the largest cluster
- for (var i = 0; i < 16; i++)
+ histogram[bucket + cluster]++;
+ if (histogram[bucket + cluster] > histogram[bucket + largestCluster[subset]])
{
- if (partitionTable[i] == 0)
- {
- if (indices[i] != max0Idx) error++;
- }
- else if (partitionTable[i] == 1)
- {
- if (indices[i] != max1Idx) error++;
- }
- else
- {
- if (indices[i] != max2Idx) error++;
- }
+ largestCluster[subset] = cluster;
}
+ }
- return error;
+ var error = 0;
+ for (var i = 0; i < 16; i++)
+ {
+ if (indices[i] != largestCluster[partitionTable[i]]) error++;
}
- output = output.OrderBy(CalculatePartitionError).ToArray();
+ return error;
+ }
+
+ ///
+ /// Stable ascending sort of partition indices by error. Errors are pixel counts, so a
+ /// counting sort over the 0..16 range replaces a comparison sort entirely.
+ ///
+ private static int[] RankByError(ReadOnlySpan errors)
+ {
+ Span offsets = stackalloc int[18];
+ offsets.Clear();
+
+ for (var i = 0; i < errors.Length; i++)
+ {
+ offsets[errors[i] + 1]++;
+ }
+ for (var i = 1; i < offsets.Length; i++)
+ {
+ offsets[i] += offsets[i - 1];
+ }
+ var output = new int[errors.Length];
+ for (var i = 0; i < errors.Length; i++)
+ {
+ output[offsets[errors[i]]++] = i;
+ }
return output;
}
}
diff --git a/BCnEnc.Net/Encoder/ColorChooser.cs b/BCnEnc.Net/Encoder/ColorChooser.cs
index 0fbf6ef..edf5148 100644
--- a/BCnEnc.Net/Encoder/ColorChooser.cs
+++ b/BCnEnc.Net/Encoder/ColorChooser.cs
@@ -1,40 +1,54 @@
using System;
+using System.Numerics;
+using System.Runtime.CompilerServices;
using BCnEncoder.Shared;
namespace BCnEncoder.Encoder
{
internal static class ColorChooser
{
+ ///
+ /// Weighted per-channel distance of to all four palette entries at
+ /// once. One lane per entry, so the four candidates cost a single set of SIMD operations.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static Vector4 DistancesTo4(ReadOnlySpan colors, ColorRgba32 color, float rWeight, float gWeight, float bWeight)
+ {
+ var dr = Vector4.Abs(new Vector4(colors[0].r, colors[1].r, colors[2].r, colors[3].r) - new Vector4(color.r));
+ var dg = Vector4.Abs(new Vector4(colors[0].g, colors[1].g, colors[2].g, colors[3].g) - new Vector4(color.g));
+ var db = Vector4.Abs(new Vector4(colors[0].b, colors[1].b, colors[2].b, colors[3].b) - new Vector4(color.b));
+
+ return dr * rWeight + dg * gWeight + db * bWeight;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static float Select(Vector4 v, int index)
+ {
+ switch (index)
+ {
+ case 0: return v.X;
+ case 1: return v.Y;
+ case 2: return v.Z;
+ default: return v.W;
+ }
+ }
public static int ChooseClosestColor4(ReadOnlySpan colors, ColorRgba32 color, float rWeight, float gWeight, float bWeight, out float error)
{
- ReadOnlySpan d = stackalloc float[4] {
- MathF.Abs(colors[0].r - color.r) * rWeight
- + MathF.Abs(colors[0].g - color.g) * gWeight
- + MathF.Abs(colors[0].b - color.b) * bWeight,
- MathF.Abs(colors[1].r - color.r) * rWeight
- + MathF.Abs(colors[1].g - color.g) * gWeight
- + MathF.Abs(colors[1].b - color.b) * bWeight,
- MathF.Abs(colors[2].r - color.r) * rWeight
- + MathF.Abs(colors[2].g - color.g) * gWeight
- + MathF.Abs(colors[2].b - color.b) * bWeight,
- MathF.Abs(colors[3].r - color.r) * rWeight
- + MathF.Abs(colors[3].g - color.g) * gWeight
- + MathF.Abs(colors[3].b - color.b) * bWeight,
- };
-
- var b0 = d[0] > d[3] ? 1 : 0;
- var b1 = d[1] > d[2] ? 1 : 0;
- var b2 = d[0] > d[2] ? 1 : 0;
- var b3 = d[1] > d[3] ? 1 : 0;
- var b4 = d[2] > d[3] ? 1 : 0;
+ var d = DistancesTo4(colors, color, rWeight, gWeight, bWeight);
+
+ var b0 = d.X > d.W ? 1 : 0;
+ var b1 = d.Y > d.Z ? 1 : 0;
+ var b2 = d.X > d.Z ? 1 : 0;
+ var b3 = d.Y > d.W ? 1 : 0;
+ var b4 = d.Z > d.W ? 1 : 0;
var x0 = b1 & b2;
var x1 = b0 & b3;
var x2 = b0 & b4;
var idx = x2 | ((x0 | x1) << 1);
- error = d[idx];
+ error = Select(d, idx);
return idx;
}
@@ -48,34 +62,23 @@ public static int ChooseClosestColor4AlphaCutoff(ReadOnlySpan colors
return 3;
}
- ReadOnlySpan d = stackalloc float[4] {
- MathF.Abs(colors[0].r - color.r) * rWeight
- + MathF.Abs(colors[0].g - color.g) * gWeight
- + MathF.Abs(colors[0].b - color.b) * bWeight,
- MathF.Abs(colors[1].r - color.r) * rWeight
- + MathF.Abs(colors[1].g - color.g) * gWeight
- + MathF.Abs(colors[1].b - color.b) * bWeight,
- MathF.Abs(colors[2].r - color.r) * rWeight
- + MathF.Abs(colors[2].g - color.g) * gWeight
- + MathF.Abs(colors[2].b - color.b) * bWeight,
-
- hasAlpha ? 999 :
- MathF.Abs(colors[3].r - color.r) * rWeight
- + MathF.Abs(colors[3].g - color.g) * gWeight
- + MathF.Abs(colors[3].b - color.b) * bWeight,
- };
-
- var b0 = d[0] > d[2] ? 1 : 0;
- var b1 = d[1] > d[3] ? 1 : 0;
- var b2 = d[0] > d[3] ? 1 : 0;
- var b3 = d[1] > d[2] ? 1 : 0;
- var nb3 = d[1] > d[2] ? 0 : 1;
- var b4 = d[0] > d[1] ? 1 : 0;
- var b5 = d[2] > d[3] ? 1 : 0;
+ var d = DistancesTo4(colors, color, rWeight, gWeight, bWeight);
+ if (hasAlpha)
+ {
+ d.W = 999;
+ }
+
+ var b0 = d.X > d.Z ? 1 : 0;
+ var b1 = d.Y > d.W ? 1 : 0;
+ var b2 = d.X > d.W ? 1 : 0;
+ var b3 = d.Y > d.Z ? 1 : 0;
+ var nb3 = d.Y > d.Z ? 0 : 1;
+ var b4 = d.X > d.Y ? 1 : 0;
+ var b5 = d.Z > d.W ? 1 : 0;
var idx = (nb3 & b4) | (b2 & b5) | (((b0 & b3) | (b1 & b2)) << 1);
- error = d[idx];
+ error = Select(d, idx);
return idx;
}
diff --git a/BCnEnc.Net/Shared/ByteHelper.cs b/BCnEnc.Net/Shared/ByteHelper.cs
index 7c60b60..3fc89fe 100644
--- a/BCnEnc.Net/Shared/ByteHelper.cs
+++ b/BCnEnc.Net/Shared/ByteHelper.cs
@@ -1,7 +1,10 @@
-namespace BCnEncoder.Shared
+using System.Runtime.CompilerServices;
+
+namespace BCnEncoder.Shared
{
internal static class ByteHelper
{
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte ClampToByte(int i)
{
if (i < 0) i = 0;
@@ -9,15 +12,18 @@ public static byte ClampToByte(int i)
return (byte)i;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte ClampToByte(float f)
=> ClampToByte((int)f);
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte Extract1(ulong source, int index)
{
const ulong mask = 0b1UL;
return (byte)((source >> index) & mask);
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong Store1(ulong dest, int index, byte value)
{
const ulong mask = 0b1UL;
@@ -26,12 +32,14 @@ public static ulong Store1(ulong dest, int index, byte value)
return dest;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte Extract2(ulong source, int index)
{
const ulong mask = 0b11UL;
return (byte)((source >> index) & mask);
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong Store2(ulong dest, int index, byte value)
{
const ulong mask = 0b11UL;
@@ -40,12 +48,14 @@ public static ulong Store2(ulong dest, int index, byte value)
return dest;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte Extract3(ulong source, int index)
{
const ulong mask = 0b111UL;
return (byte)((source >> index) & mask);
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong Store3(ulong dest, int index, byte value)
{
const ulong mask = 0b111UL;
@@ -54,12 +64,14 @@ public static ulong Store3(ulong dest, int index, byte value)
return dest;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte Extract4(ulong source, int index)
{
const ulong mask = 0b1111UL;
return (byte)((source >> index) & mask);
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong Store4(ulong dest, int index, byte value)
{
const ulong mask = 0b1111UL;
@@ -68,12 +80,14 @@ public static ulong Store4(ulong dest, int index, byte value)
return dest;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte Extract5(ulong source, int index)
{
const ulong mask = 0b1_1111UL;
return (byte)((source >> index) & mask);
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong Store5(ulong dest, int index, byte value)
{
const ulong mask = 0b1_1111UL;
@@ -82,12 +96,14 @@ public static ulong Store5(ulong dest, int index, byte value)
return dest;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte Extract6(ulong source, int index)
{
const ulong mask = 0b11_1111UL;
return (byte)((source >> index) & mask);
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong Store6(ulong dest, int index, byte value)
{
const ulong mask = 0b11_1111UL;
@@ -96,12 +112,14 @@ public static ulong Store6(ulong dest, int index, byte value)
return dest;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte Extract7(ulong source, int index)
{
const ulong mask = 0b111_1111UL;
return (byte)((source >> index) & mask);
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong Store7(ulong dest, int index, byte value)
{
const ulong mask = 0b111_1111UL;
@@ -110,12 +128,14 @@ public static ulong Store7(ulong dest, int index, byte value)
return dest;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte Extract8(ulong source, int index)
{
const ulong mask = 0b1111_1111UL;
return (byte)((source >> index) & mask);
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong Store8(ulong dest, int index, byte value)
{
const ulong mask = 0b1111_1111UL;
@@ -124,6 +144,7 @@ public static ulong Store8(ulong dest, int index, byte value)
return dest;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong Extract(ulong source, int index, int bitCount)
{
unchecked
@@ -133,6 +154,7 @@ public static ulong Extract(ulong source, int index, int bitCount)
}
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong Store(ulong dest, int index, int bitCount, ulong value)
{
unchecked
@@ -144,6 +166,7 @@ public static ulong Store(ulong dest, int index, int bitCount, ulong value)
}
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong ExtractFrom128(ulong low, ulong high, int index, int bitCount)
{
if (index + bitCount <= 64)
@@ -168,6 +191,7 @@ public static ulong ExtractFrom128(ulong low, ulong high, int index, int bitCoun
}
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static (ulong, ulong) StoreTo128(ulong low, ulong high, int index, int bitCount, ulong value)
{
if (index + bitCount <= 64)
diff --git a/BCnEnc.Net/Shared/Colors.cs b/BCnEnc.Net/Shared/Colors.cs
index 488d0f6..ec45cef 100644
--- a/BCnEnc.Net/Shared/Colors.cs
+++ b/BCnEnc.Net/Shared/Colors.cs
@@ -1,12 +1,52 @@
-using System;
+using System;
using System.Numerics;
+using System.Runtime.CompilerServices;
namespace BCnEncoder.Shared
{
+ ///
+ /// Precomputed tables for the per-byte conversions that dominate the encoder hot loops.
+ /// Every entry holds exactly the value the equivalent expression would have produced.
+ ///
+ internal static class ColorTables
+ {
+ ///
+ /// Normalized[i] == i / 255f
+ ///
+ public static readonly float[] Normalized = CreateNormalized();
+
+ ///
+ /// PivotRgb[i] == ColorXyz.PivotRgb(i / 255f)
+ ///
+ public static readonly float[] PivotRgb = CreatePivotRgb();
+
+ private static float[] CreateNormalized()
+ {
+ var table = new float[256];
+ for (var i = 0; i < table.Length; i++)
+ {
+ table[i] = i / 255f;
+ }
+ return table;
+ }
+
+ private static float[] CreatePivotRgb()
+ {
+ var table = new float[256];
+ for (var i = 0; i < table.Length; i++)
+ {
+ var n = i / 255f;
+ table[i] = (n > 0.04045f ? MathF.Pow((n + 0.055f) / 1.055f, 2.4f) : n / 12.92f) * 100;
+ }
+ return table;
+ }
+ }
+
public struct ColorRgba32 : IEquatable
{
public byte r, g, b, a;
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public ColorRgba32(byte r, byte g, byte b, byte a)
{
this.r = r;
@@ -199,12 +239,13 @@ public ColorRgbaFloat(float r, float g, float b, float a)
this.a = a;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public ColorRgbaFloat(ColorRgba32 other)
{
- this.r = other.r / 255f;
- this.g = other.g / 255f;
- this.b = other.b / 255f;
- this.a = other.a / 255f;
+ this.r = ColorTables.Normalized[other.r];
+ this.g = ColorTables.Normalized[other.g];
+ this.b = ColorTables.Normalized[other.b];
+ this.a = ColorTables.Normalized[other.a];
}
public ColorRgbaFloat(float r, float g, float b)
@@ -316,6 +357,7 @@ public struct ColorRgbFloat : IEquatable
{
public float r, g, b;
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public ColorRgbFloat(float r, float g, float b)
{
this.r = r;
@@ -323,13 +365,15 @@ public ColorRgbFloat(float r, float g, float b)
this.b = b;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public ColorRgbFloat(ColorRgba32 other)
{
- this.r = other.r / 255f;
- this.g = other.g / 255f;
- this.b = other.b / 255f;
+ this.r = ColorTables.Normalized[other.r];
+ this.g = ColorTables.Normalized[other.g];
+ this.b = ColorTables.Normalized[other.b];
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public ColorRgbFloat(Vector3 vector)
{
r = vector.X;
@@ -426,11 +470,13 @@ public ColorRgba32 ToRgba32()
);
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector3 ToVector3()
{
return new Vector3(r, g, b);
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
internal float CalcLogDist(ColorRgbFloat other)
{
var dr = Math.Sign(other.r) * MathF.Log(1 + MathF.Abs(other.r)) - Math.Sign(r) * MathF.Log(1 + MathF.Abs(r));
@@ -439,6 +485,7 @@ internal float CalcLogDist(ColorRgbFloat other)
return MathF.Sqrt((dr * dr) + (dg * dg) + (db * db));
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
internal float CalcDist(ColorRgbFloat other)
{
var dr = other.r - r;
@@ -478,17 +525,19 @@ public ColorYCbCr(float y, float cb, float cr)
this.cr = cr;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
internal ColorYCbCr(ColorRgb24 rgb)
{
- var fr = (float)rgb.r / 255;
- var fg = (float)rgb.g / 255;
- var fb = (float)rgb.b / 255;
+ var fr = ColorTables.Normalized[rgb.r];
+ var fg = ColorTables.Normalized[rgb.g];
+ var fb = ColorTables.Normalized[rgb.b];
y = 0.2989f * fr + 0.5866f * fg + 0.1145f * fb;
cb = -0.1687f * fr - 0.3313f * fg + 0.5000f * fb;
cr = 0.5000f * fr - 0.4184f * fg - 0.0816f * fb;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
internal ColorYCbCr(ColorRgbaFloat rgb)
{
var fr = rgb.r;
@@ -500,6 +549,7 @@ internal ColorYCbCr(ColorRgbaFloat rgb)
cr = 0.5000f * fr - 0.4184f * fg - 0.0816f * fb;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
internal ColorYCbCr(ColorRgbFloat rgb)
{
var fr = rgb.r;
@@ -511,28 +561,31 @@ internal ColorYCbCr(ColorRgbFloat rgb)
cr = 0.5000f * fr - 0.4184f * fg - 0.0816f * fb;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
internal ColorYCbCr(ColorRgb565 rgb)
{
- var fr = (float)rgb.R / 255;
- var fg = (float)rgb.G / 255;
- var fb = (float)rgb.B / 255;
+ var fr = ColorTables.Normalized[rgb.R];
+ var fg = ColorTables.Normalized[rgb.G];
+ var fb = ColorTables.Normalized[rgb.B];
y = 0.2989f * fr + 0.5866f * fg + 0.1145f * fb;
cb = -0.1687f * fr - 0.3313f * fg + 0.5000f * fb;
cr = 0.5000f * fr - 0.4184f * fg - 0.0816f * fb;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public ColorYCbCr(ColorRgba32 rgba)
{
- var fr = (float)rgba.r / 255;
- var fg = (float)rgba.g / 255;
- var fb = (float)rgba.b / 255;
+ var fr = ColorTables.Normalized[rgba.r];
+ var fg = ColorTables.Normalized[rgba.g];
+ var fb = ColorTables.Normalized[rgba.b];
y = 0.2989f * fr + 0.5866f * fg + 0.1145f * fb;
cb = -0.1687f * fr - 0.3313f * fg + 0.5000f * fb;
cr = 0.5000f * fr - 0.4184f * fg - 0.0816f * fb;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public ColorYCbCr(Vector3 vec)
{
var fr = (float)vec.X;
@@ -571,13 +624,24 @@ public override string ToString()
return $"r : {r * 255} g : {g * 255} b : {b * 255}";
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public float CalcDistWeighted(ColorYCbCr other, float yWeight = 4)
{
- var dy = (y - other.y) * (y - other.y) * yWeight;
- var dcb = (cb - other.cb) * (cb - other.cb);
- var dcr = (cr - other.cr) * (cr - other.cr);
+ return MathF.Sqrt(CalcDistWeightedSquared(other, yWeight));
+ }
+
+ ///
+ /// Squared counterpart of . Ordering is identical, so it can
+ /// replace it in nearest-color searches without taking a square root per candidate.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public float CalcDistWeightedSquared(ColorYCbCr other, float yWeight = 4)
+ {
+ var dy = y - other.y;
+ var dcb = cb - other.cb;
+ var dcr = cr - other.cr;
- return MathF.Sqrt(dy + dcb + dcr);
+ return dy * dy * yWeight + dcb * dcb + dcr * dcr;
}
public static ColorYCbCr operator +(ColorYCbCr left, ColorYCbCr right)
@@ -636,11 +700,13 @@ public override int GetHashCode()
public byte Mode
{
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
readonly get
{
var mode = (data & ModeMask) >> ModeShift;
return (byte)mode;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
set
{
var mode = value;
@@ -651,11 +717,13 @@ readonly get
public byte R
{
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
readonly get
{
var r5 = (data & RedMask) >> RedShift;
return (byte)((r5 << 3) | (r5 >> 2));
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
set
{
var r5 = value >> 3;
@@ -666,11 +734,13 @@ readonly get
public byte G
{
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
readonly get
{
var g5 = (data & GreenMask) >> GreenShift;
return (byte)((g5 << 3) | (g5 >> 2));
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
set
{
var g5 = value >> 3;
@@ -681,11 +751,13 @@ readonly get
public byte B
{
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
readonly get
{
var b5 = data & BlueMask;
return (byte)((b5 << 3) | (b5 >> 2));
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
set
{
var b5 = value >> 3;
@@ -697,6 +769,7 @@ readonly get
public int RawR
{
readonly get => (data & RedMask) >> RedShift;
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
set
{
if (value > 31) value = 31;
@@ -709,6 +782,7 @@ public int RawR
public int RawG
{
readonly get => (data & GreenMask) >> GreenShift;
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
set
{
if (value > 31) value = 31;
@@ -721,6 +795,7 @@ public int RawG
public int RawB
{
readonly get => data & BlueMask;
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
set
{
if (value > 31) value = 31;
@@ -754,6 +829,7 @@ public ColorRgb555(ColorRgb24 color)
B = color.b;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly ColorRgb24 ToColorRgb24()
{
return new ColorRgb24(R, G, B);
@@ -807,11 +883,13 @@ public override int GetHashCode()
public byte R
{
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
readonly get
{
var r5 = (data & RedMask) >> RedShift;
return (byte)((r5 << 3) | (r5 >> 2));
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
set
{
var r5 = value >> 3;
@@ -822,11 +900,13 @@ readonly get
public byte G
{
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
readonly get
{
var g6 = (data & GreenMask) >> GreenShift;
return (byte)((g6 << 2) | (g6 >> 4));
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
set
{
var g6 = value >> 2;
@@ -837,11 +917,13 @@ readonly get
public byte B
{
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
readonly get
{
var b5 = data & BlueMask;
return (byte)((b5 << 3) | (b5 >> 2));
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
set
{
var b5 = value >> 3;
@@ -853,6 +935,7 @@ readonly get
public int RawR
{
readonly get { return (data & RedMask) >> RedShift; }
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
set
{
if (value > 31) value = 31;
@@ -865,6 +948,7 @@ public int RawR
public int RawG
{
readonly get { return (data & GreenMask) >> GreenShift; }
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
set
{
if (value > 63) value = 63;
@@ -877,6 +961,7 @@ public int RawG
public int RawB
{
readonly get { return data & BlueMask; }
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
set
{
if (value > 31) value = 31;
@@ -910,6 +995,7 @@ public ColorRgb565(ColorRgb24 color)
B = color.b;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly ColorRgb24 ToColorRgb24()
{
return new ColorRgb24(R, G, B);
@@ -930,6 +1016,7 @@ internal struct ColorRgb24 : IEquatable
{
public byte r, g, b;
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public ColorRgb24(byte r, byte g, byte b)
{
this.r = r;
@@ -937,6 +1024,7 @@ public ColorRgb24(byte r, byte g, byte b)
this.b = b;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public ColorRgb24(ColorRgb565 color)
{
this.r = color.R;
@@ -944,6 +1032,7 @@ public ColorRgb24(ColorRgb565 color)
this.b = color.B;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public ColorRgb24(ColorRgba32 color)
{
this.r = color.r;
@@ -1037,11 +1126,12 @@ public ColorYCbCrAlpha(float y, float cb, float cr, float alpha)
this.alpha = alpha;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public ColorYCbCrAlpha(ColorRgb24 rgb)
{
- var fr = (float)rgb.r / 255;
- var fg = (float)rgb.g / 255;
- var fb = (float)rgb.b / 255;
+ var fr = ColorTables.Normalized[rgb.r];
+ var fg = ColorTables.Normalized[rgb.g];
+ var fb = ColorTables.Normalized[rgb.b];
y = 0.2989f * fr + 0.5866f * fg + 0.1145f * fb;
cb = -0.1687f * fr - 0.3313f * fg + 0.5000f * fb;
@@ -1049,11 +1139,12 @@ public ColorYCbCrAlpha(ColorRgb24 rgb)
alpha = 1;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public ColorYCbCrAlpha(ColorRgb565 rgb)
{
- var fr = (float)rgb.R / 255;
- var fg = (float)rgb.G / 255;
- var fb = (float)rgb.B / 255;
+ var fr = ColorTables.Normalized[rgb.R];
+ var fg = ColorTables.Normalized[rgb.G];
+ var fb = ColorTables.Normalized[rgb.B];
y = 0.2989f * fr + 0.5866f * fg + 0.1145f * fb;
cb = -0.1687f * fr - 0.3313f * fg + 0.5000f * fb;
@@ -1061,18 +1152,20 @@ public ColorYCbCrAlpha(ColorRgb565 rgb)
alpha = 1;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public ColorYCbCrAlpha(ColorRgba32 rgba)
{
- var fr = (float)rgba.r / 255;
- var fg = (float)rgba.g / 255;
- var fb = (float)rgba.b / 255;
+ var fr = ColorTables.Normalized[rgba.r];
+ var fg = ColorTables.Normalized[rgba.g];
+ var fb = ColorTables.Normalized[rgba.b];
y = 0.2989f * fr + 0.5866f * fg + 0.1145f * fb;
cb = -0.1687f * fr - 0.3313f * fg + 0.5000f * fb;
cr = 0.5000f * fr - 0.4184f * fg - 0.0816f * fb;
- alpha = rgba.a / 255f;
+ alpha = ColorTables.Normalized[rgba.a];
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public ColorYCbCrAlpha(ColorRgbaFloat rgba)
{
var fr = rgba.r;
@@ -1086,6 +1179,20 @@ public ColorYCbCrAlpha(ColorRgbaFloat rgba)
}
+ ///
+ /// Converts four colors at once, given their already normalized channels. Produces exactly
+ /// what the constructor above would, one color per vector lane,
+ /// and keeps the coefficients defined in a single place.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void FromNormalized(Vector4 r, Vector4 g, Vector4 b,
+ out Vector4 y, out Vector4 cb, out Vector4 cr)
+ {
+ y = r * 0.2989f + g * 0.5866f + b * 0.1145f;
+ cb = r * -0.1687f - g * 0.3313f + b * 0.5000f;
+ cr = r * 0.5000f - g * 0.4184f - b * 0.0816f;
+ }
+
public ColorRgb565 ToColorRgb565()
{
var r = Math.Max(0.0f, Math.Min(1.0f, (float)(y + 0.0000 * cb + 1.4022 * cr)));
@@ -1104,14 +1211,25 @@ public override string ToString()
return $"r : {r * 255} g : {g * 255} b : {b * 255}";
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public float CalcDistWeighted(ColorYCbCrAlpha other, float yWeight = 4, float aWeight = 1)
{
- var dy = (y - other.y) * (y - other.y) * yWeight;
- var dcb = (cb - other.cb) * (cb - other.cb);
- var dcr = (cr - other.cr) * (cr - other.cr);
- var da = (alpha - other.alpha) * (alpha - other.alpha) * aWeight;
+ return MathF.Sqrt(CalcDistWeightedSquared(other, yWeight, aWeight));
+ }
+
+ ///
+ /// Squared counterpart of . Ordering is identical, so it can
+ /// replace it in nearest-color searches without taking a square root per candidate.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public float CalcDistWeightedSquared(ColorYCbCrAlpha other, float yWeight = 4, float aWeight = 1)
+ {
+ var dy = y - other.y;
+ var dcb = cb - other.cb;
+ var dcr = cr - other.cr;
+ var da = alpha - other.alpha;
- return MathF.Sqrt(dy + dcb + dcr + da);
+ return dy * dy * yWeight + dcb * dcb + dcr * dcr + da * da * aWeight;
}
public static ColorYCbCrAlpha operator +(ColorYCbCrAlpha left, ColorYCbCrAlpha right)
@@ -1146,11 +1264,13 @@ public ColorXyz(float x, float y, float z)
this.z = z;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public ColorXyz(ColorRgb24 color)
{
this = ColorToXyz(color);
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public ColorXyz(ColorRgbFloat color)
{
this = ColorToXyz(color);
@@ -1165,11 +1285,13 @@ public ColorRgbFloat ToColorRgbFloat()
);
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ColorXyz ColorToXyz(ColorRgb24 color)
{
- var r = PivotRgb(color.r / 255.0f);
- var g = PivotRgb(color.g / 255.0f);
- var b = PivotRgb(color.b / 255.0f);
+ var pivot = ColorTables.PivotRgb;
+ var r = pivot[color.r];
+ var g = pivot[color.g];
+ var b = pivot[color.b];
// Observer. = 2°, Illuminant = D65
return new ColorXyz(r * 0.4124f + g * 0.3576f + b * 0.1805f, r * 0.2126f + g * 0.7152f + b * 0.0722f, r * 0.0193f + g * 0.1192f + b * 0.9505f);
@@ -1197,6 +1319,7 @@ internal struct ColorLab
public float a;
public float b;
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public ColorLab(float l, float a, float b)
{
this.l = l;
@@ -1204,21 +1327,25 @@ public ColorLab(float l, float a, float b)
this.b = b;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public ColorLab(ColorRgb24 color)
{
this = ColorToLab(color);
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public ColorLab(ColorRgba32 color)
{
this = ColorToLab(new ColorRgb24(color.r, color.g, color.b));
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public ColorLab(ColorRgbFloat color)
{
this = XyzToLab(new ColorXyz(color));
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ColorLab ColorToLab(ColorRgb24 color)
{
var xyz = new ColorXyz(color);
@@ -1226,6 +1353,7 @@ public static ColorLab ColorToLab(ColorRgb24 color)
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ColorLab XyzToLab(ColorXyz xyz)
{
var refX = 95.047f; // Observer= 2°, Illuminant= D65
@@ -1239,6 +1367,7 @@ public static ColorLab XyzToLab(ColorXyz xyz)
return new ColorLab(116 * y - 16, 500 * (x - y), 200 * (y - z));
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static float PivotXyz(float n)
{
var i = MathCbrt.Cbrt(n);
diff --git a/BCnEnc.Net/Shared/LinearClustering.cs b/BCnEnc.Net/Shared/LinearClustering.cs
index 7e23f5b..b04e632 100644
--- a/BCnEnc.Net/Shared/LinearClustering.cs
+++ b/BCnEnc.Net/Shared/LinearClustering.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Runtime.CompilerServices;
#if NETSTANDARD2_0
using Array = BCnEncoder.Shared.ArrayPolyfills;
@@ -22,6 +23,7 @@ private struct LabXy
public float y;
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static LabXy operator +(LabXy left, LabXy right)
{
return new LabXy()
@@ -34,6 +36,7 @@ private struct LabXy
};
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static LabXy operator /(LabXy left, int right)
{
return new LabXy()
@@ -56,6 +59,7 @@ private struct ClusterCenter
public float y;
public int count;
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public ClusterCenter(LabXy labxy)
{
this.l = labxy.l;
@@ -66,32 +70,35 @@ public ClusterCenter(LabXy labxy)
count = 0;
}
- public readonly float Distance(LabXy other, float m, float s)
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public readonly float Distance(LabXy other, float mOverS)
{
- var dLab = MathF.Sqrt(
- MathF.Pow(l - other.l, 2) +
- MathF.Pow(a - other.a, 2) +
- MathF.Pow(b - other.b, 2));
-
- var dXy = MathF.Sqrt(
- MathF.Pow(x - other.x, 2) +
- MathF.Pow(y - other.y, 2));
- return dLab + m / s * dXy;
+ var dl = l - other.l;
+ var da = a - other.a;
+ var db = b - other.b;
+ var dx = x - other.x;
+ var dy = y - other.y;
+
+ var dLab = MathF.Sqrt(dl * dl + da * da + db * db);
+ var dXy = MathF.Sqrt(dx * dx + dy * dy);
+ return dLab + mOverS * dXy;
}
- public readonly float Distance(ClusterCenter other, float m, float s)
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public readonly float Distance(ClusterCenter other, float mOverS)
{
- var dLab = MathF.Sqrt(
- (l - other.l) * (l - other.l) +
- (a - other.a) * (a - other.a) +
- (b - other.b) * (b - other.b));
-
- var dXy = MathF.Sqrt(
- (x - other.x) * (x - other.x) +
- (y - other.y) * (y - other.y));
- return dLab + m / s * dXy;
+ var dl = l - other.l;
+ var da = a - other.a;
+ var db = b - other.b;
+ var dx = x - other.x;
+ var dy = y - other.y;
+
+ var dLab = MathF.Sqrt(dl * dl + da * da + db * db);
+ var dXy = MathF.Sqrt(dx * dx + dy * dy);
+ return dLab + mOverS * dXy;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ClusterCenter operator +(ClusterCenter left, LabXy right)
{
return new ClusterCenter()
@@ -105,6 +112,7 @@ public readonly float Distance(ClusterCenter other, float m, float s)
};
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ClusterCenter operator /(ClusterCenter left, int right)
{
return new ClusterCenter()
@@ -127,79 +135,12 @@ public readonly float Distance(ClusterCenter other, float m, float s)
public static int[] ClusterPixels(ReadOnlySpan pixels, int width, int height,
int clusters, float m = 10, int maxIterations = 10, bool enforceConnectivity = true)
{
-
- var floats = new ColorRgbFloat[pixels.Length];
- for (var i = 0; i < pixels.Length; i++)
- {
- floats[i] = pixels[i].ToRgbFloat();
- }
-
- return ClusterPixels(floats, width, height, clusters, m, maxIterations, enforceConnectivity);
-
- //Grid interval S
- var s = MathF.Sqrt(pixels.Length / (float)clusters);
- var clusterIndices = new int[pixels.Length];
-
- var labXys = ConvertToLabXy(pixels, width, height);
-
-
- Span clusterCenters = InitialClusterCenters(width, height, clusters, s, labXys);
- Span previousCenters = new ClusterCenter[clusters];
-
- float error = 999;
- const float threshold = 0.1f;
- var iter = 0;
- while (error > threshold)
+ if (clusters < 2)
{
- if (maxIterations > 0 && iter >= maxIterations)
- {
- break;
- }
- iter++;
-
- clusterCenters.CopyTo(previousCenters);
-
- Array.Fill(clusterIndices, -1);
-
- // Find closest cluster for pixels
- for (var j = 0; j < clusters; j++)
- {
- var xL = Math.Max(0, (int)(clusterCenters[j].x - s));
- var xH = Math.Min(width, (int)(clusterCenters[j].x + s));
- var yL = Math.Max(0, (int)(clusterCenters[j].y - s));
- var yH = Math.Min(height, (int)(clusterCenters[j].y + s));
-
- for (var x = xL; x < xH; x++)
- {
- for (var y = yL; y < yH; y++)
- {
- var i = x + y * width;
-
- if (clusterIndices[i] == -1)
- {
- clusterIndices[i] = j;
- }
- else
- {
- var prevDistance = clusterCenters[clusterIndices[i]].Distance(labXys[i], m, s);
- var distance = clusterCenters[j].Distance(labXys[i], m, s);
- if (distance < prevDistance)
- {
- clusterIndices[i] = j;
- }
- }
- }
- }
- }
-
- error = RecalculateCenters(clusters, m, labXys, clusterIndices, previousCenters, s, ref clusterCenters);
- }
-
- if (enforceConnectivity) {
- clusterIndices = EnforceConnectivity(clusterIndices, width, height, clusters);
+ throw new ArgumentException("Number of clusters should be more than 1");
}
- return clusterIndices;
+ return Cluster(ConvertToLabXy(pixels, width, height), width, height, clusters, m, maxIterations, enforceConnectivity);
}
///
@@ -216,12 +157,16 @@ public static int[] ClusterPixels(ReadOnlySpan pixels, int width,
throw new ArgumentException("Number of clusters should be more than 1");
}
- //Grid interval S
- var s = MathF.Sqrt(pixels.Length / (float)clusters);
- var clusterIndices = new int[pixels.Length];
-
- var labXys = ConvertToLabXy(pixels, width, height);
+ return Cluster(ConvertToLabXy(pixels, width, height), width, height, clusters, m, maxIterations, enforceConnectivity);
+ }
+ private static int[] Cluster(LabXy[] labXys, int width, int height,
+ int clusters, float m, int maxIterations, bool enforceConnectivity)
+ {
+ //Grid interval S
+ var s = MathF.Sqrt(labXys.Length / (float)clusters);
+ var mOverS = m / s;
+ var clusterIndices = new int[labXys.Length];
Span clusterCenters = InitialClusterCenters(width, height, clusters, s, labXys);
Span previousCenters = new ClusterCenter[clusters];
@@ -244,10 +189,11 @@ public static int[] ClusterPixels(ReadOnlySpan pixels, int width,
// Find closest cluster for pixels
for (var j = 0; j < clusters; j++)
{
- var xL = Math.Max(0, (int)(clusterCenters[j].x - s));
- var xH = Math.Min(width, (int)(clusterCenters[j].x + s));
- var yL = Math.Max(0, (int)(clusterCenters[j].y - s));
- var yH = Math.Min(height, (int)(clusterCenters[j].y + s));
+ var center = clusterCenters[j];
+ var xL = Math.Max(0, (int)(center.x - s));
+ var xH = Math.Min(width, (int)(center.x + s));
+ var yL = Math.Max(0, (int)(center.y - s));
+ var yH = Math.Min(height, (int)(center.y + s));
for (var x = xL; x < xH; x++)
{
@@ -261,8 +207,9 @@ public static int[] ClusterPixels(ReadOnlySpan pixels, int width,
}
else
{
- var prevDistance = clusterCenters[clusterIndices[i]].Distance(labXys[i], m, s);
- var distance = clusterCenters[j].Distance(labXys[i], m, s);
+ var labXy = labXys[i];
+ var prevDistance = clusterCenters[clusterIndices[i]].Distance(labXy, mOverS);
+ var distance = center.Distance(labXy, mOverS);
if (distance < prevDistance)
{
clusterIndices[i] = j;
@@ -272,7 +219,7 @@ public static int[] ClusterPixels(ReadOnlySpan pixels, int width,
}
}
- error = RecalculateCenters(clusters, m, labXys, clusterIndices, previousCenters, s, ref clusterCenters);
+ error = RecalculateCenters(clusters, labXys, clusterIndices, previousCenters, mOverS, ref clusterCenters);
}
if (enforceConnectivity)
@@ -283,31 +230,32 @@ public static int[] ClusterPixels(ReadOnlySpan pixels, int width,
return clusterIndices;
}
- private static float RecalculateCenters(int clusters, float m, LabXy[] labXys, int[] clusterIndices,
- Span previousCenters, float s, ref Span clusterCenters)
+ private static float RecalculateCenters(int clusters, LabXy[] labXys, int[] clusterIndices,
+ Span previousCenters, float mOverS, ref Span clusterCenters)
{
clusterCenters.Clear();
for (var i = 0; i < labXys.Length; i++)
{
+ var labXy = labXys[i];
var clusterIndex = clusterIndices[i];
// Sometimes a pixel is out of the range of any cluster,
// in that case, find the nearest cluster and add it to it
if (clusterIndex == -1)
{
var bestCluster = 0;
- var bestDistance = previousCenters[0].Distance(labXys[i], m, s);
+ var bestDistance = previousCenters[0].Distance(labXy, mOverS);
for (var j = 1; j < clusters; j++) {
- var dist = previousCenters[j].Distance(labXys[i], m, s);
+ var dist = previousCenters[j].Distance(labXy, mOverS);
if (dist < bestDistance) {
bestDistance = dist;
bestCluster = j;
}
}
- clusterCenters[bestCluster] += labXys[i];
+ clusterCenters[bestCluster] += labXy;
clusterIndices[i] = bestCluster;
}
else {
- clusterCenters[clusterIndex] += labXys[i];
+ clusterCenters[clusterIndex] += labXy;
}
}
@@ -317,7 +265,7 @@ private static float RecalculateCenters(int clusters, float m, LabXy[] labXys, i
if (clusterCenters[i].count > 0)
{
clusterCenters[i] /= clusterCenters[i].count;
- error += clusterCenters[i].Distance(previousCenters[i], m, s);
+ error += clusterCenters[i].Distance(previousCenters[i], mOverS);
}
}
@@ -383,9 +331,9 @@ private static LabXy[] ConvertToLabXy(ReadOnlySpan pixels, int widt
{
var labXys = new LabXy[pixels.Length];
//Convert pixels to LabXy
- for (var x = 0; x < width; x++)
+ for (var y = 0; y < height; y++)
{
- for (var y = 0; y < height; y++)
+ for (var x = 0; x < width; x++)
{
var i = x + y * width;
var lab = new ColorLab(pixels[i]);
@@ -407,9 +355,9 @@ private static LabXy[] ConvertToLabXy(ReadOnlySpan pixels, int wi
{
var labXys = new LabXy[pixels.Length];
//Convert pixels to LabXy
- for (var x = 0; x < width; x++)
+ for (var y = 0; y < height; y++)
{
- for (var y = 0; y < height; y++)
+ for (var x = 0; x < width; x++)
{
var i = x + y * width;
var lab = new ColorLab(pixels[i]);
diff --git a/BCnEnc.Net/Shared/MathHelper.cs b/BCnEnc.Net/Shared/MathHelper.cs
index d5a47d9..c36eb50 100644
--- a/BCnEnc.Net/Shared/MathHelper.cs
+++ b/BCnEnc.Net/Shared/MathHelper.cs
@@ -1,12 +1,16 @@
using System;
using System.Collections.Generic;
+using System.Runtime.CompilerServices;
using System.Text;
namespace BCnEncoder.Shared
{
public static unsafe class MathHelper
{
- private static double two54 = 1.80143985094819840000e+16; /* 0x43500000, 0x00000000 */
+ private const double two54 = 1.80143985094819840000e+16; /* 0x43500000, 0x00000000 */
+
+ private const int MinExponent = -126;
+ private const int MaxExponent = 127;
// FrExp modified to C# from http://www.netlib.org
/* @(#)fdlibm.h 1.5 04/04/22 */
@@ -25,6 +29,7 @@ public static unsafe class MathHelper
///
///
/// Returns the normalized fraction m. If x is 0, the function returns 0 for both the fraction and exponent. The fraction has the same sign as the argument x. The result of the function cannot have a range error.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double FrExp(double x, out int eptr)
{
unchecked
@@ -57,9 +62,25 @@ public static double FrExp(double x, out int eptr)
///
///
///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static float LdExp(float arg, int exp)
{
+ if (exp >= MinExponent && exp <= MaxExponent)
+ {
+ return arg * Exp2(exp);
+ }
+
return arg * MathF.Pow(2, exp);
}
+
+ ///
+ /// Builds 2^exp directly from its IEEE-754 exponent field. Only valid for normal exponents.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static float Exp2(int exp)
+ {
+ var bits = (exp + 127) << 23;
+ return *(float*)&bits;
+ }
}
}
diff --git a/BCnEnc.Net/Shared/PcaVectors.cs b/BCnEnc.Net/Shared/PcaVectors.cs
index 63c2685..c3ad1e0 100644
--- a/BCnEnc.Net/Shared/PcaVectors.cs
+++ b/BCnEnc.Net/Shared/PcaVectors.cs
@@ -1,5 +1,6 @@
using System;
using System.Numerics;
+using System.Runtime.CompilerServices;
namespace BCnEncoder.Shared
{
@@ -10,12 +11,11 @@ internal static class PcaVectors
private static void ConvertToVector4(ReadOnlySpan colors, Span vectors)
{
+ var normalized = ColorTables.Normalized;
for (var i = 0; i < colors.Length; i++)
{
- vectors[i].X += colors[i].r / 255f;
- vectors[i].Y += colors[i].g / 255f;
- vectors[i].Z += colors[i].b / 255f;
- vectors[i].W += colors[i].a / 255f;
+ var c = colors[i];
+ vectors[i] = new Vector4(normalized[c.r], normalized[c.g], normalized[c.b], normalized[c.a]);
}
}
@@ -23,35 +23,20 @@ private static void ConvertToVector4(ReadOnlySpan colors, Span colors)
+ private static Vector4 CalculateMean(ReadOnlySpan colors)
{
-
- float r = 0;
- float g = 0;
- float b = 0;
- float a = 0;
-
+ var sum = Vector4.Zero;
for (var i = 0; i < colors.Length; i++)
{
- r += colors[i].X;
- g += colors[i].Y;
- b += colors[i].Z;
- a += colors[i].W;
+ sum += colors[i];
}
- return new Vector4(
- r / colors.Length,
- g / colors.Length,
- b / colors.Length,
- a / colors.Length
- );
+ return sum / colors.Length;
}
internal static Matrix4x4 CalculateCovariance(Span values, out Vector4 mean) {
@@ -61,36 +46,33 @@ internal static Matrix4x4 CalculateCovariance(Span values, out Vector4
values[i] -= mean;
}
- //4x4 matrix
- var mat = new Matrix4x4();
+ // Each row of the symmetric covariance matrix is one broadcasted component times the
+ // whole vector, so the outer product accumulates four products per SIMD multiply.
+ var row1 = Vector4.Zero;
+ var row2 = Vector4.Zero;
+ var row3 = Vector4.Zero;
+ var row4 = Vector4.Zero;
for (var i = 0; i < values.Length; i++)
{
- mat.M11 += values[i].X * values[i].X;
- mat.M12 += values[i].X * values[i].Y;
- mat.M13 += values[i].X * values[i].Z;
- mat.M14 += values[i].X * values[i].W;
-
- mat.M22 += values[i].Y * values[i].Y;
- mat.M23 += values[i].Y * values[i].Z;
- mat.M24 += values[i].Y * values[i].W;
-
- mat.M33 += values[i].Z * values[i].Z;
- mat.M34 += values[i].Z * values[i].W;
-
- mat.M44 += values[i].W * values[i].W;
+ var v = values[i];
+ row1 += new Vector4(v.X) * v;
+ row2 += new Vector4(v.Y) * v;
+ row3 += new Vector4(v.Z) * v;
+ row4 += new Vector4(v.W) * v;
}
- mat = Matrix4x4.Multiply(mat, 1f / (values.Length - 1));
-
- mat.M21 = mat.M12;
- mat.M31 = mat.M13;
- mat.M32 = mat.M23;
- mat.M41 = mat.M14;
- mat.M42 = mat.M24;
- mat.M43 = mat.M34;
-
- return mat;
+ var scale = 1f / (values.Length - 1);
+ row1 *= scale;
+ row2 *= scale;
+ row3 *= scale;
+ row4 *= scale;
+
+ return new Matrix4x4(
+ row1.X, row1.Y, row1.Z, row1.W,
+ row2.X, row2.Y, row2.Z, row2.W,
+ row3.X, row3.Y, row3.Z, row3.W,
+ row4.X, row4.Y, row4.Z, row4.W);
}
///
@@ -99,16 +81,27 @@ internal static Matrix4x4 CalculateCovariance(Span values, out Vector4
///
///
internal static Vector4 CalculatePrincipalAxis(Matrix4x4 covarianceMatrix) {
+ // Held as rows so the iteration below is four multiply-adds instead of a matrix copy.
+ var row1 = new Vector4(covarianceMatrix.M11, covarianceMatrix.M12, covarianceMatrix.M13, covarianceMatrix.M14);
+ var row2 = new Vector4(covarianceMatrix.M21, covarianceMatrix.M22, covarianceMatrix.M23, covarianceMatrix.M24);
+ var row3 = new Vector4(covarianceMatrix.M31, covarianceMatrix.M32, covarianceMatrix.M33, covarianceMatrix.M34);
+ var row4 = new Vector4(covarianceMatrix.M41, covarianceMatrix.M42, covarianceMatrix.M43, covarianceMatrix.M44);
+
var lastDa = Vector4.UnitY;
for (var i = 0; i < 30; i++) {
- var dA = Vector4.Transform(lastDa, covarianceMatrix);
-
- if(dA.LengthSquared() == 0) {
+ var dA = new Vector4(lastDa.X) * row1
+ + new Vector4(lastDa.Y) * row2
+ + new Vector4(lastDa.Z) * row3
+ + new Vector4(lastDa.W) * row4;
+
+ // Vector4.Normalize would recompute the squared length internally, so divide directly.
+ var lengthSquared = dA.LengthSquared();
+ if(lengthSquared == 0) {
break;
}
- dA = Vector4.Normalize(dA);
+ dA /= MathF.Sqrt(lengthSquared);
if (Vector4.Dot(lastDa, dA) > 0.999999) {
lastDa = dA;
break;
@@ -178,10 +171,12 @@ public static void GetExtremePoints(Span colors, Vector3 mean, Vect
float minD = 0;
float maxD = 0;
+ var normalized = ColorTables.Normalized;
for (var i = 0; i < colors.Length; i++)
{
- var colorVec = new Vector3(colors[i].r / 255f, colors[i].g / 255f, colors[i].b / 255f);
+ var c = colors[i];
+ var colorVec = new Vector3(normalized[c.r], normalized[c.g], normalized[c.b]);
var v = colorVec - mean;
var d = Vector3.Dot(v, principalAxis);
@@ -218,10 +213,12 @@ public static void GetMinMaxColor565(Span colors, Vector3 mean, Vec
float minD = 0;
float maxD = 0;
+ var normalized = ColorTables.Normalized;
for (var i = 0; i < colors.Length; i++)
{
- var colorVec = new Vector3(colors[i].r / 255f, colors[i].g / 255f, colors[i].b / 255f);
+ var c = colors[i];
+ var colorVec = new Vector3(normalized[c.r], normalized[c.g], normalized[c.b]);
var v = colorVec - mean;
var d = Vector3.Dot(v, principalAxis);
@@ -272,10 +269,12 @@ public static void GetExtremePointsWithAlpha(Span colors, Vector4 m
float minD = 0;
float maxD = 0;
+ var normalized = ColorTables.Normalized;
for (var i = 0; i < colors.Length; i++)
{
- var colorVec = new Vector4(colors[i].r / 255f, colors[i].g / 255f, colors[i].b / 255f, colors[i].a / 255f);
+ var c = colors[i];
+ var colorVec = new Vector4(normalized[c.r], normalized[c.g], normalized[c.b], normalized[c.a]);
var v = colorVec - mean;
var d = Vector4.Dot(v, principalAxis);