From ccd4a8d5c8f1b2f368c40ae170404d7353c8bea6 Mon Sep 17 00:00:00 2001 From: Darth Affe Date: Sun, 7 Jul 2024 17:57:08 +0200 Subject: [PATCH] Implemented a first draft of a ColorFormat-conversion --- HPPH.Generators/ColorFormatData.cs | 51 +++++- HPPH.Generators/Features/Colors.cs | 4 +- HPPH.Test/ConvertTests.cs | 88 ++++++++++ HPPH/Colors/IColorFormat.cs | 2 + .../ColorFormatABGR.g.cs | 2 + .../ColorFormatARGB.g.cs | 2 + .../ColorFormatBGR.g.cs | 2 + .../ColorFormatBGRA.g.cs | 2 + .../ColorFormatRGB.g.cs | 2 + .../ColorFormatRGBA.g.cs | 2 + ...es.cs.g.cs => IColorFormat.Instances.g.cs} | 0 HPPH/PixelHelper.Convert.cs | 155 ++++++++++++++++++ 12 files changed, 309 insertions(+), 3 deletions(-) create mode 100644 HPPH.Test/ConvertTests.cs rename HPPH/Generated/HPPH.Generators/HPPH.Generators.ColorSourceGenerator/{IColorFormat.Instances.cs.g.cs => IColorFormat.Instances.g.cs} (100%) create mode 100644 HPPH/PixelHelper.Convert.cs diff --git a/HPPH.Generators/ColorFormatData.cs b/HPPH.Generators/ColorFormatData.cs index 02625b7..ae5c62c 100644 --- a/HPPH.Generators/ColorFormatData.cs +++ b/HPPH.Generators/ColorFormatData.cs @@ -1,9 +1,12 @@ -using System.Text; +using System; +using System.Text; namespace HPPH.Generators; internal readonly struct ColorFormatData(string typeName, int bpp, char firstEntry, char secondEntry, char thirdEntry, char fourthEntry) { + #region Properties & Fields + public readonly string TypeName = typeName; public readonly int Bpp = bpp; public readonly string FirstEntry = firstEntry.ToString().ToLowerInvariant(); @@ -16,6 +19,8 @@ internal readonly struct ColorFormatData(string typeName, int bpp, char firstEnt public string ThirdEntryName => GetEntryName(ThirdEntry); public string FourthEntryName => GetEntryName(FourthEntry); + public string ByteMapping => CreateByteMapping(); + public string Format { get @@ -38,13 +43,55 @@ internal readonly struct ColorFormatData(string typeName, int bpp, char firstEnt } } + #endregion + + #region Methods + + private string CreateByteMapping() + { + string[] mapping = new string[Bpp]; + if (Bpp > 0) + { + mapping[0] = GetByteMappingIndex(FirstEntry).ToString(); + + if (Bpp > 1) + { + mapping[1] = GetByteMappingIndex(SecondEntry).ToString(); + + if (Bpp > 2) + { + mapping[2] = GetByteMappingIndex(ThirdEntry).ToString(); + + if (Bpp > 3) + { + mapping[3] = GetByteMappingIndex(FourthEntry).ToString(); + } + } + } + } + + return string.Join(", ", mapping); + } + private static string GetEntryName(string entry) => entry switch { - "a" => "Alpha", "r" => "Red", "g" => "Green", "b" => "Blue", + "a" => "Alpha", _ => string.Empty }; + + private static int GetByteMappingIndex(string entry) + => entry switch + { + "r" => 0, + "g" => 1, + "b" => 2, + "a" => 3, + _ => throw new IndexOutOfRangeException() + }; + + #endregion } \ No newline at end of file diff --git a/HPPH.Generators/Features/Colors.cs b/HPPH.Generators/Features/Colors.cs index 1e5835c..a091c78 100644 --- a/HPPH.Generators/Features/Colors.cs +++ b/HPPH.Generators/Features/Colors.cs @@ -18,7 +18,7 @@ internal class Colors : IGeneratorFeature public IEnumerable<(string name, string source)> GenerateFor(ImmutableArray colorFormats) { - yield return ("IColorFormat.Instances.cs", GenerateColorFormats(colorFormats)); + yield return ("IColorFormat.Instances", GenerateColorFormats(colorFormats)); } private static string GenerateColorStructCode(ColorFormatData colorFormat) @@ -165,6 +165,8 @@ internal class Colors : IGeneratorFeature public string Name => "{{colorFormat.Format}}"; + ReadOnlySpan IColorFormat.ByteMapping => [{{colorFormat.ByteMapping}}]; + #endregion #region Constructors diff --git a/HPPH.Test/ConvertTests.cs b/HPPH.Test/ConvertTests.cs new file mode 100644 index 0000000..2376953 --- /dev/null +++ b/HPPH.Test/ConvertTests.cs @@ -0,0 +1,88 @@ +namespace HPPH.Test; + +[TestClass] +public class ConvertTests +{ + private static IEnumerable GetTestImages() => Directory.EnumerateFiles(@"..\..\..\..\sample_data", "*.png", SearchOption.AllDirectories); + + [TestMethod] + public void Convert3ByteSameBppRGBToBGR() + { + foreach (string image in GetTestImages()) + { + ColorRGB[] data = ImageHelper.Get3ByteColorsFromImage(image); + ReadOnlySpan referenceData = data; + + Span sourceData = new ColorRGB[referenceData.Length]; + referenceData.CopyTo(sourceData); + + Span result = PixelHelper.Convert(sourceData); + + Assert.AreEqual(referenceData.Length, result.Length); + for (int i = 0; i < referenceData.Length; i++) + { + ColorRGB reference = referenceData[i]; + ColorBGR test = result[i]; + + Assert.AreEqual(reference.R, test.R, "R differs"); + Assert.AreEqual(reference.G, test.G, "G differs"); + Assert.AreEqual(reference.B, test.B, "B differs"); + Assert.AreEqual(reference.A, test.A, "A differs"); + } + } + } + + [TestMethod] + public void Convert4ByteSameBppRGBAToARGB() + { + foreach (string image in GetTestImages()) + { + ColorRGBA[] data = ImageHelper.Get4ByteColorsFromImage(image); + ReadOnlySpan referenceData = data; + + Span sourceData = new ColorRGBA[referenceData.Length]; + referenceData.CopyTo(sourceData); + + Span result = PixelHelper.Convert(sourceData); + + Assert.AreEqual(referenceData.Length, result.Length); + for (int i = 0; i < referenceData.Length; i++) + { + ColorRGBA reference = referenceData[i]; + ColorARGB test = result[i]; + + Assert.AreEqual(reference.R, test.R, "R differs"); + Assert.AreEqual(reference.G, test.G, "G differs"); + Assert.AreEqual(reference.B, test.B, "B differs"); + Assert.AreEqual(reference.A, test.A, "A differs"); + } + } + } + + [TestMethod] + public void Convert4ByteSameBppRGBAToBGRA() + { + foreach (string image in GetTestImages()) + { + ColorRGBA[] data = ImageHelper.Get4ByteColorsFromImage(image); + ReadOnlySpan referenceData = data; + + Span sourceData = new ColorRGBA[referenceData.Length]; + referenceData.CopyTo(sourceData); + + Span result = PixelHelper.Convert(sourceData); + + Assert.AreEqual(referenceData.Length, result.Length); + for (int i = 0; i < referenceData.Length; i++) + { + ColorRGBA reference = referenceData[i]; + ColorBGRA test = result[i]; + + Assert.AreEqual(reference.R, test.R, "R differs"); + Assert.AreEqual(reference.G, test.G, "G differs"); + Assert.AreEqual(reference.B, test.B, "B differs"); + Assert.AreEqual(reference.A, test.A, "A differs"); + } + } + } +} \ No newline at end of file diff --git a/HPPH/Colors/IColorFormat.cs b/HPPH/Colors/IColorFormat.cs index 7c33a71..557c2b3 100644 --- a/HPPH/Colors/IColorFormat.cs +++ b/HPPH/Colors/IColorFormat.cs @@ -12,4 +12,6 @@ public partial interface IColorFormat int BytesPerPixel { get; } string Name { get; } + + internal ReadOnlySpan ByteMapping { get; } } \ No newline at end of file diff --git a/HPPH/Generated/HPPH.Generators/HPPH.Generators.ColorSourceGenerator/ColorFormatABGR.g.cs b/HPPH/Generated/HPPH.Generators/HPPH.Generators.ColorSourceGenerator/ColorFormatABGR.g.cs index 076f185..1ac7a80 100644 --- a/HPPH/Generated/HPPH.Generators/HPPH.Generators.ColorSourceGenerator/ColorFormatABGR.g.cs +++ b/HPPH/Generated/HPPH.Generators/HPPH.Generators.ColorSourceGenerator/ColorFormatABGR.g.cs @@ -10,6 +10,8 @@ public sealed partial class ColorFormatABGR : IColorFormat public string Name => "ABGR"; + ReadOnlySpan IColorFormat.ByteMapping => [3, 2, 1, 0]; + #endregion #region Constructors diff --git a/HPPH/Generated/HPPH.Generators/HPPH.Generators.ColorSourceGenerator/ColorFormatARGB.g.cs b/HPPH/Generated/HPPH.Generators/HPPH.Generators.ColorSourceGenerator/ColorFormatARGB.g.cs index 5dcb76d..b677ae5 100644 --- a/HPPH/Generated/HPPH.Generators/HPPH.Generators.ColorSourceGenerator/ColorFormatARGB.g.cs +++ b/HPPH/Generated/HPPH.Generators/HPPH.Generators.ColorSourceGenerator/ColorFormatARGB.g.cs @@ -10,6 +10,8 @@ public sealed partial class ColorFormatARGB : IColorFormat public string Name => "ARGB"; + ReadOnlySpan IColorFormat.ByteMapping => [3, 0, 1, 2]; + #endregion #region Constructors diff --git a/HPPH/Generated/HPPH.Generators/HPPH.Generators.ColorSourceGenerator/ColorFormatBGR.g.cs b/HPPH/Generated/HPPH.Generators/HPPH.Generators.ColorSourceGenerator/ColorFormatBGR.g.cs index 1de747c..f536251 100644 --- a/HPPH/Generated/HPPH.Generators/HPPH.Generators.ColorSourceGenerator/ColorFormatBGR.g.cs +++ b/HPPH/Generated/HPPH.Generators/HPPH.Generators.ColorSourceGenerator/ColorFormatBGR.g.cs @@ -10,6 +10,8 @@ public sealed partial class ColorFormatBGR : IColorFormat public string Name => "BGR"; + ReadOnlySpan IColorFormat.ByteMapping => [2, 1, 0]; + #endregion #region Constructors diff --git a/HPPH/Generated/HPPH.Generators/HPPH.Generators.ColorSourceGenerator/ColorFormatBGRA.g.cs b/HPPH/Generated/HPPH.Generators/HPPH.Generators.ColorSourceGenerator/ColorFormatBGRA.g.cs index 17feeb9..db43afa 100644 --- a/HPPH/Generated/HPPH.Generators/HPPH.Generators.ColorSourceGenerator/ColorFormatBGRA.g.cs +++ b/HPPH/Generated/HPPH.Generators/HPPH.Generators.ColorSourceGenerator/ColorFormatBGRA.g.cs @@ -10,6 +10,8 @@ public sealed partial class ColorFormatBGRA : IColorFormat public string Name => "BGRA"; + ReadOnlySpan IColorFormat.ByteMapping => [2, 1, 0, 3]; + #endregion #region Constructors diff --git a/HPPH/Generated/HPPH.Generators/HPPH.Generators.ColorSourceGenerator/ColorFormatRGB.g.cs b/HPPH/Generated/HPPH.Generators/HPPH.Generators.ColorSourceGenerator/ColorFormatRGB.g.cs index 1208503..6625f97 100644 --- a/HPPH/Generated/HPPH.Generators/HPPH.Generators.ColorSourceGenerator/ColorFormatRGB.g.cs +++ b/HPPH/Generated/HPPH.Generators/HPPH.Generators.ColorSourceGenerator/ColorFormatRGB.g.cs @@ -10,6 +10,8 @@ public sealed partial class ColorFormatRGB : IColorFormat public string Name => "RGB"; + ReadOnlySpan IColorFormat.ByteMapping => [0, 1, 2]; + #endregion #region Constructors diff --git a/HPPH/Generated/HPPH.Generators/HPPH.Generators.ColorSourceGenerator/ColorFormatRGBA.g.cs b/HPPH/Generated/HPPH.Generators/HPPH.Generators.ColorSourceGenerator/ColorFormatRGBA.g.cs index 1e29a15..7410b75 100644 --- a/HPPH/Generated/HPPH.Generators/HPPH.Generators.ColorSourceGenerator/ColorFormatRGBA.g.cs +++ b/HPPH/Generated/HPPH.Generators/HPPH.Generators.ColorSourceGenerator/ColorFormatRGBA.g.cs @@ -10,6 +10,8 @@ public sealed partial class ColorFormatRGBA : IColorFormat public string Name => "RGBA"; + ReadOnlySpan IColorFormat.ByteMapping => [0, 1, 2, 3]; + #endregion #region Constructors diff --git a/HPPH/Generated/HPPH.Generators/HPPH.Generators.ColorSourceGenerator/IColorFormat.Instances.cs.g.cs b/HPPH/Generated/HPPH.Generators/HPPH.Generators.ColorSourceGenerator/IColorFormat.Instances.g.cs similarity index 100% rename from HPPH/Generated/HPPH.Generators/HPPH.Generators.ColorSourceGenerator/IColorFormat.Instances.cs.g.cs rename to HPPH/Generated/HPPH.Generators/HPPH.Generators.ColorSourceGenerator/IColorFormat.Instances.g.cs diff --git a/HPPH/PixelHelper.Convert.cs b/HPPH/PixelHelper.Convert.cs new file mode 100644 index 0000000..80cfdf1 --- /dev/null +++ b/HPPH/PixelHelper.Convert.cs @@ -0,0 +1,155 @@ +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +namespace HPPH; + +public static unsafe partial class PixelHelper +{ + #region Methods + + public static Span Convert(Span data) + where TSource : struct, IColor + where TTarget : struct, IColor + { + if (data == null) throw new ArgumentNullException(nameof(data)); + + Convert(MemoryMarshal.AsBytes(data), TSource.ColorFormat, TTarget.ColorFormat); + + return MemoryMarshal.Cast(data); + } + + internal static void Convert(Span data, IColorFormat sourceFormat, IColorFormat targetFormat) + { + if (data == null) throw new ArgumentNullException(nameof(data)); + ArgumentNullException.ThrowIfNull(sourceFormat); + ArgumentNullException.ThrowIfNull(targetFormat); + + if (sourceFormat == targetFormat) return; + + if (sourceFormat.BytesPerPixel == targetFormat.BytesPerPixel) + ConvertEqualBpp(data, sourceFormat, targetFormat); + else if ((sourceFormat.BytesPerPixel == 3) && (targetFormat.BytesPerPixel == 4)) + ConvertWiden3To4Bytes(data, sourceFormat, targetFormat); + else if ((sourceFormat.BytesPerPixel == 4) && (targetFormat.BytesPerPixel == 3)) + ConvertNarrow4To3Bytes(data, sourceFormat, targetFormat); + else + throw new NotSupportedException("Data is not of a supported valid color-type."); + } + + private static void ConvertEqualBpp(Span data, IColorFormat sourceFormat, IColorFormat targetFormat) + { + ReadOnlySpan sourceMapping = sourceFormat.ByteMapping; + ReadOnlySpan targetMapping = targetFormat.ByteMapping; + + switch (sourceFormat.BytesPerPixel) + { + case 3: + ReadOnlySpan mapping3 = [targetMapping[sourceMapping[0]], targetMapping[sourceMapping[1]], targetMapping[sourceMapping[2]]]; + ReadOnlySpan mask3 = + [ + mapping3[0], + mapping3[1], + mapping3[2], + + (byte)(mapping3[0] + 3), + (byte)(mapping3[1] + 3), + (byte)(mapping3[2] + 3), + + (byte)(mapping3[0] + 6), + (byte)(mapping3[1] + 6), + (byte)(mapping3[2] + 6), + + (byte)(mapping3[0] + 9), + (byte)(mapping3[1] + 9), + (byte)(mapping3[2] + 9), + + (byte)(mapping3[0] + 12), + (byte)(mapping3[1] + 12), + (byte)(mapping3[2] + 12), + + 15 + ]; + + ConvertEqualBpp(data, mask3, 3); + break; + + case 4: + ReadOnlySpan mapping4 = [targetMapping[sourceMapping[0]], targetMapping[sourceMapping[1]], targetMapping[sourceMapping[2]], targetMapping[sourceMapping[3]]]; + ReadOnlySpan mask4 = + [ + mapping4[0], + mapping4[1], + mapping4[2], + mapping4[3], + + (byte)(mapping4[0] + 4), + (byte)(mapping4[1] + 4), + (byte)(mapping4[2] + 4), + (byte)(mapping4[3] + 4), + + (byte)(mapping4[0] + 8), + (byte)(mapping4[1] + 8), + (byte)(mapping4[2] + 8), + (byte)(mapping4[3] + 8), + + (byte)(mapping4[0] + 12), + (byte)(mapping4[1] + 12), + (byte)(mapping4[2] + 12), + (byte)(mapping4[3] + 12), + ]; + + ConvertEqualBpp(data, mask4, 4); + break; + + default: + throw new NotSupportedException("Data is not of a supported valid color-type."); + } + } + + // DarthAffe 07.07.2024: No fallback-implementation here. Shuffle Requires only Seee3 which should be supported nearly anywhere and if not the fallback of Vector128.Shuffle is perfectly fine. + private static void ConvertEqualBpp(Span data, ReadOnlySpan mask, int bpp) + { + int elementsPerVector = Vector128.Count / bpp; + int bytesPerVector = elementsPerVector * bpp; + + int chunks = data.Length / bytesPerVector; + Vector128 maskVector = Vector128.LoadUnsafe(ref MemoryMarshal.GetReference(mask)); + + int missingElements = (data.Length - (chunks * bytesPerVector)) / bpp; + + fixed (byte* dataPtr = data) + { + byte* ptr = dataPtr; + + for (int i = 0; i < chunks; i++) + { + Vector128 vector = Vector128.Load(ptr); + Vector128.Shuffle(vector, maskVector).Store(ptr); + + ptr += bytesPerVector; + } + + Span buffer = stackalloc byte[missingElements * bpp]; // DarthAffe 07.07.2024: This is fine as it's always < 16 bytes + for (int i = 0; i < missingElements; i++) + { + int elementIndex = i * buffer.Length; + for (int j = 0; j < buffer.Length; j++) + buffer[elementIndex + j] = ptr[elementIndex + mask[j]]; + } + + buffer.CopyTo(new Span(ptr, buffer.Length)); + } + } + + private static void ConvertWiden3To4Bytes(Span data, IColorFormat sourceFormat, IColorFormat targetFormat) + { + throw new NotImplementedException(); + } + + private static void ConvertNarrow4To3Bytes(Span data, IColorFormat sourceFormat, IColorFormat targetFormat) + { + throw new NotImplementedException(); + } + + #endregion +}