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);